Compare commits

..

No commits in common. "develop" and "hotfix-0.13.1" have entirely different histories.

149 changed files with 659 additions and 4194 deletions

View file

@ -1 +1,2 @@
Dockerfile Dockerfile
.git

View file

@ -1,10 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.go]
indent_style = tab

View file

@ -5,11 +5,3 @@ updates:
open-pull-requests-limit: 50 open-pull-requests-limit: 50
schedule: schedule:
interval: "monthly" interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/Dockerfile"
schedule:
interval: "daily"

View file

@ -2,4 +2,4 @@
--- ---
- [ ] I have signed the [CLA](https://todo.musing.studio/L1) - [ ] I have signed the [CLA](https://phabricator.write.as/L1)

View file

@ -1,70 +0,0 @@
name: Build container image, publish as GitHub-package
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ main, develop ]
# Publish semver tags as releases.
tags:
- 'v*.*.*'
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Set up QEMU for cross-building
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
# Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4.6.0
with:
images: |
ghcr.io/${{ github.repository }}
flavor: latest=true
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker images
uses: docker/build-push-action@v5.0.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

1
.gitignore vendored
View file

@ -3,7 +3,6 @@ node_modules
*.swp *.swp
*.swo *.swo
static/local/custom.css
build build
tmp tmp
*.ini *.ini

View file

