Merge branch 'develop' into import-text
This commit is contained in:
commit
af6e5dea3a
48
Makefile
48
Makefile
|
@ -7,6 +7,7 @@ GOBUILD=$(GOCMD) build $(LDFLAGS)
|
|||
GOTEST=$(GOCMD) test $(LDFLAGS)
|
||||
GOGET=$(GOCMD) get
|
||||
BINARY_NAME=writefreely
|
||||
BUILDPATH=build/$(BINARY_NAME)
|
||||
DOCKERCMD=docker
|
||||
IMAGE_NAME=writeas/writefreely
|
||||
TMPBIN=./tmp
|
||||
|
@ -69,39 +70,40 @@ install : build
|
|||
cd less/; $(MAKE) install $(MFLAGS)
|
||||
|
||||
release : clean ui assets
|
||||
mkdir build
|
||||
cp -r templates build
|
||||
cp -r pages build
|
||||
cp -r static build
|
||||
mkdir build/keys
|
||||
mkdir -p $(BUILDPATH)
|
||||
cp -r templates $(BUILDPATH)
|
||||
cp -r pages $(BUILDPATH)
|
||||
cp -r static $(BUILDPATH)
|
||||
mkdir $(BUILDPATH)/keys
|
||||
$(MAKE) build-linux
|
||||
mv build/$(BINARY_NAME)-linux-amd64 build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz *
|
||||
rm build/$(BINARY_NAME)
|
||||
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-arm7
|
||||
mv build/$(BINARY_NAME)-linux-arm-7 build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz *
|
||||
rm build/$(BINARY_NAME)
|
||||
mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(BINARY_NAME)
|
||||
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz -C build $(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
$(MAKE) build-darwin
|
||||
mv build/$(BINARY_NAME)-darwin-10.6-amd64 build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz *
|
||||
rm build/$(BINARY_NAME)
|
||||
mv build/$(BINARY_NAME)-darwin-10.6-amd64 $(BUILDPATH)/$(BINARY_NAME)
|
||||
tar -cvzf $(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz -C build $(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
$(MAKE) build-windows
|
||||
mv build/$(BINARY_NAME)-windows-4.0-amd64.exe build/$(BINARY_NAME).exe
|
||||
cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./*
|
||||
mv build/$(BINARY_NAME)-windows-4.0-amd64.exe $(BUILDPATH)/$(BINARY_NAME).exe
|
||||
cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./$(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
$(MAKE) build-docker
|
||||
$(MAKE) release-docker
|
||||
|
||||
# This assumes you're on linux/amd64
|
||||
release-linux : clean ui
|
||||
mkdir build
|
||||
cp -r templates build
|
||||
cp -r pages build
|
||||
cp -r static build
|
||||
mkdir build/keys
|
||||
mkdir -p $(BUILDPATH)
|
||||
cp -r templates $(BUILDPATH)
|
||||
cp -r pages $(BUILDPATH)
|
||||
cp -r static $(BUILDPATH)
|
||||
mkdir $(BUILDPATH)/keys
|
||||
$(MAKE) build-no-sqlite
|
||||
mv cmd/writefreely/$(BINARY_NAME) build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz *
|
||||
mv cmd/writefreely/$(BINARY_NAME) $(BUILDPATH)/$(BINARY_NAME)
|
||||
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(BINARY_NAME)
|
||||
|
||||
release-docker :
|
||||
$(DOCKERCMD) push $(IMAGE_NAME)
|
||||
|
|
10
README.md
10
README.md
|
@ -47,15 +47,15 @@ It's designed to be flexible and share your writing widely, so it's built around
|
|||
|
||||
## Hosting
|
||||
|
||||
We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as](https://write.as) for individuals, and [WriteFreely.host](https://writefreely.host) for communities. Besides saving you time, as a customer you directly help fund WriteFreely development.
|
||||
We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as Pro](https://write.as/pro) for individuals, and [Write.as for Teams](https://write.as/for/teams) for businesses. Besides saving you time and effort, both services directly fund WriteFreely development and ensure the long-term sustainability of our open source work.
|
||||
|
||||
### [![Write.as](https://write.as/img/writeas-wf-readme.png)](https://write.as/)
|
||||
### [![Write.as Pro](https://writefreely.org/img/writeas-pro-readme.png)](https://write.as/pro)
|
||||
|
||||
Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pricing).
|
||||
Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pro).
|
||||
|
||||
### [![WriteFreely.host](https://writefreely.host/img/wfhost-wf-readme.png)](https://writefreely.host)
|
||||
### [![Write.as for Teams](https://writefreely.org/img/writeas-for-teams-readme.png)](https://write.as/for/teams)
|
||||
|
||||
[WriteFreely.host](https://writefreely.host) makes it easy to start a close-knit community — to share knowledge, complement your Mastodon instance, or publish updates in your organization. We take care of the hosting, upgrades, backups, and maintenance so you can focus on writing.
|
||||
[Write.as for Teams](https://write.as/for/teams) gives your organization, business, or [open source project](https://write.as/for/open-source) a clutter-free space to share updates or proposals and build your collective knowledge. We take care of hosting, upgrades, backups, and maintenance so your team can focus on writing.
|
||||
|
||||
## Quick start
|
||||
|
||||
|
|
86
account.go
86
account.go
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/writeas/web-core/data"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
)
|
||||
|
||||
|
@ -59,11 +60,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str
|
|||
up.Flashes = flashes
|
||||
up.Path = r.URL.Path
|
||||
up.IsAdmin = u.IsAdmin()
|
||||
up.CanInvite = app.cfg.App.UserInvites != "" &&
|
||||
(up.IsAdmin || app.cfg.App.UserInvites != "admin")
|
||||
up.CanInvite = canUserInvite(app.cfg, up.IsAdmin)
|
||||
return up
|
||||
}
|
||||
|
||||
func canUserInvite(cfg *config.Config, isAdmin bool) bool {
|
||||
return cfg.App.UserInvites != "" &&
|
||||
(isAdmin || cfg.App.UserInvites != "admin")
|
||||
}
|
||||
|
||||
func (up *UserPage) SetMessaging(u *User) {
|
||||
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
|
||||
}
|
||||
|
@ -80,7 +85,7 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Get params
|
||||
var ur userRegistration
|
||||
|
@ -115,7 +120,7 @@ func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error)
|
|||
}
|
||||
|
||||
func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Validate required params (alias)
|
||||
if signup.Alias == "" {
|
||||
|
@ -305,10 +310,10 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := &struct {
|
||||
page.StaticPage
|
||||
To string
|
||||
Message template.HTML
|
||||
Flashes []template.HTML
|
||||
Username string
|
||||
To string
|
||||
Message template.HTML
|
||||
Flashes []template.HTML
|
||||
LoginUsername string
|
||||
}{
|
||||
pageForReq(app, r),
|
||||
r.FormValue("to"),
|
||||
|
@ -372,7 +377,7 @@ func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
var loginAttemptUsers = sync.Map{}
|
||||
|
||||
func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
oneTimeToken := r.FormValue("with")
|
||||
verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
|
||||
|
||||
|
@ -547,7 +552,7 @@ func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser
|
|||
if err != nil {
|
||||
log.Error("Login: Unable to get user posts: %v", err)
|
||||
}
|
||||
colls, err := app.db.GetCollections(u)
|
||||
colls, err := app.db.GetCollections(u, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("Login: Unable to get user collections: %v", err)
|
||||
}
|
||||
|
@ -575,7 +580,7 @@ func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request
|
|||
func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||
var filename string
|
||||
var u = &User{}
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
if reqJSON {
|
||||
// Use given Authorization header
|
||||
accessToken := r.Header.Get("Authorization")
|
||||
|
@ -620,7 +625,7 @@ func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte,
|
|||
|
||||
// Export as CSV
|
||||
if strings.HasSuffix(r.URL.Path, ".csv") {
|
||||
data = exportPostsCSV(u, posts)
|
||||
data = exportPostsCSV(app.cfg.App.Host, u, posts)
|
||||
return data, filename, err
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, ".zip") {
|
||||
|
@ -657,7 +662,7 @@ func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, s
|
|||
}
|
||||
|
||||
func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
uObj := struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
|
@ -681,7 +686,7 @@ func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
if !reqJSON {
|
||||
return ErrBadRequestedType
|
||||
}
|
||||
|
@ -712,12 +717,12 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
|
|||
}
|
||||
|
||||
func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
if !reqJSON {
|
||||
return ErrBadRequestedType
|
||||
}
|
||||
|
||||
p, err := app.db.GetCollections(u)
|
||||
p, err := app.db.GetCollections(u, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -740,19 +745,25 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
log.Error("unable to fetch flashes: %v", err)
|
||||
}
|
||||
|
||||
c, err := app.db.GetPublishableCollections(u)
|
||||
c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view articles: %v", err)
|
||||
}
|
||||
d := struct {
|
||||
*UserPage
|
||||
AnonymousPosts *[]PublicPost
|
||||
Collections *[]Collection
|
||||
Suspended bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
|
||||
AnonymousPosts: p,
|
||||
Collections: c,
|
||||
Suspended: suspended,
|
||||
}
|
||||
d.UserPage.SetMessaging(u)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
@ -763,7 +774,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
}
|
||||
|
||||
func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
c, err := app.db.GetCollections(u)
|
||||
c, err := app.db.GetCollections(u, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
return fmt.Errorf("No collections")
|
||||
|
@ -774,6 +785,11 @@ 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)
|
||||
if err != nil {
|
||||
log.Error("view collections %v", err)
|
||||
return fmt.Errorf("view collections: %v", err)
|
||||
}
|
||||
d := struct {
|
||||
*UserPage
|
||||
Collections *[]Collection
|
||||
|
@ -781,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
|
|||
UsedCollections, TotalCollections int
|
||||
|
||||
NewBlogsDisabled bool
|
||||
Suspended bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
|
||||
Collections: c,
|
||||
UsedCollections: int(uc),
|
||||
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
|
||||
Suspended: suspended,
|
||||
}
|
||||
d.UserPage.SetMessaging(u)
|
||||
showUserPage(w, "collections", d)
|
||||
|
@ -803,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
return ErrCollectionNotFound
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view edit collection %v", err)
|
||||
return fmt.Errorf("view edit collection: %v", err)
|
||||
}
|
||||
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||
obj := struct {
|
||||
*UserPage
|
||||
*Collection
|
||||
Suspended bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
|
||||
Collection: c,
|
||||
Suspended: suspended,
|
||||
}
|
||||
|
||||
showUserPage(w, "collection", obj)
|
||||
|
@ -817,7 +842,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
var s userSettings
|
||||
var u *User
|
||||
|
@ -971,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
titleStats = c.DisplayTitle() + " "
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view stats: %v", err)
|
||||
return err
|
||||
}
|
||||
obj := struct {
|
||||
*UserPage
|
||||
VisitsBlog string
|
||||
Collection *Collection
|
||||
TopPosts *[]PublicPost
|
||||
APFollowers int
|
||||
Suspended bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
|
||||
VisitsBlog: alias,
|
||||
Collection: c,
|
||||
TopPosts: topPosts,
|
||||
Suspended: suspended,
|
||||
}
|
||||
if app.cfg.App.Federation {
|
||||
folls, err := app.db.GetAPFollowers(c)
|
||||
|
@ -1012,14 +1044,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
|
||||
obj := struct {
|
||||
*UserPage
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Suspended bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Suspended: fullUser.IsSilenced(),
|
||||
}
|
||||
|
||||
showUserPage(w, "settings", obj)
|
||||
|
|
|
@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection activities: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
p := c.PersonObject()
|
||||
|
@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection outbox: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if app.cfg.App.SingleUser {
|
||||
|
@ -129,10 +145,10 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
|
|||
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p)
|
||||
ocp.OrderedItems = []interface{}{}
|
||||
|
||||
posts, err := app.db.GetPosts(c, p, false, true, false)
|
||||
posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
|
||||
for _, pp := range *posts {
|
||||
pp.Collection = res
|
||||
o := pp.ActivityObject()
|
||||
o := pp.ActivityObject(app.cfg)
|
||||
a := activitystreams.NewCreateActivity(o)
|
||||
ocp.OrderedItems = append(ocp.OrderedItems, *a)
|
||||
}
|
||||
|
@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection followers: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
accountRoot := c.FederatedAccount()
|
||||
|
@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection following: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
accountRoot := c.FederatedAccount()
|
||||
|
@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
|||
// TODO: return Reject?
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection inbox: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if debugging {
|
||||
|
@ -375,11 +415,11 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
|||
// Add follower locally, since it wasn't found before
|
||||
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
||||
if err != nil {
|
||||
if !app.db.isDuplicateKeyErr(err) {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||
return
|
||||
}
|
||||
// if duplicate key, res will be nil and panic on
|
||||
// res.LastInsertId below
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
followerID, err = res.LastInsertId()
|
||||
|
@ -524,7 +564,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()
|
||||
na := p.ActivityObject(app.cfg)
|
||||
|
||||
// Add followers
|
||||
p.Collection.ID = collID
|
||||
|
@ -570,7 +610,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
}
|
||||
}
|
||||
actor := p.Collection.PersonObject(collID)
|
||||
na := p.ActivityObject()
|
||||
na := p.ActivityObject(app.cfg)
|
||||
|
||||
// Add followers
|
||||
p.Collection.ID = collID
|
||||
|
|
135
admin.go
135
admin.go
|
@ -13,16 +13,19 @@ package writefreely
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/passgen"
|
||||
"github.com/writeas/writefreely/appstats"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -169,11 +172,12 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
Config config.AppCfg
|
||||
Message string
|
||||
|
||||
User *User
|
||||
Colls []inspectedCollection
|
||||
LastPost string
|
||||
|
||||
TotalPosts int64
|
||||
User *User
|
||||
Colls []inspectedCollection
|
||||
LastPost string
|
||||
NewPassword string
|
||||
TotalPosts int64
|
||||
ClearEmail string
|
||||
}{
|
||||
Config: app.cfg.App,
|
||||
Message: r.FormValue("m"),
|
||||
|
@ -185,6 +189,14 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
|
||||
}
|
||||
|
||||
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||
for _, flash := range flashes {
|
||||
if strings.HasPrefix(flash, "SUCCESS: ") {
|
||||
p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
|
||||
p.ClearEmail = p.User.EmailClear(app.keys)
|
||||
}
|
||||
}
|
||||
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
|
||||
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
|
||||
lp, err := app.db.GetUserLastPostTime(p.User.ID)
|
||||
|
@ -195,7 +207,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
p.LastPost = lp.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
colls, err := app.db.GetCollections(p.User)
|
||||
colls, err := app.db.GetCollections(p.User, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)}
|
||||
}
|
||||
|
@ -229,6 +241,62 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
if username == "" {
|
||||
return impart.HTTPError{http.StatusFound, "/admin/users"}
|
||||
}
|
||||
|
||||
user, err := app.db.GetUserForAuth(username)
|
||||
if err != nil {
|
||||
log.Error("failed to get user: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
|
||||
}
|
||||
if user.IsSilenced() {
|
||||
err = app.db.SetUserStatus(user.ID, UserActive)
|
||||
} else {
|
||||
err = app.db.SetUserStatus(user.ID, UserSilenced)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("toggle user suspended: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")}
|
||||
}
|
||||
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
|
||||
}
|
||||
|
||||
func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
if username == "" {
|
||||
return impart.HTTPError{http.StatusFound, "/admin/users"}
|
||||
}
|
||||
|
||||
// Generate new random password since none supplied
|
||||
pass := passgen.NewWordish()
|
||||
hashedPass, err := auth.HashPass([]byte(pass))
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
|
||||
}
|
||||
|
||||
userIDVal := r.FormValue("user")
|
||||
log.Info("ADMIN: Changing user %s password", userIDVal)
|
||||
id, err := strconv.Atoi(userIDVal)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid user ID: %v", err)}
|
||||
}
|
||||
|
||||
err = app.db.ChangePassphrase(int64(id), true, "", hashedPass)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
|
||||
}
|
||||
log.Info("ADMIN: Successfully changed.")
|
||||
|
||||
addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: %s", pass), nil)
|
||||
|
||||
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s", username)}
|
||||
}
|
||||
|
||||
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p := struct {
|
||||
*UserPage
|
||||
|
@ -319,6 +387,8 @@ func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
p.Content, err = getLandingBody(app)
|
||||
p.Content.ID = "landing"
|
||||
} else if slug == "reader" {
|
||||
p.Content, err = getReaderSection(app)
|
||||
} else {
|
||||
p.Content, err = app.db.GetDynamicContent(slug)
|
||||
}
|
||||
|
@ -342,7 +412,7 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req
|
|||
id := vars["page"]
|
||||
|
||||
// Validate
|
||||
if id != "about" && id != "privacy" && id != "landing" {
|
||||
if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
|
||||
return impart.HTTPError{http.StatusNotFound, "No such page."}
|
||||
}
|
||||
|
||||
|
@ -356,6 +426,9 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req
|
|||
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
|
||||
}
|
||||
err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
|
||||
} else if id == "reader" {
|
||||
// Update sections with titles
|
||||
err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section")
|
||||
} else {
|
||||
// Update page
|
||||
err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
|
||||
|
@ -402,37 +475,37 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
|
|||
}
|
||||
|
||||
func updateAppStats() {
|
||||
sysStatus.Uptime = tool.TimeSincePro(appStartTime)
|
||||
sysStatus.Uptime = appstats.TimeSincePro(appStartTime)
|
||||
|
||||
m := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(m)
|
||||
sysStatus.NumGoroutine = runtime.NumGoroutine()
|
||||
|
||||
sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc))
|
||||
sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc))
|
||||
sysStatus.MemSys = tool.FileSize(int64(m.Sys))
|
||||
sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc))
|
||||
sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc))
|
||||
sysStatus.MemSys = appstats.FileSize(int64(m.Sys))
|
||||
sysStatus.Lookups = m.Lookups
|
||||
sysStatus.MemMallocs = m.Mallocs
|
||||
sysStatus.MemFrees = m.Frees
|
||||
|
||||
sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc))
|
||||
sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys))
|
||||
sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle))
|
||||
sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse))
|
||||
sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased))
|
||||
sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc))
|
||||
sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys))
|
||||
sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle))
|
||||
sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse))
|
||||
sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased))
|
||||
sysStatus.HeapObjects = m.HeapObjects
|
||||
|
||||
sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse))
|
||||
sysStatus.StackSys = tool.FileSize(int64(m.StackSys))
|
||||
sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse))
|
||||
sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys))
|
||||
sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse))
|
||||
sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys))
|
||||
sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys))
|
||||
sysStatus.GCSys = tool.FileSize(int64(m.GCSys))
|
||||
sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys))
|
||||
sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse))
|
||||
sysStatus.StackSys = appstats.FileSize(int64(m.StackSys))
|
||||
sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse))
|
||||
sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys))
|
||||
sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse))
|
||||
sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys))
|
||||
sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys))
|
||||
sysStatus.GCSys = appstats.FileSize(int64(m.GCSys))
|
||||
sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys))
|
||||
|
||||
sysStatus.NextGC = tool.FileSize(int64(m.NextGC))
|
||||
sysStatus.NextGC = appstats.FileSize(int64(m.NextGC))
|
||||
sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
|
||||
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
|
||||
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
|
||||
|
|
36
app.go
36
app.go
|
@ -56,7 +56,7 @@ var (
|
|||
debugging bool
|
||||
|
||||
// Software version can be set from git env using -ldflags
|
||||
softwareVer = "0.10.0"
|
||||
softwareVer = "0.11.1"
|
||||
|
||||
// DEPRECATED VARS
|
||||
isSingleUser bool
|
||||
|
@ -185,8 +185,8 @@ func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) str
|
|||
return fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, timeSince, r.UserAgent())
|
||||
}
|
||||
|
||||
// handleViewHome shows page at root path. Will be the Pad if logged in and the
|
||||
// catch-all landing page otherwise.
|
||||
// handleViewHome shows page at root path. It checks the configuration and
|
||||
// authentication state to show the correct page.
|
||||
func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
if app.cfg.App.SingleUser {
|
||||
// Render blog index
|
||||
|
@ -198,6 +198,15 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
if !forceLanding {
|
||||
// Show correct page based on user auth status and configured landing path
|
||||
u := getUserSession(app, r)
|
||||
|
||||
if app.cfg.App.Chorus {
|
||||
// This instance is focused on reading, so show Reader on home route if not
|
||||
// private or a private-instance user is logged in.
|
||||
if !app.cfg.App.Private || u != nil {
|
||||
return viewLocalTimeline(app, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
// User is logged in, so show the Pad
|
||||
return handleViewPad(app, w, r)
|
||||
|
@ -208,6 +217,12 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
}
|
||||
|
||||
return handleViewLanding(app, w, r)
|
||||
}
|
||||
|
||||
func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
forceLanding := r.FormValue("landing") == "1"
|
||||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
Flashes []template.HTML
|
||||
|
@ -225,14 +240,14 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
log.Error("unable to get landing banner: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
|
||||
}
|
||||
p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), ""))
|
||||
p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "", app.cfg))
|
||||
|
||||
content, err := getLandingBody(app)
|
||||
if err != nil {
|
||||
log.Error("unable to get landing content: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get content: %v", err)}
|
||||
}
|
||||
p.Content = template.HTML(applyMarkdown([]byte(content.Content), ""))
|
||||
p.Content = template.HTML(applyMarkdown([]byte(content.Content), "", app.cfg))
|
||||
|
||||
// Get error messages
|
||||
session, err := app.sessionStore.Get(r, cookieName)
|
||||
|
@ -280,7 +295,7 @@ func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *te
|
|||
return err
|
||||
}
|
||||
p.ContentTitle = c.Title.String
|
||||
p.Content = template.HTML(applyMarkdown([]byte(c.Content), ""))
|
||||
p.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg))
|
||||
p.PlainContent = shortPostDescription(stripmd.Strip(c.Content))
|
||||
if !c.Updated.IsZero() {
|
||||
p.Updated = c.Updated.Format("January 2, 2006")
|
||||
|
@ -318,6 +333,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage {
|
|||
u = getUserSession(app, r)
|
||||
if u != nil {
|
||||
p.Username = u.Username
|
||||
p.IsAdmin = u != nil && u.IsAdmin()
|
||||
p.CanInvite = canUserInvite(app.cfg, p.IsAdmin)
|
||||
}
|
||||
}
|
||||
p.CanViewReader = !app.cfg.App.Private || u != nil
|
||||
|
@ -486,9 +503,14 @@ func ConnectToDatabase(app *App) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FormatVersion constructs the version string for the application
|
||||
func FormatVersion() string {
|
||||
return serverSoftware + " " + softwareVer
|
||||
}
|
||||
|
||||
// OutputVersion prints out the version of the application.
|
||||
func OutputVersion() {
|
||||
fmt.Println(serverSoftware + " " + softwareVer)
|
||||
fmt.Println(FormatVersion())
|
||||
}
|
||||
|
||||
// NewApp creates a new app instance.
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2014-2018 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can be
|
||||
// found in the LICENSE file of the Gogs project (github.com/gogs/gogs).
|
||||
|
||||
package appstats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Borrowed from github.com/gogs/gogs/pkg/tool
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Minute = 60
|
||||
Hour = 60 * Minute
|
||||
Day = 24 * Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
)
|
||||
|
||||
func computeTimeDiff(diff int64) (int64, string) {
|
||||
diffStr := ""
|
||||
switch {
|
||||
case diff <= 0:
|
||||
diff = 0
|
||||
diffStr = "now"
|
||||
case diff < 2:
|
||||
diff = 0
|
||||
diffStr = "1 second"
|
||||
case diff < 1*Minute:
|
||||
diffStr = fmt.Sprintf("%d seconds", diff)
|
||||
diff = 0
|
||||
|
||||
case diff < 2*Minute:
|
||||
diff -= 1 * Minute
|
||||
diffStr = "1 minute"
|
||||
case diff < 1*Hour:
|
||||
diffStr = fmt.Sprintf("%d minutes", diff/Minute)
|
||||
diff -= diff / Minute * Minute
|
||||
|
||||
case diff < 2*Hour:
|
||||
diff -= 1 * Hour
|
||||
diffStr = "1 hour"
|
||||
case diff < 1*Day:
|
||||
diffStr = fmt.Sprintf("%d hours", diff/Hour)
|
||||
diff -= diff / Hour * Hour
|
||||
|
||||
case diff < 2*Day:
|
||||
diff -= 1 * Day
|
||||
diffStr = "1 day"
|
||||
case diff < 1*Week:
|
||||
diffStr = fmt.Sprintf("%d days", diff/Day)
|
||||
diff -= diff / Day * Day
|
||||
|
||||
case diff < 2*Week:
|
||||
diff -= 1 * Week
|
||||
diffStr = "1 week"
|
||||
case diff < 1*Month:
|
||||
diffStr = fmt.Sprintf("%d weeks", diff/Week)
|
||||
diff -= diff / Week * Week
|
||||
|
||||
case diff < 2*Month:
|
||||
diff -= 1 * Month
|
||||
diffStr = "1 month"
|
||||
case diff < 1*Year:
|
||||
diffStr = fmt.Sprintf("%d months", diff/Month)
|
||||
diff -= diff / Month * Month
|
||||
|
||||
case diff < 2*Year:
|
||||
diff -= 1 * Year
|
||||
diffStr = "1 year"
|
||||
default:
|
||||
diffStr = fmt.Sprintf("%d years", diff/Year)
|
||||
diff = 0
|
||||
}
|
||||
return diff, diffStr
|
||||
}
|
||||
|
||||
// TimeSincePro calculates the time interval and generate full user-friendly string.
|
||||
func TimeSincePro(then time.Time) string {
|
||||
now := time.Now()
|
||||
diff := now.Unix() - then.Unix()
|
||||
|
||||
if then.After(now) {
|
||||
return "future"
|
||||
}
|
||||
|
||||
var timeStr, diffStr string
|
||||
for {
|
||||
if diff == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
diff, diffStr = computeTimeDiff(diff)
|
||||
timeStr += ", " + diffStr
|
||||
}
|
||||
return strings.TrimPrefix(timeStr, ", ")
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := float64(s) / math.Pow(base, math.Floor(e))
|
||||
f := "%.0f"
|
||||
if val < 10 {
|
||||
f = "%.1f"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f+" %s", val, suffix)
|
||||
}
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSize(s int64) string {
|
||||
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(uint64(s), 1024, sizes)
|
||||
}
|
|
@ -113,6 +113,7 @@ func main() {
|
|||
|
||||
// Initialize the application
|
||||
var err error
|
||||
log.Info("Starting %s...", writefreely.FormatVersion())
|
||||
app, err = writefreely.Initialize(app, *debugPtr)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
|
|
|
@ -71,6 +71,7 @@ type (
|
|||
CurrentPage int
|
||||
TotalPages int
|
||||
Format *CollectionFormat
|
||||
Suspended bool
|
||||
}
|
||||
SubmittedCollection struct {
|
||||
// Data used for updating a given collection
|
||||
|
@ -338,7 +339,7 @@ func (c *Collection) RenderMathJax() bool {
|
|||
}
|
||||
|
||||
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
alias := r.FormValue("alias")
|
||||
title := r.FormValue("title")
|
||||
|
||||
|
@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
var userID int64
|
||||
var err error
|
||||
if reqJSON && !c.Web {
|
||||
accessToken = r.Header.Get("Authorization")
|
||||
if accessToken == "" {
|
||||
|
@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
userID = u.ID
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(userID)
|
||||
if err != nil {
|
||||
log.Error("new collection: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
if !author.IsValidUsername(app.cfg, c.Alias) {
|
||||
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
|
||||
|
@ -454,7 +464,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Redirect users who aren't requesting JSON
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
if !reqJSON {
|
||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
|
||||
}
|
||||
|
@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
res.Owner = u
|
||||
}
|
||||
}
|
||||
// TODO: check suspended
|
||||
app.db.GetPostsCount(res, isCollOwner)
|
||||
// Strip non-public information
|
||||
res.Collection.ForPublic()
|
||||
|
@ -512,7 +523,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
}
|
||||
}
|
||||
|
||||
posts, err := app.db.GetPosts(c, page, isCollOwner, false, false)
|
||||
posts, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -541,6 +552,8 @@ type CollectionPage struct {
|
|||
Username string
|
||||
Collections *[]Collection
|
||||
PinnedPosts *[]PublicPost
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
}
|
||||
|
||||
func (c *CollectionObj) ScriptDisplay() template.JS {
|
||||
|
@ -723,6 +736,13 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
if c == nil || err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("view collection: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
// Serve ActivityStreams data now, if requested
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
|
||||
|
@ -744,7 +764,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
return impart.HTTPError{http.StatusFound, redirURL}
|
||||
}
|
||||
|
||||
coll.Posts, _ = app.db.GetPosts(c, page, cr.isCollOwner, false, false)
|
||||
coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false)
|
||||
|
||||
// Serve collection
|
||||
displayPage := CollectionPage{
|
||||
|
@ -753,6 +773,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
IsCustomDomain: cr.isCustomDomain,
|
||||
IsWelcome: r.FormValue("greeting") != "",
|
||||
}
|
||||
displayPage.IsAdmin = u != nil && u.IsAdmin()
|
||||
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin)
|
||||
var owner *User
|
||||
if u != nil {
|
||||
displayPage.Username = u.Username
|
||||
|
@ -762,14 +784,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
owner = u
|
||||
displayPage.CanPin = true
|
||||
|
||||
pubColls, err := app.db.GetPublishableCollections(owner)
|
||||
pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
displayPage.Collections = pubColls
|
||||
}
|
||||
}
|
||||
if owner == nil {
|
||||
isOwner := owner != nil
|
||||
if !isOwner {
|
||||
// Current user doesn't own collection; retrieve owner information
|
||||
owner, err = app.db.GetUserByID(coll.OwnerID)
|
||||
if err != nil {
|
||||
|
@ -777,14 +800,22 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
log.Error("Error getting user for collection: %v", err)
|
||||
}
|
||||
}
|
||||
if !isOwner && suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
displayPage.Suspended = isOwner && suspended
|
||||
displayPage.Owner = owner
|
||||
coll.Owner = displayPage.Owner
|
||||
|
||||
// Add more data
|
||||
// TODO: fix this mess of collections inside collections
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner)
|
||||
|
||||
err = templates["collection"].ExecuteTemplate(w, "collection", displayPage)
|
||||
collTmpl := "collection"
|
||||
if app.cfg.App.Chorus {
|
||||
collTmpl = "chorus-collection"
|
||||
}
|
||||
err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage)
|
||||
if err != nil {
|
||||
log.Error("Unable to render collection index: %v", err)
|
||||
}
|
||||
|
@ -833,7 +864,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
|
|||
|
||||
coll := newDisplayCollection(c, cr, page)
|
||||
|
||||
coll.Posts, _ = app.db.GetPostsTagged(c, tag, page, cr.isCollOwner)
|
||||
coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, page, cr.isCollOwner)
|
||||
if coll.Posts != nil && len(*coll.Posts) == 0 {
|
||||
return ErrCollectionPageNotFound
|
||||
}
|
||||
|
@ -859,26 +890,31 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
|
|||
owner = u
|
||||
displayPage.CanPin = true
|
||||
|
||||
pubColls, err := app.db.GetPublishableCollections(owner)
|
||||
pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
displayPage.Collections = pubColls
|
||||
}
|
||||
}
|
||||
if owner == nil {
|
||||
isOwner := owner != nil
|
||||
if !isOwner {
|
||||
// Current user doesn't own collection; retrieve owner information
|
||||
owner, err = app.db.GetUserByID(coll.OwnerID)
|
||||
if err != nil {
|
||||
// Log the error and just continue
|
||||
log.Error("Error getting user for collection: %v", err)
|
||||
}
|
||||
if owner.IsSilenced() {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
}
|
||||
displayPage.Suspended = owner != nil && owner.IsSilenced()
|
||||
displayPage.Owner = owner
|
||||
coll.Owner = displayPage.Owner
|
||||
// Add more data
|
||||
// TODO: fix this mess of collections inside collections
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
|
||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner)
|
||||
|
||||
err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage)
|
||||
if err != nil {
|
||||
|
@ -907,16 +943,15 @@ func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
|
||||
func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
vars := mux.Vars(r)
|
||||
collAlias := vars["alias"]
|
||||
isWeb := r.FormValue("web") == "1"
|
||||
|
||||
var u *User
|
||||
u := &User{}
|
||||
if reqJSON && !isWeb {
|
||||
// Ensure an access token was given
|
||||
accessToken := r.Header.Get("Authorization")
|
||||
u = &User{}
|
||||
u.ID = app.db.GetUserID(accessToken)
|
||||
if u.ID == -1 {
|
||||
return ErrBadAccessToken
|
||||
|
@ -928,6 +963,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||
if err != nil {
|
||||
log.Error("existing collection: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
if r.Method == "DELETE" {
|
||||
err := app.db.DeleteCollection(collAlias, u.ID)
|
||||
if err != nil {
|
||||
|
@ -940,7 +985,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
|
||||
c := SubmittedCollection{OwnerID: uint64(u.ID)}
|
||||
var err error
|
||||
|
||||
if reqJSON {
|
||||
// Decode JSON request
|
||||
|
|
|
@ -68,8 +68,13 @@ type (
|
|||
JSDisabled bool `ini:"disable_js"`
|
||||
WebFonts bool `ini:"webfonts"`
|
||||
Landing string `ini:"landing"`
|
||||
SimpleNav bool `ini:"simple_nav"`
|
||||
WFModesty bool `ini:"wf_modesty"`
|
||||
|
||||
// Site functionality
|
||||
Chorus bool `ini:"chorus"`
|
||||
DisableDrafts bool `ini:"disable_drafts"`
|
||||
|
||||
// Users
|
||||
SingleUser bool `ini:"single_user"`
|
||||
OpenRegistration bool `ini:"open_registration"`
|
||||
|
|
98
database.go
98
database.go
|
@ -65,8 +65,8 @@ type writestore interface {
|
|||
ChangeSettings(app *App, u *User, s *userSettings) error
|
||||
ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error
|
||||
|
||||
GetCollections(u *User) (*[]Collection, error)
|
||||
GetPublishableCollections(u *User) (*[]Collection, error)
|
||||
GetCollections(u *User, hostName string) (*[]Collection, error)
|
||||
GetPublishableCollections(u *User, hostName string) (*[]Collection, error)
|
||||
GetMeStats(u *User) userMeStats
|
||||
GetTotalCollections() (int64, error)
|
||||
GetTotalPosts() (int64, error)
|
||||
|
@ -94,7 +94,7 @@ type writestore interface {
|
|||
|
||||
UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error
|
||||
GetLastPinnedPostPos(collID int64) int64
|
||||
GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error)
|
||||
GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error)
|
||||
RemoveCollectionRedirect(t *sql.Tx, alias string) error
|
||||
GetCollectionRedirect(alias string) (new string)
|
||||
IsCollectionAttributeOn(id int64, attr string) bool
|
||||
|
@ -106,8 +106,8 @@ type writestore interface {
|
|||
ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error)
|
||||
|
||||
GetPostsCount(c *CollectionObj, includeFuture bool)
|
||||
GetPosts(c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error)
|
||||
GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error)
|
||||
GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error)
|
||||
GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error)
|
||||
|
||||
GetAPFollowers(c *Collection) (*[]RemoteUser, error)
|
||||
GetAPActorKeys(collectionID int64) ([]byte, []byte)
|
||||
|
@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
|
|||
func (db *datastore) GetUserByID(id int64) (*User, error) {
|
||||
u := &User{ID: id}
|
||||
|
||||
err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created)
|
||||
err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, ErrUserNotFound
|
||||
|
@ -308,6 +308,23 @@ 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) {
|
||||
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)
|
||||
case err != nil:
|
||||
log.Error("Couldn't SELECT user password: %v", err)
|
||||
return false, fmt.Errorf("is user suspended: %v", err)
|
||||
}
|
||||
|
||||
return u.IsSilenced(), nil
|
||||
}
|
||||
|
||||
// DoesUserNeedAuth returns true if the user hasn't provided any methods for
|
||||
// authenticating with the account, such a passphrase or email address.
|
||||
// Any errors are reported to admin and silently quashed, returning false as the
|
||||
|
@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) {
|
|||
func (db *datastore) GetUserForAuth(username string) (*User, error) {
|
||||
u := &User{Username: username}
|
||||
|
||||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
|
||||
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
// Check if they've entered the wrong, unnormalized username
|
||||
|
@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) {
|
|||
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
|
||||
u := &User{ID: userID}
|
||||
|
||||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
|
||||
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, ErrUserNotFound
|
||||
|
@ -1070,7 +1087,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) {
|
|||
// It will return future posts if `includeFuture` is true.
|
||||
// It will include only standard (non-pinned) posts unless `includePinned` is true.
|
||||
// TODO: change includeFuture to isOwner, since that's how it's used
|
||||
func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) {
|
||||
func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) {
|
||||
collID := c.ID
|
||||
|
||||
cf := c.NewFormat()
|
||||
|
@ -1115,7 +1132,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.formatContent(c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1131,7 +1148,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen
|
|||
// given tag.
|
||||
// It will return future posts if `includeFuture` is true.
|
||||
// TODO: change includeFuture to isOwner, since that's how it's used
|
||||
func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) {
|
||||
func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) {
|
||||
collID := c.ID
|
||||
|
||||
cf := c.NewFormat()
|
||||
|
@ -1179,7 +1196,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.formatContent(c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1533,9 +1550,13 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 {
|
|||
return lastPos.Int64
|
||||
}
|
||||
|
||||
func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) {
|
||||
func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) {
|
||||
// FIXME: sqlite-backed instances don't include ellipsis on truncated titles
|
||||
rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
|
||||
timeCondition := ""
|
||||
if !includeFuture {
|
||||
timeCondition = "AND created <= " + db.now()
|
||||
}
|
||||
rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting pinned posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."}
|
||||
|
@ -1559,7 +1580,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error)
|
|||
return &posts, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetCollections(u *User) (*[]Collection, error) {
|
||||
func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) {
|
||||
rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from collections: %v", err)
|
||||
|
@ -1575,6 +1596,7 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) {
|
|||
log.Error("Failed scanning row: %v", err)
|
||||
break
|
||||
}
|
||||
c.hostName = hostName
|
||||
c.URL = c.CanonicalURL()
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
|
@ -1588,8 +1610,8 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) {
|
|||
return &colls, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetPublishableCollections(u *User) (*[]Collection, error) {
|
||||
c, err := db.GetCollections(u)
|
||||
func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) {
|
||||
c, err := db.GetCollections(u, hostName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1624,7 +1646,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
|
|||
}
|
||||
|
||||
func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
||||
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM collections c
|
||||
LEFT JOIN users u ON u.id = c.owner_id
|
||||
WHERE u.status = 0`).Scan(&collCount)
|
||||
if err != nil {
|
||||
log.Error("Unable to fetch collections count: %v", err)
|
||||
}
|
||||
|
@ -1632,7 +1658,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
|||
}
|
||||
|
||||
func (db *datastore) GetTotalPosts() (postCount int64, err error) {
|
||||
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM posts p
|
||||
LEFT JOIN users u ON u.id = p.owner_id
|
||||
WHERE u.status = 0`).Scan(&postCount)
|
||||
if err != nil {
|
||||
log.Error("Unable to fetch posts count: %v", err)
|
||||
}
|
||||
|
@ -2252,6 +2282,19 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) {
|
|||
return &i, nil
|
||||
}
|
||||
|
||||
// IsUsersInvite returns true if the user with ID created the invite with code
|
||||
// and an error other than sql no rows, if any. Will return false in the event
|
||||
// of an error.
|
||||
func (db *datastore) IsUsersInvite(code string, userID int64) (bool, error) {
|
||||
var id string
|
||||
err := db.QueryRow("SELECT id FROM userinvites WHERE id = ? AND owner_id = ?", code, userID).Scan(&id)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("Failed selecting invite: %v", err)
|
||||
return false, err
|
||||
}
|
||||
return id != "", nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetUsersInvitedCount(id string) int64 {
|
||||
var count int64
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM usersinvited WHERE invite_id = ?", id).Scan(&count)
|
||||
|
@ -2341,17 +2384,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
|
|||
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage)
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr)
|
||||
rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."}
|
||||
log.Error("Failed selecting from users: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := []User{}
|
||||
for rows.Next() {
|
||||
u := User{}
|
||||
err = rows.Scan(&u.ID, &u.Username, &u.Created)
|
||||
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status)
|
||||
if err != nil {
|
||||
log.Error("Failed scanning GetAllUsers() row: %v", err)
|
||||
break
|
||||
|
@ -2388,6 +2431,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
|
|||
return &t, nil
|
||||
}
|
||||
|
||||
// SetUserStatus changes a user's status in the database. see Users.UserStatus
|
||||
func (db *datastore) SetUserStatus(id int64, status UserStatus) error {
|
||||
_, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user status: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
|
||||
var t time.Time
|
||||
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"github.com/writeas/impart"
|
||||
"net/http"
|
||||
|
||||
"github.com/writeas/impart"
|
||||
)
|
||||
|
||||
// Commonly returned HTTP errors
|
||||
|
@ -46,6 +47,8 @@ var (
|
|||
|
||||
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
|
||||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||
|
||||
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||
)
|
||||
|
||||
// Post operation errors
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"github.com/writeas/web-core/log"
|
||||
)
|
||||
|
||||
func exportPostsCSV(u *User, posts *[]PublicPost) []byte {
|
||||
func exportPostsCSV(hostName string, u *User, posts *[]PublicPost) []byte {
|
||||
var b bytes.Buffer
|
||||
|
||||
r := [][]string{
|
||||
|
@ -30,8 +30,9 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte {
|
|||
var blog string
|
||||
if p.Collection != nil {
|
||||
blog = p.Collection.Alias
|
||||
p.Collection.hostName = hostName
|
||||
}
|
||||
f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)}
|
||||
f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(hostName), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)}
|
||||
r = append(r, f)
|
||||
}
|
||||
|
||||
|
@ -104,7 +105,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
|
|||
User: u,
|
||||
}
|
||||
|
||||
colls, err := app.db.GetCollections(u)
|
||||
colls, err := app.db.GetCollections(u, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
|
@ -118,7 +119,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
|
|||
var collObjs []CollectionObj
|
||||
for _, c := range *colls {
|
||||
co := &CollectionObj{Collection: c}
|
||||
co.Posts, err = app.db.GetPosts(&c, 0, true, false, true)
|
||||
co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true)
|
||||
if err != nil {
|
||||
log.Error("unable to get collection posts: %v", err)
|
||||
}
|
||||
|
|
20
feed.go
20
feed.go
|
@ -12,12 +12,13 @@ package writefreely
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/gorilla/feeds"
|
||||
"github.com/gorilla/mux"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
"github.com/writeas/web-core/log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
||||
|
@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("view feed: get user: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if c.IsPrivate() || c.IsProtected() {
|
||||
|
@ -55,9 +65,9 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
|
||||
tag := mux.Vars(req)["tag"]
|
||||
if tag != "" {
|
||||
coll.Posts, _ = app.db.GetPostsTagged(c, tag, 1, false)
|
||||
coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false)
|
||||
} else {
|
||||
coll.Posts, _ = app.db.GetPosts(c, 1, false, true, false)
|
||||
coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false)
|
||||
}
|
||||
|
||||
author := ""
|
||||
|
@ -94,7 +104,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
Title: title,
|
||||
Link: &Link{Href: permalink},
|
||||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
|
||||
Content: applyMarkdown([]byte(p.Content), ""),
|
||||
Content: applyMarkdown([]byte(p.Content), "", app.cfg),
|
||||
Author: &Author{author, ""},
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
|
|
21
go.mod
21
go.mod
|
@ -2,25 +2,14 @@ module github.com/writeas/writefreely
|
|||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f // indirect
|
||||
github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 // indirect
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 // indirect
|
||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 // indirect
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 // indirect
|
||||
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 // indirect
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/go-test/deep v1.0.1 // indirect
|
||||
github.com/gogits/gogs v0.11.86
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 // indirect
|
||||
github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 // indirect
|
||||
github.com/gogs/gogs v0.11.86 // indirect
|
||||
github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a // indirect
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/gorilla/feeds v1.1.0
|
||||
|
@ -31,15 +20,12 @@ require (
|
|||
github.com/hashicorp/go-multierror v1.0.0
|
||||
github.com/ikeikeikeike/go-sitemap-generator v1.0.1
|
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/manifoldco/promptui v0.3.2
|
||||
github.com/mattn/go-colorable v0.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.10.0
|
||||
github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.2
|
||||
github.com/mitchellh/go-wordwrap v1.0.0
|
||||
github.com/nicksnyder/go-i18n v1.10.0 // indirect
|
||||
|
@ -47,7 +33,6 @@ require (
|
|||
github.com/pelletier/go-toml v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
|
@ -63,7 +48,7 @@ require (
|
|||
github.com/writeas/openssl-go v1.0.0 // indirect
|
||||
github.com/writeas/saturday v1.7.1
|
||||
github.com/writeas/slug v1.2.0
|
||||
github.com/writeas/web-core v1.0.0
|
||||
github.com/writeas/web-core v1.2.0
|
||||
github.com/writefreely/go-nodeinfo v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||
|
@ -72,11 +57,7 @@ require (
|
|||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
|
||||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect
|
||||
gopkg.in/clog.v1 v1.2.0 // indirect
|
||||
gopkg.in/ini.v1 v1.41.0
|
||||
gopkg.in/macaron.v1 v1.3.2 // indirect
|
||||
gopkg.in/redis.v2 v2.3.2 // indirect
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
||||
|
|
42
go.sum
42
go.sum
|
@ -2,10 +2,6 @@ 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/Unknwon/com v0.0.0-20181010210213-41959bdd855f h1:m1tYqjD/N0vF/S8s/ZKz/eccUr8RAAcrOK2MhXeTegA=
|
||||
github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no=
|
||||
github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 h1:Mp8GNJ/tdTZIEdLdZfykEJaL3mTyEYrSzYNcdoQKpJk=
|
||||
github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966/go.mod h1:SFtfq0zFPsENI7DpE87QM2hcYu5QQ0fRdCgP+P1Hrqo=
|
||||
github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU=
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
|
||||
|
@ -13,8 +9,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 h1:jWNY1NDg6a/c8RSXkai7IX6UOhir0LD39I4Dukg+4Ks=
|
||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
|
@ -38,26 +32,10 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||
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/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 h1:UYIHS1r0WotqB5cIa0PAiV0m6GzD9rDBcn4alp5JgCw=
|
||||
github.com/go-macaron/cache v0.0.0-20151013081102-561735312776/go.mod h1:hHAsZm/oBZVcY+S7qdQL6Vbg5VrXF6RuKGuqsszt3Ok=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
|
||||
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 h1:z/nqwd+ql/r6Q3QGnwNd6B89UjPytM0be5pDQV9TuWw=
|
||||
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A=
|
||||
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=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogits/gogs v0.11.86 h1:IujCpA+F/mYDXTcqdy593rl2donWakAWoL2HYZn7spw=
|
||||
github.com/gogits/gogs v0.11.86/go.mod h1:H8FMbPPb+o/TgI6YnmQmT8nmEIHypXDau+f2CChYoCk=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 h1:UdOSIHZpkYcajRbfebBYzFDsL3SuqObH3bvKYBqgKmI=
|
||||
github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w=
|
||||
github.com/gogs/gogs v0.11.86 h1:D+dXuY/6XjJ2t74W/dxo7ogx5+xW05Va8sJiQSS4WXA=
|
||||
github.com/gogs/gogs v0.11.86/go.mod h1:qlbvdn16XTC6q7eR+thjW+OLdN+mi2PBZ8KqVT39T88=
|
||||
github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a h1:8DZwxETOVWIinYxDK+i6L+rMb7eGATGaakD6ZucfHVk=
|
||||
github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU=
|
||||
github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
|
@ -90,10 +68,6 @@ github.com/ikeikeikeike/go-sitemap-generator v1.0.1 h1:49Fn8gro/B12vCY8pf5/+/Jpr
|
|||
github.com/ikeikeikeike/go-sitemap-generator v1.0.1/go.mod h1:QI+zWsz6yQyxkG9LWNcnu0f7aiAE5tPdsZOsICgmd1c=
|
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM=
|
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts=
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
|
@ -119,8 +93,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs
|
|||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa h1:XvNrttGMJfVrUqblGju4IkjYXwx6l5OAAyjaIsydzsk=
|
||||
github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
||||
|
@ -137,8 +109,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
|
@ -185,8 +155,8 @@ github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
|||
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
||||
github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0=
|
||||
github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE=
|
||||
github.com/writefreely/go-nodeinfo v1.1.0 h1:dp/ieEu0/gTeNKFvJTYhzBBouyFn7aiWtWzkb8J1JLg=
|
||||
github.com/writefreely/go-nodeinfo v1.1.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
||||
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
||||
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
|
||||
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||
|
@ -213,19 +183,11 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50
|
|||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y=
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=
|
||||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU=
|
||||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/clog.v1 v1.2.0 h1:BHfwHRNQy497iBNsRBassPixSAxRbn2z5KVkdBFbwxc=
|
||||
gopkg.in/clog.v1 v1.2.0/go.mod h1:L6fgdpdhFgKX4eGuDvt+N6X2GwZE160NRrIHzvaF8ZM=
|
||||
gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE=
|
||||
gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/macaron.v1 v1.3.2 h1:AvWIaPmwBUA87/OWzePkoxeaw6YJWDfBt1pDFPBnLf8=
|
||||
gopkg.in/macaron.v1 v1.3.2/go.mod h1:PrsiawTWAGZs6wFbT5hlr7SQ2Ns9h7cUVtcUu4lQOVo=
|
||||
gopkg.in/redis.v2 v2.3.2 h1:GPVIIB/JnL1wvfULefy3qXmPu1nfNu2d0yA09FHgwfs=
|
||||
gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||
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=
|
||||
|
|
|
@ -772,7 +772,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
|
|||
return
|
||||
}
|
||||
|
||||
if IsJSON(r.Header.Get("Content-Type")) {
|
||||
if IsJSON(r) {
|
||||
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
|
||||
return
|
||||
}
|
||||
|
|
51
invites.go
51
invites.go
|
@ -12,15 +12,16 @@ package writefreely
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Invite struct {
|
||||
|
@ -77,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
|
|||
muVal := r.FormValue("uses")
|
||||
expVal := r.FormValue("expires")
|
||||
|
||||
if u.IsSilenced() {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
var err error
|
||||
var maxUses int
|
||||
if muVal != "0" {
|
||||
|
@ -114,6 +119,36 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
return err
|
||||
}
|
||||
|
||||
expired := i.Expired()
|
||||
if !expired && i.MaxUses.Valid && i.MaxUses.Int64 > 0 {
|
||||
// Invite has a max-use number, so check if we're past that limit
|
||||
i.uses = app.db.GetUsersInvitedCount(inviteCode)
|
||||
expired = i.uses >= i.MaxUses.Int64
|
||||
}
|
||||
|
||||
if u := getUserSession(app, r); u != nil {
|
||||
// check if invite belongs to another user
|
||||
// error can be ignored as not important in this case
|
||||
if ownInvite, _ := app.db.IsUsersInvite(inviteCode, u.ID); !ownInvite {
|
||||
addSessionFlash(app, w, r, "You're already registered and logged in.", nil)
|
||||
// show homepage
|
||||
return impart.HTTPError{http.StatusFound, "/me/settings"}
|
||||
}
|
||||
|
||||
// show invite instructions
|
||||
p := struct {
|
||||
*UserPage
|
||||
Invite *Invite
|
||||
Expired bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Invite to "+app.cfg.App.SiteName, nil),
|
||||
Invite: i,
|
||||
Expired: expired,
|
||||
}
|
||||
showUserPage(w, "invite-help", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
Error string
|
||||
|
@ -124,16 +159,10 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
Invite: inviteCode,
|
||||
}
|
||||
|
||||
if i.Expired() {
|
||||
if expired {
|
||||
p.Error = "This invite link has expired."
|
||||
}
|
||||
|
||||
if i.MaxUses.Valid && i.MaxUses.Int64 > 0 {
|
||||
if c := app.db.GetUsersInvitedCount(inviteCode); c >= i.MaxUses.Int64 {
|
||||
p.Error = "This invite link has expired."
|
||||
}
|
||||
}
|
||||
|
||||
// Get error messages
|
||||
session, err := app.sessionStore.Get(r, cookieName)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
ifeq ($(shell which lessc),/usr/bin/lessc)
|
||||
LESSC=/usr/bin/lessc
|
||||
else ifeq ($(shell which lessc),/usr/local/bin/lessc)
|
||||
LESSC=/usr/local/bin/lessc
|
||||
else ifeq ($(shell which lessc),/bin/lessc)
|
||||
LESSC=/bin/lessc
|
||||
else
|
||||
LESSC=node_modules/.bin/lessc
|
||||
endif
|
||||
export LESSC
|
||||
|
||||
CSSDIR=../static/css/
|
||||
|
||||
all :
|
||||
$(LESSC) app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css
|
||||
$(LESSC) fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css
|
||||
$(LESSC) icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css
|
||||
@command -v lessc >/dev/null 2>&1 || { echo >&2 "lessc is not installed, please run: make install or: less/install-less.sh"; exit 1; }
|
||||
lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css
|
||||
lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css
|
||||
lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css
|
||||
|
||||
install :
|
||||
./install-less.sh
|
||||
|
|
|
@ -405,6 +405,31 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
nav#full-nav {
|
||||
margin: 0;
|
||||
|
||||
.left-side {
|
||||
display: inline-block;
|
||||
|
||||
a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
nav#full-nav a.simple-btn, .tool button {
|
||||
font-family: @sansFont;
|
||||
border: 1px solid #ccc !important;
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
.rounded(.25em);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
a {
|
||||
&:link {
|
||||
|
@ -491,10 +516,17 @@ abbr {
|
|||
body#collection article p, body#subpage article p {
|
||||
.article-p;
|
||||
}
|
||||
pre, body#post article, body#collection article, body#subpage article, body#subpage #wrapper h1 {
|
||||
pre, body#post article, #post .alert, #subpage .alert, body#collection article, body#subpage article, body#subpage #wrapper h1 {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#collection header .alert, #post .alert, #subpage .alert {
|
||||
margin-bottom: 1em;
|
||||
p {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article p {
|
||||
&.norm, &.sans, &.wrap {
|
||||
line-height: 1.4em;
|
||||
|
|
|
@ -63,7 +63,7 @@ body#pad, body#pad-sub {
|
|||
}
|
||||
}
|
||||
#belt {
|
||||
a {
|
||||
a, button {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ body#pad, body#pad-sub {
|
|||
}
|
||||
}
|
||||
#belt {
|
||||
a {
|
||||
a, button {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,6 +222,13 @@ body#pad, body#pad-sub {
|
|||
font-style: italic;
|
||||
}
|
||||
}
|
||||
button {
|
||||
font-family: @sansFont;
|
||||
background-color: transparent;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ package migrations
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/writeas/web-core/log"
|
||||
)
|
||||
|
||||
|
@ -57,6 +58,7 @@ 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)
|
||||
}
|
||||
|
||||
// CurrentVer returns the current migration version the application is on
|
||||
|
|
|
@ -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 supportUserStatus(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.Commit()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
33
pad.go
33
pad.go
|
@ -11,12 +11,13 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
|
@ -34,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
appData := &struct {
|
||||
page.StaticPage
|
||||
Post *RawPost
|
||||
User *User
|
||||
Blogs *[]Collection
|
||||
Post *RawPost
|
||||
User *User
|
||||
Blogs *[]Collection
|
||||
Suspended bool
|
||||
|
||||
Editing bool // True if we're modifying an existing post
|
||||
EditCollection *Collection // Collection of the post we're editing, if any
|
||||
|
@ -47,23 +49,26 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
var err error
|
||||
if appData.User != nil {
|
||||
appData.Blogs, err = app.db.GetPublishableCollections(appData.User)
|
||||
appData.Blogs, err = app.db.GetPublishableCollections(appData.User, app.cfg.App.Host)
|
||||
if err != nil {
|
||||
log.Error("Unable to get user's blogs for Pad: %v", err)
|
||||
}
|
||||
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get users suspension status for Pad: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
padTmpl := app.cfg.App.Editor
|
||||
if padTmpl == "" {
|
||||
if templates[padTmpl] == nil {
|
||||
if padTmpl != "" {
|
||||
log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl)
|
||||
}
|
||||
padTmpl = "pad"
|
||||
}
|
||||
|
||||
if action == "" && slug == "" {
|
||||
// Not editing any post; simply render the Pad
|
||||
if templates[padTmpl] == nil {
|
||||
log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl)
|
||||
padTmpl = "pad"
|
||||
}
|
||||
if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil {
|
||||
log.Error("Unable to execute template: %v", err)
|
||||
}
|
||||
|
@ -121,12 +126,18 @@ 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
|
||||
}{
|
||||
StaticPage: pageForReq(app, r),
|
||||
Post: &RawPost{Font: "norm"},
|
||||
User: getUserSession(app, r),
|
||||
}
|
||||
var err error
|
||||
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
|
||||
if err != nil {
|
||||
log.Error("view meta: get user suspended status: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
if action == "" && slug == "" {
|
||||
return ErrPostNotFound
|
||||
|
|
|
@ -28,6 +28,8 @@ type StaticPage struct {
|
|||
Values map[string]string
|
||||
Flashes []string
|
||||
CanViewReader bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
}
|
||||
|
||||
// SanitizeHost alters the StaticPage to contain a real hostname. This is
|
||||
|
|
27
pages.go
27
pages.go
|
@ -135,3 +135,30 @@ WriteFreely can communicate with other federated platforms like Mastodon, so peo
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getReaderSection(app *App) (*instanceContent, error) {
|
||||
c, err := app.db.GetDynamicContent("reader")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c == nil {
|
||||
c = &instanceContent{
|
||||
ID: "reader",
|
||||
Type: "section",
|
||||
Content: defaultReaderBanner(app.cfg),
|
||||
Updated: defaultPageUpdatedTime,
|
||||
}
|
||||
}
|
||||
if !c.Title.Valid {
|
||||
c.Title = defaultReaderTitle(app.cfg)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func defaultReaderTitle(cfg *config.Config) sql.NullString {
|
||||
return sql.NullString{String: "Reader", Valid: true}
|
||||
}
|
||||
|
||||
func defaultReaderBanner(cfg *config.Config) string {
|
||||
return "Read the latest posts from " + cfg.App.SiteName + "."
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
</ul>{{end}}
|
||||
|
||||
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
|
||||
<input type="text" name="alias" placeholder="Username" value="{{.Username}}" {{if not .Username}}autofocus{{end}} /><br />
|
||||
<input type="password" name="pass" placeholder="Password" {{if .Username}}autofocus{{end}} /><br />
|
||||
<input type="text" name="alias" placeholder="Username" value="{{.LoginUsername}}" {{if not .LoginUsername}}autofocus{{end}} /><br />
|
||||
<input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br />
|
||||
{{if .To}}<input type="hidden" name="to" value="{{.To}}" />{{end}}
|
||||
<input type="submit" id="btn-login" value="Login" />
|
||||
</form>
|
||||
|
|
|
@ -12,17 +12,19 @@ package writefreely
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
"github.com/writeas/saturday"
|
||||
"github.com/writeas/web-core/stringmanip"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
"html"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
blackfriday "github.com/writeas/saturday"
|
||||
"github.com/writeas/web-core/stringmanip"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -34,27 +36,28 @@ var (
|
|||
markeddownReg = regexp.MustCompile("<p>(.+)</p>")
|
||||
)
|
||||
|
||||
func (p *Post) formatContent(c *Collection, isOwner bool) {
|
||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
||||
baseURL := c.CanonicalURL()
|
||||
// TODO: redundant
|
||||
if !isSingleUser {
|
||||
baseURL = "/" + c.Alias + "/"
|
||||
}
|
||||
p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String)))
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL))
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg))
|
||||
if exc := strings.Index(string(p.Content), "<!--more-->"); exc > -1 {
|
||||
p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL))
|
||||
p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PublicPost) formatContent(isOwner bool) {
|
||||
p.Post.formatContent(&p.Collection.Collection, isOwner)
|
||||
func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) {
|
||||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner)
|
||||
}
|
||||
|
||||
func applyMarkdown(data []byte, baseURL string) string {
|
||||
return applyMarkdownSpecial(data, false, baseURL)
|
||||
func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
|
||||
return applyMarkdownSpecial(data, false, baseURL, cfg)
|
||||
}
|
||||
|
||||
func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string {
|
||||
func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string {
|
||||
mdExtensions := 0 |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
blackfriday.EXTENSION_FENCED_CODE |
|
||||
|
@ -74,7 +77,11 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string
|
|||
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
|
||||
if baseURL != "" {
|
||||
// Replace special text generated by Markdown parser
|
||||
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+baseURL+"tag:$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>")))
|
||||
tagPrefix := baseURL + "tag:"
|
||||
if cfg.App.Chorus {
|
||||
tagPrefix = "/read/t/"
|
||||
}
|
||||
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>")))
|
||||
}
|
||||
// Strip out bad HTML
|
||||
policy := getSanitizationPolicy()
|
||||
|
@ -163,6 +170,7 @@ func getSanitizationPolicy() *bluemonday.Policy {
|
|||
policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video")
|
||||
policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio")
|
||||
policy.AllowAttrs("target").OnElements("a")
|
||||
policy.AllowAttrs("title").OnElements("abbr")
|
||||
policy.AllowAttrs("style", "class", "id").Globally()
|
||||
policy.AllowURLSchemes("http", "https", "mailto", "xmpp")
|
||||
return policy
|
||||
|
|
123
posts.go
123
posts.go
|
@ -35,6 +35,7 @@ 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"
|
||||
)
|
||||
|
@ -376,10 +377,16 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
Direction: d,
|
||||
}
|
||||
if !isRaw {
|
||||
post.HTMLContent = template.HTML(applyMarkdown([]byte(content), ""))
|
||||
post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "", app.cfg))
|
||||
}
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(ownerID.Int64)
|
||||
if err != nil {
|
||||
log.Error("view post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
// Check if post has been unpublished
|
||||
if content == "" {
|
||||
gone = true
|
||||
|
@ -427,9 +434,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
page := struct {
|
||||
*AnonymousPost
|
||||
page.StaticPage
|
||||
Username string
|
||||
IsOwner bool
|
||||
SiteURL string
|
||||
Username string
|
||||
IsOwner bool
|
||||
SiteURL string
|
||||
Suspended bool
|
||||
}{
|
||||
AnonymousPost: post,
|
||||
StaticPage: pageForReq(app, r),
|
||||
|
@ -440,6 +448,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
|
||||
}
|
||||
|
||||
if !page.IsOwner && suspended {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
page.Suspended = suspended
|
||||
err = templates["post"].ExecuteTemplate(w, "post", page)
|
||||
if err != nil {
|
||||
log.Error("Post template execute error: %v", err)
|
||||
|
@ -471,7 +483,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
// /posts?collection={alias}
|
||||
// ? /collections/{alias}/posts
|
||||
func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
vars := mux.Vars(r)
|
||||
collAlias := vars["alias"]
|
||||
if collAlias == "" {
|
||||
|
@ -496,6 +508,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
} else {
|
||||
userID = app.db.GetUserID(accessToken)
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(userID)
|
||||
if err != nil {
|
||||
log.Error("new post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
if userID == -1 {
|
||||
return ErrNotLoggedIn
|
||||
}
|
||||
|
@ -508,7 +529,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
var p *SubmittedPost
|
||||
if reqJSON {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&p)
|
||||
err = decoder.Decode(&p)
|
||||
if err != nil {
|
||||
log.Error("Couldn't parse new post JSON request: %v\n", err)
|
||||
return ErrBadJSON
|
||||
|
@ -554,7 +575,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
var newPost *PublicPost = &PublicPost{}
|
||||
var coll *Collection
|
||||
var err error
|
||||
if accessToken != "" {
|
||||
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
|
||||
} else {
|
||||
|
@ -597,7 +617,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
vars := mux.Vars(r)
|
||||
postID := vars["post"]
|
||||
|
||||
|
@ -662,6 +682,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(userID)
|
||||
if err != nil {
|
||||
log.Error("existing post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
// Modify post struct
|
||||
p.ID = postID
|
||||
|
||||
|
@ -856,11 +885,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
ownerID = u.ID
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(ownerID)
|
||||
if err != nil {
|
||||
log.Error("add post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
// Parse claimed posts in format:
|
||||
// [{"id": "...", "token": "..."}]
|
||||
var claims *[]ClaimPostRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&claims)
|
||||
err = decoder.Decode(&claims)
|
||||
if err != nil {
|
||||
return ErrBadJSONArray
|
||||
}
|
||||
|
@ -950,13 +988,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
userID = u.ID
|
||||
}
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(userID)
|
||||
if err != nil {
|
||||
log.Error("pin post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
if suspended {
|
||||
return ErrUserSuspended
|
||||
}
|
||||
|
||||
// Parse request
|
||||
var posts []struct {
|
||||
ID string `json:"id"`
|
||||
Position int64 `json:"position"`
|
||||
}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&posts)
|
||||
err = decoder.Decode(&posts)
|
||||
if err != nil {
|
||||
return ErrBadJSONArray
|
||||
}
|
||||
|
@ -992,6 +1039,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var collID int64
|
||||
var ownerID int64
|
||||
var coll *Collection
|
||||
var err error
|
||||
vars := mux.Vars(r)
|
||||
|
@ -1007,12 +1055,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
return err
|
||||
}
|
||||
collID = coll.ID
|
||||
ownerID = coll.OwnerID
|
||||
}
|
||||
|
||||
p, err := app.db.GetPost(vars["post"], collID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suspended, err := app.db.IsUserSuspended(ownerID)
|
||||
if err != nil {
|
||||
log.Error("fetch post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
if suspended {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
|
||||
p.extractData()
|
||||
|
||||
|
@ -1032,7 +1090,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
p.Collection = &CollectionObj{Collection: *coll}
|
||||
po := p.ActivityObject()
|
||||
po := p.ActivityObject(app.cfg)
|
||||
po.Context = []interface{}{activitystreams.Namespace}
|
||||
return impart.RenderActivityJSON(w, po, http.StatusOK)
|
||||
}
|
||||
|
@ -1060,25 +1118,25 @@ func (p *Post) processPost() PublicPost {
|
|||
return *res
|
||||
}
|
||||
|
||||
func (p *PublicPost) CanonicalURL() string {
|
||||
func (p *PublicPost) CanonicalURL(hostName string) string {
|
||||
if p.Collection == nil || p.Collection.Alias == "" {
|
||||
return p.Collection.hostName + "/" + p.ID
|
||||
return hostName + "/" + p.ID
|
||||
}
|
||||
return p.Collection.CanonicalURL() + p.Slug.String
|
||||
}
|
||||
|
||||
func (p *PublicPost) ActivityObject() *activitystreams.Object {
|
||||
func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object {
|
||||
o := activitystreams.NewArticleObject()
|
||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||
o.Published = p.Created
|
||||
o.URL = p.CanonicalURL()
|
||||
o.URL = p.CanonicalURL(cfg.App.Host)
|
||||
o.AttributedTo = p.Collection.FederatedAccount()
|
||||
o.CC = []string{
|
||||
p.Collection.FederatedAccount() + "/followers",
|
||||
}
|
||||
o.Name = p.DisplayTitle()
|
||||
if p.HTMLContent == template.HTML("") {
|
||||
p.formatContent(false)
|
||||
p.formatContent(cfg, false)
|
||||
}
|
||||
o.Content = string(p.HTMLContent)
|
||||
if p.Language.Valid {
|
||||
|
@ -1093,7 +1151,11 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object {
|
|||
if isSingleUser {
|
||||
tagBaseURL = p.Collection.CanonicalURL() + "tag:"
|
||||
} else {
|
||||
tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias)
|
||||
if cfg.App.Chorus {
|
||||
tagBaseURL = fmt.Sprintf("%s/read/t/", p.Collection.hostName)
|
||||
} else {
|
||||
tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias)
|
||||
}
|
||||
}
|
||||
for _, t := range p.Tags {
|
||||
o.Tag = append(o.Tag, activitystreams.Tag{
|
||||
|
@ -1270,6 +1332,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("view collection post: %v", err)
|
||||
return ErrInternalGeneral
|
||||
}
|
||||
|
||||
// Check collection permissions
|
||||
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
|
||||
return ErrPostNotFound
|
||||
|
@ -1326,6 +1394,9 @@ Are you sure it was ever here?`,
|
|||
p.Collection = coll
|
||||
p.IsTopLevel = app.cfg.App.SingleUser
|
||||
|
||||
if !p.IsOwner && suspended {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
// Check if post has been unpublished
|
||||
if p.Content == "" && p.Title.String == "" {
|
||||
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
|
||||
|
@ -1357,14 +1428,14 @@ Are you sure it was ever here?`,
|
|||
return ErrCollectionPageNotFound
|
||||
}
|
||||
p.extractData()
|
||||
ap := p.ActivityObject()
|
||||
ap := p.ActivityObject(app.cfg)
|
||||
ap.Context = []interface{}{activitystreams.Namespace}
|
||||
return impart.RenderActivityJSON(w, ap, http.StatusOK)
|
||||
} else {
|
||||
p.extractData()
|
||||
p.Content = strings.Replace(p.Content, "<!--more-->", "", 1)
|
||||
// TODO: move this to function
|
||||
p.formatContent(cr.isCollOwner)
|
||||
p.formatContent(app.cfg, cr.isCollOwner)
|
||||
tp := struct {
|
||||
*PublicPost
|
||||
page.StaticPage
|
||||
|
@ -1373,20 +1444,30 @@ Are you sure it was ever here?`,
|
|||
IsCustomDomain bool
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
Suspended bool
|
||||
}{
|
||||
PublicPost: p,
|
||||
StaticPage: pageForReq(app, r),
|
||||
IsOwner: cr.isCollOwner,
|
||||
IsCustomDomain: cr.isCustomDomain,
|
||||
IsFound: postFound,
|
||||
Suspended: suspended,
|
||||
}
|
||||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll)
|
||||
tp.IsAdmin = u != nil && u.IsAdmin()
|
||||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
||||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner)
|
||||
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
|
||||
|
||||
if !postFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil {
|
||||
postTmpl := "collection-post"
|
||||
if app.cfg.App.Chorus {
|
||||
postTmpl = "chorus-collection-post"
|
||||
}
|
||||
if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil {
|
||||
log.Error("Error in collection-post template: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
49
read.go
49
read.go
|
@ -13,6 +13,12 @@ package writefreely
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
. "github.com/gorilla/feeds"
|
||||
"github.com/gorilla/mux"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
|
@ -20,11 +26,6 @@ import (
|
|||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/memo"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -47,6 +48,13 @@ type readPublication struct {
|
|||
Posts *[]PublicPost
|
||||
CurrentPage int
|
||||
TotalPages int
|
||||
SelTopic string
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
|
||||
// Customizable page content
|
||||
ContentTitle string
|
||||
Content template.HTML
|
||||
}
|
||||
|
||||
func initLocalTimeline(app *App) {
|
||||
|
@ -62,7 +70,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
|
|||
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
|
||||
FROM collections c
|
||||
LEFT JOIN posts p ON p.collection_id = c.id
|
||||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
|
||||
LEFT JOIN users u ON u.id = p.owner_id
|
||||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0
|
||||
ORDER BY p.created DESC`)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
|
@ -97,7 +106,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
|
|||
}
|
||||
|
||||
p.extractData()
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), ""))
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "", app.cfg))
|
||||
fp := p.processPost()
|
||||
if isCollectionPost {
|
||||
fp.Collection = &CollectionObj{Collection: *c}
|
||||
|
@ -197,13 +206,25 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in
|
|||
}
|
||||
|
||||
d := &readPublication{
|
||||
pageForReq(app, r),
|
||||
&posts,
|
||||
page,
|
||||
ttlPages,
|
||||
StaticPage: pageForReq(app, r),
|
||||
Posts: &posts,
|
||||
CurrentPage: page,
|
||||
TotalPages: ttlPages,
|
||||
SelTopic: tag,
|
||||
}
|
||||
if app.cfg.App.Chorus {
|
||||
u := getUserSession(app, r)
|
||||
d.IsAdmin = u != nil && u.IsAdmin()
|
||||
d.CanInvite = canUserInvite(app.cfg, d.IsAdmin)
|
||||
}
|
||||
c, err := getReaderSection(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ContentTitle = c.Title.String
|
||||
d.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg))
|
||||
|
||||
err := templates["read"].ExecuteTemplate(w, "base", d)
|
||||
err = templates["read"].ExecuteTemplate(w, "base", d)
|
||||
if err != nil {
|
||||
log.Error("Unable to render reader: %v", err)
|
||||
fmt.Fprintf(w, ":(")
|
||||
|
@ -274,7 +295,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e
|
|||
}
|
||||
|
||||
title = p.PlainDisplayTitle()
|
||||
permalink = p.CanonicalURL()
|
||||
permalink = p.CanonicalURL(app.cfg.App.Host)
|
||||
if p.Collection != nil {
|
||||
author = p.Collection.Title
|
||||
} else {
|
||||
|
@ -286,7 +307,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e
|
|||
Title: title,
|
||||
Link: &Link{Href: permalink},
|
||||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
|
||||
Content: applyMarkdown([]byte(p.Content), ""),
|
||||
Content: applyMarkdown([]byte(p.Content), "", app.cfg),
|
||||
Author: &Author{author, ""},
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
|
|
12
request.go
12
request.go
|
@ -10,9 +10,13 @@
|
|||
|
||||
package writefreely
|
||||
|
||||
import "mime"
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func IsJSON(h string) bool {
|
||||
ct, _, _ := mime.ParseMediaType(h)
|
||||
return ct == "application/json"
|
||||
func IsJSON(r *http.Request) bool {
|
||||
ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
accept := r.Header.Get("Accept")
|
||||
return ct == "application/json" || accept == "application/json"
|
||||
}
|
||||
|
|
|
@ -146,6 +146,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
|
||||
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
|
||||
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
|
||||
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
|
||||
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
|
||||
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
|
||||
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
|
||||
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
|
||||
|
@ -153,7 +155,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
|
||||
// Handle special pages first
|
||||
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
|
||||
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET")
|
||||
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
|
||||
write.HandleFunc("/invite/{code}", 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())
|
||||
|
|
|
@ -66,7 +66,7 @@ func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
host = c.CanonicalURL()
|
||||
|
||||
sm := buildSitemap(host, pre)
|
||||
posts, err := app.db.GetPosts(c, 0, false, false, false)
|
||||
posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false)
|
||||
if err != nil {
|
||||
log.Error("Error getting posts: %v", err)
|
||||
return err
|
||||
|
|
19
templates.go
19
templates.go
|
@ -11,10 +11,6 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/writeas/web-core/l10n"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -22,6 +18,11 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/writeas/web-core/l10n"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -63,12 +64,16 @@ 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"),
|
||||
}
|
||||
if name == "collection" || name == "collection-tags" {
|
||||
if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
|
||||
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
|
||||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl"))
|
||||
}
|
||||
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" {
|
||||
if name == "chorus-collection" || name == "chorus-collection-post" {
|
||||
files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"))
|
||||
}
|
||||
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" {
|
||||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl"))
|
||||
}
|
||||
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
||||
|
@ -83,6 +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"),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -95,6 +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"),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
{{define "pad"}}<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="google" value="notranslate">
|
||||
</head>
|
||||
<body id="pad" class="light">
|
||||
|
||||
<div id="overlay"></div>
|
||||
|
||||
<textarea id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}}
|
||||
|
||||
{{end}}{{.Post.Content}}</textarea>
|
||||
|
||||
<header id="tools">
|
||||
<div id="clip">
|
||||
{{if not .SingleUser}}<h1>{{if .Chorus}}<a href="/" title="Home">{{else}}<a href="/me/c/" title="View blogs">{{end}}{{.SiteName}}</a></h1>{{end}}
|
||||
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
|
||||
<li>{{if .Blogs}}<a href="{{$c := index .Blogs 0}}{{$c.CanonicalURL}}">My Posts</a>{{else}}<a>Draft</a>{{end}}</li>
|
||||
</ul></nav>
|
||||
<span id="wc" class="hidden if-room room-4">0 words</span>
|
||||
</div>
|
||||
<noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript>
|
||||
<div id="belt">
|
||||
{{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}}
|
||||
<div class="tool"><button title="Publish your writing" id="publish" style="font-weight: bold">Post</button></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script>
|
||||
var $writer = H.getEl('writer');
|
||||
var $btnPublish = H.getEl('publish');
|
||||
var $wc = H.getEl("wc");
|
||||
var updateWordCount = function() {
|
||||
var words = 0;
|
||||
var val = $writer.el.value.trim();
|
||||
if (val != '') {
|
||||
words = $writer.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
|
||||
}
|
||||
$wc.el.innerText = words + " word" + (words != 1 ? "s" : "");
|
||||
};
|
||||
var setButtonStates = function() {
|
||||
if (!canPublish) {
|
||||
$btnPublish.el.className = 'disabled';
|
||||
return;
|
||||
}
|
||||
if ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.el.value == origDoc)) {
|
||||
$btnPublish.el.className = 'disabled';
|
||||
} else {
|
||||
$btnPublish.el.className = '';
|
||||
}
|
||||
};
|
||||
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
|
||||
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
|
||||
H.load($writer, draftDoc, true);
|
||||
updateWordCount();
|
||||
|
||||
var typingTimer;
|
||||
var doneTypingInterval = 200;
|
||||
|
||||
var posts;
|
||||
{{if and .Post.Id (not .Post.Slug)}}
|
||||
var token = null;
|
||||
var curPostIdx;
|
||||
posts = JSON.parse(H.get('posts', '[]'));
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == "{{.Post.Id}}") {
|
||||
token = posts[i].token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var canPublish = token != null;
|
||||
{{else}}var canPublish = true;{{end}}
|
||||
var publishing = false;
|
||||
var justPublished = false;
|
||||
|
||||
var publish = function(content, font) {
|
||||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
||||
if (!token) {
|
||||
alert("You don't have permission to update this post.");
|
||||
return;
|
||||
}
|
||||
{{end}}
|
||||
publishing = true;
|
||||
$btnPublish.el.textContent = 'Posting...';
|
||||
$btnPublish.el.disabled = true;
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
|
||||
lang = lang.substring(0, 2);
|
||||
var post = H.getTitleStrict(content);
|
||||
|
||||
var params = {
|
||||
body: post.content,
|
||||
title: post.title,
|
||||
font: font,
|
||||
lang: lang
|
||||
};
|
||||
{{ if .Post.Slug }}
|
||||
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
|
||||
{{ else if .Post.Id }}
|
||||
var url = "/api/posts/{{.Post.Id}}";
|
||||
if (typeof token === 'undefined' || !token) {
|
||||
token = "";
|
||||
}
|
||||
params.token = token;
|
||||
{{ else }}
|
||||
var url = "/api/posts";
|
||||
var postTarget = '{{if .Blogs}}{{$c := index .Blogs 0}}{{$c.Alias}}{{else}}anonymous{{end}}';
|
||||
if (postTarget != 'anonymous') {
|
||||
url = "/api/collections/" + postTarget + "/posts";
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
http.open("POST", url, true);
|
||||
|
||||
// Send the proper header information along with the request
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
publishing = false;
|
||||
if (http.status == 200 || http.status == 201) {
|
||||
data = JSON.parse(http.responseText);
|
||||
id = data.data.id;
|
||||
nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
|
||||
|
||||
{{ if not .Post.Id }}
|
||||
// Post created
|
||||
if (postTarget != 'anonymous') {
|
||||
nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug;
|
||||
}
|
||||
editToken = data.data.token;
|
||||
|
||||
{{ if not .User }}if (postTarget == 'anonymous') {
|
||||
// Save the data
|
||||
var posts = JSON.parse(H.get('posts', '[]'));
|
||||
|
||||
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == "{{.Post.Id}}") {
|
||||
posts[i].title = newPost.title;
|
||||
posts[i].summary = newPost.summary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
|
||||
|
||||
H.set('posts', JSON.stringify(posts));
|
||||
}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
justPublished = true;
|
||||
if (draftDoc != 'lastDoc') {
|
||||
H.remove(draftDoc);
|
||||
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
|
||||
} else {
|
||||
H.set(draftDoc, '');
|
||||
}
|
||||
|
||||
{{if .EditCollection}}
|
||||
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
|
||||
{{else}}
|
||||
window.location = nextURL;
|
||||
{{end}}
|
||||
} else {
|
||||
$btnPublish.el.textContent = 'Post';
|
||||
alert("Failed to post. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
setButtonStates();
|
||||
$writer.on('keyup input', function() {
|
||||
setButtonStates();
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
}, false);
|
||||
$writer.on('keydown', function(e) {
|
||||
clearTimeout(typingTimer);
|
||||
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
|
||||
$btnPublish.el.click();
|
||||
}
|
||||
});
|
||||
$btnPublish.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (!publishing && $writer.el.value) {
|
||||
var content = $writer.el.value;
|
||||
publish(content, selectedFont);
|
||||
}
|
||||
});
|
||||
|
||||
WebFontConfig = {
|
||||
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||
};
|
||||
var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}');
|
||||
|
||||
var doneTyping = function() {
|
||||
if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) {
|
||||
H.save($writer, draftDoc);
|
||||
updateWordCount();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) {
|
||||
H.remove(draftDoc);
|
||||
} else if (!justPublished) {
|
||||
doneTyping();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
(function() {
|
||||
var wf=document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type='text/javascript';
|
||||
wf.async='true';
|
||||
var s=document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) {
|
||||
// whatevs
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>{{end}}
|
|
@ -13,14 +13,49 @@
|
|||
<body {{template "body-attrs" .}}>
|
||||
<div id="overlay"></div>
|
||||
<header>
|
||||
<h2><a href="/">{{.SiteName}}</a></h2>
|
||||
{{ if .Chorus }}<nav id="full-nav">
|
||||
<div class="left-side">
|
||||
<h2><a href="/">{{.SiteName}}</a></h2>
|
||||
</div>
|
||||
{{ else }}
|
||||
<h2><a href="/">{{.SiteName}}</a></h2>
|
||||
{{ end }}
|
||||
{{if not .SingleUser}}
|
||||
<nav id="user-nav">
|
||||
<nav class="tabs">
|
||||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>
|
||||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
|
||||
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{end}}
|
||||
{{if .Username}}
|
||||
<nav class="dropdown-nav">
|
||||
<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/export">Export</a></li>
|
||||
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}}
|
||||
<li class="separator"><hr /></li>
|
||||
<li><a href="/me/logout">Log out</a></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<nav class="tabs">
|
||||
{{ if and .SimpleNav (not .SingleUser) }}
|
||||
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
|
||||
{{ end }}
|
||||
{{if or .Chorus (not .Username)}}<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>{{end}}
|
||||
{{ if not .SingleUser }}
|
||||
{{ if .Username }}
|
||||
{{if or (not .Chorus) (gt .MaxBlogs 1)}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
|
||||
{{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
|
||||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
||||
{{ end }}
|
||||
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
|
||||
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}}
|
||||
{{if not .Username}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{else if .SimpleNav}}<a href="/me/logout">Log out</a>{{end}}
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{if .Chorus}}{{if .Username}}<div class="right-side" style="font-size: 0.86em;">
|
||||
<a class="simple-btn" href="/new">New Post</a>
|
||||
</div>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
</header>
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
{{define "post"}}<!DOCTYPE HTML>
|
||||
<html {{if .Language.Valid}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">
|
||||
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#">
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="canonical" href="{{.CanonicalURL .Host}}" />
|
||||
<meta name="generator" content="WriteFreely">
|
||||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
||||
<meta name="description" content="{{.Summary}}">
|
||||
{{if gt .Views 1}}<meta name="twitter:label1" value="Views">
|
||||
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}}
|
||||
<meta name="author" content="{{.Collection.Title}}" />
|
||||
<meta itemprop="description" content="{{.Summary}}">
|
||||
<meta itemprop="datePublished" content="{{.CreatedDate}}" />
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:description" content="{{.Summary}}">
|
||||
<meta name="twitter:title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
||||
{{if gt (len .Images) 0}}<meta name="twitter:image" content="{{index .Images 0}}">{{else}}<meta name="twitter:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="og:title" content="{{.PlainDisplayTitle}}" />
|
||||
<meta property="og:description" content="{{.Summary}}" />
|
||||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{.CanonicalURL .Host}}" />
|
||||
<meta property="og:updated_time" content="{{.Created8601}}" />
|
||||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="article:published_time" content="{{.Created8601}}">
|
||||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body footer {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
body#post header {
|
||||
padding: 1em 1rem;
|
||||
}
|
||||
article time.dt-published {
|
||||
display: block;
|
||||
color: #666;
|
||||
}
|
||||
body#post article h2#title{
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
article time.dt-published {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{if .Collection.RenderMathJax}}
|
||||
<!-- Add mathjax logic -->
|
||||
{{template "mathjax" . }}
|
||||
{{end}}
|
||||
|
||||
<!-- Add highlighting logic -->
|
||||
{{template "highlighting" .}}
|
||||
|
||||
</head>
|
||||
<body id="post">
|
||||
|
||||
<div id="overlay"></div>
|
||||
|
||||
{{template "user-navigation" .}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article>
|
||||
|
||||
{{ if .Collection.ShowFooterBranding }}
|
||||
<footer dir="ltr">
|
||||
<p style="text-align: left">Published by <a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a>
|
||||
{{ if .IsOwner }} · <span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
|
||||
· <a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
|
||||
{{if .IsPinned}} · <a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}}
|
||||
{{ end }}
|
||||
</p>
|
||||
<nav>
|
||||
{{if .PinnedPosts}}
|
||||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL $.Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
<hr>
|
||||
<nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav>
|
||||
</footer>
|
||||
{{ end }}
|
||||
</body>
|
||||
|
||||
{{if .Collection.CanShowScript}}
|
||||
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
|
||||
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script type="text/javascript">
|
||||
|
||||
var pinning = false;
|
||||
function unpinPost(e, postID) {
|
||||
e.preventDefault();
|
||||
if (pinning) {
|
||||
return;
|
||||
}
|
||||
pinning = true;
|
||||
|
||||
var $footer = document.getElementsByTagName('footer')[0];
|
||||
var callback = function() {
|
||||
// Hide current page
|
||||
var $pinnedNavLink = $footer.getElementsByTagName('nav')[0].querySelector('.pinned.selected');
|
||||
$pinnedNavLink.style.display = 'none';
|
||||
};
|
||||
|
||||
var $pinBtn = $footer.getElementsByClassName('unpin')[0];
|
||||
$pinBtn.innerHTML = '...';
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/collections/{{.Collection.Alias}}/unpin";
|
||||
var params = [ { "id": postID } ];
|
||||
http.open("POST", url, true);
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
pinning = false;
|
||||
if (http.status == 200) {
|
||||
callback();
|
||||
$pinBtn.style.display = 'none';
|
||||
$pinBtn.innerHTML = 'Pin';
|
||||
} else if (http.status == 409) {
|
||||
$pinBtn.innerHTML = 'Unpin';
|
||||
} else {
|
||||
$pinBtn.innerHTML = 'Unpin';
|
||||
alert("Failed to unpin." + (http.status>=500?" Please try again.":""));
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
try { // Fonts
|
||||
WebFontConfig = {
|
||||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||
};
|
||||
(function() {
|
||||
var wf = document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type = 'text/javascript';
|
||||
wf.async = 'true';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) { /* ¯\_(ツ)_/¯ */ }
|
||||
</script>
|
||||
</html>{{end}}
|
|
@ -0,0 +1,233 @@
|
|||
{{define "collection"}}<!DOCTYPE HTML>
|
||||
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
{{if gt .CurrentPage 1}}<link rel="prev" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}}
|
||||
{{if lt .CurrentPage .TotalPages}}<link rel="next" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}}
|
||||
{{if not .IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.DisplayTitle}} » Feed" href="{{.CanonicalURL}}feed/" />{{end}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="generator" content="WriteFreely">
|
||||
<meta name="description" content="{{.Description}}">
|
||||
<meta itemprop="name" content="{{.DisplayTitle}}">
|
||||
<meta itemprop="description" content="{{.Description}}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="{{.DisplayTitle}}">
|
||||
<meta name="twitter:image" content="{{.AvatarURL}}">
|
||||
<meta name="twitter:description" content="{{.Description}}">
|
||||
<meta property="og:title" content="{{.DisplayTitle}}" />
|
||||
<meta property="og:site_name" content="{{.DisplayTitle}}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:description" content="{{.Description}}" />
|
||||
<meta property="og:image" content="{{.AvatarURL}}">
|
||||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
|
||||
<style type="text/css">
|
||||
body#collection header {
|
||||
max-width: 40em;
|
||||
margin: 1em auto;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
}
|
||||
body#collection header.multiuser {
|
||||
max-width: 100%;
|
||||
margin: 1em;
|
||||
}
|
||||
body#collection header nav:not(.pinned-posts) {
|
||||
display: inline;
|
||||
}
|
||||
body#collection header nav.dropdown-nav,
|
||||
body#collection header nav.tabs,
|
||||
body#collection header nav.tabs a:first-child {
|
||||
margin: 0 0 0 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{if .RenderMathJax}}
|
||||
<!-- Add mathjax logic -->
|
||||
{{template "mathjax" .}}
|
||||
{{end}}
|
||||
|
||||
<!-- Add highlighting logic -->
|
||||
{{template "highlighting" . }}
|
||||
|
||||
</head>
|
||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||
{{template "user-navigation" .}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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>
|
||||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
||||
{{/*if not .Public/*}}
|
||||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
||||
{{/*end*/}}
|
||||
{{if .PinnedPosts}}<nav class="pinned-posts">
|
||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL $.Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
|
||||
|
||||
{{if .IsWelcome}}
|
||||
<div id="welcome">
|
||||
<h2>Welcome, <strong>{{.Username}}</strong>!</h2>
|
||||
<p>This is your new blog.</p>
|
||||
<p><a class="simple-cta" href="/#{{.Alias}}">Start writing</a>, or <a class="simple-cta" href="/me/c/{{.Alias}}">customize</a> your blog.</p>
|
||||
<p>Check out our <a class="simple-cta" href="https://guides.write.as/writing/?pk_campaign=welcome">writing guide</a> to see what else you can do, and <a class="simple-cta" href="/contact">get in touch</a> anytime with questions or feedback.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{template "posts" .}}
|
||||
|
||||
{{if gt .TotalPages 1}}<nav id="paging" class="content-container clearfix">
|
||||
{{if or (and .Format.Ascending (lt .CurrentPage .TotalPages)) (isRTL .Direction)}}
|
||||
{{if gt .CurrentPage 1}}<a href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ {{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Previous{{else}}Newer{{end}}</a>{{end}}
|
||||
{{if lt .CurrentPage .TotalPages}}<a style="float:right;" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Next{{else}}Older{{end}} ⇢</a>{{end}}
|
||||
{{else}}
|
||||
{{if lt .CurrentPage .TotalPages}}<a href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ Older</a>{{end}}
|
||||
{{if gt .CurrentPage 1}}<a style="float:right;" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">Newer ⇢</a>{{end}}
|
||||
{{end}}
|
||||
</nav>{{end}}
|
||||
|
||||
{{if .Posts}}</section>{{else}}</div>{{end}}
|
||||
|
||||
{{if .ShowFooterBranding }}
|
||||
<footer>
|
||||
<hr />
|
||||
<nav dir="ltr">
|
||||
{{if not .SingleUser}}<a class="home pubd" href="/">{{.SiteName}}</a> · {{end}}powered by <a style="margin-left:0" href="https://writefreely.org">writefreely</a>
|
||||
</nav>
|
||||
</footer>
|
||||
{{ end }}
|
||||
</body>
|
||||
|
||||
{{if .CanShowScript}}
|
||||
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
|
||||
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script type="text/javascript">
|
||||
var deleting = false;
|
||||
function delPost(e, id, owned) {
|
||||
e.preventDefault();
|
||||
if (deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: UNDO!
|
||||
if (window.confirm('Are you sure you want to delete this post?')) {
|
||||
// AJAX
|
||||
deletePost(id, "", function() {
|
||||
// Remove post from list
|
||||
var $postEl = document.getElementById('post-' + id);
|
||||
$postEl.parentNode.removeChild($postEl);
|
||||
// TODO: add next post from this collection at the bottom
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var deletePost = function(postID, token, callback) {
|
||||
deleting = true;
|
||||
|
||||
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0];
|
||||
$delBtn.innerHTML = '...';
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/posts/" + postID;
|
||||
http.open("DELETE", url, true);
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
deleting = false;
|
||||
if (http.status == 204) {
|
||||
callback();
|
||||
} else if (http.status == 409) {
|
||||
$delBtn.innerHTML = 'delete';
|
||||
alert("Post is synced to another account. Delete the post from that account instead.");
|
||||
// TODO: show "remove" button instead of "delete" now
|
||||
// Persist that state.
|
||||
// Have it remove the post locally only.
|
||||
} else {
|
||||
$delBtn.innerHTML = 'delete';
|
||||
alert("Failed to delete." + (http.status>=500?" Please try again.":""));
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send();
|
||||
};
|
||||
|
||||
var pinning = false;
|
||||
function pinPost(e, postID, slug, title) {
|
||||
e.preventDefault();
|
||||
if (pinning) {
|
||||
return;
|
||||
}
|
||||
pinning = true;
|
||||
|
||||
var callback = function() {
|
||||
// Visibly remove post from collection
|
||||
var $postEl = document.getElementById('post-' + postID);
|
||||
$postEl.parentNode.removeChild($postEl);
|
||||
var $header = document.querySelector('header:not(.multiuser)');
|
||||
var $pinnedNavs = $header.getElementsByTagName('nav');
|
||||
// Add link to nav
|
||||
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>';
|
||||
if ($pinnedNavs.length == 0) {
|
||||
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>');
|
||||
} else {
|
||||
$pinnedNavs[0].insertAdjacentHTML("beforeend", link);
|
||||
}
|
||||
};
|
||||
|
||||
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0];
|
||||
$pinBtn.innerHTML = '...';
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/collections/{{.Alias}}/pin";
|
||||
var params = [ { "id": postID } ];
|
||||
http.open("POST", url, true);
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
pinning = false;
|
||||
if (http.status == 200) {
|
||||
callback();
|
||||
} else if (http.status == 409) {
|
||||
$pinBtn.innerHTML = 'pin';
|
||||
alert("Post is synced to another account. Delete the post from that account instead.");
|
||||
// TODO: show "remove" button instead of "delete" now
|
||||
// Persist that state.
|
||||
// Have it remove the post locally only.
|
||||
} else {
|
||||
$pinBtn.innerHTML = 'pin';
|
||||
alert("Failed to pin." + (http.status>=500?" Please try again.":""));
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
try {
|
||||
WebFontConfig = {
|
||||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||
};
|
||||
(function() {
|
||||
var wf = document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type = 'text/javascript';
|
||||
wf.async = 'true';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</html>{{end}}
|
|
@ -9,7 +9,7 @@
|
|||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{{ if .IsFound }}
|
||||
<link rel="canonical" href="{{.CanonicalURL}}" />
|
||||
<link rel="canonical" href="{{.CanonicalURL .Host}}" />
|
||||
<meta name="generator" content="WriteFreely">
|
||||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
||||
<meta name="description" content="{{.Summary}}">
|
||||
|
@ -26,7 +26,7 @@
|
|||
<meta property="og:description" content="{{.Summary}}" />
|
||||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
||||
<meta property="og:url" content="{{.CanonicalURL .Host}}" />
|
||||
<meta property="og:updated_time" content="{{.Created8601}}" />
|
||||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||
<meta property="article:published_time" content="{{.Created8601}}">
|
||||
|
@ -50,7 +50,7 @@
|
|||
<h1 dir="{{.Direction}}" id="blog-title"><a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
||||
<nav>
|
||||
{{if .PinnedPosts}}
|
||||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
||||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL $.Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
||||
{{end}}
|
||||
{{ if and .IsOwner .IsFound }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
|
||||
<a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
|
||||
|
@ -59,6 +59,9 @@
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
|
||||
|
||||
{{ if .Collection.ShowFooterBranding }}
|
||||
|
|
|
@ -48,11 +48,14 @@
|
|||
<h1 dir="{{.Direction}}" id="blog-title"><a href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
||||
<nav>
|
||||
{{if .PinnedPosts}}
|
||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.DisplayTitle}}</a>{{end}}
|
||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL $.Host}}{{end}}">{{.DisplayTitle}}</a>{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{end}}
|
||||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
|
||||
<h1>{{.Tag}}</h1>
|
||||
{{template "posts" .}}
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
{{else}}
|
||||
<li><a href="/#{{.Alias}}" class="write">{{.SiteName}}</a></li>
|
||||
{{end}}
|
||||
{{if .SimpleNav}}<li><a href="/new#{{.Alias}}">New Post</a></li>{{end}}
|
||||
<li><a href="/me/c/{{.Alias}}">Customize</a></li>
|
||||
<li><a href="/me/c/{{.Alias}}/stats">Stats</a></li>
|
||||
<li class="separator"><hr /></li>
|
||||
|
@ -61,13 +62,16 @@
|
|||
</ul></nav>{{end}}
|
||||
|
||||
<header>
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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}}
|
||||
{{/*if not .Public/*}}
|
||||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
||||
{{/*end*/}}
|
||||
{{if .PinnedPosts}}<nav>
|
||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL $.Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
|
|
|
@ -269,6 +269,10 @@
|
|||
<script src="/js/h.js"></script>
|
||||
<script>
|
||||
function updateMeta() {
|
||||
if ({{.Suspended}}) {
|
||||
alert('Your account is currently supsended, editing posts is disabled.');
|
||||
return
|
||||
}
|
||||
document.getElementById('create-error').style.display = 'none';
|
||||
var $created = document.getElementById('created');
|
||||
var dateStr = $created.value.trim();
|
||||
|
|
|
@ -25,10 +25,10 @@
|
|||
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||
<ul>
|
||||
<li class="menu-heading">Publish to...</li>
|
||||
<li class="target selected" id="anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
||||
{{if .Blogs}}{{range .Blogs}}
|
||||
<li class="target" id="blog-{{.Alias}}"><a href="#{{.Alias}}"><i class="material-icons md-18">public</i> {{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a></li>
|
||||
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
||||
<li class="target{{if eq $idx 0}} selected{{end}}" id="blog-{{$el.Alias}}"><a href="#{{$el.Alias}}"><i class="material-icons md-18">public</i> {{if $el.Title}}{{$el.Title}}{{else}}{{$el.Alias}}{{end}}</a></li>
|
||||
{{end}}{{end}}
|
||||
<li class="target" id="blog-anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
||||
<li id="user-separator" class="separator"><hr /></li>
|
||||
{{ if .SingleUser }}
|
||||
<li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
|
||||
|
@ -131,8 +131,12 @@
|
|||
{{else}}var canPublish = true;{{end}}
|
||||
var publishing = false;
|
||||
var justPublished = false;
|
||||
|
||||
var suspended = {{.Suspended}};
|
||||
var publish = function(content, font) {
|
||||
if (suspended === true) {
|
||||
alert("Your account is silenced, so you can't publish or update posts.");
|
||||
return;
|
||||
}
|
||||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
||||
if (!token) {
|
||||
alert("You don't have permission to update this post.");
|
||||
|
@ -278,7 +282,7 @@
|
|||
document.getElementById('target-name').innerText = newText.join(' ');
|
||||
});
|
||||
}
|
||||
var postTarget = H.get('postTarget', 'anonymous');
|
||||
var postTarget = H.get('postTarget', '{{if .Blogs}}{{$blog := index .Blogs 0}}{{$blog.Alias}}{{else}}anonymous{{end}}');
|
||||
if (location.hash != '') {
|
||||
postTarget = location.hash.substring(1);
|
||||
// TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
|
||||
</head>
|
||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||
{{if .Suspended}}
|
||||
{{template "user-supsended"}}
|
||||
{{end}}
|
||||
<header>
|
||||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{.Alias}}/" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
||||
</header>
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
{{template "highlighting" .}}
|
||||
</head>
|
||||
<body id="post">
|
||||
|
||||
<header>
|
||||
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
|
||||
<nav>
|
||||
|
@ -49,6 +48,10 @@
|
|||
{{ end }}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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>
|
||||
|
||||
|
|
|
@ -65,34 +65,39 @@
|
|||
}
|
||||
body#collection header nav {
|
||||
display: inline !important;
|
||||
}
|
||||
body#collection header nav:not(#full-nav):not(#user-nav) {
|
||||
margin: 0 0 0 1em !important;
|
||||
}
|
||||
header nav#user-nav {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
body#collection header nav.tabs a:first-child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
{{define "body-attrs"}}id="collection"{{end}}
|
||||
{{define "content"}}
|
||||
<div class="content-container snug" style="max-width: 40rem;">
|
||||
<h1 style="text-align:center">Reader</h1>
|
||||
<p>Read the latest posts from {{.SiteName}}. {{if .Username}}To showcase your writing here, go to your <a href="/me/c/">blog</a> settings and select the <em>Public</em> option.{{end}}</p>
|
||||
<h1>{{.ContentTitle}}</h1>
|
||||
<p{{if .SelTopic}} style="text-align:center"{{end}}>{{if .SelTopic}}#{{.SelTopic}} posts{{else}}{{.Content}}{{end}}</p>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
{{ if gt (len .Posts) 0 }}
|
||||
<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}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2>
|
||||
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name"><a href="{{if .Slug.String}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2>
|
||||
<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{.Collection.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>
|
||||
{{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}}.md{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time></h2>
|
||||
<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>
|
||||
{{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>
|
||||
|
||||
<a class="read-more" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{else}}<div class="e-content preview" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{ if not .HTMLContent }}<p id="post-body" class="e-content preview">{{.Content}}</p>{{ else }}{{.HTMLContent}}{{ end }}<div class="over"> </div></div>
|
||||
<a class="read-more" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{else}}<div class="e-content preview" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{ if not .HTMLContent }}<p id="post-body" class="e-content preview">{{.Content}}</p>{{ else }}{{.HTMLContent}}{{ end }}<div class="over"> </div></div>
|
||||
|
||||
<a class="read-more maybe" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{end}}</article>
|
||||
<a class="read-more maybe" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{end}}</article>
|
||||
{{end}}
|
||||
</section>
|
||||
{{ else }}
|
||||
|
|
|
@ -20,6 +20,9 @@ table.classy.export .disabled, table.classy.export a {
|
|||
<tr>
|
||||
<td colspan="2"><a href="/admin/page/landing">Home</a></td>
|
||||
</tr>
|
||||
{{if .LocalTimeline}}<tr>
|
||||
<td colspan="2"><a href="/admin/page/reader">Reader</a></td>
|
||||
</tr>{{end}}
|
||||
{{range .Pages}}
|
||||
<tr>
|
||||
<td><a href="/admin/page/{{.ID}}">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.ID}}{{end}}</a></td>
|
||||
|
|
|
@ -11,12 +11,14 @@
|
|||
<th>User</th>
|
||||
<th>Joined</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td>
|
||||
<td>{{.CreatedFriendly}}</td>
|
||||
<td style="text-align:center">{{if .IsAdmin}}Admin{{else}}User{{end}}</td>
|
||||
<td style="text-align:center">{{if .IsSilenced}}Silenced{{else}}Active{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
|
|
@ -31,6 +31,8 @@ input[type=text] {
|
|||
<p class="page-desc content-desc">Describe what your instance is <a href="/about" target="page">about</a>.</p>
|
||||
{{else if eq .Content.ID "privacy"}}
|
||||
<p class="page-desc content-desc">Outline your <a href="/privacy" target="page">privacy policy</a>.</p>
|
||||
{{else if eq .Content.ID "reader"}}
|
||||
<p class="page-desc content-desc">Customize your <a href="/read" target="page">Reader</a> page.</p>
|
||||
{{else if eq .Content.ID "landing"}}
|
||||
<p class="page-desc content-desc">Customize your <a href="/?landing=1" target="page">home page</a>.</p>
|
||||
{{end}}
|
||||
|
@ -38,7 +40,7 @@ input[type=text] {
|
|||
{{if .Message}}<p>{{.Message}}</p>{{end}}
|
||||
|
||||
<form method="post" action="/admin/update/{{.Content.ID}}" onsubmit="savePage(this)">
|
||||
{{if eq .Content.Type "section"}}
|
||||
{{if .Banner}}
|
||||
<label for="banner">
|
||||
Banner
|
||||
</label>
|
||||
|
|
|
@ -7,12 +7,43 @@ table.classy th {
|
|||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
td.active-suspend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
td.active-suspend > input[type="submit"] {
|
||||
margin-left: auto;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
td.active-suspend {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
td.active-suspend > input[type="submit"] {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
input.copy-text {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
color: #555;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<div class="snug content-container">
|
||||
{{template "admin-header" .}}
|
||||
|
||||
<h2 id="posts-header">{{.User.Username}}</h2>
|
||||
|
||||
{{if .NewPassword}}<div class="alert success">
|
||||
<p>This user's password has been reset to:</p>
|
||||
<p><input type="text" class="copy-text" value="{{.NewPassword}}" onfocus="if (this.select) this.select(); else this.setSelectionRange(0, this.value.length);" readonly /></p>
|
||||
<p>They can use this new password to log in to their account. <strong>This will only be shown once</strong>, so be sure to copy it and send it to them now.</p>
|
||||
{{if .ClearEmail}}<p>Their email address is: <a href="mailto:{{.ClearEmail}}">{{.ClearEmail}}</a></p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
|
@ -38,6 +69,34 @@ h3 {
|
|||
<th>Last Post</th>
|
||||
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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">
|
||||
{{if .User.IsSilenced}}
|
||||
<p>Silenced</p>
|
||||
<input type="submit" value="Unsilence"/>
|
||||
{{else}}
|
||||
<p>Active</p>
|
||||
<input class="danger" type="submit" value="Silence" {{if .User.IsAdmin}}disabled{{end}}/>
|
||||
{{end}}
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password</th>
|
||||
<td>
|
||||
{{if ne .Username .User.Username}}
|
||||
<form id="reset-form" action="/admin/user/{{.User.Username}}/passphrase" method="post" autocomplete="false">
|
||||
<input type="hidden" name="user" value="{{.User.ID}}"/>
|
||||
<button type="submit">Reset</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<a href="/me/settings" title="Go to reset password page">Change your password</a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Blogs</h2>
|
||||
|
@ -83,5 +142,19 @@ h3 {
|
|||
{{end}}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function confirmSilence() {
|
||||
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
|
||||
}
|
||||
|
||||
form = document.getElementById("reset-form");
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
agreed = confirm("Reset this user's password? This will generate a new temporary password that you'll need to share with them, and invalidate their old one.");
|
||||
if (agreed === true) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{end}}
|
||||
|
||||
<h2 id="posts-header">drafts</h2>
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
<div class="content-container snug">
|
||||
<div id="overlay"></div>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{end}}
|
||||
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>
|
||||
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
|
|
|
@ -7,13 +7,16 @@
|
|||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{end}}
|
||||
<h2>blogs</h2>
|
||||
<ul class="atoms collections">
|
||||
{{range $i, $el := .Collections}}<li class="collection"><h3>
|
||||
<a class="title" href="/{{.Alias}}/">{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a>
|
||||
</h3>
|
||||
<h4>
|
||||
<a class="action new-post" href="/#{{.Alias}}">new post</a>
|
||||
<a class="action new-post" href="{{if $.Chorus}}/new{{else}}/{{end}}#{{.Alias}}">new post</a>
|
||||
<a class="action" href="/me/c/{{.Alias}}">customize</a>
|
||||
<a class="action" href="/me/c/{{.Alias}}/stats">stats</a>
|
||||
</h4>
|
||||
|
|
|
@ -1,21 +1,5 @@
|
|||
{{define "header"}}<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#888888" />
|
||||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png">
|
||||
</head>
|
||||
<body id="me">
|
||||
<header{{if .SingleUser}} class="singleuser"{{end}}>
|
||||
{{define "user-navigation"}}
|
||||
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
|
||||
{{if .SingleUser}}
|
||||
<nav id="user-nav">
|
||||
<nav class="dropdown-nav">
|
||||
|
@ -39,8 +23,12 @@
|
|||
</nav>
|
||||
</nav>
|
||||
{{else}}
|
||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||
<nav id="full-nav">
|
||||
<div class="left-side">
|
||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||
</div>
|
||||
<nav id="user-nav">
|
||||
{{if .Username}}
|
||||
<nav class="dropdown-nav">
|
||||
<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}}
|
||||
|
@ -53,13 +41,56 @@
|
|||
</ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
<nav class="tabs">
|
||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
||||
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
|
||||
{{if .SimpleNav}}
|
||||
{{ if not .SingleUser }}
|
||||
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
|
||||
{{ end }}
|
||||
<a href="/about">About</a>
|
||||
{{ if not .SingleUser }}
|
||||
{{ if .Username }}
|
||||
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
|
||||
{{if and .Chorus (eq .MaxBlogs 1)}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
|
||||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
||||
{{ end }}
|
||||
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
|
||||
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}}
|
||||
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}}
|
||||
{{ end }}
|
||||
{{else}}
|
||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
||||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
||||
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
</nav>
|
||||
{{if .Chorus}}{{if .Username}}<div class="right-side">
|
||||
<a class="simple-btn" href="/new">New Post</a>
|
||||
</div>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</header>
|
||||
{{end}}
|
||||
{{define "header"}}<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#888888" />
|
||||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png">
|
||||
</head>
|
||||
<body id="me">
|
||||
{{template "user-navigation" .}}
|
||||
<div id="official-writing">
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{{define "user-suspended"}}
|
||||
<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>
|
||||
{{end}}
|
|
@ -0,0 +1,32 @@
|
|||
{{define "invite-help"}}
|
||||
{{template "header" .}}
|
||||
<style>
|
||||
.copy-link {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
<div class="snug content-container">
|
||||
<h1>Invite to {{.SiteName}}</h1>
|
||||
{{ if .Expired }}
|
||||
<p style="font-style: italic">This invite link is expired.</p>
|
||||
{{ else }}
|
||||
<p>Copy the link below and send it to anyone that you want to join <em>{{ .SiteName }}</em>. You could paste it into an email, instant message, text message, or write it down on paper. Anyone who navigates to this special page will be able to create an account.</p>
|
||||
<input class="copy-link" type="text" name="invite-url" value="{{$.Host}}/invite/{{.Invite.ID}}" onfocus="if (this.select) this.select(); else this.setSelectionRange(0, this.value.length);" readonly />
|
||||
<p>
|
||||
{{ if gt .Invite.MaxUses.Int64 0 }}
|
||||
{{if eq .Invite.MaxUses.Int64 1}}Only <strong>one</strong> user{{else}}Up to <strong>{{.Invite.MaxUses.Int64}}</strong> users{{end}} can sign up with this link.
|
||||
{{if gt .Invite.Uses 0}}So far, <strong>{{.Invite.Uses}}</strong> {{pluralize "person has" "people have" .Invite.Uses}} used it.{{end}}
|
||||
{{if .Invite.Expires}}It expires on <strong>{{.Invite.ExpiresFriendly}}</strong>.{{end}}
|
||||
{{ else }}
|
||||
It can be used as many times as you like{{if .Invite.Expires}} before <strong>{{.Invite.ExpiresFriendly}}</strong>, when it expires{{end}}.
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
|
@ -7,6 +7,9 @@ h3 { font-weight: normal; }
|
|||
.section > *:not(input) { font-size: 0.86em; }
|
||||
</style>
|
||||
<div class="content-container snug regular">
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{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">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
|
|
|
@ -17,6 +17,9 @@ td.none {
|
|||
</style>
|
||||
|
||||
<div class="content-container snug">
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{end}}
|
||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
||||
|
||||
<p>Stats for all time.</p>
|
||||
|
|
|
@ -13,13 +13,14 @@ package writefreely
|
|||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Get params
|
||||
var ur userRegistration
|
||||
|
@ -47,6 +48,9 @@ func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
ur.Normalize = true
|
||||
|
||||
to := "/"
|
||||
if app.cfg.App.SimpleNav {
|
||||
to = "/new"
|
||||
}
|
||||
if ur.InviteCode != "" {
|
||||
to = "/invite/" + ur.InviteCode
|
||||
}
|
||||
|
@ -68,7 +72,7 @@ func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
// { "username": "asdf" }
|
||||
// result: { code: 204 }
|
||||
func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Get params
|
||||
var d struct {
|
||||
|
|
12
users.go
12
users.go
|
@ -19,6 +19,13 @@ import (
|
|||
"github.com/writeas/writefreely/key"
|
||||
)
|
||||
|
||||
type UserStatus int
|
||||
|
||||
const (
|
||||
UserActive = iota
|
||||
UserSilenced
|
||||
)
|
||||
|
||||
type (
|
||||
userCredentials struct {
|
||||
Alias string `json:"alias" schema:"alias"`
|
||||
|
@ -59,6 +66,7 @@ type (
|
|||
HasPass bool `json:"has_pass"`
|
||||
Email zero.String `json:"email"`
|
||||
Created time.Time `json:"created"`
|
||||
Status UserStatus `json:"status"`
|
||||
|
||||
clearEmail string `json:"email"`
|
||||
}
|
||||
|
@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool {
|
|||
// TODO: get this from database
|
||||
return u.ID == 1
|
||||
}
|
||||
|
||||
func (u *User) IsSilenced() bool {
|
||||
return u.Status&UserSilenced != 0
|
||||
}
|
||||
|
|
11
webfinger.go
11
webfinger.go
|
@ -11,11 +11,12 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/writeas/go-webfinger"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type wfResolver struct {
|
||||
|
@ -37,6 +38,14 @@ 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)
|
||||
if err != nil {
|
||||
log.Error("webfinger find user: check is suspended: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if suspended {
|
||||
return nil, wfUserNotFoundErr
|
||||
}
|
||||
c.hostName = wfr.cfg.App.Host
|
||||
if wfr.cfg.App.SingleUser {
|
||||
// Ensure handle matches user-chosen one on single-user blogs
|
||||
|
|
Loading…
Reference in New Issue