Compare commits
No commits in common. "develop" and "hotfix-0.13.1" have entirely different histories.
develop
...
hotfix-0.1
149 changed files with 659 additions and 4194 deletions
|
@ -1 +1,2 @@
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
.git
|
||||||
|
|
|
@ -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
|
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
@ -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"
|
|
||||||
|
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] I have signed the [CLA](https://todo.musing.studio/L1)
|
- [ ] I have signed the [CLA](https://phabricator.write.as/L1)
|
||||||
|
|
70
.github/workflows/docker-publish.yml
vendored
70
.github/workflows/docker-publish.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -3,7 +3,6 @@ node_modules
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
static/local/custom.css
|
|
||||||
build
|
build
|
||||||
tmp
|
tmp
|
||||||
*.ini
|
*.ini
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
31
Dockerfile
31
Dockerfile
|
@ -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
|
|
|
@ -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"]
|
|
82
Makefile
82
Makefile
|
@ -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,12 +133,35 @@ 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
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
To report a vulnerability, send an email to security@writefreely.org.
|
|
316
account.go
316
account.go
|
@ -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,9 +1038,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -1067,15 +1047,12 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
||||||
Collection *Collection
|
Collection *Collection
|
||||||
TopPosts *[]PublicPost
|
TopPosts *[]PublicPost
|
||||||
APFollowers int
|
APFollowers int
|
||||||
EmailEnabled bool
|
|
||||||
EmailSubscribers int
|
|
||||||
Silenced bool
|
Silenced bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
|
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
|
||||||
VisitsBlog: alias,
|
VisitsBlog: alias,
|
||||||
Collection: c,
|
Collection: c,
|
||||||
TopPosts: topPosts,
|
TopPosts: topPosts,
|
||||||
EmailEnabled: app.cfg.Email.Enabled(),
|
|
||||||
Silenced: silenced,
|
Silenced: silenced,
|
||||||
}
|
}
|
||||||
obj.UserPage.CollAlias = c.Alias
|
obj.UserPage.CollAlias = c.Alias
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
336
activitypub.go
336
activitypub.go
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
32
admin.go
32
admin.go
|
@ -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."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
114
app.go
114
app.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
2
auth.go
2
auth.go
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
106
bindata-lib.go
Normal 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{}},
|
||||||
|
}}
|
2
cache.go
2
cache.go
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
292
collections.go
292
collections.go
|
@ -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,15 +577,11 @@ 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
|
||||||
|
|
||||||
|
@ -650,30 +589,6 @@ type CollectionPage struct {
|
||||||
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 {
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
567
database.go
567
database.go
|
@ -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."}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
477
email.go
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
18
feed.go
|
@ -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
99
go.mod
|
@ -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
296
go.sum
|
@ -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=
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
18
handle.go
18
handle.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
72
jobs.go
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
7
keys.go
7
keys.go
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
181
mailer/mailer.go
181
mailer/mailer.go
|
@ -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
|
|
||||||
}
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
nodeinfo.go
12
nodeinfo.go
|
@ -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{
|
||||||
|
|
15
oauth.go
15
oauth.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
|
|
||||||
[provider_sect]
|
|
||||||
default = default_sect
|
|
||||||
legacy = legacy_sect
|
|
||||||
|
|
||||||
[default_sect]
|
|
||||||
activate = 1
|
|
||||||
|
|
||||||
[legacy_sect]
|
|
||||||
activate = 1
|
|
5
pad.go
5
pad.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
38
pages.go
38
pages.go
|
@ -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.`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{{define "head"}}<title>Page not found — {{.SiteName}}</title>{{end}}
|
{{define "head"}}<title>Page not found — {{.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}}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="content-container tight">
|
<div class="content-container tight">
|
||||||
<h1>Server error 😵</h1>
|
<h1>Server error 😵</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>– {{.SiteName}} 🤖</p>
|
<p>– {{.SiteName}} 🤖</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{{define "head"}}<title>{{.ContentTitle}} — {{.SiteName}}</title>
|
|
||||||
<meta name="description" content="{{.PlainContent}}">
|
|
||||||
{{end}}
|
|
||||||
{{define "content"}}<div class="content-container snug">
|
|
||||||
<h1>{{.ContentTitle}}</h1>
|
|
||||||
{{.Content}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
|
@ -1,19 +1,13 @@
|
||||||
{{define "head"}}<title>Log in — {{.SiteName}}</title>
|
{{define "head"}}<title>Log in — {{.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>
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
{{define "head"}}<title>Reset password — {{.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}}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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,7 +281,6 @@ 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)
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
104
posts.go
104
posts.go
|
@ -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,18 +651,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !app.cfg.App.Private && app.cfg.App.Federation {
|
||||||
for _, pRes := range *res {
|
for _, pRes := range *res {
|
||||||
if pRes.Code != http.StatusOK {
|
if pRes.Code != http.StatusOK {
|
||||||
continue
|
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, "<!--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
Loading…
Add table
Reference in a new issue