@ -18,11 +18,11 @@ First, you'll want to clone the WriteFreely repo, install development dependenci
### Starting development ### Starting development
Next, [join our forum](https://discuss.write.as) so you can discuss development with the team. Then take a look at [our roadmap on Phabricator](https://todo.musing.studio/tag/writefreely/) to see where the project is today and where it's headed. Next, [join our forum](https://discuss.write.as) so you can discuss development with the team. Then take a look at [our roadmap on Phabricator](https://phabricator.write.as/tag/write_freely/) to see where the project is today and where it's headed.
When you find something you want to work on, start a new topic on the forum or jump into an existing discussion, if there is one. The team will respond and continue the conversation there. When you find something you want to work on, start a new topic on the forum or jump into an existing discussion, if there is one. The team will respond and continue the conversation there.
Lastly, **before submitting any code**, please sign our [contributor's agreement](https://todo.musing.studio/L1) so we can accept your contributions. It is substantially similar to the _Apache Individual Contributor License Agreement_. If you'd like to know about the rationale behind this requirement, you can [read more about that here](https://todo.musing.studio/w/writefreely/cla/). Lastly, **before submitting any code**, please sign our [contributor's agreement](https://phabricator.write.as/L1) so we can accept your contributions. It is substantially similar to the _Apache Individual Contributor License Agreement_. If you'd like to know about the rationale behind this requirement, you can [read more about that here](https://phabricator.write.as/w/writefreely/cla/).
### Branching ### Branching
@ -49,7 +49,7 @@ We value reliable, readable, and maintainable code over all else in our work. To
* Write code for other humans, not computers. * Write code for other humans, not computers.
* The less complexity, the better. The more someone can understand code just by looking at it, the better. * The less complexity, the better. The more someone can understand code just by looking at it, the better.
* Functionality, readability, and maintainability over senseless elegance. * Functionality, readability, and maintainability over senseless elegance.
* Only abstract when necessary. * Only abstract when necessary.
* Keep an eye to the future, but don't pre-optimize at the expense of today's simplicity. * Keep an eye to the future, but don't pre-optimize at the expense of today's simplicity.
#### Code guidelines #### Code guidelines
@ -96,4 +96,4 @@ Beyond that, we prioritize pull requests in this order:
2. Superficial changes and improvements that don't adversely impact users 2. Superficial changes and improvements that don't adversely impact users
3. New features and changes that have been discussed before with the team 3. New features and changes that have been discussed before with the team
Any pull requests that haven't previously been discussed with the team may be extensively delayed or closed, especially if they require a wider consideration before integrating into the project. When in doubt, please reach out [on the forum](https://discuss.write.as) before submitting a pull request. Any pull requests that haven't previously been discussed with the team may be extensively delayed or closed, especially if they require a wider consideration before integrating into the project. When in doubt, please reach out [on the forum](https://discuss.write.as) before submitting a pull request.

View file

@ -1,27 +1,21 @@
# Build image # Build image
FROM golang:1.21-alpine3.18 as build FROM golang:1.15-alpine as build
LABEL org.opencontainers.image.source="https://github.com/writefreely/writefreely" RUN apk add --update nodejs nodejs-npm make g++ git
LABEL org.opencontainers.image.description="WriteFreely is a clean, minimalist publishing platform made for writers. Start a blog, share knowledge within your organization, or build a community around the shared act of writing." RUN npm install -g less less-plugin-clean-css
RUN go get -u github.com/go-bindata/go-bindata/...
RUN apk -U upgrade \
&& apk add --no-cache nodejs npm make g++ git \
&& npm install -g less less-plugin-clean-css \
&& mkdir -p /go/src/github.com/writefreely/writefreely
RUN mkdir -p /go/src/github.com/writefreely/writefreely
WORKDIR /go/src/github.com/writefreely/writefreely WORKDIR /go/src/github.com/writefreely/writefreely
COPY . . COPY . .
RUN cat ossl_legacy.cnf > /etc/ssl/openssl.cnf
ENV GO111MODULE=on ENV GO111MODULE=on
ENV NODE_OPTIONS=--openssl-legacy-provider
RUN make build \ RUN make build \
&& make ui \ && make ui
&& mkdir /stage \ RUN mkdir /stage && \
&& cp -R /go/bin \ cp -R /go/bin \
/go/src/github.com/writefreely/writefreely/templates \ /go/src/github.com/writefreely/writefreely/templates \
/go/src/github.com/writefreely/writefreely/static \ /go/src/github.com/writefreely/writefreely/static \
/go/src/github.com/writefreely/writefreely/pages \ /go/src/github.com/writefreely/writefreely/pages \
@ -30,11 +24,9 @@ RUN make build \
/stage /stage
# Final image # Final image
FROM alpine:3.18.4 FROM alpine:3.12
RUN apk -U upgrade \
&& apk add --no-cache openssl ca-certificates
RUN apk add --no-cache openssl ca-certificates
COPY --from=build --chown=daemon:daemon /stage /go COPY --from=build --chown=daemon:daemon /stage /go
WORKDIR /go WORKDIR /go
@ -43,6 +35,3 @@ EXPOSE 8080
USER daemon USER daemon
ENTRYPOINT ["cmd/writefreely/writefreely"] ENTRYPOINT ["cmd/writefreely/writefreely"]
HEALTHCHECK --start-period=5s --interval=15s --timeout=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1

View file

@ -1,34 +0,0 @@
FROM golang:alpine AS build
LABEL org.opencontainers.image.source="https://github.com/writefreely/writefreely"
LABEL org.opencontainers.image.description="WriteFreely is a clean, minimalist publishing platform made for writers. Start a blog, share knowledge within your organization, or build a community around the shared act of writing."
RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache nodejs npm make g++ git sqlite-dev patch && \
npm install -g less less-plugin-clean-css && \
mkdir -p /go/src/github.com/writefreely/writefreely
COPY . /go/src/github.com/writefreely/writefreely
WORKDIR /go/src/github.com/writefreely/writefreely
ENV NODE_OPTIONS=--openssl-legacy-provider
RUN cat ossl_legacy.cnf >> /etc/ssl/openssl.cnf && \
make build && \
make ui
FROM alpine
RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache openssl ca-certificates && \
mkdir /usr/share/writefreely
COPY --from=build /go/src/github.com/writefreely/writefreely/cmd/writefreely/writefreely /usr/bin
COPY --from=build /go/src/github.com/writefreely/writefreely/pages /usr/share/writefreely/pages
COPY --from=build /go/src/github.com/writefreely/writefreely/static /usr/share/writefreely/static
COPY --from=build /go/src/github.com/writefreely/writefreely/templates /usr/share/writefreely/templates
ENV WRITEFREELY_DOCKER=True
ENV HOME=/data
WORKDIR /data
CMD ["/usr/bin/writefreely"]

View file

@ -1,5 +1,5 @@
GITREV=`git describe | cut -c 2-` GITREV=`git describe | cut -c 2-`
LDFLAGS=-ldflags="-s -w -X 'github.com/writefreely/writefreely.softwareVer=$(GITREV)' -extldflags '-static'" LDFLAGS=-ldflags="-X 'github.com/writefreely/writefreely.softwareVer=$(GITREV)'"
GOCMD=go GOCMD=go
GOINSTALL=$(GOCMD) install $(LDFLAGS) GOINSTALL=$(GOCMD) install $(LDFLAGS)
@ -14,56 +14,50 @@ TMPBIN=./tmp
all : build all : build
ci: deps ci: ci-assets deps
cd cmd/writefreely; $(GOBUILD) -v cd cmd/writefreely; $(GOBUILD) -v
build: deps build: assets deps
cd cmd/writefreely; $(GOBUILD) -v -tags='netgo sqlite' cd cmd/writefreely; $(GOBUILD) -v -tags='sqlite'
build-no-sqlite: deps-no-sqlite build-no-sqlite: assets-no-sqlite deps-no-sqlite
cd cmd/writefreely; $(GOBUILD) -v -tags='netgo' -o $(BINARY_NAME) cd cmd/writefreely; $(GOBUILD) -v -o $(BINARY_NAME)
build-linux: deps build-linux: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-windows: deps build-windows: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-darwin: deps build-darwin: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-darwin-arm64: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
fi
xgo --targets=darwin/arm64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely .
build-arm6: deps build-arm6: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/arm-6, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=linux/arm-6, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-arm7: deps build-arm7: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-arm64: deps build-arm64: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOCMD) install src.techknowlogick.com/xgo@latest; \ $(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.21.x -out writefreely -pkg ./cmd/writefreely . xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='sqlite' -go go-1.15.x -out writefreely ./cmd/writefreely
build-docker : build-docker :
$(DOCKERCMD) build -t $(IMAGE_NAME):latest -t $(IMAGE_NAME):$(GITREV) . $(DOCKERCMD) build -t $(IMAGE_NAME):latest -t $(IMAGE_NAME):$(GITREV) .
@ -71,8 +65,8 @@ build-docker :
test: test:
$(GOTEST) -v ./... $(GOTEST) -v ./...
run: run: dev-assets
$(GOINSTALL) -tags='netgo sqlite' ./... $(GOINSTALL) -tags='sqlite' ./...
$(BINARY_NAME) --debug $(BINARY_NAME) --debug
deps : deps :
@ -87,12 +81,11 @@ install : build
cmd/writefreely/$(BINARY_NAME) --init-db cmd/writefreely/$(BINARY_NAME) --init-db
cd less/; $(MAKE) install $(MFLAGS) cd less/; $(MAKE) install $(MFLAGS)
release : clean ui release : clean ui assets
mkdir -p $(BUILDPATH) mkdir -p $(BUILDPATH)
rsync -av --exclude=".*" templates $(BUILDPATH) cp -r templates $(BUILDPATH)
rsync -av --exclude=".*" pages $(BUILDPATH) cp -r pages $(BUILDPATH)
rsync -av --exclude=".*" static $(BUILDPATH) cp -r static $(BUILDPATH)
rm -r $(BUILDPATH)/static/local
scripts/invalidate-css.sh $(BUILDPATH) scripts/invalidate-css.sh $(BUILDPATH)
mkdir $(BUILDPATH)/keys mkdir $(BUILDPATH)/keys
$(MAKE) build-linux $(MAKE) build-linux
@ -115,10 +108,6 @@ release : clean ui
mv build/$(BINARY_NAME)-darwin-10.12-amd64 $(BUILDPATH)/$(BINARY_NAME) mv build/$(BINARY_NAME)-darwin-10.12-amd64 $(BUILDPATH)/$(BINARY_NAME)
tar -cvzf $(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz -C build $(BINARY_NAME) tar -cvzf $(BINARY_NAME)_$(GITREV)_macos_amd64.tar.gz -C build $(BINARY_NAME)
rm $(BUILDPATH)/$(BINARY_NAME) rm $(BUILDPATH)/$(BINARY_NAME)
$(MAKE) build-darwin-arm64
mv build/$(BINARY_NAME)-darwin-arm64 $(BUILDPATH)/$(BINARY_NAME)
tar -cvzf $(BINARY_NAME)_$(GITREV)_macos_arm64.tar.gz -C build $(BINARY_NAME)
rm $(BUILDPATH)/$(BINARY_NAME)
$(MAKE) build-windows $(MAKE) build-windows
mv build/$(BINARY_NAME)-windows-4.0-amd64.exe $(BUILDPATH)/$(BINARY_NAME).exe 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) cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./$(BINARY_NAME)
@ -144,16 +133,39 @@ ui : force_look
cd less/; $(MAKE) $(MFLAGS) cd less/; $(MAKE) $(MFLAGS)
cd prose/; $(MAKE) $(MFLAGS) cd prose/; $(MAKE) $(MFLAGS)
assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
assets-no-sqlite: generate
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql
dev-assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore -debug -tags="!wflib" schema.sql sqlite.sql
lib-assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore -o bindata-lib.go -tags="wflib" schema.sql
generate :
@hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/jteeuwen/go-bindata/go-bindata; \
fi
$(TMPBIN): $(TMPBIN):
mkdir -p $(TMPBIN) mkdir -p $(TMPBIN)
$(TMPBIN)/go-bindata: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/go-bindata github.com/jteeuwen/go-bindata/go-bindata
$(TMPBIN)/xgo: deps $(TMPBIN) $(TMPBIN)/xgo: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/xgo src.techknowlogick.com/xgo $(GOBUILD) -o $(TMPBIN)/xgo src.techknowlogick.com/xgo
ci-assets : $(TMPBIN)/go-bindata
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
clean : clean :
-rm -rf build -rm -rf build
-rm -rf tmp -rm -rf tmp
cd less/; $(MAKE) clean $(MFLAGS) cd less/; $(MAKE) clean $(MFLAGS)
force_look : force_look :
true true

View file

@ -69,7 +69,6 @@ For common platforms, start with our [pre-built binaries](https://github.com/wri
You can also find WriteFreely in these package repositories, thanks to our wonderful community! You can also find WriteFreely in these package repositories, thanks to our wonderful community!
* [Arch User Repository](https://aur.archlinux.org/packages/writefreely/) * [Arch User Repository](https://aur.archlinux.org/packages/writefreely/)
* [Nanos Repository](https://repo.ops.city/v2/packages/eyberg/writefreely/show)
## Documentation ## Documentation
@ -87,4 +86,4 @@ Before contributing anything, please read our [Contributing Guide](https://githu
## License ## License
Copyright © 2018-2025 [Musing Studio LLC](https://musing.studio) and contributing authors. Licensed under the [AGPL](https://github.com/writefreely/writefreely/blob/develop/LICENSE). Copyright © 2018-2021 [A Bunch Tell LLC](https://abunchtell.com) and contributing authors. Licensed under the [AGPL](https://github.com/writefreely/writefreely/blob/develop/LICENSE).

View file

@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
To report a vulnerability, send an email to security@writefreely.org.

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -13,8 +13,6 @@ package writefreely
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/writefreely/writefreely/mailer"
"github.com/writefreely/writefreely/spam"
"html/template" "html/template"
"net/http" "net/http"
"regexp" "regexp"
@ -325,7 +323,6 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
To string To string
Message template.HTML Message template.HTML
Flashes []template.HTML Flashes []template.HTML
EmailEnabled bool
LoginUsername string LoginUsername string
}{ }{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
@ -333,7 +330,6 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
To: r.FormValue("to"), To: r.FormValue("to"),
Message: template.HTML(""), Message: template.HTML(""),
Flashes: []template.HTML{}, Flashes: []template.HTML{},
EmailEnabled: app.cfg.Email.Enabled(),
LoginUsername: getTempInfo(app, "login-user", r, w), LoginUsername: getTempInfo(app, "login-user", r, w),
} }
@ -508,7 +504,7 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error {
// User has no email set, so check if they haven't added a password, either, // User has no email set, so check if they haven't added a password, either,
// so we can return a more helpful error message. // so we can return a more helpful error message.
if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass { if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass {
log.Info("Tried logging into %s, but no password or email.", signin.Alias) log.Info("Tried logging in to %s, but no password or email.", signin.Alias)
return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."} return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."}
} }
} }
@ -581,7 +577,7 @@ func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser
} }
passIsSet, err := app.db.IsUserPassSet(u.ID) passIsSet, err := app.db.IsUserPassSet(u.ID)
if err != nil { if err != nil {
// TODO: correct error message // TODO: correct error meesage
log.Error("Login: Unable to get user collections: %v", err) log.Error("Login: Unable to get user collections: %v", err)
} }
@ -791,9 +787,6 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
silenced, err := app.db.IsUserSilenced(u.ID) silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("view articles: %v", err) log.Error("view articles: %v", err)
} }
d := struct { d := struct {
@ -829,10 +822,7 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
silenced, err := app.db.IsUserSilenced(u.ID) silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound { log.Error("view collections %v", err)
return err
}
log.Error("view collections: %v", err)
return fmt.Errorf("view collections: %v", err) return fmt.Errorf("view collections: %v", err)
} }
d := struct { d := struct {
@ -866,11 +856,11 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
return ErrCollectionNotFound return ErrCollectionNotFound
} }
// Add collection properties
c.Monetization = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
silenced, err := app.db.IsUserSilenced(u.ID) silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("view edit collection %v", err) log.Error("view edit collection %v", err)
return fmt.Errorf("view edit collection: %v", err) return fmt.Errorf("view edit collection: %v", err)
} }
@ -879,19 +869,12 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
*UserPage *UserPage
*Collection *Collection
Silenced bool Silenced bool
config.EmailCfg
LetterReplyTo string
}{ }{
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
Collection: c, Collection: c,
Silenced: silenced, Silenced: silenced,
EmailCfg: app.cfg.Email,
} }
obj.UserPage.CollAlias = c.Alias obj.UserPage.CollAlias = c.Alias
if obj.EmailCfg.Enabled() {
obj.LetterReplyTo = app.db.GetCollectionAttribute(c.ID, collAttrLetterReplyTo)
}
showUserPage(w, "collection", obj) showUserPage(w, "collection", obj)
return nil return nil
@ -1055,28 +1038,22 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
silenced, err := app.db.IsUserSilenced(u.ID) silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("view stats: %v", err) log.Error("view stats: %v", err)
return err return err
} }
obj := struct { obj := struct {
*UserPage *UserPage
VisitsBlog string VisitsBlog string
Collection *Collection Collection *Collection
TopPosts *[]PublicPost TopPosts *[]PublicPost
APFollowers int APFollowers int
EmailEnabled bool Silenced bool
EmailSubscribers int
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
VisitsBlog: alias, VisitsBlog: alias,
Collection: c, Collection: c,
TopPosts: topPosts, TopPosts: topPosts,
EmailEnabled: app.cfg.Email.Enabled(), Silenced: silenced,
Silenced: silenced,
} }
obj.UserPage.CollAlias = c.Alias obj.UserPage.CollAlias = c.Alias
if app.cfg.App.Federation { if app.cfg.App.Federation {
@ -1086,79 +1063,14 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
} }
obj.APFollowers = len(*folls) obj.APFollowers = len(*folls)
} }
if obj.EmailEnabled {
subs, err := app.db.GetEmailSubscribers(c.ID, true)
if err != nil {
return err
}
obj.EmailSubscribers = len(subs)
}
showUserPage(w, "stats", obj) showUserPage(w, "stats", obj)
return nil return nil
} }
func handleViewSubscribers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
c, err := app.db.GetCollection(vars["collection"])
if err != nil {
return err
}
filter := r.FormValue("filter")
flashes, _ := getSessionFlashes(app, w, r, nil)
obj := struct {
*UserPage
Collection CollectionNav
EmailSubs []*EmailSubscriber
Followers *[]RemoteUser
Silenced bool
Filter string
FederationEnabled bool
CanEmailSub bool
CanAddSubs bool
EmailSubsEnabled bool
}{
UserPage: NewUserPage(app, r, u, c.DisplayTitle()+" Subscribers", flashes),
Collection: CollectionNav{
Collection: c,
Path: r.URL.Path,
SingleUser: app.cfg.App.SingleUser,
},
Silenced: u.IsSilenced(),
Filter: filter,
FederationEnabled: app.cfg.App.Federation,
CanEmailSub: app.cfg.Email.Enabled(),
EmailSubsEnabled: c.EmailSubsEnabled(),
}
obj.Followers, err = app.db.GetAPFollowers(c)
if err != nil {
return err
}
obj.EmailSubs, err = app.db.GetEmailSubscribers(c.ID, true)
if err != nil {
return err
}
if obj.Filter == "" {
// Set permission to add email subscribers
//obj.CanAddSubs = app.db.GetUserAttribute(c.OwnerID, userAttrCanAddEmailSubs) == "1"
}
showUserPage(w, "subscribers", obj)
return nil
}
func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error { func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
fullUser, err := app.db.GetUserByID(u.ID) fullUser, err := app.db.GetUserByID(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("Unable to get user for settings: %s", err) log.Error("Unable to get user for settings: %s", err)
return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."} return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
} }
@ -1241,220 +1153,6 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
return nil return nil
} }
func viewResetPassword(app *App, w http.ResponseWriter, r *http.Request) error {
token := r.FormValue("t")
resetting := false
var userID int64 = 0
if token != "" {
// Show new password page
userID = app.db.GetUserFromPasswordReset(token)
if userID == 0 {
return impart.HTTPError{http.StatusNotFound, ""}
}
resetting = true
}
if r.Method == http.MethodPost {
newPass := r.FormValue("new-pass")
if newPass == "" {
// Send password reset email
return handleResetPasswordInit(app, w, r)
}
// Do actual password reset
// Assumes token has been validated above
err := doAutomatedPasswordChange(app, userID, newPass)
if err != nil {
return err
}
err = app.db.ConsumePasswordResetToken(token)
if err != nil {
log.Error("Couldn't consume token %s for user %d!!! %s", token, userID, err)
}
addSessionFlash(app, w, r, "Your password was reset. Now you can log in below.", nil)
return impart.HTTPError{http.StatusFound, "/login"}
}
f, _ := getSessionFlashes(app, w, r, nil)
// Show reset password page
d := struct {
page.StaticPage
Flashes []string
EmailEnabled bool
CSRFField template.HTML
Token string
IsResetting bool
IsSent bool
}{
StaticPage: pageForReq(app, r),
Flashes: f,
EmailEnabled: app.cfg.Email.Enabled(),
CSRFField: csrf.TemplateField(r),
Token: token,
IsResetting: resetting,
IsSent: r.FormValue("sent") == "1",
}
err := pages["reset.tmpl"].ExecuteTemplate(w, "base", d)
if err != nil {
log.Error("Unable to render password reset page: %v", err)
return err
}
return err
}
func doAutomatedPasswordChange(app *App, userID int64, newPass string) error {
// Do password reset
hashedPass, err := auth.HashPass([]byte(newPass))
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
}
// Do update
err = app.db.ChangePassphrase(userID, true, "", hashedPass)
if err != nil {
return err
}
return nil
}
func handleResetPasswordInit(app *App, w http.ResponseWriter, r *http.Request) error {
returnLoc := impart.HTTPError{http.StatusFound, "/reset"}
if !app.cfg.Email.Enabled() {
// Email isn't configured, so there's nothing to do; send back to the reset form, where they'll get an explanation
return returnLoc
}
ip := spam.GetIP(r)
alias := r.FormValue("alias")
u, err := app.db.GetUserForAuth(alias)
if err != nil {
if strings.IndexAny(alias, "@") > 0 {
addSessionFlash(app, w, r, ErrUserNotFoundEmail.Message, nil)
return returnLoc
}
addSessionFlash(app, w, r, ErrUserNotFound.Message, nil)
return returnLoc
}
if u.IsAdmin() {
// Prevent any reset emails on admin accounts
log.Error("Admin reset attempt", `Someone just tried to reset the password for an admin (ID %d - %s). IP address: %s`, u.ID, u.Username, ip)
return returnLoc
}
if u.Email.String == "" {
err := impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Please contact us (" + app.cfg.App.Host + "/contact) to reset your password."}
addSessionFlash(app, w, r, err.Message, nil)
return returnLoc
}
if isSet, _ := app.db.IsUserPassSet(u.ID); !isSet {
err = loginViaEmail(app, u.Username, "/me/settings")
if err != nil {
return err
}
addSessionFlash(app, w, r, "We've emailed you a link to log in with.", nil)
return returnLoc
}
token, err := app.db.CreatePasswordResetToken(u.ID)
if err != nil {
log.Error("Error resetting password: %s", err)
addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil)
return returnLoc
}
err = emailPasswordReset(app, u.EmailClear(app.keys), token)
if err != nil {
log.Error("Error emailing password reset: %s", err)
addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil)
return returnLoc
}
addSessionFlash(app, w, r, "We sent an email to the address associated with this account.", nil)
returnLoc.Message += "?sent=1"
return returnLoc
}
func emailPasswordReset(app *App, toEmail, token string) error {
// Send email
mlr, err := mailer.New(app.cfg.Email)
if err != nil {
return err
}
footerPara := "Didn't request this password reset? Your account is still safe, and you can safely ignore this email."
plainMsg := fmt.Sprintf("We received a request to reset your password on %s. Please click the following link to continue (or copy and paste it into your browser): %s/reset?t=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara)
m, err := mlr.NewMessage(app.cfg.App.SiteName+" <noreply-password@"+app.cfg.Email.Domain+">", "Reset Your "+app.cfg.App.SiteName+" Password", plainMsg, fmt.Sprintf("<%s>", toEmail))
if err != nil {
return err
}
m.AddTag("Password Reset")
m.SetHTML(fmt.Sprintf(`<html>
<body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
<div style="margin:0 auto; max-width: 40em; font-size: 1.2em;">
<h1 style="font-size:1.75em"><a style="text-decoration:none;color:#000;" href="%s">%s</a></h1>
<p>We received a request to reset your password on %s. Please click the following link to continue:</p>
<p style="font-size:1.2em;margin-bottom:1.5em;"><a href="%s/reset?t=%s">Reset your password</a></p>
<p style="font-size: 0.86em;margin:1em auto">%s</p>
</div>
</body>
</html>`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara))
return mlr.Send(m)
}
func loginViaEmail(app *App, alias, redirectTo string) error {
if !app.cfg.Email.Enabled() {
return fmt.Errorf("EMAIL ISN'T CONFIGURED on this server")
}
// Make sure user has added an email
// TODO: create a new func to just get user's email; "ForAuth" doesn't match here
u, _ := app.db.GetUserForAuth(alias)
if u == nil {
if strings.IndexAny(alias, "@") > 0 {
return ErrUserNotFoundEmail
}
return ErrUserNotFound
}
if u.Email.String == "" {
return impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Log in with password, instead."}
}
// Generate one-time login token
t, err := app.db.GetTemporaryOneTimeAccessToken(u.ID, 60*15, true)
if err != nil {
log.Error("Unable to generate token for email login: %s", err)
return impart.HTTPError{http.StatusInternalServerError, "Unable to generate token."}
}
// Send email
mlr, err := mailer.New(app.cfg.Email)
if err != nil {
return err
}
toEmail := u.EmailClear(app.keys)
footerPara := "This link will only work once and expires in 15 minutes. Didn't ask us to log in? You can safely ignore this email."
plainMsg := fmt.Sprintf("Log in to %s here: %s/login?to=%s&with=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, footerPara)
m, err := mlr.NewMessage(app.cfg.App.SiteName+" <noreply-login@"+app.cfg.Email.Domain+">", "Log in to "+app.cfg.App.SiteName, plainMsg, fmt.Sprintf("<%s>", toEmail))
if err != nil {
return err
}
m.AddTag("Email Login")
m.SetHTML(fmt.Sprintf(`<html>
<body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
<div style="margin:0 auto; max-width: 40em; font-size: 1.2em;">
<h1 style="font-size:1.75em"><a style="text-decoration:none;color:#000;" href="%s">%s</a></h1>
<p style="font-size:1.2em;margin-bottom:1.5em;text-align:center"><a href="%s/login?to=%s&with=%s">Log in to %s here</a>.</p>
<p style="font-size: 0.86em;color:#666;text-align:center;max-width:35em;margin:1em auto">%s</p>
</div>
</body>
</html>`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, app.cfg.App.SiteName, footerPara))
return mlr.Send(m)
}
func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error { func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
session, err := app.sessionStore.Get(r, "t") session, err := app.sessionStore.Get(r, "t")
if err != nil { if err != nil {

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -99,7 +100,7 @@ func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) err
} }
defer file.Close() defer file.Close()
tempFile, err := os.CreateTemp("", "post-upload-*.txt") tempFile, err := ioutil.TempFile("", "post-upload-*.txt")
if err != nil { if err != nil {
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename)) fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename))
log.Error("import file: create temp file %s: %v", formFile.Filename, err) log.Error("import file: create temp file %s: %v", formFile.Filename, err)

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -17,26 +17,22 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io/ioutil"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/activity/streams" "github.com/writeas/activity/streams"
"github.com/writeas/activityserve"
"github.com/writeas/httpsig" "github.com/writeas/httpsig"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitypub"
"github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/id" "github.com/writeas/web-core/id"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/web-core/silobridge"
) )
const ( const (
@ -46,11 +42,6 @@ const (
apCacheTime = time.Minute apCacheTime = time.Minute
) )
var (
apCollectionPostIRIRegex = regexp.MustCompile("/api/collections/([a-z0-9\\-]+)/posts/([a-z0-9\\-]+)$")
apDraftPostIRIRegex = regexp.MustCompile("/api/posts/([a-z0-9\\-]+)$")
)
var instanceColl *Collection var instanceColl *Collection
func initActivityPub(app *App) { func initActivityPub(app *App) {
@ -69,22 +60,7 @@ type RemoteUser struct {
ActorID string ActorID string
Inbox string Inbox string
SharedInbox string SharedInbox string
URL string
Handle string Handle string
Created time.Time
}
func (ru *RemoteUser) CreatedFriendly() string {
return ru.Created.Format("January 2, 2006")
}
func (ru *RemoteUser) EstimatedHandle() string {
if ru.Handle != "" {
return ru.Handle
}
username := filepath.Base(ru.ActorID)
host, _ := url.Parse(ru.ActorID)
return username + "@" + host.Host
} }
func (ru *RemoteUser) AsPerson() *activitystreams.Person { func (ru *RemoteUser) AsPerson() *activitystreams.Person {
@ -201,7 +177,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p) ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p)
ocp.OrderedItems = []interface{}{} ocp.OrderedItems = []interface{}{}
posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false, "") posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
for _, pp := range *posts { for _, pp := range *posts {
pp.Collection = res pp.Collection = res
o := pp.ActivityObject(app) o := pp.ActivityObject(app)
@ -357,60 +333,11 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
a := streams.NewAccept() a := streams.NewAccept()
p := c.PersonObject() p := c.PersonObject()
var to *url.URL var to *url.URL
var isFollow, isUnfollow, isLike, isUnlike bool var isFollow, isUnfollow bool
var likePostID, unlikePostID string
fullActor := &activitystreams.Person{} fullActor := &activitystreams.Person{}
var remoteUser *RemoteUser var remoteUser *RemoteUser
res := &streams.Resolver{ res := &streams.Resolver{
LikeCallback: func(l *streams.Like) error {
isLike = true
// 1) Use the Like concrete type here
// 2) Errors are propagated to res.Deserialize call below
m["@context"] = []string{activitystreams.Namespace}
b, _ := json.Marshal(m)
if debugging {
log.Info("Like: %s", b)
}
_, likeID := l.GetId()
if likeID == nil {
log.Error("Didn't resolve Like ID")
}
if p := l.HasObject(0); p == streams.NoPresence {
return fmt.Errorf("no object for Like activity at index 0")
}
obj := l.Raw().GetObjectIRI(0)
/*
// TODO: handle this more robustly
l.ResolveObject(&streams.Resolver{
LinkCallback: func(link *streams.Link) error {
return nil
},
}, 0)
*/
if obj == nil {
return fmt.Errorf("didn't get ObjectIRI to Like")
}
likePostID, err = parsePostIDFromURL(app, obj)
if err != nil {
return err
}
// Finally, get actor information
_, from := l.GetActor(0)
if from == nil {
return fmt.Errorf("No valid actor string")
}
fullActor, remoteUser, err = getActor(app, from.String())
if err != nil {
return err
}
return nil
},
FollowCallback: func(f *streams.Follow) error { FollowCallback: func(f *streams.Follow) error {
isFollow = true isFollow = true
@ -436,17 +363,6 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
a.AppendObject(f.Raw()) a.AppendObject(f.Raw())
_, to = f.GetActor(0) _, to = f.GetActor(0)
obj := f.Raw().GetObjectIRI(0) obj := f.Raw().GetObjectIRI(0)
if obj == nil {
if debugging {
log.Error("GetObjectIRI on Follow for actor is empty; trying object")
}
ao := f.Raw().GetObject(0)
if ao == nil {
log.Error("Fell back to GetObject and none parsed, so no actor ID! Follow request probably FAILED!")
} else {
obj = ao.GetId()
}
}
a.AppendActor(obj) a.AppendActor(obj)
// First get actor information // First get actor information
@ -460,6 +376,8 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
return impart.RenderActivityJSON(w, m, http.StatusOK) return impart.RenderActivityJSON(w, m, http.StatusOK)
}, },
UndoCallback: func(u *streams.Undo) error { UndoCallback: func(u *streams.Undo) error {
isUnfollow = true
m["@context"] = []string{activitystreams.Namespace} m["@context"] = []string{activitystreams.Namespace}
b, _ := json.Marshal(m) b, _ := json.Marshal(m)
if debugging { if debugging {
@ -467,37 +385,6 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
} }
a.AppendObject(u.Raw()) a.AppendObject(u.Raw())
// Check type -- we handle Undo:Like and Undo:Follow
_, err := u.ResolveObject(&streams.Resolver{
LikeCallback: func(like *streams.Like) error {
isUnlike = true
_, from := like.GetActor(0)
obj := like.Raw().GetObjectIRI(0)
if obj == nil {
return fmt.Errorf("didn't get ObjectIRI for Undo Like")
}
unlikePostID, err = parsePostIDFromURL(app, obj)
if err != nil {
return err
}
fullActor, remoteUser, err = getActor(app, from.String())
if err != nil {
return err
}
return nil
},
// TODO: add FollowCallback for more robust handling
}, 0)
if err != nil {
return err
}
if isUnlike {
return nil
}
isUnfollow = true
_, to = u.GetActor(0) _, to = u.GetActor(0)
// TODO: get actor from object.object, not object // TODO: get actor from object.object, not object
obj := u.Raw().GetObjectIRI(0) obj := u.Raw().GetObjectIRI(0)
@ -530,81 +417,6 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
return err return err
} }
// Handle synchronous activities
if isLike {
t, err := app.db.Begin()
if err != nil {
log.Error("Unable to start transaction: %v", err)
return fmt.Errorf("unable to start transaction: %v", err)
}
var remoteUserID int64
if remoteUser != nil {
remoteUserID = remoteUser.ID
} else {
remoteUserID, err = apAddRemoteUser(app, t, fullActor)
}
// Add like
_, err = t.Exec("INSERT INTO remote_likes (post_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", likePostID, remoteUserID)
if err != nil {
if !app.db.isDuplicateKeyErr(err) {
t.Rollback()
log.Error("Couldn't add like in DB: %v\n", err)
return fmt.Errorf("Couldn't add like in DB: %v", err)
} else {
t.Rollback()
log.Error("Couldn't add like in DB: %v\n", err)
return fmt.Errorf("Couldn't add like in DB: %v", err)
}
}
err = t.Commit()
if err != nil {
t.Rollback()
log.Error("Rolling back after Commit(): %v\n", err)
return fmt.Errorf("Rolling back after Commit(): %v\n", err)
}
if debugging {
log.Info("Successfully liked post %s by remote user %s", likePostID, remoteUser.URL)
}
return impart.RenderActivityJSON(w, "", http.StatusOK)
} else if isUnlike {
t, err := app.db.Begin()
if err != nil {
log.Error("Unable to start transaction: %v", err)
return fmt.Errorf("unable to start transaction: %v", err)
}
var remoteUserID int64
if remoteUser != nil {
remoteUserID = remoteUser.ID
} else {
remoteUserID, err = apAddRemoteUser(app, t, fullActor)
}
// Remove like
_, err = t.Exec("DELETE FROM remote_likes WHERE post_id = ? AND remote_user_id = ?", unlikePostID, remoteUserID)
if err != nil {
t.Rollback()
log.Error("Couldn't delete Like from DB: %v\n", err)
return fmt.Errorf("Couldn't delete Like from DB: %v", err)
}
err = t.Commit()
if err != nil {
t.Rollback()
log.Error("Rolling back after Commit(): %v\n", err)
return fmt.Errorf("Rolling back after Commit(): %v\n", err)
}
if debugging {
log.Info("Successfully un-liked post %s by remote user %s", unlikePostID, remoteUser.URL)
}
return impart.RenderActivityJSON(w, "", http.StatusOK)
}
go func() { go func() {
if to == nil { if to == nil {
if debugging { if debugging {
@ -639,9 +451,8 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
if remoteUser != nil { if remoteUser != nil {
followerID = remoteUser.ID followerID = remoteUser.ID
} else { } else {
// TODO: use apAddRemoteUser() here, instead!
// Add follower locally, since it wasn't found before // Add follower locally, since it wasn't found before
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox, fullActor.URL) res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
if err != nil { if err != nil {
// if duplicate key, res will be nil and panic on // if duplicate key, res will be nil and panic on
// res.LastInsertId below // res.LastInsertId below
@ -738,7 +549,7 @@ func makeActivityPost(hostName string, p *activitystreams.Person, url string, m
defer resp.Body.Close() defer resp.Body.Close()
} }
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
@ -790,7 +601,7 @@ func resolveIRI(hostName, url string) ([]byte, error) {
defer resp.Body.Close() defer resp.Body.Close()
} }
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -833,7 +644,10 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
for si, instFolls := range inboxes { for si, instFolls := range inboxes {
na.CC = []string{} na.CC = []string{}
na.CC = append(na.CC, instFolls...) for _, f := range instFolls {
na.CC = append(na.CC, f)
}
da := activitystreams.NewDeleteActivity(na) da := activitystreams.NewDeleteActivity(na)
// Make the ID unique to ensure it works in Pleroma // Make the ID unique to ensure it works in Pleroma
// See: https://git.pleroma.social/pleroma/pleroma/issues/1481 // See: https://git.pleroma.social/pleroma/pleroma/issues/1481
@ -899,11 +713,12 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
// add all followers from that instance // add all followers from that instance
// to the CC field // to the CC field
na.CC = []string{} na.CC = []string{}
na.CC = append(na.CC, instFolls...) for _, f := range instFolls {
na.CC = append(na.CC, f)
}
// create a new "Create" activity // create a new "Create" activity
// with our article as object // with our article as object
if isUpdate { if isUpdate {
na.Updated = &p.Updated
activity = activitystreams.NewUpdateActivity(na) activity = activitystreams.NewUpdateActivity(na)
} else { } else {
activity = activitystreams.NewCreateActivity(na) activity = activitystreams.NewCreateActivity(na)
@ -949,8 +764,8 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) { func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
u := RemoteUser{ActorID: actorID} u := RemoteUser{ActorID: actorID}
var urlVal, handle sql.NullString var handle sql.NullString
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, url, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &urlVal, &handle) err := app.db.QueryRow("SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &handle)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."} return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
@ -959,7 +774,6 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
return nil, err return nil, err
} }
u.URL = urlVal.String
u.Handle = handle.String u.Handle = handle.String
return &u, nil return &u, nil
@ -969,8 +783,7 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
// from the @user@server.tld handle // from the @user@server.tld handle
func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) { func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) {
u := RemoteUser{Handle: handle} u := RemoteUser{Handle: handle}
var urlVal sql.NullString err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox)
err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox, url FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox, &urlVal)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrRemoteUserNotFound return nil, ErrRemoteUserNotFound
@ -978,7 +791,6 @@ func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) {
log.Error("Couldn't get remote user %s: %v", handle, err) log.Error("Couldn't get remote user %s: %v", handle, err)
return nil, err return nil, err
} }
u.URL = urlVal.String
return &u, nil return &u, nil
} }
@ -993,28 +805,13 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser,
log.Info("Not found; fetching actor %s remotely", actorIRI) log.Info("Not found; fetching actor %s remotely", actorIRI)
actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI) actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI)
if err != nil { if err != nil {
log.Error("Unable to get base actor! %v", err) log.Error("Unable to get actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}
} }
if err := unmarshalActor(actorResp, actor); err != nil { if err := unmarshalActor(actorResp, actor); err != nil {
log.Error("Unable to unmarshal base actor! %v", err) log.Error("Unable to unmarshal actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."}
} }
baseActor := &activitystreams.Person{}
if err := unmarshalActor(actorResp, baseActor); err != nil {
log.Error("Unable to unmarshal actual actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actual actor."}
}
// Fetch the actual actor using the owner field from the publicKey object
actualActorResp, err := resolveIRI(app.cfg.App.Host, baseActor.PublicKey.Owner)
if err != nil {
log.Error("Unable to get actual actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actual actor."}
}
if err := unmarshalActor(actualActorResp, actor); err != nil {
log.Error("Unable to unmarshal actual actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actual actor."}
}
} else { } else {
return nil, nil, err return nil, nil, err
} }
@ -1027,69 +824,6 @@ func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser,
return actor, remoteUser, nil return actor, remoteUser, nil
} }
func GetProfileURLFromHandle(app *App, handle string) (string, error) {
handle = strings.TrimLeft(handle, "@")
actorIRI := ""
parts := strings.Split(handle, "@")
if len(parts) != 2 {
return "", fmt.Errorf("invalid handle format")
}
domain := parts[1]
// Check non-AP instances
if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" {
return siloProfileURL, nil
}
remoteUser, err := getRemoteUserFromHandle(app, handle)
if err != nil {
// can't find using handle in the table but the table may already have this user without
// handle from a previous version
// TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all
actorIRI = RemoteLookup(handle)
_, errRemoteUser := getRemoteUser(app, actorIRI)
// if it exists then we need to update the handle
if errRemoteUser == nil {
_, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI)
if err != nil {
log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI)
}
} else {
// this probably means we don't have the user in the table so let's try to insert it
// here we need to ask the server for the inboxes
remoteActor, err := activityserve.NewRemoteActor(actorIRI)
if err != nil {
log.Error("Couldn't fetch remote actor: %v", err)
}
if debugging {
log.Info("Got remote actor: %s %s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle)
}
_, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url, handle) VALUES(?, ?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle)
if err != nil {
log.Error("Couldn't insert remote user: %v", err)
return "", err
}
actorIRI = remoteActor.URL()
}
} else if remoteUser.URL == "" {
log.Info("Remote user %s URL empty, fetching", remoteUser.ActorID)
newRemoteActor, err := activityserve.NewRemoteActor(remoteUser.ActorID)
if err != nil {
log.Error("Couldn't fetch remote actor: %v", err)
} else {
_, err := app.db.Exec("UPDATE remoteusers SET url = ? WHERE actor_id = ?", newRemoteActor.URL(), remoteUser.ActorID)
if err != nil {
log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI)
} else {
actorIRI = newRemoteActor.URL()
}
}
} else {
actorIRI = remoteUser.URL
}
return actorIRI, nil
}
// unmarshal actor normalizes the actor response to conform to // unmarshal actor normalizes the actor response to conform to
// the type Person from github.com/writeas/web-core/activitysteams // the type Person from github.com/writeas/web-core/activitysteams
// //
@ -1135,34 +869,6 @@ func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error {
return nil return nil
} }
func parsePostIDFromURL(app *App, u *url.URL) (string, error) {
// Get post ID from URL
var collAlias, slug, postID string
if m := apCollectionPostIRIRegex.FindStringSubmatch(u.String()); len(m) == 3 {
collAlias = m[1]
slug = m[2]
} else if m = apDraftPostIRIRegex.FindStringSubmatch(u.String()); len(m) == 2 {
postID = m[1]
} else {
return "", fmt.Errorf("unable to match objectIRI: %s", u)
}
// Get postID if all we have is collection and slug
if collAlias != "" && slug != "" {
c, err := app.db.GetCollection(collAlias)
if err != nil {
return "", err
}
p, err := app.db.GetPost(slug, c.ID)
if err != nil {
return "", err
}
postID = p.ID
}
return postID, nil
}
func setCacheControl(w http.ResponseWriter, ttl time.Duration) { func setCacheControl(w http.ResponseWriter, ttl time.Duration) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", ttl.Seconds())) w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", ttl.Seconds()))
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -13,7 +13,6 @@ package writefreely
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"runtime" "runtime"
"strconv" "strconv"
@ -103,16 +102,13 @@ func NewAdminPage(app *App) *AdminPage {
return ap return ap
} }
func (c instanceContent) UpdatedFriendly() template.HTML { func (c instanceContent) UpdatedFriendly() string {
/* /*
// TODO: accept a locale in this method and use that for the format // TODO: accept a locale in this method and use that for the format
var loc monday.Locale = monday.LocaleEnUS var loc monday.Locale = monday.LocaleEnUS
return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc) return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
*/ */
if c.Updated.IsZero() { return c.Updated.Format("January 2, 2006, 3:04 PM")
return "<em>Never</em>"
}
return template.HTML(c.Updated.Format("January 2, 2006, 3:04 PM"))
} }
func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
@ -208,7 +204,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
p.Flashes, _ = getSessionFlashes(app, w, r, nil) p.Flashes, _ = getSessionFlashes(app, w, r, nil)
p.TotalUsers = app.db.GetAllUsersCount() p.TotalUsers = app.db.GetAllUsersCount()
ttlPages := (p.TotalUsers - 1) / adminUsersPerPage + 1 ttlPages := p.TotalUsers / adminUsersPerPage
p.TotalPages = []int{} p.TotalPages = []int{}
for i := 1; i <= int(ttlPages); i++ { for i := 1; i <= int(ttlPages); i++ {
p.TotalPages = append(p.TotalPages, i) p.TotalPages = append(p.TotalPages, i)
@ -430,9 +426,9 @@ func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Requ
} }
// Add in default pages // Add in default pages
var hasAbout, hasContact, hasPrivacy bool var hasAbout, hasPrivacy bool
for i, c := range p.Pages { for i, c := range p.Pages {
if hasAbout && hasContact && hasPrivacy { if hasAbout && hasPrivacy {
break break
} }
if c.ID == "about" { if c.ID == "about" {
@ -440,11 +436,6 @@ func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Requ
if !c.Title.Valid { if !c.Title.Valid {
p.Pages[i].Title = defaultAboutTitle(app.cfg) p.Pages[i].Title = defaultAboutTitle(app.cfg)
} }
} else if c.ID == "contact" {
hasContact = true
if !c.Title.Valid {
p.Pages[i].Title = defaultContactTitle()
}
} else if c.ID == "privacy" { } else if c.ID == "privacy" {
hasPrivacy = true hasPrivacy = true
if !c.Title.Valid { if !c.Title.Valid {
@ -460,13 +451,6 @@ func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Requ
Updated: defaultPageUpdatedTime, Updated: defaultPageUpdatedTime,
}) })
} }
if !hasContact {
p.Pages = append(p.Pages, &instanceContent{
ID: "contact",
Title: defaultContactTitle(),
Content: defaultContactPage(app),
})
}
if !hasPrivacy { if !hasPrivacy {
p.Pages = append(p.Pages, &instanceContent{ p.Pages = append(p.Pages, &instanceContent{
ID: "privacy", ID: "privacy",
@ -505,8 +489,6 @@ func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Reque
// Get pre-defined pages, or select slug // Get pre-defined pages, or select slug
if slug == "about" { if slug == "about" {
p.Content, err = getAboutPage(app) p.Content, err = getAboutPage(app)
} else if slug == "contact" {
p.Content, err = getContactPage(app)
} else if slug == "privacy" { } else if slug == "privacy" {
p.Content, err = getPrivacyPage(app) p.Content, err = getPrivacyPage(app)
} else if slug == "landing" { } else if slug == "landing" {
@ -541,7 +523,7 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req
id := vars["page"] id := vars["page"]
// Validate // Validate
if id != "about" && id != "contact" && id != "privacy" && id != "landing" && id != "reader" { if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
return impart.HTTPError{http.StatusNotFound, "No such page."} return impart.HTTPError{http.StatusNotFound, "No such page."}
} }

120
app.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -13,10 +13,9 @@ package writefreely
import ( import (
"crypto/tls" "crypto/tls"
"database/sql" "database/sql"
_ "embed"
"fmt" "fmt"
"html/template" "html/template"
"net" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -36,20 +35,18 @@ import (
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"golang.org/x/crypto/acme/autocert"
"github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/author"
"github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/config"
"github.com/writefreely/writefreely/key" "github.com/writefreely/writefreely/key"
"github.com/writefreely/writefreely/migrations" "github.com/writefreely/writefreely/migrations"
"github.com/writefreely/writefreely/page" "github.com/writefreely/writefreely/page"
"golang.org/x/crypto/acme/autocert"
) )
const ( const (
staticDir = "static" staticDir = "static"
assumedTitleLen = 80 assumedTitleLen = 80
postsPerPage = 10 postsPerPage = 10
postsPerArchPage = 40
serverSoftware = "WriteFreely" serverSoftware = "WriteFreely"
softwareURL = "https://writefreely.org" softwareURL = "https://writefreely.org"
@ -59,7 +56,7 @@ var (
debugging bool debugging bool
// Software version can be set from git env using -ldflags // Software version can be set from git env using -ldflags
softwareVer = "0.15.1" softwareVer = "0.13.1"
// DEPRECATED VARS // DEPRECATED VARS
isSingleUser bool isSingleUser bool
@ -177,7 +174,7 @@ func (app *App) LoadKeys() error {
executable = filepath.Base(executable) executable = filepath.Base(executable)
} }
app.keys.EmailKey, err = os.ReadFile(emailKeyPath) app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
if err != nil { if err != nil {
return err return err
} }
@ -185,7 +182,7 @@ func (app *App) LoadKeys() error {
if debugging { if debugging {
log.Info(" %s", cookieAuthKeyPath) log.Info(" %s", cookieAuthKeyPath)
} }
app.keys.CookieAuthKey, err = os.ReadFile(cookieAuthKeyPath) app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
if err != nil { if err != nil {
return err return err
} }
@ -193,7 +190,7 @@ func (app *App) LoadKeys() error {
if debugging { if debugging {
log.Info(" %s", cookieKeyPath) log.Info(" %s", cookieKeyPath)
} }
app.keys.CookieKey, err = os.ReadFile(cookieKeyPath) app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath)
if err != nil { if err != nil {
return err return err
} }
@ -201,7 +198,7 @@ func (app *App) LoadKeys() error {
if debugging { if debugging {
log.Info(" %s", csrfKeyPath) log.Info(" %s", csrfKeyPath)
} }
app.keys.CSRFKey, err = os.ReadFile(csrfKeyPath) app.keys.CSRFKey, err = ioutil.ReadFile(csrfKeyPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Error(`Missing key: %s. log.Error(`Missing key: %s.
@ -318,7 +315,7 @@ func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *te
}{ }{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
} }
if r.URL.Path == "/about" || r.URL.Path == "/contact" || r.URL.Path == "/privacy" { if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
var c *instanceContent var c *instanceContent
var err error var err error
@ -329,12 +326,6 @@ func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *te
p.AboutStats = &InstanceStats{} p.AboutStats = &InstanceStats{}
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts() p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections() p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
} else if r.URL.Path == "/contact" {
c, err = getContactPage(app)
if c.Updated.IsZero() {
// Page was never set up, so return 404
return ErrPostNotFound
}
} else { } else {
c, err = getPrivacyPage(app) c, err = getPrivacyPage(app)
} }
@ -365,11 +356,6 @@ func pageForReq(app *App, r *http.Request) page.StaticPage {
Version: "v" + softwareVer, Version: "v" + softwareVer,
} }
// Use custom style, if file exists
if _, err := os.Stat(filepath.Join(app.cfg.Server.StaticParentDir, staticDir, "local", "custom.css")); err == nil {
p.CustomCSS = true
}
// Add user information, if given // Add user information, if given
var u *User var u *User
accessToken := r.FormValue("t") accessToken := r.FormValue("t")
@ -429,13 +415,6 @@ func Initialize(apper Apper, debug bool) (*App, error) {
initActivityPub(apper.App()) initActivityPub(apper.App())
if apper.App().cfg.Email.Enabled() {
log.Info("Starting publish jobs queue...")
go startPublishJobsQueue(apper.App())
} else {
log.Error("[FAILED] Starting publish jobs queue: no email provider is configured.")
}
// Handle local timeline, if enabled // Handle local timeline, if enabled
if apper.App().cfg.App.LocalTimeline { if apper.App().cfg.App.LocalTimeline {
log.Info("Initializing local timeline...") log.Info("Initializing local timeline...")
@ -524,41 +503,9 @@ requests. We recommend supplying a valid host name.`)
err = http.ListenAndServeTLS(fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r) err = http.ListenAndServeTLS(fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r)
} }
} else { } else {
network := "tcp" log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
protocol := "http"
if strings.HasPrefix(bindAddress, "/") {
network = "unix"
protocol = "http+unix"
// old sockets will remain after server closes;
// we need to delete them in order to open new ones
err = os.Remove(bindAddress)
if err != nil && !os.IsNotExist(err) {
log.Error("%s already exists but could not be removed: %v", bindAddress, err)
os.Exit(1)
}
} else {
bindAddress = fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port)
}
log.Info("Serving on %s://%s", protocol, bindAddress)
log.Info("---") log.Info("---")
listener, err := net.Listen(network, bindAddress) err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
if err != nil {
log.Error("Could not bind to address: %v", err)
os.Exit(1)
}
if network == "unix" {
err = os.Chmod(bindAddress, 0o666)
if err != nil {
log.Error("Could not update socket permissions: %v", err)
os.Exit(1)
}
}
defer listener.Close()
err = http.Serve(listener, r)
} }
if err != nil { if err != nil {
log.Error("Unable to start: %v", err) log.Error("Unable to start: %v", err)
@ -582,8 +529,8 @@ func (app *App) InitDecoder() {
// tests the connection. // tests the connection.
func ConnectToDatabase(app *App) error { func ConnectToDatabase(app *App) error {
// Check database configuration // Check database configuration
if app.cfg.Database.Type == driverMySQL && app.cfg.Database.User == "" { if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
return fmt.Errorf("Database user not set.") return fmt.Errorf("Database user or password not set.")
} }
if app.cfg.Database.Host == "" { if app.cfg.Database.Host == "" {
app.cfg.Database.Host = "localhost" app.cfg.Database.Host = "localhost"
@ -865,16 +812,6 @@ func connectToDatabase(app *App) {
func shutdown(app *App) { func shutdown(app *App) {
log.Info("Closing database connection...") log.Info("Closing database connection...")
app.db.Close() app.db.Close()
if strings.HasPrefix(app.cfg.Server.Bind, "/") {
// Clean up socket
log.Info("Removing socket file...")
err := os.Remove(app.cfg.Server.Bind)
if err != nil {
log.Error("Unable to remove socket: %s", err)
os.Exit(1)
}
log.Info("Success.")
}
} }
// CreateUser creates a new admin or normal user from the given credentials. // CreateUser creates a new admin or normal user from the given credentials.
@ -889,12 +826,12 @@ func CreateUser(apper Apper, username, password string, isAdmin bool) error {
if isAdmin { if isAdmin {
// Abort if trying to create admin user, but one already exists // Abort if trying to create admin user, but one already exists
if firstUser != nil { if firstUser != nil {
return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely user create [USER]:[PASSWORD]", firstUser.Username) return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely --create-user", firstUser.Username)
} }
} else { } else {
// Abort if trying to create regular user, but no admin exists yet // Abort if trying to create regular user, but no admin exists yet
if firstUser == nil { if firstUser == nil {
return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely user create --admin [USER]:[PASSWORD]") return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely --create-admin")
} }
} }
@ -937,18 +874,15 @@ func CreateUser(apper Apper, username, password string, isAdmin bool) error {
return nil return nil
} }
//go:embed schema.sql
var schemaSql string
//go:embed sqlite.sql
var sqliteSql string
func adminInitDatabase(app *App) error { func adminInitDatabase(app *App) error {
var schema string schemaFileName := "schema.sql"
if app.cfg.Database.Type == driverSQLite { if app.cfg.Database.Type == driverSQLite {
schema = sqliteSql schemaFileName = "sqlite.sql"
} else { }
schema = schemaSql
schema, err := Asset(schemaFileName)
if err != nil {
return fmt.Errorf("Unable to load schema file: %v", err)
} }
tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`") tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
@ -964,7 +898,7 @@ func adminInitDatabase(app *App) error {
} else { } else {
log.Info("Creating table ??? (Weird query) No match in: %v", parts) log.Info("Creating table ??? (Weird query) No match in: %v", parts)
} }
_, err := app.db.Exec(q) _, err = app.db.Exec(q)
if err != nil { if err != nil {
log.Error("%s", err) log.Error("%s", err)
} else { } else {
@ -974,7 +908,7 @@ func adminInitDatabase(app *App) error {
// Set up migrations table // Set up migrations table
log.Info("Initializing appmigrations table...") log.Info("Initializing appmigrations table...")
err := migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName)) err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil { if err != nil {
return fmt.Errorf("Unable to set initial migrations: %v", err) return fmt.Errorf("Unable to set initial migrations: %v", err)
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -11,7 +11,6 @@
package author package author
import ( import (
"github.com/writeas/web-core/log"
"github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/config"
"os" "os"
"path/filepath" "path/filepath"
@ -114,17 +113,10 @@ func IsValidUsername(cfg *config.Config, username string) bool {
// Username is invalid if page with the same name exists. So traverse // Username is invalid if page with the same name exists. So traverse
// available pages, adding them to reservedUsernames map that'll be checked // available pages, adding them to reservedUsernames map that'll be checked
// later. // later.
err := filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error { filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error {
if err != nil {
return err
}
reservedUsernames[i.Name()] = true reservedUsernames[i.Name()] = true
return nil return nil
}) })
if err != nil {
log.Error("[IMPORTANT WARNING]: Could not determine IsValidUsername! %s", err)
return false
}
// Username is invalid if it is reserved! // Username is invalid if it is reserved!
if _, reserved := reservedUsernames[username]; reserved { if _, reserved := reservedUsernames[username]; reserved {

106
bindata-lib.go Normal file
View file

@ -0,0 +1,106 @@
// +build wflib
package writefreely
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
func bindata_read(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
return buf.Bytes(), nil
}
var _schema_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x59\x5f\x6f\xa3\x38\x10\x7f\xef\xa7\xf0\xdb\xa6\x52\x23\x6d\x7a\xdd\xaa\xba\xd3\x3e\x64\x53\x76\x2f\xba\x94\xee\x25\x44\xba\x7d\x02\x03\x93\xd4\xaa\xb1\x91\x6d\x92\xe6\xdb\x9f\x8c\x49\x08\x86\x24\xd0\xdb\x3b\x71\x7d\x2a\xcc\x6f\x8c\xfd\x9b\x3f\x9e\x99\x0c\x87\x57\xc3\x21\x7a\xc4\x0a\x87\x58\xc2\xaf\x28\xd8\x0a\xa2\x60\x25\x00\xe8\x2e\xb8\x1a\x0e\xaf\xb4\x78\xf8\xce\x3f\xad\xac\xf5\x3d\x1c\x52\x40\x52\x89\x2c\x52\x99\x00\xb4\xe2\x02\xa9\xfc\x5d\x80\xa3\x08\xa4\x54\xfc\x15\x98\x34\xdf\x9b\xcc\x9d\xb1\xe7\x20\x6f\xfc\x65\xe6\xa0\xe9\x57\xe4\x3e\x7b\xc8\xf9\x6b\xba\xf0\x16\x16\x1a\x0d\xae\x10\x0a\xf2\x87\x00\x85\x84\x61\xb1\x1b\x8c\xee\xaf\x73\x05\x77\x39\x9b\xdd\x68\x71\x26\x41\xf8\x24\x0e\x10\x61\x6a\x60\x0b\x65\x16\xf3\x00\x29\xc2\x76\x5a\x3a\x2a\xa5\xe8\xd1\xf9\x3a\x5e\xce\x3c\xf4\xe1\xe3\x87\x1c\xc9\x19\xf8\x8a\x24\xd0\x0e\x1d\x09\xc0\x0a\xe2\x00\xc5\x58\x81\x56\xab\x43\x27\xcb\xf9\xdc\x71\x3d\xdf\x9b\x3e\x39\x0b\x6f\xfc\xf4\x3d\x57\x84\xb7\x94\x08\x90\x47\x8a\x7b\x7c\xf5\x40\x78\x0d\x4c\x05\x68\x83\x45\xf4\x82\xc5\xe0\xf6\xd3\xa7\xeb\x1a\xf2\xfb\x7c\xfa\x34\x9e\xff\x40\x7f\x38\x3f\xd0\xa0\xa0\xe9\xfa\xea\x1a\x39\xee\xb7\xa9\xeb\x7c\x9e\x32\xc6\x1f\xbf\x94\xfb\xf9\x7d\x3c\x5f\x38\xde\x67\x8a\x15\x61\xa3\xdf\xfe\x75\xb3\xa7\x69\xc4\x99\xd2\xa7\xb8\x6c\xf4\x12\x6b\x4c\xae\xcd\xb9\x3f\xfa\x2f\xb6\x4d\x0f\xd0\x04\x62\x92\x25\x0a\xde\x54\x7e\xb8\xf1\xc4\x73\xe6\x68\xe1\x78\x28\x53\xab\x07\x34\x79\x9e\xcd\xf4\x17\xf5\x83\x1f\x12\x66\x79\x4d\x1a\xbf\xcb\x80\x55\xce\x49\xdc\x2b\xc2\x13\xb2\x16\x58\x11\xde\x18\x68\x16\xc0\x10\xbd\x01\x21\x09\x67\x26\x78\x46\x23\x8b\x69\x03\x6f\x64\x29\x97\x0b\x90\x19\x55\x01\xca\x4d\xb0\x97\xf4\x85\x8f\x88\x53\x0a\x91\x3e\x2c\x56\x4a\x90\x30\x53\xd0\x22\xff\x34\x6a\x19\xae\x4a\xd1\xc9\x74\x73\xd0\x29\xdd\x77\x74\xfb\x60\x81\x36\x98\x66\x60\x85\x76\xdd\x7f\x93\xf0\xae\xe2\xc2\x49\x78\x57\xf3\xe2\xaa\x33\x56\xf7\x77\x73\xb4\x99\xde\xf8\x68\xb9\xc5\x57\xd8\x75\xb2\x46\x8e\x6f\x6d\x87\x34\x0b\x29\x89\xfc\x57\xd8\x05\x28\xa4\x3c\xb4\xa4\x82\x6c\xb0\x82\x13\xe2\x73\xa4\xf6\x90\xc8\x14\x4b\xb9\xe5\x22\xee\xc4\x66\xa9\xd4\x9e\xd2\x42\x25\x40\xb9\xd7\xde\x7f\xbc\xfe\x3f\xb3\x26\x20\x26\x02\x22\xd5\x89\xb5\x52\xc9\xb0\x96\x0a\xd8\xf8\x98\x12\x2c\x8f\xc2\xfd\xa3\x45\x4c\xc0\x60\x7b\x11\x54\x65\xef\x68\xdd\x1e\x52\xd7\x89\x32\x79\x74\xa1\x5b\x5e\x85\xc6\x4b\xef\xd9\x9f\xba\x93\xb9\xf3\xe4\xb8\x9e\xc9\x9f\x0d\x3c\xb5\x4f\x8d\xb5\x4a\x4a\x11\x45\x7f\x4e\xa6\x0d\x62\x90\x91\x20\xa9\xca\x2f\xcb\xc3\xfe\xee\x3b\xed\xaf\x5a\x99\xaa\x1d\x05\x5f\xbe\x00\x14\x17\xa8\x79\x9b\x7f\xa4\xb8\x51\x5b\xaf\x9c\xab\xae\xb8\x48\xf0\x51\xc9\xf8\x50\x2f\x18\x4d\xe6\x8b\x76\x8d\x35\xae\xa9\x82\xb7\xec\x4c\x35\xbd\x21\xb0\xf5\x23\x9e\xe9\xe2\xab\x41\x5e\xaf\x8d\xf4\xdb\xa5\x3b\xfd\x73\xe9\xe4\x2f\xf7\xf6\x1d\x04\x3d\xf3\xee\x94\xcb\x36\xa9\xc0\xc0\x4a\x8f\x2e\x9c\xc0\xee\x39\x68\xb6\xb6\x7c\xb8\x66\x88\x84\xc7\x64\xb5\xf3\x8b\xd6\xc6\xd4\xb9\xb7\x0d\x38\xed\x07\x3e\x4e\x53\xc0\x02\xb3\x08\x0a\xe8\x5d\x53\x67\xc2\xb8\x48\x4c\x73\x42\x31\x5b\x67\x78\xbd\x47\x37\xad\x2b\x14\xad\x38\xc1\x4f\xf0\x94\xda\x12\xcd\x97\x4a\xfd\x4b\x84\x31\x88\xfd\x94\x4b\x62\xa2\xeb\xe8\x8b\x4b\x77\x31\xfd\xe6\x3a\x8f\x0d\x8b\xef\x1b\x30\x5d\x95\x4a\x85\x93\xb4\x6d\x07\x76\xa8\xfc\x3b\x6b\x5e\x70\x7f\x3b\xdd\xfc\x93\xec\x70\xe8\x71\xba\x25\x82\x8e\xe1\x48\x62\xdf\x38\x6b\xbd\x78\xcc\xdf\xd7\x14\x4a\xa3\x0f\xca\xff\x6f\x0e\x6b\xe7\x98\xc2\x73\x0a\xd4\xde\x8f\x6e\x7a\xd5\x2b\x09\x48\xb8\x82\x15\xa7\x94\x6f\x5b\xc4\x7d\x15\x7e\xb2\x64\xaa\xf5\x4f\x46\xcf\xaf\x4c\x28\x6a\xa0\xd3\xa3\x84\xcb\x25\xbe\xf5\x81\x9e\xf1\xab\xb7\xd5\xae\xce\xb7\xf0\xf5\x21\x40\x7e\x75\x77\xe7\xf6\x6c\x1f\x70\x39\x3e\x8c\xc5\x0f\x1e\xdf\x7f\xb6\x3b\x51\x6d\xd7\x66\xc7\xec\x35\x16\x67\x91\xe2\x86\x8a\xd3\x56\x21\x2c\xe4\x6f\xe7\x00\xf2\x05\x0b\x88\xfd\x4b\xb8\xcb\xb6\xb1\xe2\x6f\x50\x6e\xaf\x37\x76\xd1\x24\x77\x99\x3d\x58\x78\x63\x9d\xb3\xe3\xcd\x86\x79\xc3\xfd\xdd\x7f\x34\x6e\xd8\x6f\xac\x97\x83\x06\xbd\x39\xc2\x36\xa4\x99\xf7\x8a\xd8\x2a\xe7\x6c\x8a\xab\x75\x4e\x7d\x44\x86\xdf\x74\x42\x90\x01\x92\x09\xa6\xf4\x64\x2d\x74\x36\xc9\xb7\x99\x0a\x13\x86\x23\x45\x36\xcd\xf3\xe9\x3e\xd1\xde\xd2\xd1\x3b\x76\x86\x5a\x85\xe1\x04\xde\xdd\x1c\x5e\x1a\x66\x54\x57\x32\x7c\x1d\x16\x32\x8f\xf5\x75\x20\xc1\x84\xe6\x5b\x2a\x7e\x9d\x68\x9c\xd3\xbf\xfb\xd7\x82\xcb\x59\xb0\xa4\x65\x50\xfe\xdf\xab\x28\x94\x26\xce\xe2\x53\x61\x78\x90\x17\xee\x90\x3f\xf9\x27\xc3\xf1\xe4\x7d\xdf\xfa\xcc\x7f\x07\x00\x00\xff\xff\xbe\x79\x68\xa8\x10\x1b\x00\x00")
func schema_sql() ([]byte, error) {
return bindata_read(
_schema_sql,
"schema.sql",
)
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"schema.sql": schema_sql,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"schema.sql": &_bintree_t{schema_sql, map[string]*_bintree_t{}},
}}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2022 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -24,28 +24,18 @@ import (
"unicode" "unicode"
"github.com/gorilla/mux" "github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown/v2"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"github.com/writeas/web-core/bots" "github.com/writeas/web-core/bots"
"github.com/writeas/web-core/i18n"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/web-core/posts" waposts "github.com/writeas/web-core/posts"
"github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/author"
"github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/config"
"github.com/writefreely/writefreely/page" "github.com/writefreely/writefreely/page"
"github.com/writefreely/writefreely/spam"
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
const (
collAttrLetterReplyTo = "letter_reply_to"
collMaxLengthTitle = 255
collMaxLengthDescription = 160
)
type ( type (
// TODO: add Direction to db // TODO: add Direction to db
// TODO: add Language to db // TODO: add Language to db
@ -68,7 +58,6 @@ type (
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Monetization string `json:"monetization_pointer,omitempty"` Monetization string `json:"monetization_pointer,omitempty"`
Verification string `json:"verification_link"`
db *datastore db *datastore
hostName string hostName string
@ -83,20 +72,11 @@ type (
DisplayCollection struct { DisplayCollection struct {
*CollectionObj *CollectionObj
Prefix string Prefix string
NavSuffix string
IsTopLevel bool IsTopLevel bool
CurrentPage int CurrentPage int
TotalPages int TotalPages int
Silenced bool Silenced bool
} }
CollectionNav struct {
*Collection
Path string
SingleUser bool
CanPost bool
}
SubmittedCollection struct { SubmittedCollection struct {
// Data used for updating a given collection // Data used for updating a given collection
ID int64 ID int64
@ -107,19 +87,16 @@ type (
Privacy int `schema:"privacy" json:"privacy"` Privacy int `schema:"privacy" json:"privacy"`
Pass string `schema:"password" json:"password"` Pass string `schema:"password" json:"password"`
MathJax bool `schema:"mathjax" json:"mathjax"` MathJax bool `schema:"mathjax" json:"mathjax"`
EmailSubs bool `schema:"email_subs" json:"email_subs"`
Handle string `schema:"handle" json:"handle"` Handle string `schema:"handle" json:"handle"`
// Actual collection values updated in the DB // Actual collection values updated in the DB
Alias *string `schema:"alias" json:"alias"` Alias *string `schema:"alias" json:"alias"`
Title *string `schema:"title" json:"title"` Title *string `schema:"title" json:"title"`
Description *string `schema:"description" json:"description"` Description *string `schema:"description" json:"description"`
StyleSheet *string `schema:"style_sheet" json:"style_sheet"` StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
Script *string `schema:"script" json:"script"` Script *sql.NullString `schema:"script" json:"script"`
Signature *string `schema:"signature" json:"signature"` Signature *sql.NullString `schema:"signature" json:"signature"`
Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"` Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"`
Verification *string `schema:"verification_link" json:"verification_link"`
LetterReply *string `schema:"letter_reply" json:"letter_reply"`
Visibility *int `schema:"visibility" json:"public"` Visibility *int `schema:"visibility" json:"public"`
Format *sql.NullString `schema:"format" json:"format"` Format *sql.NullString `schema:"format" json:"format"`
} }
@ -281,16 +258,16 @@ func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
// PrevPageURL provides a full URL for the previous page of collection posts, // PrevPageURL provides a full URL for the previous page of collection posts,
// returning a /page/N result for pages >1 // returning a /page/N result for pages >1
func (c *Collection) PrevPageURL(prefix, navSuffix string, n int, tl bool) string { func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string {
u := "" u := ""
if n == 2 { if n == 2 {
// Previous page is 1; no need for /page/ prefix // Previous page is 1; no need for /page/ prefix
if prefix == "" { if prefix == "" {
u = navSuffix + "/" u = "/"
} }
// Else leave off trailing slash // Else leave off trailing slash
} else { } else {
u = fmt.Sprintf("%s/page/%d", navSuffix, n-1) u = fmt.Sprintf("/page/%d", n-1)
} }
if tl { if tl {
@ -300,12 +277,11 @@ func (c *Collection) PrevPageURL(prefix, navSuffix string, n int, tl bool) strin
} }
// NextPageURL provides a full URL for the next page of collection posts // NextPageURL provides a full URL for the next page of collection posts
func (c *Collection) NextPageURL(prefix, navSuffix string, n int, tl bool) string { func (c *Collection) NextPageURL(prefix string, n int, tl bool) string {
if tl { if tl {
return fmt.Sprintf("%s/page/%d", navSuffix, n+1) return fmt.Sprintf("/page/%d", n+1)
} }
return fmt.Sprintf("/%s%s%s/page/%d", prefix, c.Alias, navSuffix, n+1) return fmt.Sprintf("/%s%s/page/%d", prefix, c.Alias, n+1)
} }
func (c *Collection) DisplayTitle() string { func (c *Collection) DisplayTitle() string {
@ -379,10 +355,6 @@ func (c *Collection) RenderMathJax() bool {
return c.db.CollectionHasAttribute(c.ID, "render_mathjax") return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
} }
func (c *Collection) EmailSubsEnabled() bool {
return c.db.CollectionHasAttribute(c.ID, "email_subs")
}
func (c *Collection) MonetizationURL() string { func (c *Collection) MonetizationURL() string {
if c.Monetization == "" { if c.Monetization == "" {
return "" return ""
@ -390,40 +362,10 @@ func (c *Collection) MonetizationURL() string {
return strings.Replace(c.Monetization, "$", "https://", 1) return strings.Replace(c.Monetization, "$", "https://", 1)
} }
// DisplayDescription returns the description with rendered Markdown and HTML.
func (c *Collection) DisplayDescription() *template.HTML {
if c.Description == "" {
s := template.HTML("")
return &s
}
t := template.HTML(posts.ApplyBasicAccessibleMarkdown([]byte(c.Description)))
return &t
}
// PlainDescription returns the description with all Markdown and HTML removed.
func (c *Collection) PlainDescription() string {
if c.Description == "" {
return ""
}
desc := stripHTMLWithoutEscaping(c.Description)
desc = stripmd.Strip(desc)
return desc
}
func (c CollectionPage) DisplayMonetization() string { func (c CollectionPage) DisplayMonetization() string {
return displayMonetization(c.Monetization, c.Alias) return displayMonetization(c.Monetization, c.Alias)
} }
func (c *DisplayCollection) Direction() string {
if c.Language == "" {
return "auto"
}
if i18n.LangIsRTL(c.Language) {
return "rtl"
}
return "ltr"
}
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r) reqJSON := IsJSON(r)
alias := r.FormValue("alias") alias := r.FormValue("alias")
@ -533,7 +475,8 @@ func apiCheckCollectionPermissions(app *App, r *http.Request, c *Collection) (in
// fetchCollection handles the API endpoint for retrieving collection data. // fetchCollection handles the API endpoint for retrieving collection data.
func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
if IsActivityPubRequest(r) { accept := r.Header.Get("Accept")
if strings.Contains(accept, "application/activity+json") {
return handleFetchCollectionActivities(app, w, r) return handleFetchCollectionActivities(app, w, r)
} }
@ -608,11 +551,11 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro
} }
} }
ps, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false, "") posts, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false)
if err != nil { if err != nil {
return err return err
} }
coll := &CollectionObj{Collection: *c, Posts: ps} coll := &CollectionObj{Collection: *c, Posts: posts}
app.db.GetPostsCount(coll, isCollOwner) app.db.GetPostsCount(coll, isCollOwner)
// Strip non-public information // Strip non-public information
coll.Collection.ForPublic() coll.Collection.ForPublic()
@ -620,7 +563,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro
// Transform post bodies if needed // Transform post bodies if needed
if r.FormValue("body") == "html" { if r.FormValue("body") == "html" {
for _, p := range *coll.Posts { for _, p := range *coll.Posts {
p.Content = posts.ApplyMarkdown([]byte(p.Content)) p.Content = waposts.ApplyMarkdown([]byte(p.Content))
} }
} }
@ -634,46 +577,18 @@ type CollectionPage struct {
IsWelcome bool IsWelcome bool
IsOwner bool IsOwner bool
IsCollLoggedIn bool IsCollLoggedIn bool
Honeypot string
IsSubscriber bool
CanPin bool CanPin bool
Username string Username string
Monetization string Monetization string
Flash template.HTML
Collections *[]Collection Collections *[]Collection
PinnedPosts *[]PublicPost PinnedPosts *[]PublicPost
IsAdmin bool
IsAdmin bool CanInvite bool
CanInvite bool
// Helper field for Chorus mode // Helper field for Chorus mode
CollAlias string CollAlias string
} }
type TagCollectionPage struct {
CollectionPage
Tag string
}
func (tcp TagCollectionPage) PrevPageURL(prefix string, n int, tl bool) string {
u := fmt.Sprintf("/tag:%s", tcp.Tag)
if n > 2 {
u += fmt.Sprintf("/page/%d", n-1)
}
if tl {
return u
}
return "/" + prefix + tcp.Alias + u
}
func (tcp TagCollectionPage) NextPageURL(prefix string, n int, tl bool) string {
if tl {
return fmt.Sprintf("/tag:%s/page/%d", tcp.Tag, n+1)
}
return fmt.Sprintf("/%s%s/tag:%s/page/%d", prefix, tcp.Alias, tcp.Tag, n+1)
}
func NewCollectionObj(c *Collection) *CollectionObj { func NewCollectionObj(c *Collection) *CollectionObj {
return &CollectionObj{ return &CollectionObj{
Collection: *c, Collection: *c,
@ -828,18 +743,15 @@ func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPost
return u, nil return u, nil
} }
func newDisplayCollection(c *Collection, cr *collectionReq, page int) (*DisplayCollection, error) { func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCollection {
coll := &DisplayCollection{ coll := &DisplayCollection{
CollectionObj: NewCollectionObj(c), CollectionObj: NewCollectionObj(c),
CurrentPage: page, CurrentPage: page,
Prefix: cr.prefix, Prefix: cr.prefix,
IsTopLevel: isSingleUser, IsTopLevel: isSingleUser,
} }
err := c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner) c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner)
if err != nil { return coll
return nil, err
}
return coll, nil
} }
// getCollectionPage returns the collection page as an int. If the parsed page value is not // getCollectionPage returns the collection page as an int. If the parsed page value is not
@ -882,31 +794,18 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
} }
// Serve ActivityStreams data now, if requested // Serve ActivityStreams data now, if requested
if IsActivityPubRequest(r) { if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
ac := c.PersonObject() ac := c.PersonObject()
ac.Context = []interface{}{activitystreams.Namespace}
setCacheControl(w, apCacheTime) setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ac, http.StatusOK) return impart.RenderActivityJSON(w, ac, http.StatusOK)
} }
// Fetch extra data about the Collection // Fetch extra data about the Collection
// TODO: refactor out this logic, shared in collection.go:fetchCollection() // TODO: refactor out this logic, shared in collection.go:fetchCollection()
coll, err := newDisplayCollection(c, cr, page) coll := newDisplayCollection(c, cr, page)
if err != nil {
return err
}
var ct PostType coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(coll.Format.PostsPerPage())))
if isArchiveView(r) {
ct = postArch
}
// FIXME: this number will be off when user has pinned posts but isn't a Pro user
ppp := coll.Format.PostsPerPage()
if ct == postArch {
ppp = postsPerArchPage
}
coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(ppp)))
if coll.TotalPages > 0 && page > coll.TotalPages { if coll.TotalPages > 0 && page > coll.TotalPages {
redirURL := fmt.Sprintf("/page/%d", coll.TotalPages) redirURL := fmt.Sprintf("/page/%d", coll.TotalPages)
if !app.cfg.App.SingleUser { if !app.cfg.App.SingleUser {
@ -915,7 +814,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
return impart.HTTPError{http.StatusFound, redirURL} return impart.HTTPError{http.StatusFound, redirURL}
} }
coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false, "") coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false)
// Serve collection // Serve collection
displayPage := CollectionPage{ displayPage := CollectionPage{
@ -924,20 +823,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
IsCustomDomain: cr.isCustomDomain, IsCustomDomain: cr.isCustomDomain,
IsWelcome: r.FormValue("greeting") != "", IsWelcome: r.FormValue("greeting") != "",
Honeypot: spam.HoneypotFieldName(),
CollAlias: c.Alias, CollAlias: c.Alias,
} }
flashes, _ := getSessionFlashes(app, w, r, nil)
for _, f := range flashes {
displayPage.Flash = template.HTML(f)
}
displayPage.IsAdmin = u != nil && u.IsAdmin() displayPage.IsAdmin = u != nil && u.IsAdmin()
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin)
var owner *User var owner *User
if u != nil { if u != nil {
displayPage.Username = u.Username displayPage.Username = u.Username
displayPage.IsOwner = u.ID == coll.OwnerID displayPage.IsOwner = u.ID == coll.OwnerID
displayPage.IsSubscriber = u.IsEmailSubscriber(app, coll.ID)
if displayPage.IsOwner { if displayPage.IsOwner {
// Add in needed information for users viewing their own collection // Add in needed information for users viewing their own collection
owner = u owner = u
@ -974,9 +867,6 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
collTmpl := "collection" collTmpl := "collection"
if app.cfg.App.Chorus { if app.cfg.App.Chorus {
collTmpl = "chorus-collection" collTmpl = "chorus-collection"
} else if isArchiveView(r) {
displayPage.NavSuffix = "/archive/"
collTmpl = "collection-archive"
} }
err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage)
if err != nil { if err != nil {
@ -1003,10 +893,6 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
return err return err
} }
func isArchiveView(r *http.Request) bool {
return strings.HasSuffix(r.RequestURI, "/archive/") || mux.Vars(r)["archive"] == "archive"
}
func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error { func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
handle := vars["handle"] handle := vars["handle"]
@ -1042,23 +928,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
return err return err
} }
coll, _ := newDisplayCollection(c, cr, page) coll := newDisplayCollection(c, cr, page)
taggedPostIDs, err := app.db.GetAllPostsTaggedIDs(c, tag, cr.isCollOwner)
if err != nil {
return err
}
ttlPosts := len(taggedPostIDs)
pagePosts := coll.Format.PostsPerPage()
coll.TotalPages = int(math.Ceil(float64(ttlPosts) / float64(pagePosts)))
if coll.TotalPages > 0 && page > coll.TotalPages {
redirURL := fmt.Sprintf("/page/%d", coll.TotalPages)
if !app.cfg.App.SingleUser {
redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL)
}
return impart.HTTPError{http.StatusFound, redirURL}
}
coll.Posts, _ = app.db.GetPostsTagged(app.cfg, 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 { if coll.Posts != nil && len(*coll.Posts) == 0 {
@ -1066,7 +936,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
} }
// Serve collection // Serve collection
displayPage := TagCollectionPage{ displayPage := struct {
CollectionPage
Tag string
}{
CollectionPage: CollectionPage{ CollectionPage: CollectionPage{
DisplayCollection: coll, DisplayCollection: coll,
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
@ -1118,111 +991,6 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
return nil return nil
} }
func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
lang := vars["lang"]
cr := &collectionReq{}
err := processCollectionRequest(cr, vars, w, r)
if err != nil {
return err
}
u, err := checkUserForCollection(app, cr, r, false)
if err != nil {
return err
}
page := getCollectionPage(vars)
c, err := processCollectionPermissions(app, cr, u, w, r)
if c == nil || err != nil {
return err
}
coll, _ := newDisplayCollection(c, cr, page)
coll.Language = lang
coll.NavSuffix = fmt.Sprintf("/lang:%s", lang)
ttlPosts, err := app.db.GetCollLangTotalPosts(coll.ID, lang)
if err != nil {
log.Error("Unable to getCollLangTotalPosts: %s", err)
}
pagePosts := coll.Format.PostsPerPage()
coll.TotalPages = int(math.Ceil(float64(ttlPosts) / float64(pagePosts)))
if coll.TotalPages > 0 && page > coll.TotalPages {
redirURL := fmt.Sprintf("/lang:%s/page/%d", lang, coll.TotalPages)
if !app.cfg.App.SingleUser {
redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL)
}
return impart.HTTPError{http.StatusFound, redirURL}
}
coll.Posts, _ = app.db.GetLangPosts(app.cfg, c, lang, page, cr.isCollOwner)
if err != nil {
return ErrCollectionPageNotFound
}
// Serve collection
displayPage := struct {
CollectionPage
Tag string
}{
CollectionPage: CollectionPage{
DisplayCollection: coll,
StaticPage: pageForReq(app, r),
IsCustomDomain: cr.isCustomDomain,
},
Tag: lang,
}
var owner *User
if u != nil {
displayPage.Username = u.Username
displayPage.IsOwner = u.ID == coll.OwnerID
if displayPage.IsOwner {
// Add in needed information for users viewing their own collection
owner = u
displayPage.CanPin = true
pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host)
if err != nil {
log.Error("unable to fetch collections: %v", err)
}
displayPage.Collections = pubColls
}
}
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.Silenced = 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, isOwner)
displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
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 lang page: %v", err)
}
return nil
}
func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error { func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
slug := vars["slug"] slug := vars["slug"]
@ -1307,7 +1075,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
} }
} }
err = app.db.UpdateCollection(app, &c, collAlias) err = app.db.UpdateCollection(&c, collAlias)
if err != nil { if err != nil {
if err, ok := err.(impart.HTTPError); ok { if err, ok := err.(impart.HTTPError); ok {
if reqJSON { if reqJSON {

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -15,9 +15,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/go-ini/ini"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"gopkg.in/ini.v1"
) )
const ( const (
@ -170,26 +170,11 @@ type (
DisablePasswordAuth bool `ini:"disable_password_auth"` DisablePasswordAuth bool `ini:"disable_password_auth"`
} }
EmailCfg struct {
// SMTP configuration values
Host string `ini:"smtp_host"`
Port int `ini:"smtp_port"`
Username string `ini:"smtp_username"`
Password string `ini:"smtp_password"`
EnableStartTLS bool `ini:"smtp_enable_start_tls"`
// Mailgun configuration values
Domain string `ini:"domain"`
MailgunPrivate string `ini:"mailgun_private"`
MailgunEurope bool `ini:"mailgun_europe"`
}
// Config holds the complete configuration for running a writefreely instance // Config holds the complete configuration for running a writefreely instance
Config struct { Config struct {
Server ServerCfg `ini:"server"` Server ServerCfg `ini:"server"`
Database DatabaseCfg `ini:"database"` Database DatabaseCfg `ini:"database"`
App AppCfg `ini:"app"` App AppCfg `ini:"app"`
Email EmailCfg `ini:"email"`
SlackOauth SlackOauthCfg `ini:"oauth.slack"` SlackOauth SlackOauthCfg `ini:"oauth.slack"`
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
@ -250,11 +235,6 @@ func (ac *AppCfg) LandingPath() string {
return ac.Landing return ac.Landing
} }
func (lc EmailCfg) Enabled() bool {
return (lc.Domain != "" && lc.MailgunPrivate != "") ||
lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0
}
func (ac AppCfg) SignupPath() string { func (ac AppCfg) SignupPath() string {
if !ac.OpenRegistration { if !ac.OpenRegistration {
return "" return ""

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018, 2020-2021 Musing Studio LLC. * Copyright © 2018, 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -12,14 +12,12 @@ package config
import ( import (
"fmt" "fmt"
"os"
"strconv"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/mitchellh/go-wordwrap" "github.com/mitchellh/go-wordwrap"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"strconv"
"strings"
) )
type SetupData struct { type SetupData struct {
@ -59,7 +57,7 @@ func Configure(fname string, configSections string) (*SetupData, error) {
Success: "{{ . | bold | faint }}: ", Success: "{{ . | bold | faint }}: ",
} }
selTmpls := &promptui.SelectTemplates{ selTmpls := &promptui.SelectTemplates{
Selected: `{{.Label}} {{ . | faint }}`, Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
} }
var selPrompt promptui.Select var selPrompt promptui.Select
@ -82,8 +80,6 @@ func Configure(fname string, configSections string) (*SetupData, error) {
isDevEnv := envType == "Development" isDevEnv := envType == "Development"
isStandalone := envType == "Production, standalone" isStandalone := envType == "Production, standalone"
_, isDocker := os.LookupEnv("WRITEFREELY_DOCKER")
data.Config.Server.Dev = isDevEnv data.Config.Server.Dev = isDevEnv
if isDevEnv || !isStandalone { if isDevEnv || !isStandalone {
@ -154,16 +150,6 @@ func Configure(fname string, configSections string) (*SetupData, error) {
data.Config.Server.TLSKeyPath = "" data.Config.Server.TLSKeyPath = ""
} }
// If running in docker:
// 1. always bind to 0.0.0.0 instead of localhost
// 2. set paths of static files in UNIX manners
if !isDevEnv && isDocker {
data.Config.Server.TemplatesParentDir = "/usr/share/writefreely"
data.Config.Server.StaticParentDir = "/usr/share/writefreely"
data.Config.Server.PagesParentDir = "/usr/share/writefreely"
data.Config.Server.Bind = "0.0.0.0"
}
fmt.Println() fmt.Println()
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,8 +1,7 @@
//go:build wflib
// +build wflib // +build wflib
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,8 +1,7 @@
//go:build !sqlite && !wflib
// +build !sqlite,!wflib // +build !sqlite,!wflib
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,8 +1,7 @@
//go:build sqlite && !wflib
// +build sqlite,!wflib // +build sqlite,!wflib
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -14,17 +14,11 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/writeas/monday"
"github.com/go-sql-driver/mysql"
"github.com/writeas/web-core/silobridge" "github.com/writeas/web-core/silobridge"
wf_db "github.com/writefreely/writefreely/db" wf_db "github.com/writefreely/writefreely/db"
"github.com/writefreely/writefreely/parse" "net/http"
"strings"
"time"
"github.com/guregu/null" "github.com/guregu/null"
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
@ -101,7 +95,7 @@ type writestore interface {
GetCollection(alias string) (*Collection, error) GetCollection(alias string) (*Collection, error)
GetCollectionForPad(alias string) (*Collection, error) GetCollectionForPad(alias string) (*Collection, error)
GetCollectionByID(id int64) (*Collection, error) GetCollectionByID(id int64) (*Collection, error)
UpdateCollection(app *App, c *SubmittedCollection, alias string) error UpdateCollection(c *SubmittedCollection, alias string) error
DeleteCollection(alias string, userID int64) error DeleteCollection(alias string, userID int64) error
UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error
@ -117,10 +111,8 @@ type writestore interface {
DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error) DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error)
ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error)
GetPostLikeCounts(postID string) (int64, error) GetPostsCount(c *CollectionObj, includeFuture bool)
GetPostsCount(c *CollectionObj, includeFuture bool) error GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error)
GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error)
GetAllPostsTaggedIDs(c *Collection, tag string, includeFuture bool) ([]string, error)
GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error)
GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPFollowers(c *Collection) (*[]RemoteUser, error)
@ -179,13 +171,6 @@ func (db *datastore) upsert(indexedCols ...string) string {
return "ON DUPLICATE KEY UPDATE" return "ON DUPLICATE KEY UPDATE"
} }
func (db *datastore) dateAdd(l int, unit string) string {
if db.driverName == driverSQLite {
return fmt.Sprintf("DATETIME('now', '%d %s')", l, unit)
}
return fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d %s)", l, unit)
}
func (db *datastore) dateSub(l int, unit string) string { func (db *datastore) dateSub(l int, unit string) string {
if db.driverName == driverSQLite { if db.driverName == driverSQLite {
return fmt.Sprintf("DATETIME('now', '-%d %s')", l, unit) return fmt.Sprintf("DATETIME('now', '-%d %s')", l, unit)
@ -347,7 +332,7 @@ func (db *datastore) IsUserSilenced(id int64) (bool, error) {
err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return false, ErrUserNotFound return false, fmt.Errorf("is user silenced: %v", ErrUserNotFound)
case err != nil: case err != nil:
log.Error("Couldn't SELECT user status: %v", err) log.Error("Couldn't SELECT user status: %v", err)
return false, fmt.Errorf("is user silenced: %v", err) return false, fmt.Errorf("is user silenced: %v", err)
@ -579,7 +564,7 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
expirationVal := "NULL" expirationVal := "NULL"
if validSecs > 0 { if validSecs > 0 {
expirationVal = db.dateAdd(validSecs, "SECOND") expirationVal = fmt.Sprintf("DATE_ADD("+db.now()+", INTERVAL %d SECOND)", validSecs)
} }
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime) _, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime)
@ -591,37 +576,6 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
return u.String(), nil return u.String(), nil
} }
func (db *datastore) CreatePasswordResetToken(userID int64) (string, error) {
t := id.Generate62RandomString(32)
_, err := db.Exec("INSERT INTO password_resets (user_id, token, used, created) VALUES (?, ?, 0, "+db.now()+")", userID, t)
if err != nil {
log.Error("Couldn't INSERT password_resets: %v", err)
return "", err
}
return t, nil
}
func (db *datastore) GetUserFromPasswordReset(token string) int64 {
var userID int64
err := db.QueryRow("SELECT user_id FROM password_resets WHERE token = ? AND used = 0 AND created > "+db.dateSub(3, "HOUR"), token).Scan(&userID)
if err != nil {
return 0
}
return userID
}
func (db *datastore) ConsumePasswordResetToken(t string) error {
_, err := db.Exec("UPDATE password_resets SET used = 1 WHERE token = ?", t)
if err != nil {
log.Error("Couldn't UPDATE password_resets: %v", err)
return err
}
return nil
}
func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) { func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) {
var userID, collID int64 = -1, -1 var userID, collID int64 = -1, -1
var coll *Collection var coll *Collection
@ -707,7 +661,7 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
// SQLite stores datetimes in UTC, so convert time.Now() to it here // SQLite stores datetimes in UTC, so convert time.Now() to it here
created = created.UTC() created = created.UTC()
} }
if post.Created != nil && *post.Created != "" { if post.Created != nil {
created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created) created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created)
if err != nil { if err != nil {
log.Error("Unable to parse Created time '%s': %v", *post.Created, err) log.Error("Unable to parse Created time '%s': %v", *post.Created, err)
@ -860,7 +814,6 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
c.Format = format.String c.Format = format.String
c.Public = c.IsPublic() c.Public = c.IsPublic()
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
c.Verification = db.GetCollectionAttribute(c.ID, "verification_link")
c.db = db c.db = db
@ -897,21 +850,13 @@ func (db *datastore) GetCollectionFromDomain(host string) (*Collection, error) {
return db.GetCollectionBy("host = ?", host) return db.GetCollectionBy("host = ?", host)
} }
func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias string) error { func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) error {
// Truncate fields correctly, so we don't get "Data too long for column" errors in MySQL (writefreely#600)
if c.Title != nil {
*c.Title = parse.Truncate(*c.Title, collMaxLengthTitle)
}
if c.Description != nil {
*c.Description = parse.Truncate(*c.Description, collMaxLengthDescription)
}
q := query.NewUpdate(). q := query.NewUpdate().
SetStringPtr(c.Title, "title"). SetStringPtr(c.Title, "title").
SetStringPtr(c.Description, "description"). SetStringPtr(c.Description, "description").
SetStringPtr(c.StyleSheet, "style_sheet"). SetNullString(c.StyleSheet, "style_sheet").
SetStringPtr(c.Script, "script"). SetNullString(c.Script, "script").
SetStringPtr(c.Signature, "post_signature") SetNullString(c.Signature, "post_signature")
if c.Format != nil { if c.Format != nil {
cf := &CollectionFormat{Format: c.Format.String} cf := &CollectionFormat{Format: c.Format.String}
@ -964,44 +909,6 @@ func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias st
} }
} }
// Update Verification link value
if c.Verification != nil {
skipUpdate := false
if *c.Verification != "" {
// Strip away any excess spaces
trimmed := strings.TrimSpace(*c.Verification)
if strings.HasPrefix(trimmed, "@") && strings.Count(trimmed, "@") == 2 {
// This looks like a fediverse handle, so resolve profile URL
profileURL, err := GetProfileURLFromHandle(app, trimmed)
if err != nil || profileURL == "" {
log.Error("Couldn't find user %s: %v", trimmed, err)
skipUpdate = true
} else {
c.Verification = &profileURL
}
} else {
if !strings.HasPrefix(trimmed, "http") {
trimmed = "https://" + trimmed
}
vu, err := url.Parse(trimmed)
if err != nil {
// Value appears invalid, so don't update
skipUpdate = true
} else {
s := vu.String()
c.Verification = &s
}
}
}
if !skipUpdate {
err = db.SetCollectionAttribute(collID, "verification_link", *c.Verification)
if err != nil {
log.Error("Unable to insert verification_link value: %v", err)
return err
}
}
}
// Update Monetization value // Update Monetization value
if c.Monetization != nil { if c.Monetization != nil {
skipUpdate := false skipUpdate := false
@ -1017,7 +924,7 @@ func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias st
} }
} }
if !skipUpdate { if !skipUpdate {
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization) _, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization)
if err != nil { if err != nil {
log.Error("Unable to insert monetization_pointer value: %v", err) log.Error("Unable to insert monetization_pointer value: %v", err)
return err return err
@ -1025,40 +932,6 @@ func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias st
} }
} }
// Update EmailSub value
if c.EmailSubs {
err = db.SetCollectionAttribute(collID, "email_subs", "1")
if err != nil {
log.Error("Unable to insert email_subs value: %v", err)
return err
}
skipUpdate := false
if c.LetterReply != nil {
// Strip away any excess spaces
trimmed := strings.TrimSpace(*c.LetterReply)
// Only update value when it contains "@"
if strings.IndexRune(trimmed, '@') > 0 {
c.LetterReply = &trimmed
} else {
// Value appears invalid, so don't update
skipUpdate = true
}
if !skipUpdate {
err = db.SetCollectionAttribute(collID, collAttrLetterReplyTo, *c.LetterReply)
if err != nil {
log.Error("Unable to insert %s value: %v", collAttrLetterReplyTo, err)
return err
}
}
}
} else {
_, err = db.Exec("DELETE FROM collectionattributes WHERE collection_id = ? AND attribute = ?", collID, "email_subs")
if err != nil {
log.Error("Unable to delete email_subs value: %v", err)
return err
}
}
// Update rest of the collection data // Update rest of the collection data
if q.Updates != "" { if q.Updates != "" {
res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...) res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...)
@ -1177,12 +1050,6 @@ func (db *datastore) GetPost(id string, collectionID int64) (*PublicPost, error)
return nil, ErrPostUnpublished return nil, ErrPostUnpublished
} }
// Get additional information needed before processing post data
p.LikeCount, err = db.GetPostLikeCounts(p.ID)
if err != nil {
return nil, err
}
res := p.processPost() res := p.processPost()
if ownerName.Valid { if ownerName.Valid {
res.Owner = &PublicUser{Username: ownerName.String} res.Owner = &PublicUser{Username: ownerName.String}
@ -1245,22 +1112,10 @@ func (db *datastore) GetPostProperty(id string, collectionID int64, property str
return res, nil return res, nil
} }
func (db *datastore) GetPostLikeCounts(postID string) (int64, error) {
var count int64
err := db.QueryRow("SELECT COUNT(*) FROM remote_likes WHERE post_id = ?", postID).Scan(&count)
switch {
case err == sql.ErrNoRows:
count = 0
case err != nil:
return 0, err
}
return count, nil
}
// GetPostsCount modifies the CollectionObj to include the correct number of // GetPostsCount modifies the CollectionObj to include the correct number of
// standard (non-pinned) posts. It will return future posts if `includeFuture` // standard (non-pinned) posts. It will return future posts if `includeFuture`
// is true. // is true.
func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) error { func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) {
var count int64 var count int64
timeCondition := "" timeCondition := ""
if !includeFuture { if !includeFuture {
@ -1273,18 +1128,16 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) error {
case err != nil: case err != nil:
log.Error("Failed selecting from collections: %v", err) log.Error("Failed selecting from collections: %v", err)
c.TotalPosts = 0 c.TotalPosts = 0
return err
} }
c.TotalPosts = int(count) c.TotalPosts = int(count)
return nil
} }
// GetPosts retrieves all posts for the given Collection. // GetPosts retrieves all posts for the given Collection.
// It will return future posts if `includeFuture` is true. // It will return future posts if `includeFuture` is true.
// It will include only standard (non-pinned) posts unless `includePinned` 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 // TODO: change includeFuture to isOwner, since that's how it's used
func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error) { func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) {
collID := c.ID collID := c.ID
cf := c.NewFormat() cf := c.NewFormat()
@ -1298,9 +1151,6 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
if page == 0 { if page == 0 {
start = 0 start = 0
pagePosts = 1000 pagePosts = 1000
} else if contentType == postArch {
pagePosts = postsPerArchPage
start = page*pagePosts - pagePosts
} }
limitStr := "" limitStr := ""
@ -1315,7 +1165,6 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
if !includePinned { if !includePinned {
pinnedCondition = "AND pinned_position IS NULL" pinnedCondition = "AND pinned_position IS NULL"
} }
// FUTURE: handle different post contentType's here
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? "+pinnedCondition+" "+timeCondition+" ORDER BY created "+order+limitStr, collID) rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? "+pinnedCondition+" "+timeCondition+" ORDER BY created "+order+limitStr, collID)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err) log.Error("Failed selecting from posts: %v", err)
@ -1336,13 +1185,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
p.augmentContent(c) p.augmentContent(c)
p.formatContent(cfg, c, includeFuture, false) p.formatContent(cfg, c, includeFuture, false)
pubPost := p.processPost() posts = append(posts, p.processPost())
if contentType == postArch {
// Overwrite DisplayDate with special Archive page version
loc := monday.FuzzyLocale(pubPost.Language.String)
pubPost.DisplayDate = monday.Format(pubPost.Created, monday.LongNoYrFormatsByLocale[loc], loc)
}
posts = append(posts, pubPost)
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {
@ -1352,51 +1195,6 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
return &posts, nil return &posts, nil
} }
func (db *datastore) GetAllPostsTaggedIDs(c *Collection, tag string, includeFuture bool) ([]string, error) {
collID := c.ID
cf := c.NewFormat()
order := "DESC"
if cf.Ascending() {
order = "ASC"
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= " + db.now()
}
var rows *sql.Rows
var err error
if db.driverName == driverSQLite {
rows, err = db.Query("SELECT id FROM posts WHERE collection_id = ? AND LOWER(content) regexp ? "+timeCondition+" ORDER BY created "+order, collID, `.*#`+strings.ToLower(tag)+`\b.*`)
} else {
rows, err = db.Query("SELECT id FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order, collID, "#"+strings.ToLower(tag)+"[[:>:]]")
}
if err != nil {
log.Error("Failed selecting tagged posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve tagged collection posts."}
}
defer rows.Close()
ids := []string{}
for rows.Next() {
var id string
err = rows.Scan(&id)
if err != nil {
log.Error("Failed scanning row: %v", err)
break
}
ids = append(ids, id)
}
err = rows.Err()
if err != nil {
log.Error("Error after Next() on rows: %v", err)
}
return ids, nil
}
// GetPostsTagged retrieves all posts on the given Collection that contain the // GetPostsTagged retrieves all posts on the given Collection that contain the
// given tag. // given tag.
// It will return future posts if `includeFuture` is true. // It will return future posts if `includeFuture` is true.
@ -1462,76 +1260,8 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin
return &posts, nil return &posts, nil
} }
func (db *datastore) GetCollLangTotalPosts(collID int64, lang string) (uint64, error) {
var articles uint64
err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND language = ? AND created <= "+db.now(), collID, lang).Scan(&articles)
if err != nil && err != sql.ErrNoRows {
log.Error("Couldn't get total lang posts count for collection %d: %v", collID, err)
return 0, err
}
return articles, nil
}
func (db *datastore) GetLangPosts(cfg *config.Config, c *Collection, lang string, page int, includeFuture bool) (*[]PublicPost, error) {
collID := c.ID
cf := c.NewFormat()
order := "DESC"
if cf.Ascending() {
order = "ASC"
}
pagePosts := cf.PostsPerPage()
start := page*pagePosts - pagePosts
if page == 0 {
start = 0
pagePosts = 1000
}
limitStr := ""
if page > 0 {
limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts)
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= " + db.now()
}
rows, err := db.Query(`SELECT `+postCols+`
FROM posts
WHERE collection_id = ? AND language = ? `+timeCondition+`
ORDER BY created `+order+limitStr, collID, lang)
if err != nil {
log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."}
}
defer rows.Close()
// TODO: extract this common row scanning logic for queries using `postCols`
posts := []PublicPost{}
for rows.Next() {
p := &Post{}
err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
if err != nil {
log.Error("Failed scanning row: %v", err)
break
}
p.extractData()
p.augmentContent(c)
p.formatContent(cfg, c, includeFuture, false)
posts = append(posts, p.processPost())
}
err = rows.Err()
if err != nil {
log.Error("Error after Next() on rows: %v", err)
}
return &posts, nil
}
func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) { func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) {
rows, err := db.Query("SELECT actor_id, inbox, shared_inbox, f.created FROM remotefollows f INNER JOIN remoteusers u ON f.remote_user_id = u.id WHERE collection_id = ?", c.ID) rows, err := db.Query("SELECT actor_id, inbox, shared_inbox FROM remotefollows f INNER JOIN remoteusers u ON f.remote_user_id = u.id WHERE collection_id = ?", c.ID)
if err != nil { if err != nil {
log.Error("Failed selecting from followers: %v", err) log.Error("Failed selecting from followers: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve followers."} return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve followers."}
@ -1541,7 +1271,7 @@ func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) {
followers := []RemoteUser{} followers := []RemoteUser{}
for rows.Next() { for rows.Next() {
f := RemoteUser{} f := RemoteUser{}
err = rows.Scan(&f.ActorID, &f.Inbox, &f.SharedInbox, &f.Created) err = rows.Scan(&f.ActorID, &f.Inbox, &f.SharedInbox)
followers = append(followers, f) followers = append(followers, f)
} }
return &followers, nil return &followers, nil
@ -1958,7 +1688,7 @@ func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error
FROM collections c FROM collections c
LEFT JOIN users u ON u.id = c.owner_id LEFT JOIN users u ON u.id = c.owner_id
WHERE c.privacy = 1 AND u.status = 0 WHERE c.privacy = 1 AND u.status = 0
ORDER BY title ASC`) ORDER BY id ASC`)
if err != nil { if err != nil {
log.Error("Failed selecting public collections: %v", err) log.Error("Failed selecting public collections: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."} return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."}
@ -2015,7 +1745,7 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
func (db *datastore) GetTotalCollections() (collCount int64, err error) { func (db *datastore) GetTotalCollections() (collCount int64, err error) {
err = db.QueryRow(` err = db.QueryRow(`
SELECT COUNT(*) SELECT COUNT(*)
FROM collections c FROM collections c
LEFT JOIN users u ON u.id = c.owner_id LEFT JOIN users u ON u.id = c.owner_id
WHERE u.status = 0`).Scan(&collCount) WHERE u.status = 0`).Scan(&collCount)
@ -2044,7 +1774,7 @@ func (db *datastore) GetTopPosts(u *User, alias string, hostName string) (*[]Pub
where = " AND alias = ?" where = " AND alias = ?"
params = append(params, alias) params = append(params, alias)
} }
rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, p.content, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON p.collection_id = c.id WHERE p.owner_id = ?"+where+" ORDER BY p.view_count DESC, created DESC LIMIT 25", params...) rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON p.collection_id = c.id WHERE p.owner_id = ?"+where+" ORDER BY p.view_count DESC, created DESC LIMIT 25", params...)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err) log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user top posts."} return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user top posts."}
@ -2058,7 +1788,7 @@ func (db *datastore) GetTopPosts(u *User, alias string, hostName string) (*[]Pub
c := Collection{} c := Collection{}
var alias, title, description sql.NullString var alias, title, description sql.NullString
var views sql.NullInt64 var views sql.NullInt64
err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &p.Content, &alias, &title, &description, &views) err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &alias, &title, &description, &views)
if err != nil { if err != nil {
log.Error("Failed scanning User.getPosts() row: %v", err) log.Error("Failed scanning User.getPosts() row: %v", err)
gotErr = true gotErr = true
@ -2103,7 +1833,7 @@ func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error)
if page > 0 { if page > 0 {
limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts)
} }
rows, err := db.Query("SELECT id, view_count, title, language, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID) rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err) log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."} return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}
@ -2113,7 +1843,7 @@ func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error)
posts := []PublicPost{} posts := []PublicPost{}
for rows.Next() { for rows.Next() {
p := Post{} p := Post{}
err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Language, &p.Created, &p.Updated, &p.Content) err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Created, &p.Updated, &p.Content)
if err != nil { if err != nil {
log.Error("Failed scanning row: %v", err) log.Error("Failed scanning row: %v", err)
break break
@ -2498,7 +2228,7 @@ func (db *datastore) GetCollectionAttribute(id int64, attr string) string {
} }
func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error { func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error {
_, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", id, attr, v, v) _, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", id, attr, v)
if err != nil { if err != nil {
log.Error("Unable to INSERT into collectionattributes: %v", err) log.Error("Unable to INSERT into collectionattributes: %v", err)
return err return err
@ -3035,7 +2765,6 @@ func handleFailedPostInsert(err error) error {
return err return err
} }
// Deprecated: use GetProfileURLFromHandle() instead, which returns user-facing URL instead of actor_id
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) { func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
handle = strings.TrimLeft(handle, "@") handle = strings.TrimLeft(handle, "@")
actorIRI := "" actorIRI := ""
@ -3084,247 +2813,3 @@ func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string,
} }
return actorIRI, nil return actorIRI, nil
} }
func (db *datastore) AddEmailSubscription(collID, userID int64, email string, confirmed bool) (*EmailSubscriber, error) {
friendlyChars := "0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz"
subID := id.GenerateRandomString(friendlyChars, 8)
token := id.GenerateRandomString(friendlyChars, 16)
emailVal := sql.NullString{
String: email,
Valid: email != "",
}
userIDVal := sql.NullInt64{
Int64: userID,
Valid: userID > 0,
}
_, err := db.Exec("INSERT INTO emailsubscribers (id, collection_id, user_id, email, subscribed, token, confirmed) VALUES (?, ?, ?, ?, "+db.now()+", ?, ?)", subID, collID, userIDVal, emailVal, token, confirmed)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
// Duplicate, so just return existing subscriber information
log.Info("Duplicate subscriber for email %s, user %d; returning existing subscriber", email, userID)
return db.FetchEmailSubscriber(email, userID, collID)
}
}
return nil, err
}
return &EmailSubscriber{
ID: subID,
CollID: collID,
UserID: userIDVal,
Email: emailVal,
Token: token,
}, nil
}
func (db *datastore) IsEmailSubscriber(email string, userID, collID int64) bool {
var dummy int
var err error
if email != "" {
err = db.QueryRow("SELECT 1 FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID).Scan(&dummy)
} else {
err = db.QueryRow("SELECT 1 FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID).Scan(&dummy)
}
switch {
case err == sql.ErrNoRows:
return false
case err != nil:
return false
}
return true
}
func (db *datastore) GetEmailSubscribers(collID int64, reqConfirmed bool) ([]*EmailSubscriber, error) {
cond := ""
if reqConfirmed {
cond = " AND confirmed = 1"
}
rows, err := db.Query(`SELECT s.id, collection_id, user_id, s.email, u.email, subscribed, token, confirmed, allow_export
FROM emailsubscribers s
LEFT JOIN users u
ON u.id = user_id
WHERE collection_id = ?`+cond+`
ORDER BY subscribed DESC`, collID)
if err != nil {
log.Error("Failed selecting email subscribers for collection %d: %v", collID, err)
return nil, err
}
defer rows.Close()
var subs []*EmailSubscriber
for rows.Next() {
s := &EmailSubscriber{}
err = rows.Scan(&s.ID, &s.CollID, &s.UserID, &s.Email, &s.acctEmail, &s.Subscribed, &s.Token, &s.Confirmed, &s.AllowExport)
if err != nil {
log.Error("Failed scanning row from email subscribers: %v", err)
continue
}
subs = append(subs, s)
}
return subs, nil
}
func (db *datastore) FetchEmailSubscriberEmail(subID, token string) (string, error) {
var email sql.NullString
// TODO: return user email if there's a user_id ?
err := db.QueryRow("SELECT email FROM emailsubscribers WHERE id = ? AND token = ?", subID, token).Scan(&email)
switch {
case err == sql.ErrNoRows:
return "", fmt.Errorf("Subscriber doesn't exist or token is invalid.")
case err != nil:
log.Error("Couldn't SELECT email from emailsubscribers: %v", err)
return "", fmt.Errorf("Something went very wrong.")
}
return email.String, nil
}
func (db *datastore) FetchEmailSubscriber(email string, userID, collID int64) (*EmailSubscriber, error) {
const emailSubCols = "id, collection_id, user_id, email, subscribed, token, confirmed, allow_export"
s := &EmailSubscriber{}
var row *sql.Row
if email != "" {
row = db.QueryRow("SELECT "+emailSubCols+" FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID)
} else {
row = db.QueryRow("SELECT "+emailSubCols+" FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID)
}
err := row.Scan(&s.ID, &s.CollID, &s.UserID, &s.Email, &s.Subscribed, &s.Token, &s.Confirmed, &s.AllowExport)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
}
return s, nil
}
func (db *datastore) DeleteEmailSubscriber(subID, token string) error {
res, err := db.Exec("DELETE FROM emailsubscribers WHERE id = ? AND token = ?", subID, token)
if err != nil {
return err
}
rowsAffected, _ := res.RowsAffected()
if rowsAffected == 0 {
return impart.HTTPError{http.StatusNotFound, "Invalid token, or subscriber doesn't exist"}
}
return nil
}
func (db *datastore) DeleteEmailSubscriberByUser(email string, userID, collID int64) error {
var res sql.Result
var err error
if email != "" {
res, err = db.Exec("DELETE FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID)
} else {
res, err = db.Exec("DELETE FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID)
}
if err != nil {
return err
}
rowsAffected, _ := res.RowsAffected()
if rowsAffected == 0 {
return impart.HTTPError{http.StatusNotFound, "Subscriber doesn't exist"}
}
return nil
}
func (db *datastore) UpdateSubscriberConfirmed(subID, token string) error {
email, err := db.FetchEmailSubscriberEmail(subID, token)
if err != nil {
log.Error("Didn't fetch email subscriber: %v", err)
return err
}
// TODO: ensure all addresses with original name are also confirmed, e.g. matt+fake@write.as and matt@write.as are now confirmed
_, err = db.Exec("UPDATE emailsubscribers SET confirmed = 1 WHERE email = ?", email)
if err != nil {
log.Error("Could not update email subscriber confirmation status: %v", err)
return err
}
return nil
}
func (db *datastore) IsSubscriberConfirmed(email string) bool {
var dummy int64
err := db.QueryRow("SELECT 1 FROM emailsubscribers WHERE email = ? AND confirmed = 1", email).Scan(&dummy)
switch {
case err == sql.ErrNoRows:
return false
case err != nil:
log.Error("Couldn't SELECT in isSubscriberConfirmed: %v", err)
return false
}
return true
}
func (db *datastore) InsertJob(j *PostJob) error {
res, err := db.Exec("INSERT INTO publishjobs (post_id, action, delay) VALUES (?, ?, ?)", j.PostID, j.Action, j.Delay)
if err != nil {
return err
}
jobID, err := res.LastInsertId()
if err != nil {
log.Error("[jobs] Couldn't get last insert ID! %s", err)
}
log.Info("[jobs] Queued %s job #%d for post %s, delayed %d minutes", j.Action, jobID, j.PostID, j.Delay)
return nil
}
func (db *datastore) UpdateJobForPost(postID string, delay int64) error {
_, err := db.Exec("UPDATE publishjobs SET delay = ? WHERE post_id = ?", delay, postID)
if err != nil {
return fmt.Errorf("Unable to update publish job: %s", err)
}
log.Info("Updated job for post %s: delay %d", postID, delay)
return nil
}
func (db *datastore) DeleteJob(id int64) error {
_, err := db.Exec("DELETE FROM publishjobs WHERE id = ?", id)
if err != nil {
return err
}
log.Info("[job #%d] Deleted.", id)
return nil
}
func (db *datastore) DeleteJobByPost(postID string) error {
_, err := db.Exec("DELETE FROM publishjobs WHERE post_id = ?", postID)
if err != nil {
return err
}
log.Info("[job] Deleted job for post %s", postID)
return nil
}
func (db *datastore) GetJobsToRun(action string) ([]*PostJob, error) {
timeWhere := "created < DATE_SUB(NOW(), INTERVAL delay MINUTE) AND created > DATE_SUB(NOW(), INTERVAL delay + 5 MINUTE)"
if db.driverName == driverSQLite {
timeWhere = "created < DATETIME('now', '-' || delay || ' MINUTE') AND created > DATETIME('now', '-' || (delay+5) || ' MINUTE')"
}
rows, err := db.Query(`SELECT pj.id, post_id, action, delay
FROM publishjobs pj
INNER JOIN posts p
ON post_id = p.id
WHERE action = ? AND `+timeWhere+`
ORDER BY created ASC`, action)
if err != nil {
log.Error("Failed selecting from publishjobs: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve publish jobs."}
}
defer rows.Close()
jobs := []*PostJob{}
for rows.Next() {
j := &PostJob{}
err = rows.Scan(&j.ID, &j.PostID, &j.Action, &j.Delay)
jobs = append(jobs, j)
}
return jobs, nil
}

View file

@ -1,49 +0,0 @@
/*
* Copyright © 2024 Musing Studio LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package writefreely
import (
"database/sql"
"fmt"
"github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/log"
)
func apAddRemoteUser(app *App, t *sql.Tx, fullActor *activitystreams.Person) (int64, error) {
// Add remote user locally, since it wasn't found before
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox, fullActor.URL)
if err != nil {
t.Rollback()
return -1, fmt.Errorf("couldn't add new remoteuser in DB: %v", err)
}
remoteUserID, err := res.LastInsertId()
if err != nil {
t.Rollback()
return -1, fmt.Errorf("no lastinsertid for followers, rolling back: %v", err)
}
// Add in key
_, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, remoteUserID, fullActor.PublicKey.PublicKeyPEM)
if err != nil {
if !app.db.isDuplicateKeyErr(err) {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
return -1, fmt.Errorf("couldn't add follower keys in DB: %v", err)
} else {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
return -1, fmt.Errorf("couldn't add follower keys in DB: %v", err)
}
}
return remoteUserID, nil
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -247,7 +247,10 @@ func (b *CreateTableSqlBuilder) ToSQL() (string, error) {
} }
things = append(things, columnStr) things = append(things, columnStr)
} }
things = append(things, b.Constraints...) for _, constraint := range b.Constraints {
things = append(things, constraint)
}
if thingLen := len(things); thingLen > 0 { if thingLen := len(things); thingLen > 0 {
str.WriteString(" ( ") str.WriteString(" ( ")
for i, thing := range things { for i, thing := range things {

View file

@ -1,25 +0,0 @@
services:
app:
image: writefreely
container_name: writefreely
volumes:
- ./data:/data
ports:
- 127.0.0.1:8080:8080
depends_on:
- db
restart: unless-stopped
db:
image: lscr.io/linuxserver/mariadb
container_name: writefreely-mariadb
volumes:
- ./db:/config
environment:
- PUID=65534
- PGID=65534
- TZ=Etc/UTC
- MYSQL_DATABASE=writefreely
- MYSQL_USER=writefreely
- MYSQL_PASSWORD=P@ssw0rd
restart: unless-stopped

477
email.go
View file

@ -1,477 +0,0 @@
/*
* Copyright © 2019-2021 Musing Studio LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package writefreely
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/writefreely/writefreely/mailer"
"html/template"
"net/http"
"strings"
"time"
"github.com/aymerick/douceur/inliner"
"github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown/v2"
"github.com/writeas/impart"
"github.com/writeas/web-core/data"
"github.com/writeas/web-core/log"
"github.com/writefreely/writefreely/key"
"github.com/writefreely/writefreely/spam"
)
const (
emailSendDelay = 15
)
type (
SubmittedSubscription struct {
CollAlias string
UserID int64
Email string `schema:"email" json:"email"`
Web bool `schema:"web" json:"web"`
Slug string `schema:"slug" json:"slug"`
From string `schema:"from" json:"from"`
}
EmailSubscriber struct {
ID string
CollID int64
UserID sql.NullInt64
Email sql.NullString
Subscribed time.Time
Token string
Confirmed bool
AllowExport bool
acctEmail sql.NullString
}
)
func (es *EmailSubscriber) FinalEmail(keys *key.Keychain) string {
if !es.UserID.Valid || es.Email.Valid {
return es.Email.String
}
decEmail, err := data.Decrypt(keys.EmailKey, []byte(es.acctEmail.String))
if err != nil {
log.Error("Error decrypting user email: %v", err)
return ""
}
return string(decEmail)
}
func (es *EmailSubscriber) SubscribedFriendly() string {
return es.Subscribed.Format("January 2, 2006")
}
func handleCreateEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r)
vars := mux.Vars(r)
var err error
ss := SubmittedSubscription{
CollAlias: vars["alias"],
}
u := getUserSession(app, r)
if u != nil {
ss.UserID = u.ID
}
if reqJSON {
// Decode JSON request
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&ss)
if err != nil {
log.Error("Couldn't parse new subscription JSON request: %v\n", err)
return ErrBadJSON
}
} else {
err = r.ParseForm()
if err != nil {
log.Error("Couldn't parse new subscription form request: %v\n", err)
return ErrBadFormData
}
err = app.formDecoder.Decode(&ss, r.PostForm)
if err != nil {
log.Error("Continuing, but error decoding new subscription form request: %v\n", err)
//return ErrBadFormData
}
}
c, err := app.db.GetCollection(ss.CollAlias)
if err != nil {
log.Error("getCollection: %s", err)
return err
}
c.hostName = app.cfg.App.Host
from := c.CanonicalURL()
isAuthorBanned, err := app.db.IsUserSilenced(c.OwnerID)
if isAuthorBanned {
log.Info("Author is silenced, so subscription is blocked.")
return impart.HTTPError{http.StatusFound, from}
}
if ss.Web {
if u != nil && u.ID == c.OwnerID {
from = "/" + c.Alias + "/"
}
from += ss.Slug
}
if r.FormValue(spam.HoneypotFieldName()) != "" || r.FormValue("fake_password") != "" {
log.Info("Honeypot field was filled out! Not subscribing.")
return impart.HTTPError{http.StatusFound, from}
}
if ss.Email == "" && ss.UserID < 1 {
log.Info("No subscriber data. Not subscribing.")
return impart.HTTPError{http.StatusFound, from}
}
confirmed := app.db.IsSubscriberConfirmed(ss.Email)
es, err := app.db.AddEmailSubscription(c.ID, ss.UserID, ss.Email, confirmed)
if err != nil {
log.Error("addEmailSubscription: %s", err)
return err
}
// Send confirmation email if needed
if !confirmed {
err = sendSubConfirmEmail(app, c, ss.Email, es.ID, es.Token)
if err != nil {
log.Error("Failed to send subscription confirmation email: %s", err)
return err
}
}
if ss.Web {
session, err := app.sessionStore.Get(r, userEmailCookieName)
if err != nil {
// The cookie should still save, even if there's an error.
// Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144
log.Error("Getting user email cookie: %v; ignoring", err)
}
if confirmed {
addSessionFlash(app, w, r, "<strong>Subscribed</strong>. You'll now receive future blog posts via email.", nil)
} else {
addSessionFlash(app, w, r, "Please check your email and <strong>click the confirmation link</strong> to subscribe.", nil)
}
session.Values[userEmailCookieVal] = ss.Email
err = session.Save(r, w)
if err != nil {
log.Error("save email cookie: %s", err)
return err
}
return impart.HTTPError{http.StatusFound, from}
}
return impart.WriteSuccess(w, "", http.StatusAccepted)
}
func handleDeleteEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error {
alias := collectionAliasFromReq(r)
vars := mux.Vars(r)
subID := vars["subscriber"]
email := r.FormValue("email")
token := r.FormValue("t")
slug := r.FormValue("slug")
isWeb := r.Method == "GET"
// Display collection if this is a collection
var c *Collection
var err error
if app.cfg.App.SingleUser {
c, err = app.db.GetCollectionByID(1)
} else {
c, err = app.db.GetCollection(alias)
}
if err != nil {
log.Error("Get collection: %s", err)
return err
}
from := c.CanonicalURL()
if subID != "" {
// User unsubscribing via email, so assume action is taken by either current
// user or not current user, and only use the request's information to
// satisfy this unsubscribe, i.e. subscriberID and token.
err = app.db.DeleteEmailSubscriber(subID, token)
} else {
// User unsubscribing through the web app, so assume action is taken by
// currently-auth'd user.
var userID int64
u := getUserSession(app, r)
if u != nil {
// User is logged in
userID = u.ID
if userID == c.OwnerID {
from = "/" + c.Alias + "/"
}
}
if email == "" && userID <= 0 {
// Get email address from saved cookie
session, err := app.sessionStore.Get(r, userEmailCookieName)
if err != nil {
log.Error("Unable to get email cookie: %s", err)
} else {
email = session.Values[userEmailCookieVal].(string)
}
}
if email == "" && userID <= 0 {
err = fmt.Errorf("No subscriber given.")
log.Error("Not deleting subscription: %s", err)
return err
}
err = app.db.DeleteEmailSubscriberByUser(email, userID, c.ID)
}
if err != nil {
log.Error("Unable to delete subscriber: %v", err)
return err
}
if isWeb {
from += slug
addSessionFlash(app, w, r, "<strong>Unsubscribed</strong>. You will no longer receive these blog posts via email.", nil)
return impart.HTTPError{http.StatusFound, from}
}
return impart.WriteSuccess(w, "", http.StatusAccepted)
}
func handleConfirmEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error {
alias := collectionAliasFromReq(r)
subID := mux.Vars(r)["subscriber"]
token := r.FormValue("t")
var c *Collection
var err error
if app.cfg.App.SingleUser {
c, err = app.db.GetCollectionByID(1)
} else {
c, err = app.db.GetCollection(alias)
}
if err != nil {
log.Error("Get collection: %s", err)
return err
}
from := c.CanonicalURL()
err = app.db.UpdateSubscriberConfirmed(subID, token)
if err != nil {
addSessionFlash(app, w, r, err.Error(), nil)
return impart.HTTPError{http.StatusFound, from}
}
addSessionFlash(app, w, r, "<strong>Confirmed</strong>! Thanks. Now you'll receive future blog posts via email.", nil)
return impart.HTTPError{http.StatusFound, from}
}
func emailPost(app *App, p *PublicPost, collID int64) error {
p.augmentContent()
// Do some shortcode replacement.
// Since the user is receiving this email, we can assume they're subscribed via email.
p.Content = strings.Replace(p.Content, "<!--emailsub-->", `<p id="emailsub">You're subscribed to email updates.</p>`, -1)
if p.HTMLContent == template.HTML("") {
p.formatContent(app.cfg, false, false)
}
p.augmentReadingDestination()
title := p.Title.String
if title != "" {
title = p.Title.String + "\n\n"
}
plainMsg := title + "A new post from " + p.CanonicalURL(app.cfg.App.Host) + "\n\n" + stripmd.Strip(p.Content)
plainMsg += `
---------------------------------------------------------------------------------
Originally published on ` + p.Collection.DisplayTitle() + ` (` + p.Collection.CanonicalURL() + `), a blog you subscribe to.
Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%`
mlr, err := mailer.New(app.cfg.Email)
if err != nil {
return err
}
m, err := mlr.NewMessage(p.Collection.DisplayTitle()+" <"+p.Collection.Alias+"@"+app.cfg.Email.Domain+">", stripmd.Strip(p.DisplayTitle()), plainMsg)
if err != nil {
return err
}
replyTo := app.db.GetCollectionAttribute(collID, collAttrLetterReplyTo)
if replyTo != "" {
m.SetReplyTo(replyTo)
}
subs, err := app.db.GetEmailSubscribers(collID, true)
if err != nil {
log.Error("Unable to get email subscribers: %v", err)
return err
}
if len(subs) == 0 {
return nil
}
if title != "" {
title = string(`<h2 id="title">` + p.FormattedDisplayTitle() + `</h2>`)
}
m.AddTag("New post")
fontFam := "Lora, Palatino, Baskerville, serif"
if p.IsSans() {
fontFam = `"Open Sans", Tahoma, Arial, sans-serif`
} else if p.IsMonospace() {
fontFam = `Hack, consolas, Menlo-Regular, Menlo, Monaco, monospace, monospace`
}
// TODO: move this to a templated file and LESS-generated stylesheet
fullHTML := `<html>
<head>
<style>
body {
font-size: 120%;
font-family: ` + fontFam + `;
margin: 1em 2em;
}
#article {
line-height: 1.5;
margin: 1.5em 0;
white-space: pre-wrap;
word-wrap: break-word;
}
h1, h2, h3, h4, h5, h6, p, code {
display: inline
}
img, iframe, video {
max-width: 100%
}
#title {
margin-bottom: 1em;
display: block;
}
.intro {
font-style: italic;
font-size: 0.95em;
}
div#footer {
text-align: center;
max-width: 35em;
margin: 2em auto;
}
div#footer p {
display: block;
font-size: 0.86em;
color: #666;
}
hr {
border: 1px solid #ccc;
margin: 2em 1em;
}
p#emailsub {
text-align: center;
display: inline-block !important;
width: 100%;
font-style: italic;
}
</style>
</head>
<body>
<div id="article">` + title + `<p class="intro">From <a href="` + p.CanonicalURL(app.cfg.App.Host) + `">` + p.DisplayCanonicalURL() + `</a></p>
` + string(p.HTMLContent) + `</div>
<hr />
<div id="footer">
<p>Originally published on <a href="` + p.Collection.CanonicalURL() + `">` + p.Collection.DisplayTitle() + `</a>, a blog you subscribe to.</p>
<p>Sent to %recipient.to%. <a href="` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%">Unsubscribe</a>.</p>
</div>
</body>
</html>`
// inline CSS
html, err := inliner.Inline(fullHTML)
if err != nil {
log.Error("Unable to inline email HTML: %v", err)
return err
}
m.SetHTML(html)
log.Info("[email] Adding %d recipient(s)", len(subs))
for _, s := range subs {
e := s.FinalEmail(app.keys)
log.Info("[email] Adding %s", e)
err = m.AddRecipientAndVariables(e, map[string]string{
"id": s.ID,
"to": e,
"token": s.Token,
})
if err != nil {
log.Error("Unable to add receipient %s: %s", e, err)
}
}
err = mlr.Send(m)
log.Info("[email] Email sent")
if err != nil {
log.Error("Unable to send post email: %v", err)
return err
}
return nil
}
func sendSubConfirmEmail(app *App, c *Collection, email, subID, token string) error {
if email == "" {
return fmt.Errorf("You must supply an email to verify.")
}
// Send email
mlr, err := mailer.New(app.cfg.Email)
if err != nil {
return err
}
plainMsg := "Confirm your subscription to " + c.DisplayTitle() + ` (` + c.CanonicalURL() + `) to start receiving future posts. Simply click the following link (or copy and paste it into your browser):
` + c.CanonicalURL() + "email/confirm/" + subID + "?t=" + token + `
If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.`
m, err := mlr.NewMessage(c.DisplayTitle()+" <"+c.Alias+"@"+app.cfg.Email.Domain+">", "Confirm your subscription to "+c.DisplayTitle(), plainMsg, fmt.Sprintf("<%s>", email))
if err != nil {
return err
}
m.AddTag("Email Verification")
m.SetHTML(`<html>
<body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
<div style="font-size: 1.2em;">
<p>Confirm your subscription to <a href="` + c.CanonicalURL() + `">` + c.DisplayTitle() + `</a> to start receiving future posts:</p>
<p><a href="` + c.CanonicalURL() + `email/confirm/` + subID + `?t=` + token + `">Subscribe to ` + c.DisplayTitle() + `</a></p>
<p>If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.</p>
</div>
</body>
</html>`)
err = mlr.Send(m)
if err != nil {
return err
}
return nil
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2020 Musing Studio LLC. * Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019 Musing Studio LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -119,7 +119,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
var collObjs []CollectionObj var collObjs []CollectionObj
for _, c := range *colls { for _, c := range *colls {
co := &CollectionObj{Collection: c} co := &CollectionObj{Collection: c}
co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true, "") co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true)
if err != nil { if err != nil {
log.Error("unable to get collection posts: %v", err) log.Error("unable to get collection posts: %v", err)
} }

18
feed.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -15,7 +15,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/gorilla/feeds" . "github.com/gorilla/feeds"
"github.com/gorilla/mux" "github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown/v2" stripmd "github.com/writeas/go-strip-markdown/v2"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
@ -67,7 +67,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
if tag != "" { if tag != "" {
coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false) coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false)
} else { } else {
coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false, "") coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false)
} }
author := "" author := ""
@ -87,11 +87,11 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
siteURL += "tag:" + tag siteURL += "tag:" + tag
} }
feed := &feeds.Feed{ feed := &Feed{
Title: collectionTitle, Title: collectionTitle,
Link: &feeds.Link{Href: siteURL}, Link: &Link{Href: siteURL},
Description: coll.Description, Description: coll.Description,
Author: &feeds.Author{author, ""}, Author: &Author{author, ""},
Created: time.Now(), Created: time.Now(),
} }
@ -103,13 +103,13 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
// Create the item for the feed // Create the item for the feed
title = p.PlainDisplayTitle() title = p.PlainDisplayTitle()
permalink = fmt.Sprintf("%s%s", baseUrl, p.Slug.String) permalink = fmt.Sprintf("%s%s", baseUrl, p.Slug.String)
feed.Items = append(feed.Items, &feeds.Item{ feed.Items = append(feed.Items, &Item{
Id: fmt.Sprintf("%s%s", basePermalinkUrl, p.Slug.String), Id: fmt.Sprintf("%s%s", basePermalinkUrl, p.Slug.String),
Title: title, Title: title,
Link: &feeds.Link{Href: permalink}, Link: &Link{Href: permalink},
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>", Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
Content: string(p.HTMLContent), Content: string(p.HTMLContent),
Author: &feeds.Author{author, ""}, Author: &Author{author, ""},
Created: p.Created, Created: p.Created,
Updated: p.Updated, Updated: p.Updated,
}) })

99
go.mod
View file

@ -1,98 +1,49 @@
module github.com/writefreely/writefreely module github.com/writefreely/writefreely
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/aymerick/douceur v0.2.0
github.com/clbanning/mxj v1.8.4 // indirect github.com/clbanning/mxj v1.8.4 // indirect
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.0
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/fatih/color v1.10.0
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/go-sql-driver/mysql v1.6.0
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
github.com/fatih/color v1.17.0
github.com/go-ini/ini v1.67.0
github.com/go-sql-driver/mysql v1.8.1
github.com/go-test/deep v1.0.1 // indirect github.com/go-test/deep v1.0.1 // indirect
github.com/gobuffalo/envy v1.9.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/csrf v1.7.2 github.com/gorilla/csrf v1.7.0
github.com/gorilla/feeds v1.1.2 github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.4.1 github.com/gorilla/schema v1.2.0
github.com/gorilla/sessions v1.3.0 github.com/gorilla/sessions v1.2.0
github.com/gosimple/slug v1.14.0 github.com/guregu/null v3.5.0+incompatible
github.com/guregu/null v4.0.0+incompatible
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
github.com/mailgun/mailgun-go v2.0.0+incompatible github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/manifoldco/promptui v0.9.0 github.com/manifoldco/promptui v0.8.0
github.com/mattn/go-sqlite3 v1.14.21 github.com/mattn/go-sqlite3 v1.14.6
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.5
github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/go-wordwrap v1.0.1
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/onsi/ginkgo v1.16.4 // indirect github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469
github.com/onsi/gomega v1.13.0 // indirect
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.27.4 github.com/urfave/cli/v2 v2.3.0
github.com/writeas/activity v0.1.2 github.com/writeas/activity v0.1.2
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835 github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
github.com/writeas/go-strip-markdown/v2 v2.1.1 github.com/writeas/go-strip-markdown/v2 v2.1.1
github.com/writeas/go-webfinger v1.1.0 github.com/writeas/go-webfinger v1.1.0
github.com/writeas/httpsig v1.0.0 github.com/writeas/httpsig v1.0.0
github.com/writeas/impart v1.1.1 github.com/writeas/impart v1.1.1
github.com/writeas/import v0.2.1 github.com/writeas/import v0.2.1
github.com/writeas/monday v1.3.0 github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320
github.com/writeas/web-core v1.6.1-0.20231003013047-d81124d45431 github.com/writeas/slug v1.2.0
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b github.com/writeas/web-core v1.3.1-0.20210330164422-95a3a717ed8f
github.com/writefreely/go-nodeinfo v1.2.0 github.com/writefreely/go-nodeinfo v1.2.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.30.0 golang.org/x/net v0.0.0-20200707034311-ab3426394381
gopkg.in/ini.v1 v1.62.0
) )
require github.com/xhit/go-simple-mail/v2 v2.16.0 go 1.15
require (
code.as/core/socks v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe // indirect
github.com/gofrs/uuid v3.3.0+incompatible // indirect
github.com/gologme/log v1.2.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/writeas/go-writeas/v2 v2.0.2 // indirect
github.com/writeas/openssl-go v1.0.0 // indirect
github.com/writeas/slug v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
go 1.21

296
go.sum
View file

@ -1,18 +1,14 @@
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs=
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= 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/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 h1:AFSJaASPGYNbkUa5c8ZybrcW9pP3Cy7+z5dnpcc/qG8= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 h1:AFSJaASPGYNbkUa5c8ZybrcW9pP3Cy7+z5dnpcc/qG8=
github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
@ -22,150 +18,99 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/clbanning/mxj v1.8.3/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clbanning/mxj v1.8.3/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk=
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8= github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8=
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= 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/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE=
github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= github.com/guregu/null v3.5.0+incompatible h1:fSdvRTQtmBA4B4YDZXhLtxTIJZYuUxBFTTHS4B9djG4=
github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/guregu/null v3.5.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw=
github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM=
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= 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/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=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec h1:ZXWuspqypleMuJy4bzYEqlMhJnGAYpLrWe5p7W3CdvI= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec h1:ZXWuspqypleMuJy4bzYEqlMhJnGAYpLrWe5p7W3CdvI=
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:voECJzdraJmolzPBgL9Z7ANwXf4oMXaTCsIkdiPpR/g= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:voECJzdraJmolzPBgL9Z7ANwXf4oMXaTCsIkdiPpR/g=
github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-sqlite3 v1.14.21 h1:IXocQLOykluc3xPE0Lvy8FtggMz1G+U3mEjg+0zGizc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-sqlite3 v1.14.21/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w=
github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 h1:rAbv2gekFbUcjhUkruwo0vMJ0JqhUgg9tz7t+bxHbN4=
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469/go.mod h1:c61IFFAJw8ADWu54tti30Tj5VrBstVoTprmET35UEkY=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= 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/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 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/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= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
@ -173,23 +118,20 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY= github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0= github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835 h1:bm/7gYo6y3GxtTa1qyUFyCk29CTnBAKt7z4D2MASYrw= github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0=
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o= github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o=
github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28= github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA= github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
github.com/writeas/go-webfinger v1.1.0 h1:MzNyt0ry/GMsRmJGftn2o9mPwqK1Q5MLdh4VuJCfb1Q= github.com/writeas/go-webfinger v1.1.0 h1:MzNyt0ry/GMsRmJGftn2o9mPwqK1Q5MLdh4VuJCfb1Q=
github.com/writeas/go-webfinger v1.1.0/go.mod h1:w2VxyRO/J5vfNjJHYVubsjUGHd3RLDoVciz0DE3ApOc= github.com/writeas/go-webfinger v1.1.0/go.mod h1:w2VxyRO/J5vfNjJHYVubsjUGHd3RLDoVciz0DE3ApOc=
github.com/writeas/go-writeas v1.1.0 h1:WHGm6wriBkxYAOGbvriXH8DlMUGOi6jhSZLUZKQ+4mQ=
github.com/writeas/go-writeas v1.1.0/go.mod h1:oh9U1rWaiE0p3kzdKwwvOpNXgp0P0IELI7OLOwV4fkA= github.com/writeas/go-writeas v1.1.0/go.mod h1:oh9U1rWaiE0p3kzdKwwvOpNXgp0P0IELI7OLOwV4fkA=
github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk= github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk=
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
@ -200,134 +142,46 @@ github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o=
github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg= github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg=
github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM= github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
github.com/writeas/monday v1.3.0 h1:h51wJ0DULXIDZ1w11zutLL7YCBRO5LznXISSzqVLZeA= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c=
github.com/writeas/monday v1.3.0/go.mod h1:9/CdGLDdIeAvzvf4oeihX++PE/qXUT2+tUlPQKCfRWY= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ=
github.com/writeas/openssl-go v1.0.0 h1:YXM1tDXeYOlTyJjoMlYLQH1xOloUimSR1WMF8kjFc5o= github.com/writeas/openssl-go v1.0.0 h1:YXM1tDXeYOlTyJjoMlYLQH1xOloUimSR1WMF8kjFc5o=
github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD25TppjJnETA= github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD25TppjJnETA=
github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/saturday v1.6.0/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 h1:PozPZ29CQ/xt6ym/+FvIz+KvKEObSSc5ye+95zbTjVU= github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 h1:PozPZ29CQ/xt6ym/+FvIz+KvKEObSSc5ye+95zbTjVU=
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
github.com/writeas/web-core v1.6.1-0.20231003013047-d81124d45431 h1:ruqL2u87k504PXkR/fC4DcfZyyHmCindlpjOQKmyOsY= github.com/writeas/web-core v1.3.1-0.20210330164422-95a3a717ed8f h1:ItBZYzdIbBmmqn8BZGWww00MBFgcUKy5ei0gJrzRDFk=
github.com/writeas/web-core v1.6.1-0.20231003013047-d81124d45431/go.mod h1:7+idL4Y4woF7MnUfNX2mvkaQ8nLIJXths2y5iYPtA3k= github.com/writeas/web-core v1.3.1-0.20210330164422-95a3a717ed8f/go.mod h1:DzNxa0YLV/wNeeWeHFPNa/nHmyJBFIIzXN/m9PpDm5c=
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b h1:h3NzB8OZ50NNi5k9yrFeyFszt3LyqyVK4+xUHFYY8B0=
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b/go.mod h1:T2UVVzt+R5KSSZe2xRSytnwc2M9AoDegi7foeIsik+M=
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= 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= github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= 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.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020 Musing Studio LLC. * Copyright © 2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -18,8 +18,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/prologic/go-gopher"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writefreely/go-gopher"
) )
func initGopher(apper Apper) { func initGopher(apper Apper) {
@ -111,7 +111,7 @@ func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request
w.WriteInfo(c.Description) w.WriteInfo(c.Description)
} }
posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false, "") posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -21,9 +21,9 @@ import (
"time" "time"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/prologic/go-gopher"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writefreely/go-gopher"
"github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/config"
"github.com/writefreely/writefreely/page" "github.com/writefreely/writefreely/page"
) )
@ -155,14 +155,8 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
err := f(h.app.App(), u, w, r) err := f(h.app.App(), u, w, r)
if err == nil { if err == nil {
status = http.StatusOK status = http.StatusOK
} else if impErr, ok := err.(impart.HTTPError); ok { } else if err, ok := err.(impart.HTTPError); ok {
status = impErr.Status status = err.Status
if impErr == ErrUserNotFound {
log.Info("Logged-in user not found. Logging out.")
sendRedirect(w, http.StatusFound, "/me/logout?to="+h.app.App().cfg.App.LandingPath())
// Reset err so handleHTTPError does nothing
err = nil
}
} else { } else {
status = http.StatusInternalServerError status = http.StatusInternalServerError
} }
@ -262,7 +256,7 @@ func apiAuth(app *App, r *http.Request) (*User, error) {
return u, nil return u, nil
} }
// optionalAPIAuth is used for endpoints that accept authenticated requests via // optionaAPIAuth is used for endpoints that accept authenticated requests via
// Authorization header or cookie, unlike apiAuth. It returns a different err // Authorization header or cookie, unlike apiAuth. It returns a different err
// in the case where no Authorization header is present. // in the case where no Authorization header is present.
func optionalAPIAuth(app *App, r *http.Request) (*User, error) { func optionalAPIAuth(app *App, r *http.Request) (*User, error) {
@ -818,7 +812,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
return return
} else if err.Status == http.StatusNotFound { } else if err.Status == http.StatusNotFound {
w.WriteHeader(err.Status) w.WriteHeader(err.Status)
if IsActivityPubRequest(r) { if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
// This is a fediverse request; simply return the header // This is a fediverse request; simply return the header
return return
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019 Musing Studio LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2021 Musing Studio LLC. * Copyright © 2019-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -78,9 +78,6 @@ func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Req
p.Silenced, err = app.db.IsUserSilenced(u.ID) p.Silenced, err = app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("view invites: %v", err) log.Error("view invites: %v", err)
} }

72
jobs.go
View file

@ -1,72 +0,0 @@
package writefreely
import (
"github.com/writeas/web-core/log"
"time"
)
type PostJob struct {
ID int64
PostID string
Action string
Delay int64
}
func addJob(app *App, p *PublicPost, action string, delay int64) error {
j := &PostJob{
PostID: p.ID,
Action: action,
Delay: delay,
}
return app.db.InsertJob(j)
}
func startPublishJobsQueue(app *App) {
t := time.NewTicker(62 * time.Second)
for {
log.Info("[jobs] Done.")
<-t.C
log.Info("[jobs] Fetching email publish jobs...")
jobs, err := app.db.GetJobsToRun("email")
if err != nil {
log.Error("[jobs] %s - Skipping.", err)
continue
}
log.Info("[jobs] Running %d email publish jobs...", len(jobs))
err = runJobs(app, jobs, true)
if err != nil {
log.Error("[jobs] Failed: %s", err)
}
}
}
func runJobs(app *App, jobs []*PostJob, reqColl bool) error {
for _, j := range jobs {
p, err := app.db.GetPost(j.PostID, 0)
if err != nil {
log.Info("[job #%d] Unable to get post: %s", j.ID, err)
continue
}
if !p.CollectionID.Valid && reqColl {
log.Info("[job #%d] Post %s not part of a collection", j.ID, p.ID)
app.db.DeleteJob(j.ID)
continue
}
coll, err := app.db.GetCollectionByID(p.CollectionID.Int64)
if err != nil {
log.Info("[job #%d] Unable to get collection: %s", j.ID, err)
continue
}
coll.hostName = app.cfg.App.Host
coll.ForPublic()
p.Collection = &CollectionObj{Collection: *coll}
err = emailPost(app, p, p.Collection.ID)
if err != nil {
log.Error("[job #%d] Failed to email post %s", j.ID, p.ID)
continue
}
log.Info("[job #%d] Success for post %s.", j.ID, p.ID)
app.db.DeleteJob(j.ID)
}
return nil
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019, 2021 Musing Studio LLC. * Copyright © 2019, 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019, 2021 Musing Studio LLC. * Copyright © 2018-2019, 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -13,6 +13,7 @@ package writefreely
import ( import (
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writefreely/writefreely/key" "github.com/writefreely/writefreely/key"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
) )
@ -51,7 +52,7 @@ func initKeyPaths(app *App) {
func generateKey(path string) error { func generateKey(path string) error {
// Check if key file exists // Check if key file exists
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
log.Info("%s already exists. rm the file if you understand the consequences.", path) log.Info("%s already exists. rm the file if you understand the consquences.", path)
return nil return nil
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
log.Error("%s", err) log.Error("%s", err)
@ -64,7 +65,7 @@ func generateKey(path string) error {
log.Error("FAILED. %s. Run writefreely --gen-keys again.", err) log.Error("FAILED. %s. Run writefreely --gen-keys again.", err)
return err return err
} }
err = os.WriteFile(path, b, 0600) err = ioutil.WriteFile(path, b, 0600)
if err != nil { if err != nil {
log.Error("FAILED writing file: %s", err) log.Error("FAILED writing file: %s", err)
return err return err

View file

@ -60,35 +60,6 @@ nav#admin {
background: #ccc; background: #ccc;
} }
} }
&.sub {
margin: 1em 0 2em;
a:not(.toggle) {
border: 0;
border-bottom: 2px transparent solid;
.rounded(0);
padding: 0.5em;
margin-left: 0.5em;
margin-right: 0.5em;
&:hover {
color: @primary;
background: transparent;
}
&.selected {
color: @primary;
background: transparent;
border-bottom-color: @primary;
}
&+a {
margin-left: 1em;
}
}
a.toggle {
margin-top: -0.5em;
float: right;
}
}
} }
.admin-actions { .admin-actions {

View file

@ -5,7 +5,7 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: white; background-color: white;
color: #111; color: #111;
h1, header h2 { h1, header h2 {
a { a {
color: @headerTextColor; color: @headerTextColor;
@ -210,10 +210,6 @@ body {
pre { pre {
line-height: 1.5; line-height: 1.5;
} }
.flash {
text-align: center;
margin-bottom: 4em;
}
} }
&#subpage { &#subpage {
#wrapper { #wrapper {
@ -672,26 +668,6 @@ body#collection article, body#subpage article {
} }
} }
} }
#wrapper.archive {
h1 {
margin: 0 !important;
}
ul {
list-style: none;
li {
display: flex;
justify-content: space-between;
line-height: 1.4;
margin: 0.5em 0;
}
.year {
font-weight: bold;
font-size: 1.5em;
}
}
}
body#post article { body#post article {
p.badge { p.badge {
font-size: 0.9em; font-size: 0.9em;
@ -719,7 +695,6 @@ table.downloads {
select.inputform, textarea.inputform { select.inputform, textarea.inputform {
border: 1px solid #999; border: 1px solid #999;
background: white;
} }
input, button, select.inputform, textarea.inputform, a.btn { input, button, select.inputform, textarea.inputform, a.btn {
@ -851,9 +826,6 @@ input {
margin: 0 auto 3em; margin: 0 auto 3em;
font-size: 1.2em; font-size: 1.2em;
&.toosmall {
max-width: 25em;
}
&.tight { &.tight {
max-width: 30em; max-width: 30em;
} }
@ -1121,7 +1093,7 @@ li {
} }
} }
ul.errors { ul.errors {
padding: 0; padding: 0;
text-indent: 0; text-indent: 0;
li.urgent { li.urgent {
@ -1624,18 +1596,6 @@ pre.code-block {
overflow-x: auto; overflow-x: auto;
} }
#emailsub {
text-align: center;
}
p#emailsub {
display: inline-block !important;
width: 100%;
font-style: italic;
}
#subscribe-btn {
margin-left: 0.5em;
}
#org-nav { #org-nav {
font-family: @sansFont; font-family: @sansFont;
font-size: 1.1em; font-size: 1.1em;
@ -1657,4 +1617,4 @@ p#emailsub {
font-weight: bold; font-weight: bold;
margin-left: 0.25em; margin-left: 0.25em;
} }
} }

View file

@ -3,6 +3,7 @@
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: optional;
src: url('/fonts/open-sans-v13-latin-regular.eot'); /* IE9 Compat Modes */ src: url('/fonts/open-sans-v13-latin-regular.eot'); /* IE9 Compat Modes */
src: local('Open Sans'), local('OpenSans'), src: local('Open Sans'), local('OpenSans'),
url('/fonts/open-sans-v13-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('/fonts/open-sans-v13-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
@ -16,6 +17,7 @@
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: optional;
src: url('/fonts/open-sans-v13-latin-700.eot'); /* IE9 Compat Modes */ src: url('/fonts/open-sans-v13-latin-700.eot'); /* IE9 Compat Modes */
src: local('Open Sans Bold'), local('OpenSans-Bold'), src: local('Open Sans Bold'), local('OpenSans-Bold'),
url('/fonts/open-sans-v13-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('/fonts/open-sans-v13-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
@ -29,6 +31,7 @@
font-family: 'Lora'; font-family: 'Lora';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: optional;
src: url('/fonts/Lora-Regular.eot'); /* IE9 Compat Modes */ src: url('/fonts/Lora-Regular.eot'); /* IE9 Compat Modes */
src: local('Lora'), local('Lora-Regular'), src: local('Lora'), local('Lora-Regular'),
url('/fonts/Lora-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('/fonts/Lora-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
@ -41,6 +44,7 @@
font-family: 'Lora'; font-family: 'Lora';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: optional;
src: url('/fonts/Lora-Bold.eot'); /* IE9 Compat Modes */ src: url('/fonts/Lora-Bold.eot'); /* IE9 Compat Modes */
src: local('Lora Bold'), local('Lora-Bold'), src: local('Lora Bold'), local('Lora-Bold'),
url('/fonts/Lora-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('/fonts/Lora-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
@ -52,6 +56,7 @@
font-family: 'Lora'; font-family: 'Lora';
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: optional;
src: url('/fonts/Lora-Italic.eot'); /* IE9 Compat Modes */ src: url('/fonts/Lora-Italic.eot'); /* IE9 Compat Modes */
src: local('Lora Italic'), local('Lora-Italic'), src: local('Lora Italic'), local('Lora-Italic'),
url('/fonts/Lora-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('/fonts/Lora-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020 Musing Studio LLC. * Copyright © 2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -12,7 +12,7 @@ body {
&:hover { &:hover {
.opacity(1); .opacity(1);
} }
h1 { h1 {
font-size: 1.6em; font-size: 1.6em;
} }
@ -30,7 +30,7 @@ body {
} }
} }
article, pre, .hljs, #wrapper.archive ul { article, pre, .hljs {
padding: 0.5em 2rem 1.5em; padding: 0.5em 2rem 1.5em;
} }
body#post article, pre, .hljs { body#post article, pre, .hljs {

View file

@ -1,181 +0,0 @@
/*
* Copyright © 2024 Musing Studio 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 mailer
import (
"fmt"
"github.com/mailgun/mailgun-go"
"github.com/writeas/web-core/log"
"github.com/writefreely/writefreely/config"
mail "github.com/xhit/go-simple-mail/v2"
"strings"
)
type (
// Mailer holds configurations for the preferred mailing provider.
Mailer struct {
smtp *mail.SMTPServer
mailGun *mailgun.MailgunImpl
}
// Message holds the email contents and metadata for the preferred mailing provider.
Message struct {
mgMsg *mailgun.Message
smtpMsg *SmtpMessage
}
SmtpMessage struct {
from string
replyTo string
subject string
recipients []Recipient
html string
text string
}
Recipient struct {
email string
vars map[string]string
}
)
// New creates a new Mailer from the instance's config.EmailCfg, returning an error if not properly configured.
func New(eCfg config.EmailCfg) (*Mailer, error) {
m := &Mailer{}
if eCfg.Domain != "" && eCfg.MailgunPrivate != "" {
m.mailGun = mailgun.NewMailgun(eCfg.Domain, eCfg.MailgunPrivate)
if eCfg.MailgunEurope {
m.mailGun.SetAPIBase("https://api.eu.mailgun.net/v3")
}
} else if eCfg.Username != "" && eCfg.Password != "" && eCfg.Host != "" && eCfg.Port > 0 {
m.smtp = mail.NewSMTPClient()
m.smtp.Host = eCfg.Host
m.smtp.Port = eCfg.Port
m.smtp.Username = eCfg.Username
m.smtp.Password = eCfg.Password
if eCfg.EnableStartTLS {
m.smtp.Encryption = mail.EncryptionSTARTTLS
}
// To allow sending multiple email
m.smtp.KeepAlive = true
} else {
return nil, fmt.Errorf("no email provider is configured")
}
return m, nil
}
// NewMessage creates a new Message from the given parameters.
func (m *Mailer) NewMessage(from, subject, text string, to ...string) (*Message, error) {
msg := &Message{}
if m.mailGun != nil {
msg.mgMsg = m.mailGun.NewMessage(from, subject, text, to...)
} else if m.smtp != nil {
msg.smtpMsg = &SmtpMessage{
from: from,
replyTo: "",
subject: subject,
recipients: make([]Recipient, len(to)),
html: "",
text: text,
}
for _, r := range to {
msg.smtpMsg.recipients = append(msg.smtpMsg.recipients, Recipient{r, make(map[string]string)})
}
}
return msg, nil
}
// SetHTML sets the body of the message.
func (m *Message) SetHTML(html string) {
if m.smtpMsg != nil {
m.smtpMsg.html = html
} else if m.mgMsg != nil {
m.mgMsg.SetHtml(html)
}
}
func (m *Message) SetReplyTo(replyTo string) {
if m.smtpMsg != nil {
m.smtpMsg.replyTo = replyTo
} else {
m.mgMsg.SetReplyTo(replyTo)
}
}
// AddTag attaches a tag to the Message for providers that support it.
func (m *Message) AddTag(tag string) {
if m.mgMsg != nil {
m.mgMsg.AddTag(tag)
}
}
func (m *Message) AddRecipientAndVariables(r string, vars map[string]string) error {
if m.smtpMsg != nil {
m.smtpMsg.recipients = append(m.smtpMsg.recipients, Recipient{r, vars})
return nil
} else {
varsInterfaces := make(map[string]interface{}, len(vars))
for k, v := range vars {
varsInterfaces[k] = v
}
return m.mgMsg.AddRecipientAndVariables(r, varsInterfaces)
}
}
// Send sends the given message via the preferred provider.
func (m *Mailer) Send(msg *Message) error {
if m.smtp != nil {
client, err := m.smtp.Connect()
if err != nil {
return err
}
emailSent := false
for _, r := range msg.smtpMsg.recipients {
customMsg := mail.NewMSG()
customMsg.SetFrom(msg.smtpMsg.from)
if msg.smtpMsg.replyTo != "" {
customMsg.SetReplyTo(msg.smtpMsg.replyTo)
}
customMsg.SetSubject(msg.smtpMsg.subject)
customMsg.AddTo(r.email)
cText := msg.smtpMsg.text
cHtml := msg.smtpMsg.html
for v, value := range r.vars {
placeHolder := fmt.Sprintf("%%recipient.%s%%", v)
cText = strings.ReplaceAll(cText, placeHolder, value)
cHtml = strings.ReplaceAll(cHtml, placeHolder, value)
}
customMsg.SetBody(mail.TextHTML, cHtml)
customMsg.AddAlternative(mail.TextPlain, cText)
e := customMsg.Error
if e == nil {
e = customMsg.Send(client)
}
if e == nil {
emailSent = true
} else {
log.Error("Unable to send email to %s: %v", r.email, e)
err = e
}
}
if !emailSent {
// only send an error if no email could be sent (to avoid retry of successfully sent emails)
return err
}
} else if m.mailGun != nil {
_, _, err := m.mailGun.Send(msg.mgMsg)
if err != nil {
return err
}
}
return nil
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019 Musing Studio LLC. * Copyright © 2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -36,13 +36,6 @@ func (db *datastore) typeSmallInt() string {
return "SMALLINT" return "SMALLINT"
} }
func (db *datastore) typeTinyInt() string {
if db.driverName == driverSQLite {
return "INTEGER"
}
return "TINYINT"
}
func (db *datastore) typeText() string { func (db *datastore) typeText() string {
return "TEXT" return "TEXT"
} }
@ -61,13 +54,6 @@ func (db *datastore) typeVarChar(l int) string {
return fmt.Sprintf("VARCHAR(%d)", l) return fmt.Sprintf("VARCHAR(%d)", l)
} }
func (db *datastore) typeVarBinary(l int) string {
if db.driverName == driverSQLite {
return "BLOB"
}
return fmt.Sprintf("VARBINARY(%d)", l)
}
func (db *datastore) typeBool() string { func (db *datastore) typeBool() string {
if db.driverName == driverSQLite { if db.driverName == driverSQLite {
return "INTEGER" return "INTEGER"
@ -79,15 +65,6 @@ func (db *datastore) typeDateTime() string {
return "DATETIME" return "DATETIME"
} }
func (db *datastore) typeIntPrimaryKey() string {
if db.driverName == driverSQLite {
// From docs: "In SQLite, a column with type INTEGER PRIMARY KEY is an alias for the ROWID (except in WITHOUT
// ROWID tables) which is always a 64-bit signed integer."
return "INTEGER PRIMARY KEY"
}
return "INT AUTO_INCREMENT PRIMARY KEY"
}
func (db *datastore) collateMultiByte() string { func (db *datastore) collateMultiByte() string {
if db.driverName == driverSQLite { if db.driverName == driverSQLite {
return "" return ""

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019 Musing Studio LLC. * Copyright © 2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -65,13 +65,7 @@ var migrations = []Migration{
New("support oauth attach", oauthAttach), // V6 -> V7 New("support oauth attach", oauthAttach), // V6 -> V7
New("support oauth via invite", oauthInvites), // V7 -> V8 (v0.12.0) New("support oauth via invite", oauthInvites), // V7 -> V8 (v0.12.0)
New("optimize drafts retrieval", optimizeDrafts), // V8 -> V9 New("optimize drafts retrieval", optimizeDrafts), // V8 -> V9
New("support post signatures", supportPostSignatures), // V9 -> V10 (v0.13.0) New("support post signatures", supportPostSignatures), // V9 -> V10
New("Widen oauth_users.access_token", widenOauthAcceesToken), // V10 -> V11
New("support verifying fedi profile", fediverseVerifyProfile), // V11 -> V12 (v0.14.0)
New("support newsletters", supportLetters), // V12 -> V13
New("support password resetting", supportPassReset), // V13 -> V14
New("speed up blog post retrieval", addPostRetrievalIndex), // V14 -> V15
New("support ActivityPub likes", supportRemoteLikes), // V15 -> V16 (v0.16.0)
} }
// CurrentVer returns the current migration version the application is on // CurrentVer returns the current migration version the application is on

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019 Musing Studio LLC. * Copyright © 2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020 Musing Studio LLC. * Copyright © 2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,38 +0,0 @@
/*
* Copyright © 2020 Musing Studio 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
/**
* Widen `oauth_users.access_token`, necessary only for mysql
*/
func widenOauthAcceesToken(db *datastore) error {
if db.driverName == driverMySQL {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`ALTER TABLE oauth_users MODIFY COLUMN access_token ` + db.typeText() + db.collateMultiByte() + ` NULL`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
}
return nil
}

View file

@ -1,33 +0,0 @@
/*
* Copyright © 2023 Musing Studio 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 fediverseVerifyProfile(db *datastore) error {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN url ` + db.typeVarChar(255) + ` NULL` + db.after("shared_inbox"))
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

View file

@ -1,58 +0,0 @@
/*
* Copyright © 2021 Musing Studio 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 supportLetters(db *datastore) error {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`CREATE TABLE publishjobs (
id ` + db.typeIntPrimaryKey() + `,
post_id ` + db.typeVarChar(16) + ` not null,
action ` + db.typeVarChar(16) + ` not null,
delay ` + db.typeTinyInt() + ` not null
)`)
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`CREATE TABLE emailsubscribers (
id ` + db.typeChar(8) + ` not null,
collection_id ` + db.typeInt() + ` not null,
user_id ` + db.typeInt() + ` null,
email ` + db.typeVarChar(255) + ` null,
subscribed ` + db.typeDateTime() + ` not null,
token ` + db.typeChar(16) + ` not null,
confirmed ` + db.typeBool() + ` default 0 not null,
allow_export ` + db.typeBool() + ` default 0 not null,
constraint eu_coll_email
unique (collection_id, email),
constraint eu_coll_user
unique (collection_id, user_id),
PRIMARY KEY (id)
)`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

View file

@ -1,37 +0,0 @@
/*
* Copyright © 2023 Musing Studio 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 supportPassReset(db *datastore) error {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`CREATE TABLE password_resets (
user_id ` + db.typeInt() + ` not null,
token ` + db.typeChar(32) + ` not null primary key,
used ` + db.typeBool() + ` default 0 not null,
created ` + db.typeDateTime() + ` not null
)`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

View file

@ -1,33 +0,0 @@
/*
* Copyright © 2023 Musing Studio 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 addPostRetrievalIndex(db *datastore) error {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec("CREATE INDEX posts_get_collection_index ON posts (`collection_id`, `pinned_position`, `created`)")
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

View file

@ -1,38 +0,0 @@
/*
* Copyright © 2024 Musing Studio 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 supportRemoteLikes(db *datastore) error {
t, err := db.Begin()
if err != nil {
t.Rollback()
return err
}
_, err = t.Exec(`CREATE TABLE remote_likes (
post_id ` + db.typeChar(16) + ` NOT NULL,
remote_user_id ` + db.typeInt() + ` NOT NULL,
created ` + db.typeDateTime() + ` NOT NULL,
PRIMARY KEY (post_id,remote_user_id)
)`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019 Musing Studio LLC. * Copyright © 2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019 Musing Studio LLC. * Copyright © 2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2021 Musing Studio LLC. * Copyright © 2019-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2021 Musing Studio LLC. * Copyright © 2019-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020 Musing Studio LLC. * Copyright © 2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -16,7 +16,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"io" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -144,7 +144,7 @@ func verifyReceipt(receipt, id string) error {
defer resp.Body.Close() defer resp.Body.Close()
} }
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Error("Unable to read %s response body: %s", receiptsHost, err) log.Error("Unable to read %s response body: %s", receiptsHost, err)
return err return err

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019, 2021 Musing Studio LLC. * Copyright © 2018-2019, 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -46,7 +46,7 @@ func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config {
Software: nodeinfo.SoftwareMeta{ Software: nodeinfo.SoftwareMeta{
HomePage: softwareURL, HomePage: softwareURL,
GitHub: "https://github.com/writefreely/writefreely", GitHub: "https://github.com/writefreely/writefreely",
Follow: "https://writing.exchange/@writefreely", Follow: "https://writing.exchange/@write_as",
}, },
MaxBlogs: cfg.App.MaxBlogs, MaxBlogs: cfg.App.MaxBlogs,
PublicReader: cfg.App.LocalTimeline, PublicReader: cfg.App.LocalTimeline,
@ -94,20 +94,14 @@ INNER JOIN collections c
ON collection_id = c.id ON collection_id = c.id
WHERE collection_id IS NOT NULL WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`).Scan(&activeHalfYear) AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`).Scan(&activeHalfYear)
if err != nil {
log.Error("Failed getting 6-month active user stats: %s", err)
}
err = r.db.QueryRow(`SELECT COUNT(*) FROM ( err = r.db.QueryRow(`SELECT COUNT(*) FROM (
SELECT DISTINCT collection_id SELECT DISTINCT collection_id
FROM posts FROM posts
INNER JOIN collections c INNER JOIN FROM collections c
ON collection_id = c.id ON collection_id = c.id
WHERE collection_id IS NOT NULL WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`).Scan(&activeMonth) AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`).Scan(&activeMonth)
if err != nil {
log.Error("Failed getting 1-month active user stats: %s", err)
}
} }
return nodeinfo.Usage{ return nodeinfo.Usage{

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2021 Musing Studio LLC. * Copyright © 2019-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -15,6 +15,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -292,15 +293,10 @@ func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) {
ClientID: app.Config().GiteaOauth.ClientID, ClientID: app.Config().GiteaOauth.ClientID,
ClientSecret: app.Config().GiteaOauth.ClientSecret, ClientSecret: app.Config().GiteaOauth.ClientSecret,
ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token", ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token",
InspectLocation: app.Config().GiteaOauth.Host + "/login/oauth/userinfo", InspectLocation: app.Config().GiteaOauth.Host + "/api/v1/user",
AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize", AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize",
HttpClient: config.DefaultHTTPClient(), HttpClient: config.DefaultHTTPClient(),
CallbackLocation: callbackLocation, CallbackLocation: callbackLocation,
Scope: "openid profile email",
MapUserID: "sub",
MapUsername: "login",
MapDisplayName: "full_name",
MapEmail: "email",
} }
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
} }
@ -359,7 +355,7 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
} }
if localUserID != -1 && attachUserID > 0 { if localUserID != -1 && attachUserID > 0 {
if err = addSessionFlash(app, w, r, "This OAuth account is already attached to another user.", nil); err != nil { if err = addSessionFlash(app, w, r, "This Slack account is already attached to another user.", nil); err != nil {
return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()} return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
} }
return impart.HTTPError{http.StatusFound, "/me/settings"} return impart.HTTPError{http.StatusFound, "/me/settings"}
@ -380,7 +376,6 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
} }
if attachUserID > 0 { if attachUserID > 0 {
log.Info("attaching to user %d", attachUserID) log.Info("attaching to user %d", attachUserID)
log.Info("OAuth userid: %s", tokenInfo.UserID)
err = h.DB.RecordRemoteUserID(r.Context(), attachUserID, tokenInfo.UserID, provider, clientID, tokenResponse.AccessToken) err = h.DB.RecordRemoteUserID(r.Context(), attachUserID, tokenInfo.UserID, provider, clientID, tokenResponse.AccessToken)
if err != nil { if err != nil {
return impart.HTTPError{http.StatusInternalServerError, err.Error()} return impart.HTTPError{http.StatusInternalServerError, err.Error()}
@ -449,7 +444,7 @@ func (r *callbackProxyClient) register(ctx context.Context, state string) error
func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error { func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error {
lr := io.LimitReader(body, int64(n+1)) lr := io.LimitReader(body, int64(n+1))
data, err := io.ReadAll(lr) data, err := ioutil.ReadAll(lr)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC and respective authors. * Copyright © 2020-2021 A Bunch Tell LLC and respective authors.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -70,8 +70,6 @@ func (c genericOauthClient) buildLoginURL(state string) (string, error) {
func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
form := url.Values{} form := url.Values{}
form.Add("client_id", c.ClientID)
form.Add("client_secret", c.ClientSecret)
form.Add("grant_type", "authorization_code") form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", c.CallbackLocation) form.Add("redirect_uri", c.CallbackLocation)
form.Add("scope", c.Scope) form.Add("scope", c.Scope)

View file

@ -3,8 +3,6 @@ package writefreely
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/writeas/web-core/log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -17,11 +15,6 @@ type giteaOauthClient struct {
ExchangeLocation string ExchangeLocation string
InspectLocation string InspectLocation string
CallbackLocation string CallbackLocation string
Scope string
MapUserID string
MapUsername string
MapDisplayName string
MapEmail string
HttpClient HttpClient HttpClient HttpClient
} }
@ -53,7 +46,7 @@ func (c giteaOauthClient) buildLoginURL(state string) (string, error) {
q.Set("redirect_uri", c.CallbackLocation) q.Set("redirect_uri", c.CallbackLocation)
q.Set("response_type", "code") q.Set("response_type", "code")
q.Set("state", state) q.Set("state", state)
q.Set("scope", c.Scope) // q.Set("scope", "read_user")
u.RawQuery = q.Encode() u.RawQuery = q.Encode()
return u.String(), nil return u.String(), nil
} }
@ -62,7 +55,7 @@ func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*
form := url.Values{} form := url.Values{}
form.Add("grant_type", "authorization_code") form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", c.CallbackLocation) form.Add("redirect_uri", c.CallbackLocation)
form.Add("scope", c.Scope) // form.Add("scope", "read_user")
form.Add("code", code) form.Add("code", code)
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode()))
if err != nil { if err != nil {
@ -110,24 +103,12 @@ func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
return nil, errors.New("unable to inspect access token") return nil, errors.New("unable to inspect access token")
} }
// since we don't know what the JSON from the server will look like, we create a var inspectResponse InspectResponse
// generic interface and then map manually to values set in the config if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
var genericInterface map[string]interface{}
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil {
return nil, err return nil, err
} }
if inspectResponse.Error != "" {
// map each relevant field in inspectResponse to the mapped field from the config return nil, errors.New(inspectResponse.Error)
var inspectResponse InspectResponse
inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string)
// log.Info("Userid from Gitea: %s", inspectResponse.UserID)
if inspectResponse.UserID == "" {
log.Error("[CONFIGURATION ERROR] Gitea OAuth provider returned empty UserID value (`%s`).\n Do you need to configure a different `map_user_id` value for this provider?", c.MapUserID)
return nil, fmt.Errorf("no UserID (`%s`) value returned", c.MapUserID)
} }
inspectResponse.Username, _ = genericInterface[c.MapUsername].(string)
inspectResponse.DisplayName, _ = genericInterface[c.MapDisplayName].(string)
inspectResponse.Email, _ = genericInterface[c.MapEmail].(string)
return &inspectResponse, nil return &inspectResponse, nil
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2020-2021 Musing Studio LLC. * Copyright © 2020-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2020 Musing Studio LLC. * Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -13,7 +13,7 @@ package writefreely
import ( import (
"context" "context"
"errors" "errors"
"github.com/gosimple/slug" "github.com/writeas/slug"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2019-2021 Musing Studio LLC. * Copyright © 2019-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,10 +0,0 @@
[provider_sect]
default = default_sect
legacy = legacy_sect
[default_sect]
activate = 1
[legacy_sect]
activate = 1

5
pad.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -55,9 +55,6 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
} }
appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID) appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID)
if err != nil { if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("Unable to get user status for Pad: %v", err) log.Error("Unable to get user status for Pad: %v", err)
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019, 2021 Musing Studio LLC. * Copyright © 2018-2019, 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -21,7 +21,6 @@ type StaticPage struct {
config.AppCfg config.AppCfg
Version string Version string
HeaderNav bool HeaderNav bool
CustomCSS bool
// Request values // Request values
Path string Path string

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019, 2021 Musing Studio LLC. * Copyright © 2018-2019, 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -40,28 +40,6 @@ func defaultAboutTitle(cfg *config.Config) sql.NullString {
return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true} return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true}
} }
func getContactPage(app *App) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("contact")
if err != nil {
return nil, err
}
if c == nil {
c = &instanceContent{
ID: "contact",
Type: "page",
Content: defaultContactPage(app),
}
}
if !c.Title.Valid {
c.Title = defaultContactTitle()
}
return c, nil
}
func defaultContactTitle() sql.NullString {
return sql.NullString{String: "Contact Us", Valid: true}
}
func getPrivacyPage(app *App) (*instanceContent, error) { func getPrivacyPage(app *App) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("privacy") c, err := app.db.GetDynamicContent("privacy")
if err != nil { if err != nil {
@ -92,24 +70,12 @@ func defaultAboutPage(cfg *config.Config) string {
return `_` + cfg.App.SiteName + `_ is a place for you to write and publish, powered by [WriteFreely](https://writefreely.org).` return `_` + cfg.App.SiteName + `_ is a place for you to write and publish, powered by [WriteFreely](https://writefreely.org).`
} }
func defaultContactPage(app *App) string {
c, err := app.db.GetCollectionByID(1)
if err != nil {
return ""
}
return `_` + app.cfg.App.SiteName + `_ is administered by: [**` + c.Alias + `**](/` + c.Alias + `/).
Contact them at this email address: _EMAIL GOES HERE_.
You can also reach them here...`
}
func defaultPrivacyPolicy(cfg *config.Config) string { func defaultPrivacyPolicy(cfg *config.Config) string {
return `[WriteFreely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default. return `[WriteFreely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default.
It retains as little data about you as possible, not even requiring an email address to sign up. However, if you _do_ give us your email address, it is stored encrypted in our database. We salt and hash your account's password. It retains as little data about you as possible, not even requiring an email address to sign up. However, if you _do_ give us your email address, it is stored encrypted in our database. We salt and hash your account's password.
We store log files, or data about what happens on our servers. We also use cookies to keep you logged into your account. We store log files, or data about what happens on our servers. We also use cookies to keep you logged in to your account.
Beyond this, it's important that you trust whoever runs **` + cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.` Beyond this, it's important that you trust whoever runs **` + cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.`
} }

View file

@ -1,6 +1,7 @@
{{define "head"}}<title>Page not found &mdash; {{.SiteName}}</title>{{end}} {{define "head"}}<title>Page not found &mdash; {{.SiteName}}</title>{{end}}
{{define "content"}} {{define "content"}}
<div class="error-page"> <div class="error-page">
<p class="msg">Page not found.</p> <p class="msg">This page is missing.</p>
<p>Are you sure it was ever here?</p>
</div> </div>
{{end}} {{end}}

View file

@ -2,7 +2,9 @@
{{define "content"}} {{define "content"}}
<div class="content-container tight"> <div class="content-container tight">
<h1>Server error &#x1F635;</h1> <h1>Server error &#x1F635;</h1>
<p>There seems to be an issue with this server. Please <a href="/contact">contact the admin</a> and let them know they'll need to fix it.</p> <p>Please <a href="https://github.com/writefreely/writefreely/issues/new">contact the human authors</a> of this software and remind them of their many shortcomings.</p>
<p>Be gentle, though. They are fragile mortal beings.</p>
<p style="margin-top:2em">Also, unlike the AI that will soon replace them, you will need to include an error log from the server in your report. (Utterly <em>primitive</em>, we know.)</p>
<p>&ndash; {{.SiteName}} &#x1F916;</p> <p>&ndash; {{.SiteName}} &#x1F916;</p>
</div> </div>
{{end}} {{end}}

View file

@ -1,8 +0,0 @@
{{define "head"}}<title>{{.ContentTitle}} &mdash; {{.SiteName}}</title>
<meta name="description" content="{{.PlainContent}}">
{{end}}
{{define "content"}}<div class="content-container snug">
<h1>{{.ContentTitle}}</h1>
{{.Content}}
</div>
{{end}}

View file

@ -1,19 +1,13 @@
{{define "head"}}<title>Log in &mdash; {{.SiteName}}</title> {{define "head"}}<title>Log in &mdash; {{.SiteName}}</title>
<meta name="description" content="Log into {{.SiteName}}."> <meta name="description" content="Log in to {{.SiteName}}.">
<meta itemprop="description" content="Log into {{.SiteName}}."> <meta itemprop="description" content="Log in to {{.SiteName}}.">
<style> <style>
input{margin-bottom:0.5em;} input{margin-bottom:0.5em;}
p.forgot {
font-size: 0.8em;
margin: 0 auto 1.5rem;
text-align: left;
max-width: 16rem;
}
</style> </style>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div class="tight content-container"> <div class="tight content-container">
<h1>Log into {{.SiteName}}</h1> <h1>Log in to {{.SiteName}}</h1>
{{if .Flashes}}<ul class="errors"> {{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
@ -25,7 +19,6 @@ p.forgot {
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()"> <form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
<input type="text" name="alias" placeholder="Username" value="{{.LoginUsername}}" {{if not .LoginUsername}}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 /> <input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br />
{{if .EmailEnabled}}<p class="forgot"><a href="/reset">Forgot password?</a></p>{{end}}
{{if .To}}<input type="hidden" name="to" value="{{.To}}" />{{end}} {{if .To}}<input type="hidden" name="to" value="{{.To}}" />{{end}}
<input type="submit" id="btn-login" value="Login" /> <input type="submit" id="btn-login" value="Login" />
</form> </form>

View file

@ -1,58 +0,0 @@
{{define "head"}}<title>Reset password &mdash; {{.SiteName}}</title>
<style>
input {
margin-bottom: 0.5em;
width: 100%;
box-sizing: border-box;
}
label {
display: block;
}
</style>
{{end}}
{{define "content"}}
<div class="toosmall content-container clean">
<h1>Reset your password</h1>
{{ if .DisablePasswordAuth }}
<div class="alert info">
<p><strong>Password login is disabled on this server</strong>, so it's not possible to reset your password.</p>
</div>
{{ else if not .EmailEnabled }}
<div class="alert info">
<p><strong>Email is not configured on this server!</strong> Please <a href="/contact">contact your admin</a> to reset your password.</p>
</div>
{{ else }}
{{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}}
{{if .IsResetting}}
<form method="post" action="/reset" onsubmit="disableSubmit()">
<label>
<p>New Password</p>
<input type="password" name="new-pass" autocomplete="new-password" placeholder="New password" tabindex="1" />
</label>
<input type="hidden" name="t" value="{{.Token}}" />
<input type="submit" id="btn-login" value="Reset Password" />
{{ .CSRFField }}
</form>
{{else if not .IsSent}}
<form action="/reset" method="post" onsubmit="disableSubmit()">
<label>
<p>Username</p>
<input type="text" name="alias" placeholder="Username" autofocus />
</label>
{{ .CSRFField }}
<input type="submit" id="btn-login" value="Reset Password" />
</form>
{{end}}
<script type="text/javascript">
var $btn = document.getElementById("btn-login");
function disableSubmit() {
$btn.disabled = true;
}
</script>
{{ end }}
{{end}}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2020 Musing Studio LLC. * Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -80,12 +80,3 @@ func TruncToWord(s string, l int) (string, bool) {
} }
return s, truncated return s, truncated
} }
// Truncate truncates the given text to the provided limit, returning the original string if it's shorter than the limit.
func Truncate(s string, l int) string {
c := []rune(s)
if len(c) > l {
s = string(c[:l])
}
return s
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 Musing Studio LLC. * Copyright © 2018 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -120,7 +120,7 @@ func (p *PublicPost) augmentReadingDestination() {
} }
func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
return applyMarkdownSpecial(data, baseURL, cfg, cfg.App.SingleUser) return applyMarkdownSpecial(data, false, baseURL, cfg)
} }
func disableYoutubeAutoplay(outHTML string) string { func disableYoutubeAutoplay(outHTML string) string {
@ -142,7 +142,7 @@ func disableYoutubeAutoplay(outHTML string) string {
return outHTML return outHTML
} }
func applyMarkdownSpecial(data []byte, baseURL string, cfg *config.Config, skipNoFollow bool) string { func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string {
mdExtensions := 0 | mdExtensions := 0 |
blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_TABLES |
blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_FENCED_CODE |
@ -201,9 +201,6 @@ func applyBasicMarkdown(data []byte) string {
md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
// Remove H1 markup // Remove H1 markup
md = bytes.TrimSpace(md) // blackfriday.Markdown adds a newline at the end of the <h1> md = bytes.TrimSpace(md) // blackfriday.Markdown adds a newline at the end of the <h1>
if len(md) == 0 {
return ""
}
md = md[len("<h1>") : len(md)-len("</h1>")] md = md[len("<h1>") : len(md)-len("</h1>")]
// Strip out bad HTML // Strip out bad HTML
policy := bluemonday.UGCPolicy() policy := bluemonday.UGCPolicy()
@ -270,7 +267,6 @@ func getSanitizationPolicy() *bluemonday.Policy {
policy.AllowAttrs("target").OnElements("a") policy.AllowAttrs("target").OnElements("a")
policy.AllowAttrs("title").OnElements("abbr") policy.AllowAttrs("title").OnElements("abbr")
policy.AllowAttrs("style", "class", "id").Globally() policy.AllowAttrs("style", "class", "id").Globally()
policy.AllowAttrs("alt").OnElements("img")
policy.AllowElements("header", "footer") policy.AllowElements("header", "footer")
policy.AllowURLSchemes("http", "https", "mailto", "xmpp") policy.AllowURLSchemes("http", "https", "mailto", "xmpp")
return policy return policy
@ -285,13 +281,12 @@ func sanitizePost(content string) string {
// choosing what to generate. In case a post has a title, this function will // choosing what to generate. In case a post has a title, this function will
// fail, and logic should instead be implemented to skip this when there's no // fail, and logic should instead be implemented to skip this when there's no
// title, like so: // title, like so:
// // var desc string
// var desc string // if title == "" {
// if title == "" { // desc = postDescription(content, title, friendlyId)
// desc = postDescription(content, title, friendlyId) // } else {
// } else { // desc = shortPostDescription(content)
// desc = shortPostDescription(content) // }
// }
func postDescription(content, title, friendlyId string) string { func postDescription(content, title, friendlyId string) string {
maxLen := 140 maxLen := 140

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2021 Musing Studio LLC. * Copyright © 2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *

114
posts.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2021 Musing Studio LLC. * Copyright © 2018-2021 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -14,7 +14,6 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/writefreely/writefreely/spam"
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
@ -23,7 +22,6 @@ import (
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gosimple/slug"
"github.com/guregu/null" "github.com/guregu/null"
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
"github.com/kylemcc/twitter-text-go/extract" "github.com/kylemcc/twitter-text-go/extract"
@ -31,6 +29,7 @@ import (
stripmd "github.com/writeas/go-strip-markdown/v2" stripmd "github.com/writeas/go-strip-markdown/v2"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/monday" "github.com/writeas/monday"
"github.com/writeas/slug"
"github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/bots" "github.com/writeas/web-core/bots"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
@ -49,12 +48,6 @@ const (
postIDLen = 10 postIDLen = 10
postMetaDateFormat = "2006-01-02 15:04:05" postMetaDateFormat = "2006-01-02 15:04:05"
)
type PostType string
const (
postArch PostType = "archive"
shortCodePaid = "<!--paid-->" shortCodePaid = "<!--paid-->"
) )
@ -111,7 +104,6 @@ type (
Created time.Time `db:"created" json:"created"` Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated"` Updated time.Time `db:"updated" json:"updated"`
ViewCount int64 `db:"view_count" json:"-"` ViewCount int64 `db:"view_count" json:"-"`
LikeCount int64 `db:"like_count" json:"likes"`
Title zero.String `db:"title" json:"title"` Title zero.String `db:"title" json:"title"`
HTMLTitle template.HTML `db:"title" json:"-"` HTMLTitle template.HTML `db:"title" json:"-"`
Content string `db:"content" json:"body"` Content string `db:"content" json:"body"`
@ -134,7 +126,6 @@ type (
IsTopLevel bool `json:"-"` IsTopLevel bool `json:"-"`
DisplayDate string `json:"-"` DisplayDate string `json:"-"`
Views int64 `json:"views"` Views int64 `json:"views"`
Likes int64 `json:"likes"`
Owner *PublicUser `json:"-"` Owner *PublicUser `json:"-"`
IsOwner bool `json:"-"` IsOwner bool `json:"-"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
@ -148,7 +139,6 @@ type (
IsPinned bool IsPinned bool
IsCustomDomain bool IsCustomDomain bool
Monetization string Monetization string
Verification string
PinnedPosts *[]PublicPost PinnedPosts *[]PublicPost
IsFound bool IsFound bool
IsAdmin bool IsAdmin bool
@ -322,8 +312,6 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
// Display reserved page if that is requested resource // Display reserved page if that is requested resource
if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok { if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok {
return handleTemplatedPage(app, w, r, t) return handleTemplatedPage(app, w, r, t)
} else if r.URL.Path == "/sitemap.xml" && !app.cfg.App.SingleUser {
return impart.HTTPError{Status: http.StatusNotFound, Message: "Page not found."}
} else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" {
// Serve static file // Serve static file
app.shttp.ServeHTTP(w, r) app.shttp.ServeHTTP(w, r)
@ -351,7 +339,6 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
var ownerID sql.NullInt64 var ownerID sql.NullInt64
var collectionID sql.NullInt64
var title string var title string
var content string var content string
var font string var font string
@ -367,7 +354,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/%s%s", fixedID, ext)} return impart.HTTPError{http.StatusFound, fmt.Sprintf("/%s%s", fixedID, ext)}
} }
err := app.db.QueryRow("SELECT owner_id, collection_id, title, content, text_appearance, view_count, language, rtl FROM posts WHERE id = ?", friendlyID).Scan(&ownerID, &collectionID, &title, &content, &font, &views, &language, &rtl) err := app.db.QueryRow(fmt.Sprintf("SELECT owner_id, title, content, text_appearance, view_count, language, rtl FROM posts WHERE id = ?"), friendlyID).Scan(&ownerID, &title, &content, &font, &views, &language, &rtl)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
found = false found = false
@ -437,16 +424,6 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
} }
var protectDraft bool
if found && collectionID.Valid {
collection, err := app.db.GetCollectionByID(collectionID.Int64)
if err != nil {
log.Error("view post: %v", err)
}
protectDraft = collection.IsPrivate() || collection.IsProtected()
}
// Check if post has been unpublished // Check if post has been unpublished
if title == "" && content == "" { if title == "" && content == "" {
gone = true gone = true
@ -511,10 +488,6 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
if !page.IsOwner && silenced { if !page.IsOwner && silenced {
return ErrPostNotFound return ErrPostNotFound
} }
if !page.IsOwner && protectDraft {
return ErrPostNotFound
}
page.Silenced = silenced page.Silenced = silenced
err = templates["post"].ExecuteTemplate(w, "post", page) err = templates["post"].ExecuteTemplate(w, "post", page)
if err != nil { if err != nil {
@ -543,9 +516,9 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
// newPost creates a new post with or without an owning Collection. // newPost creates a new post with or without an owning Collection.
// //
// Endpoints: // Endpoints:
// - /posts // /posts
// - /posts?collection={alias} // /posts?collection={alias}
// - ? /collections/{alias}/posts // ? /collections/{alias}/posts
func newPost(app *App, w http.ResponseWriter, r *http.Request) error { func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r) reqJSON := IsJSON(r)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -678,17 +651,8 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
// Write success now // Write success now
response := impart.WriteSuccess(w, newPost, http.StatusCreated) response := impart.WriteSuccess(w, newPost, http.StatusCreated)
if newPost.Collection != nil { if newPost.Collection != nil && !app.cfg.App.Private && app.cfg.App.Federation && !newPost.Created.After(time.Now()) {
if !app.cfg.App.Private && app.cfg.App.Federation && !newPost.Created.After(time.Now()) { go federatePost(app, newPost, newPost.Collection.ID, false)
go federatePost(app, newPost, newPost.Collection.ID, false)
}
if app.cfg.Email.Enabled() && newPost.Collection.EmailSubsEnabled() {
go app.db.InsertJob(&PostJob{
PostID: newPost.ID,
Action: "email",
Delay: emailSendDelay,
})
}
} }
return response return response
@ -988,23 +952,16 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
return err return err
} }
for _, pRes := range *res { if !app.cfg.App.Private && app.cfg.App.Federation {
if pRes.Code != http.StatusOK { for _, pRes := range *res {
continue if pRes.Code != http.StatusOK {
} continue
if !app.cfg.App.Private && app.cfg.App.Federation { }
if !pRes.Post.Created.After(time.Now()) { if !pRes.Post.Created.After(time.Now()) {
pRes.Post.Collection.hostName = app.cfg.App.Host pRes.Post.Collection.hostName = app.cfg.App.Host
go federatePost(app, pRes.Post, pRes.Post.Collection.ID, false) go federatePost(app, pRes.Post, pRes.Post.Collection.ID, false)
} }
} }
if app.cfg.Email.Enabled() && pRes.Post.Collection.EmailSubsEnabled() {
go app.db.InsertJob(&PostJob{
PostID: pRes.Post.ID,
Action: "email",
Delay: emailSendDelay,
})
}
} }
return impart.WriteSuccess(w, res, http.StatusOK) return impart.WriteSuccess(w, res, http.StatusOK)
} }
@ -1110,7 +1067,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
ppr := PinPostResult{ID: p.ID} ppr := PinPostResult{ID: p.ID}
if err != nil { if err != nil {
ppr.Code = http.StatusInternalServerError ppr.Code = http.StatusInternalServerError
// TODO: set error message // TODO: set error messsage
} else { } else {
ppr.Code = http.StatusOK ppr.Code = http.StatusOK
} }
@ -1162,7 +1119,8 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
p.extractData() p.extractData()
if IsActivityPubRequest(r) { accept := r.Header.Get("Accept")
if strings.Contains(accept, "application/activity+json") {
if coll == nil { if coll == nil {
// This is a draft post; 404 for now // This is a draft post; 404 for now
// TODO: return ActivityObject // TODO: return ActivityObject
@ -1192,7 +1150,6 @@ func fetchPostProperty(app *App, w http.ResponseWriter, r *http.Request) error {
func (p *Post) processPost() PublicPost { func (p *Post) processPost() PublicPost {
res := &PublicPost{Post: p, Views: 0} res := &PublicPost{Post: p, Views: 0}
res.Views = p.ViewCount res.Views = p.ViewCount
res.Likes = p.LikeCount
// TODO: move to own function // TODO: move to own function
loc := monday.FuzzyLocale(p.Language.String) loc := monday.FuzzyLocale(p.Language.String)
res.DisplayDate = monday.Format(p.Created, monday.LongFormatsByLocale[loc], loc) res.DisplayDate = monday.Format(p.Created, monday.LongFormatsByLocale[loc], loc)
@ -1207,15 +1164,6 @@ func (p *PublicPost) CanonicalURL(hostName string) string {
return p.Collection.CanonicalURL() + p.Slug.String return p.Collection.CanonicalURL() + p.Slug.String
} }
func (pp *PublicPost) DisplayCanonicalURL() string {
us := pp.CanonicalURL(pp.Collection.hostName)
u, err := url.Parse(us)
if err != nil {
return us
}
return u.Hostname() + u.Path
}
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object { func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
cfg := app.cfg cfg := app.cfg
var o *activitystreams.Object var o *activitystreams.Object
@ -1299,8 +1247,6 @@ func getSlug(title, lang string) string {
func getSlugFromPost(title, body, lang string) string { func getSlugFromPost(title, body, lang string) string {
if title == "" { if title == "" {
// Remove Markdown, so e.g. link URLs and image alt text don't make it into the slug
body = strings.TrimSpace(stripmd.StripOptions(body, stripmd.Options{SkipImages: true}))
title = postTitle(body, body) title = postTitle(body, body)
} }
title = parse.PostLede(title, false) title = parse.PostLede(title, false)
@ -1516,10 +1462,6 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
// User tried to access blog feed without a trailing slash, and // User tried to access blog feed without a trailing slash, and
// there's no post with a slug "feed" // there's no post with a slug "feed"
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "feed/"} return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "feed/"}
} else if slug == "archive" {
// User tried to access blog Archive without a trailing slash, and
// there's no post with a slug "archive"
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "archive/"}
} }
po := &Post{ po := &Post{
@ -1576,7 +1518,7 @@ Are you sure it was ever here?`,
fmt.Fprintf(w, "# %s\n\n", p.Title.String) fmt.Fprintf(w, "# %s\n\n", p.Title.String)
} }
fmt.Fprint(w, p.Content) fmt.Fprint(w, p.Content)
} else if IsActivityPubRequest(r) { } else if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
if !postFound { if !postFound {
return ErrCollectionPageNotFound return ErrCollectionPageNotFound
} }
@ -1588,15 +1530,6 @@ Are you sure it was ever here?`,
} else { } else {
p.extractData() p.extractData()
p.Content = strings.Replace(p.Content, "<!--more-->", "", 1) p.Content = strings.Replace(p.Content, "<!--more-->", "", 1)
if app.cfg.Email.Enabled() && c.EmailSubsEnabled() {
// TODO: indicate plan is inactive or subs disabled when OWNER is viewing their own post.
if u != nil && u.IsEmailSubscriber(app, c.ID) {
p.Content = strings.Replace(p.Content, "<!--emailsub-->", `<p id="emailsub">You're subscribed to email updates. <a href="/api/collections/`+c.Alias+`/email/unsubscribe?slug=`+p.Slug.String+`">Unsubscribe</a>.</p>`, -1)
} else {
p.Content = strings.Replace(p.Content, "<!--emailsub-->", `<form method="post" id="emailsub" action="/api/collections/`+c.Alias+`/email/subscribe"><input type="hidden" name="slug" value="`+p.Slug.String+`" /><input type="hidden" name="web" value="1" /><div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="email" name="`+spam.HoneypotFieldName()+`" tabindex="-1" value="" /><input type="password" name="fake_password" tabindex="-1" placeholder="password" autocomplete="new-password" /></div><input type="email" name="email" placeholder="me@example.com" /><input type="submit" id="subscribe-btn" value="Subscribe" /></form>`, -1)
}
}
p.Content = strings.Replace(p.Content, "&lt;!--emailsub-->", "<!--emailsub-->", 1)
// TODO: move this to function // TODO: move this to function
p.formatContent(app.cfg, cr.isCollOwner, true) p.formatContent(app.cfg, cr.isCollOwner, true)
tp := CollectionPostPage{ tp := CollectionPostPage{
@ -1612,8 +1545,7 @@ Are you sure it was ever here?`,
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner) tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner)
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
tp.Monetization = coll.Monetization tp.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer")
tp.Verification = coll.Verification
if !postFound { if !postFound {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -1661,14 +1593,6 @@ func (p *Post) extractData() {
p.extractImages() p.extractImages()
} }
func (p *Post) IsSans() bool {
return p.Font == "sans"
}
func (p *Post) IsMonospace() bool {
return p.Font == "mono"
}
func (rp *RawPost) UserFacingCreated() string { func (rp *RawPost) UserFacingCreated() string {
return rp.Created.Format(postMetaDateFormat) return rp.Created.Format(postMetaDateFormat)
} }
@ -1684,7 +1608,7 @@ func (rp *RawPost) Updated8601() string {
return rp.Updated.Format("2006-01-02T15:04:05Z") return rp.Updated.Format("2006-01-02T15:04:05Z")
} }
var imageURLRegex = regexp.MustCompile(`(?i)[^ ]+\.(gif|png|jpg|jpeg|avif|avifs|webp|jxl|image)$`) var imageURLRegex = regexp.MustCompile(`(?i)[^ ]+\.(gif|png|jpg|jpeg|image)$`)
func (p *Post) extractImages() { func (p *Post) extractImages() {
p.Images = extractImages(p.Content) p.Images = extractImages(p.Content)

Some files were not shown because too many files have changed in this diff Show more