Merge branch 'develop' into paginate-tag-collection
This commit is contained in:
commit
e65086b635
|
@ -0,0 +1,2 @@
|
|||
github: writefreely
|
||||
open_collective: writefreely
|
|
@ -5,3 +5,11 @@ updates:
|
|||
open-pull-requests-limit: 50
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/Dockerfile"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
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@v3
|
||||
|
||||
# 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@v2.2.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@v4.1.1
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -1,7 +1,9 @@
|
|||
node_modules
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
static/local/custom.css
|
||||
build
|
||||
tmp
|
||||
*.ini
|
||||
|
|
|
@ -4,7 +4,7 @@ Welcome! We're glad you're interested in contributing to WriteFreely.
|
|||
|
||||
For **questions**, **help**, **feature requests**, and **general discussion**, please use [our forum](https://discuss.write.as).
|
||||
|
||||
For **bug reports**, please [open a GitHub issue](https://github.com/writeas/writefreely/issues/new). See our guide on [submitting bug reports](https://writefreely.org/contribute#bugs).
|
||||
For **bug reports**, please [open a GitHub issue](https://github.com/writefreely/writefreely/issues/new). See our guide on [submitting bug reports](https://writefreely.org/contribute#bugs).
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
@ -80,9 +80,9 @@ We highly value commit messages that follow established form within the project.
|
|||
|
||||
When in doubt, look to our existing git history for examples of good commit messages. Here are a few:
|
||||
|
||||
* [Rename Suspend status to Silence](https://github.com/writeas/writefreely/commit/7e014ca65958750ab703e317b1ce8cfc4aad2d6e)
|
||||
* [Show 404 when remote user not found](https://github.com/writeas/writefreely/commit/867eb53b3596bd7b3f2be3c53a3faf857f4cd36d)
|
||||
* [Fix post deletion on Pleroma](https://github.com/writeas/writefreely/commit/fe82cbb96e3d5c57cfde0db76c28c4ea6dabfe50)
|
||||
* [Rename Suspend status to Silence](https://github.com/writefreely/writefreely/commit/7e014ca65958750ab703e317b1ce8cfc4aad2d6e)
|
||||
* [Show 404 when remote user not found](https://github.com/writefreely/writefreely/commit/867eb53b3596bd7b3f2be3c53a3faf857f4cd36d)
|
||||
* [Fix post deletion on Pleroma](https://github.com/writefreely/writefreely/commit/fe82cbb96e3d5c57cfde0db76c28c4ea6dabfe50)
|
||||
|
||||
### Submitting pull requests
|
||||
|
||||
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,30 +1,32 @@
|
|||
# Build image
|
||||
FROM golang:1.14-alpine as build
|
||||
FROM golang:1.19-alpine as build
|
||||
|
||||
RUN apk add --update nodejs nodejs-npm make g++ git
|
||||
RUN apk add --update nodejs npm make g++ git
|
||||
RUN npm install -g less less-plugin-clean-css
|
||||
RUN go get -u github.com/go-bindata/go-bindata/...
|
||||
|
||||
RUN mkdir -p /go/src/github.com/writeas/writefreely
|
||||
WORKDIR /go/src/github.com/writeas/writefreely
|
||||
RUN mkdir -p /go/src/github.com/writefreely/writefreely
|
||||
WORKDIR /go/src/github.com/writefreely/writefreely
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cat ossl_legacy.cnf > /etc/ssl/openssl.cnf
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV NODE_OPTIONS=--openssl-legacy-provider
|
||||
|
||||
RUN make build \
|
||||
&& make ui
|
||||
RUN mkdir /stage && \
|
||||
cp -R /go/bin \
|
||||
/go/src/github.com/writeas/writefreely/templates \
|
||||
/go/src/github.com/writeas/writefreely/static \
|
||||
/go/src/github.com/writeas/writefreely/pages \
|
||||
/go/src/github.com/writeas/writefreely/keys \
|
||||
/go/src/github.com/writeas/writefreely/cmd \
|
||||
/go/src/github.com/writefreely/writefreely/templates \
|
||||
/go/src/github.com/writefreely/writefreely/static \
|
||||
/go/src/github.com/writefreely/writefreely/pages \
|
||||
/go/src/github.com/writefreely/writefreely/keys \
|
||||
/go/src/github.com/writefreely/writefreely/cmd \
|
||||
/stage
|
||||
|
||||
# Final image
|
||||
FROM alpine:3.12
|
||||
FROM alpine:3
|
||||
|
||||
RUN apk add --no-cache openssl ca-certificates
|
||||
COPY --from=build --chown=daemon:daemon /stage /go
|
||||
|
|
70
Makefile
70
Makefile
|
@ -1,5 +1,5 @@
|
|||
GITREV=`git describe | cut -c 2-`
|
||||
LDFLAGS=-ldflags="-X 'github.com/writeas/writefreely.softwareVer=$(GITREV)'"
|
||||
LDFLAGS=-ldflags="-s -w -X 'github.com/writefreely/writefreely.softwareVer=$(GITREV)'"
|
||||
|
||||
GOCMD=go
|
||||
GOINSTALL=$(GOCMD) install $(LDFLAGS)
|
||||
|
@ -14,50 +14,50 @@ TMPBIN=./tmp
|
|||
|
||||
all : build
|
||||
|
||||
ci: ci-assets deps
|
||||
ci: deps
|
||||
cd cmd/writefreely; $(GOBUILD) -v
|
||||
|
||||
build: assets deps
|
||||
cd cmd/writefreely; $(GOBUILD) -v -tags='sqlite'
|
||||
build: deps
|
||||
cd cmd/writefreely; $(GOBUILD) -v -tags='netgo sqlite'
|
||||
|
||||
build-no-sqlite: assets-no-sqlite deps-no-sqlite
|
||||
cd cmd/writefreely; $(GOBUILD) -v -o $(BINARY_NAME)
|
||||
build-no-sqlite: deps-no-sqlite
|
||||
cd cmd/writefreely; $(GOBUILD) -v -tags='netgo' -o $(BINARY_NAME)
|
||||
|
||||
build-linux: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-windows: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-darwin: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-arm6: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=linux/arm-6, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=linux/arm-6, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-arm7: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-arm64: deps
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u src.techknowlogick.com/xgo; \
|
||||
$(GOCMD) install src.techknowlogick.com/xgo@latest; \
|
||||
fi
|
||||
xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely
|
||||
xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='netgo sqlite' -go go-1.19.x -out writefreely -pkg ./cmd/writefreely .
|
||||
|
||||
build-docker :
|
||||
$(DOCKERCMD) build -t $(IMAGE_NAME):latest -t $(IMAGE_NAME):$(GITREV) .
|
||||
|
@ -65,8 +65,8 @@ build-docker :
|
|||
test:
|
||||
$(GOTEST) -v ./...
|
||||
|
||||
run: dev-assets
|
||||
$(GOINSTALL) -tags='sqlite' ./...
|
||||
run:
|
||||
$(GOINSTALL) -tags='netgo sqlite' ./...
|
||||
$(BINARY_NAME) --debug
|
||||
|
||||
deps :
|
||||
|
@ -81,7 +81,7 @@ install : build
|
|||
cmd/writefreely/$(BINARY_NAME) --init-db
|
||||
cd less/; $(MAKE) install $(MFLAGS)
|
||||
|
||||
release : clean ui assets
|
||||
release : clean ui
|
||||
mkdir -p $(BUILDPATH)
|
||||
cp -r templates $(BUILDPATH)
|
||||
cp -r pages $(BUILDPATH)
|
||||
|
@ -105,13 +105,13 @@ release : clean ui assets
|
|||
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm64.tar.gz -C build $(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
$(MAKE) build-darwin
|
||||
mv build/$(BINARY_NAME)-darwin-10.6-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)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
$(MAKE) build-windows
|
||||
mv build/$(BINARY_NAME)-windows-4.0-amd64.exe $(BUILDPATH)/$(BINARY_NAME).exe
|
||||
cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./$(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME)
|
||||
rm $(BUILDPATH)/$(BINARY_NAME).exe
|
||||
$(MAKE) build-docker
|
||||
$(MAKE) release-docker
|
||||
|
||||
|
@ -131,36 +131,14 @@ release-docker :
|
|||
|
||||
ui : force_look
|
||||
cd less/; $(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
|
||||
cd prose/; $(MAKE) $(MFLAGS)
|
||||
|
||||
$(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)
|
||||
$(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 :
|
||||
-rm -rf build
|
||||
-rm -rf tmp
|
||||
|
|
24
README.md
24
README.md
|
@ -4,17 +4,17 @@
|
|||
</p>
|
||||
<hr />
|
||||
<p align="center">
|
||||
<a href="https://github.com/writeas/writefreely/releases/">
|
||||
<img src="https://img.shields.io/github/release/writeas/writefreely.svg" alt="Latest release" />
|
||||
<a href="https://github.com/writefreely/writefreely/releases/">
|
||||
<img src="https://img.shields.io/github/release/writefreely/writefreely.svg" alt="Latest release" />
|
||||
</a>
|
||||
<a href="https://travis-ci.org/writeas/writefreely">
|
||||
<img src="https://travis-ci.org/writeas/writefreely.svg" alt="Build status" />
|
||||
<img src="https://travis-ci.org/writefreely/writefreely.svg" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/writeas/writefreely/releases/latest">
|
||||
<img src="https://img.shields.io/github/downloads/writeas/writefreely/total.svg" />
|
||||
<a href="https://github.com/writefreely/writefreely/releases/latest">
|
||||
<img src="https://img.shields.io/github/downloads/writefreely/writefreely/total.svg" />
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/writeas/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writeas/writefreely" alt="Go Report Card" />
|
||||
<a href="https://goreportcard.com/report/github.com/writefreely/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writefreely/writefreely" alt="Go Report Card" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/writeas/writefreely/">
|
||||
<img src="https://img.shields.io/docker/pulls/writeas/writefreely.svg" />
|
||||
|
@ -22,7 +22,7 @@
|
|||
</p>
|
||||
|
||||
|
||||
WriteFreely is free and open source software for building **a writing space** on the web — whether a publication, internal blog, or writing community in the fediverse.
|
||||
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.
|
||||
|
||||
![](https://writefreely.org/img/screens/pencil-reader.png)
|
||||
|
||||
|
@ -62,7 +62,7 @@ The quickest way to deploy WriteFreely is with [Write.as](https://write.as/write
|
|||
|
||||
WriteFreely deploys as a static binary on any platform and architecture that Go supports. Just use our built-in SQLite support, or add a MySQL database, and you'll be up and running!
|
||||
|
||||
For common platforms, start with our [pre-built binaries](https://github.com/writeas/writefreely/releases/) and head over to our [installation guide](https://writefreely.org/start) to get started.
|
||||
For common platforms, start with our [pre-built binaries](https://github.com/writefreely/writefreely/releases/) and head over to our [installation guide](https://writefreely.org/start) to get started.
|
||||
|
||||
### Packages
|
||||
|
||||
|
@ -80,10 +80,10 @@ Start hacking on WriteFreely with our [developer setup guide](https://writefreel
|
|||
|
||||
## Contributing
|
||||
|
||||
We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writeas/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or [documentation](https://github.com/writefreely/documentation) improvements.
|
||||
We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writefreely/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writefreely/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or [documentation](https://github.com/writefreely/documentation) improvements.
|
||||
|
||||
Before contributing anything, please read our [Contributing Guide](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements.
|
||||
Before contributing anything, please read our [Contributing Guide](https://github.com/writefreely/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2018-2020 [A Bunch Tell LLC](https://abunchtell.com) and contributing authors. Licensed under the [AGPL](https://github.com/writeas/writefreely/blob/develop/LICENSE).
|
||||
Copyright © 2018-2022 [Musing Studio LLC](https://musing.studio) and contributing authors. Licensed under the [AGPL](https://github.com/writefreely/writefreely/blob/develop/LICENSE).
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to security@writefreely.org.
|
100
account.go
100
account.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -16,10 +16,12 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/guregu/null/zero"
|
||||
|
@ -27,10 +29,9 @@ import (
|
|||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/data"
|
||||
"github.com/writeas/web-core/log"
|
||||
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -166,7 +167,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
}
|
||||
|
||||
// Create actual user
|
||||
if err := app.db.CreateUser(app.cfg, u, desiredUsername); err != nil {
|
||||
if err := app.db.CreateUser(app.cfg, u, desiredUsername, signup.Description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -194,9 +195,27 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
{
|
||||
Alias: signup.Alias,
|
||||
Title: title,
|
||||
Description: signup.Description,
|
||||
},
|
||||
}
|
||||
|
||||
var coll *Collection
|
||||
if signup.Monetization != "" {
|
||||
if coll == nil {
|
||||
coll, err = app.db.GetCollection(signup.Alias)
|
||||
if err != nil {
|
||||
log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization)
|
||||
if err != nil {
|
||||
log.Error("Unable to add monetization on signup: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
coll.Monetization = signup.Monetization
|
||||
}
|
||||
|
||||
var token string
|
||||
if reqJSON && !signup.Web {
|
||||
token, err = app.db.GetAccessToken(u.ID)
|
||||
|
@ -691,6 +710,22 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
|
|||
return ErrBadRequestedType
|
||||
}
|
||||
|
||||
isAnonPosts := r.FormValue("anonymous") == "1"
|
||||
if isAnonPosts {
|
||||
pageStr := r.FormValue("page")
|
||||
pg, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
log.Error("Error parsing page parameter '%s': %s", pageStr, err)
|
||||
pg = 1
|
||||
}
|
||||
|
||||
p, err := app.db.GetAnonymousPosts(u, pg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
var err error
|
||||
p := GetPostsCache(u.ID)
|
||||
if p == nil {
|
||||
|
@ -731,7 +766,7 @@ func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
|||
}
|
||||
|
||||
func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p, err := app.db.GetAnonymousPosts(u)
|
||||
p, err := app.db.GetAnonymousPosts(u, 1)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch anon posts: %v", err)
|
||||
}
|
||||
|
@ -752,6 +787,9 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("view articles: %v", err)
|
||||
}
|
||||
d := struct {
|
||||
|
@ -787,7 +825,10 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
|
|||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
log.Error("view collections %v", err)
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("view collections: %v", err)
|
||||
return fmt.Errorf("view collections: %v", err)
|
||||
}
|
||||
d := struct {
|
||||
|
@ -822,10 +863,13 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
// Add collection properties
|
||||
c.MonetizationPointer = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
c.Monetization = app.db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("view edit collection %v", err)
|
||||
return fmt.Errorf("view edit collection: %v", err)
|
||||
}
|
||||
|
@ -986,9 +1030,10 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
if c.OwnerID != u.ID {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
}
|
||||
|
||||
topPosts, err := app.db.GetTopPosts(u, alias)
|
||||
topPosts, err := app.db.GetTopPosts(u, alias, c.hostName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get top posts: %v", err)
|
||||
return err
|
||||
|
@ -1002,6 +1047,9 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
|
||||
silenced, err := app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("view stats: %v", err)
|
||||
return err
|
||||
}
|
||||
|
@ -1035,6 +1083,9 @@ func viewStats(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)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return 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."}
|
||||
}
|
||||
|
@ -1083,6 +1134,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
CSRFField template.HTML
|
||||
OauthSection bool
|
||||
OauthAccounts []oauthAccountInfo
|
||||
OauthSlack bool
|
||||
|
@ -1099,6 +1151,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
CSRFField: csrf.TemplateField(r),
|
||||
OauthSection: displayOauthSection,
|
||||
OauthAccounts: oauthAccounts,
|
||||
OauthSlack: enableOauthSlack,
|
||||
|
@ -1153,6 +1206,32 @@ func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) s
|
|||
return s
|
||||
}
|
||||
|
||||
func handleUserDelete(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
if !app.cfg.App.OpenDeletion {
|
||||
return impart.HTTPError{http.StatusForbidden, "Open account deletion is disabled on this instance."}
|
||||
}
|
||||
|
||||
confirmUsername := r.PostFormValue("confirm-username")
|
||||
if u.Username != confirmUsername {
|
||||
return impart.HTTPError{http.StatusBadRequest, "Confirmation username must match your username exactly."}
|
||||
}
|
||||
|
||||
// Check for account deletion safeguards in place
|
||||
if u.IsAdmin() {
|
||||
return impart.HTTPError{http.StatusForbidden, "Cannot delete admin."}
|
||||
}
|
||||
|
||||
err := app.db.DeleteAccount(u.ID)
|
||||
if err != nil {
|
||||
log.Error("user delete account: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete account: %v", err)}
|
||||
}
|
||||
|
||||
// FIXME: This doesn't ever appear to the user, as (I believe) the value is erased when the session cookie is reset
|
||||
_ = addSessionFlash(app, w, r, "Thanks for writing with us! You account was deleted successfully.", nil)
|
||||
return impart.HTTPError{http.StatusFound, "/me/logout"}
|
||||
}
|
||||
|
||||
func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
provider := r.FormValue("provider")
|
||||
clientID := r.FormValue("client_id")
|
||||
|
@ -1174,6 +1253,7 @@ func prepareUserEmail(input string, emailKey []byte) zero.String {
|
|||
log.Error("Unable to encrypt email: %s\n", err)
|
||||
} else {
|
||||
email.String = string(encEmail)
|
||||
|
||||
}
|
||||
}
|
||||
return email
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -28,9 +29,9 @@ import (
|
|||
"github.com/writeas/activity/streams"
|
||||
"github.com/writeas/httpsig"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/web-core/activitypub"
|
||||
"github.com/writeas/web-core/activitystreams"
|
||||
"github.com/writeas/web-core/id"
|
||||
"github.com/writeas/web-core/log"
|
||||
)
|
||||
|
||||
|
@ -41,6 +42,19 @@ const (
|
|||
apCacheTime = time.Minute
|
||||
)
|
||||
|
||||
var instanceColl *Collection
|
||||
|
||||
func initActivityPub(app *App) {
|
||||
ur, _ := url.Parse(app.cfg.App.Host)
|
||||
instanceColl = &Collection{
|
||||
ID: 0,
|
||||
Alias: ur.Host,
|
||||
Title: ur.Host,
|
||||
db: app.db,
|
||||
hostName: app.cfg.App.Host,
|
||||
}
|
||||
}
|
||||
|
||||
type RemoteUser struct {
|
||||
ID int64
|
||||
ActorID string
|
||||
|
@ -76,12 +90,17 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
|
||||
vars := mux.Vars(r)
|
||||
alias := vars["alias"]
|
||||
if alias == "" {
|
||||
alias = filepath.Base(r.RequestURI)
|
||||
}
|
||||
|
||||
// TODO: enforce visibility
|
||||
// Get base Collection data
|
||||
var c *Collection
|
||||
var err error
|
||||
if app.cfg.App.SingleUser {
|
||||
if alias == r.Host {
|
||||
c = instanceColl
|
||||
} else if app.cfg.App.SingleUser {
|
||||
c, err = app.db.GetCollectionByID(1)
|
||||
} else {
|
||||
c, err = app.db.GetCollection(alias)
|
||||
|
@ -89,6 +108,9 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if !c.IsInstanceColl() {
|
||||
silenced, err := app.db.IsUserSilenced(c.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("fetch collection activities: %v", err)
|
||||
|
@ -97,7 +119,7 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
|||
if silenced {
|
||||
return ErrCollectionNotFound
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
}
|
||||
|
||||
p := c.PersonObject()
|
||||
|
||||
|
@ -331,7 +353,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
|||
if followID == nil {
|
||||
log.Error("Didn't resolve follow ID")
|
||||
} else {
|
||||
aID := c.FederatedAccount() + "#accept-" + store.GenerateFriendlyRandomString(20)
|
||||
aID := c.FederatedAccount() + "#accept-" + id.GenerateFriendlyRandomString(20)
|
||||
acceptID, err := url.Parse(aID)
|
||||
if err != nil {
|
||||
log.Error("Couldn't parse generated Accept URL '%s': %v", aID, err)
|
||||
|
@ -546,6 +568,22 @@ func resolveIRI(hostName, url string) ([]byte, error) {
|
|||
r.Header.Add("Accept", "application/activity+json")
|
||||
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||
|
||||
p := instanceColl.PersonObject()
|
||||
h := sha256.New()
|
||||
h.Write([]byte{})
|
||||
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
|
||||
// Sign using the 'Signature' header
|
||||
privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"})
|
||||
err = signer.SignSigHeader(r)
|
||||
if err != nil {
|
||||
log.Error("Can't sign: %v", err)
|
||||
}
|
||||
|
||||
if debugging {
|
||||
dump, err := httputil.DumpRequestOut(r, true)
|
||||
if err != nil {
|
||||
|
@ -624,6 +662,16 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
|
|||
}
|
||||
|
||||
func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||
// If app is private, do not federate
|
||||
if app.cfg.App.Private {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not federate posts from private or protected blogs
|
||||
if p.Collection.Visibility == CollPrivate || p.Collection.Visibility == CollProtected {
|
||||
return nil
|
||||
}
|
||||
|
||||
if debugging {
|
||||
if isUpdate {
|
||||
log.Info("Federating updated post!")
|
||||
|
@ -631,6 +679,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
log.Info("Federating new post!")
|
||||
}
|
||||
}
|
||||
|
||||
actor := p.Collection.PersonObject(collID)
|
||||
na := p.ActivityObject(app)
|
||||
|
||||
|
|
43
admin.go
43
admin.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -24,8 +24,8 @@ import (
|
|||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/passgen"
|
||||
"github.com/writeas/writefreely/appstats"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/appstats"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -189,6 +189,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
|||
*AdminPage
|
||||
Config config.AppCfg
|
||||
Message string
|
||||
Flashes []string
|
||||
|
||||
Users *[]User
|
||||
CurPage int
|
||||
|
@ -201,6 +202,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
|||
Message: r.FormValue("m"),
|
||||
}
|
||||
|
||||
p.Flashes, _ = getSessionFlashes(app, w, r, nil)
|
||||
p.TotalUsers = app.db.GetAllUsersCount()
|
||||
ttlPages := p.TotalUsers / adminUsersPerPage
|
||||
p.TotalPages = []int{}
|
||||
|
@ -312,6 +314,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleAdminDeleteUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
if !u.IsAdmin() {
|
||||
return impart.HTTPError{http.StatusForbidden, "Administrator privileges required for this action"}
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
confirmUsername := r.PostFormValue("confirm-username")
|
||||
|
||||
if confirmUsername != username {
|
||||
return impart.HTTPError{http.StatusBadRequest, "Username was not confirmed"}
|
||||
}
|
||||
|
||||
user, err := app.db.GetUserForAuth(username)
|
||||
if err == ErrUserNotFound {
|
||||
return impart.HTTPError{http.StatusNotFound, fmt.Sprintf("User '%s' was not found", username)}
|
||||
} else if err != nil {
|
||||
log.Error("get user for deletion: %v", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user with username '%s': %v", username, err)}
|
||||
}
|
||||
|
||||
err = app.db.DeleteAccount(user.ID)
|
||||
if err != nil {
|
||||
log.Error("delete user %s: %v", user.Username, err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete user account for '%s': %v", username, err)}
|
||||
}
|
||||
|
||||
_ = addSessionFlash(app, w, r, fmt.Sprintf("User \"%s\" was deleted successfully.", username), nil)
|
||||
return impart.HTTPError{http.StatusFound, "/admin/users"}
|
||||
}
|
||||
|
||||
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
|
@ -328,6 +361,9 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht
|
|||
err = app.db.SetUserStatus(user.ID, UserActive)
|
||||
} else {
|
||||
err = app.db.SetUserStatus(user.ID, UserSilenced)
|
||||
|
||||
// reset the cache to removed silence user posts
|
||||
updateTimelineCache(app.timeline, true)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("toggle user silenced: %v", err)
|
||||
|
@ -519,6 +555,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
|
|||
apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
|
||||
apper.App().cfg.App.Landing = r.FormValue("landing")
|
||||
apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
|
||||
apper.App().cfg.App.OpenDeletion = r.FormValue("open_deletion") == "on"
|
||||
mul, err := strconv.Atoi(r.FormValue("min_username_len"))
|
||||
if err == nil {
|
||||
apper.App().cfg.App.MinUsernameLen = mul
|
||||
|
|
129
app.go
129
app.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -13,9 +13,11 @@ package writefreely
|
|||
import (
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -30,17 +32,18 @@ import (
|
|||
"github.com/gorilla/schema"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/manifoldco/promptui"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
stripmd "github.com/writeas/go-strip-markdown/v2"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/converter"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writeas/writefreely/migrations"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
"github.com/writefreely/writefreely/migrations"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -56,7 +59,7 @@ var (
|
|||
debugging bool
|
||||
|
||||
// Software version can be set from git env using -ldflags
|
||||
softwareVer = "0.12.0"
|
||||
softwareVer = "0.13.2"
|
||||
|
||||
// DEPRECATED VARS
|
||||
isSingleUser bool
|
||||
|
@ -166,6 +169,14 @@ func (app *App) LoadKeys() error {
|
|||
if debugging {
|
||||
log.Info(" %s", emailKeyPath)
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
executable = "writefreely"
|
||||
} else {
|
||||
executable = filepath.Base(executable)
|
||||
}
|
||||
|
||||
app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -187,6 +198,22 @@ func (app *App) LoadKeys() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", csrfKeyPath)
|
||||
}
|
||||
app.keys.CSRFKey, err = ioutil.ReadFile(csrfKeyPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Error(`Missing key: %s.
|
||||
|
||||
Run this command to generate missing keys:
|
||||
%s keys generate
|
||||
|
||||
`, csrfKeyPath, executable)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -332,6 +359,11 @@ func pageForReq(app *App, r *http.Request) page.StaticPage {
|
|||
Version: "v" + softwareVer,
|
||||
}
|
||||
|
||||
// Use custom style, if file exists
|
||||
if _, err := os.Stat(filepath.Join(staticDir, "local", "custom.css")); err == nil {
|
||||
p.CustomCSS = true
|
||||
}
|
||||
|
||||
// Add user information, if given
|
||||
var u *User
|
||||
accessToken := r.FormValue("t")
|
||||
|
@ -389,6 +421,8 @@ func Initialize(apper Apper, debug bool) (*App, error) {
|
|||
return nil, fmt.Errorf("connect to DB: %s", err)
|
||||
}
|
||||
|
||||
initActivityPub(apper.App())
|
||||
|
||||
// Handle local timeline, if enabled
|
||||
if apper.App().cfg.App.LocalTimeline {
|
||||
log.Info("Initializing local timeline...")
|
||||
|
@ -477,9 +511,41 @@ 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)
|
||||
}
|
||||
} else {
|
||||
log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
|
||||
network := "tcp"
|
||||
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("---")
|
||||
err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
|
||||
listener, err := net.Listen(network, bindAddress)
|
||||
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 {
|
||||
log.Error("Unable to start: %v", err)
|
||||
|
@ -595,7 +661,7 @@ func DoConfig(app *App, configSections string) {
|
|||
|
||||
// Create blog
|
||||
log.Info("Creating user %s...\n", u.Username)
|
||||
err = app.db.CreateUser(app.cfg, u, app.cfg.App.SiteName)
|
||||
err = app.db.CreateUser(app.cfg, u, app.cfg.App.SiteName, "")
|
||||
if err != nil {
|
||||
log.Error("Unable to create user: %s", err)
|
||||
os.Exit(1)
|
||||
|
@ -635,6 +701,10 @@ func GenerateKeyFiles(app *App) error {
|
|||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
err = generateKey(csrfKeyPath)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
|
||||
return keyErrs
|
||||
}
|
||||
|
@ -767,7 +837,7 @@ func connectToDatabase(app *App) {
|
|||
os.Exit(1)
|
||||
}
|
||||
db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxOpenConns(2)
|
||||
} else {
|
||||
log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
|
||||
os.Exit(1)
|
||||
|
@ -782,6 +852,16 @@ func connectToDatabase(app *App) {
|
|||
func shutdown(app *App) {
|
||||
log.Info("Closing database connection...")
|
||||
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.
|
||||
|
@ -836,7 +916,7 @@ func CreateUser(apper Apper, username, password string, isAdmin bool) error {
|
|||
userType = "admin"
|
||||
}
|
||||
log.Info("Creating %s %s...", userType, usernameDesc)
|
||||
err = apper.App().db.CreateUser(apper.App().Config(), u, desiredUsername)
|
||||
err = apper.App().db.CreateUser(apper.App().Config(), u, desiredUsername, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create user: %s", err)
|
||||
}
|
||||
|
@ -844,15 +924,18 @@ func CreateUser(apper Apper, username, password string, isAdmin bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func adminInitDatabase(app *App) error {
|
||||
schemaFileName := "schema.sql"
|
||||
if app.cfg.Database.Type == driverSQLite {
|
||||
schemaFileName = "sqlite.sql"
|
||||
}
|
||||
//go:embed schema.sql
|
||||
var schemaSql string
|
||||
|
||||
schema, err := Asset(schemaFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to load schema file: %v", err)
|
||||
//go:embed sqlite.sql
|
||||
var sqliteSql string
|
||||
|
||||
func adminInitDatabase(app *App) error {
|
||||
var schema string
|
||||
if app.cfg.Database.Type == driverSQLite {
|
||||
schema = sqliteSql
|
||||
} else {
|
||||
schema = schemaSql
|
||||
}
|
||||
|
||||
tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
|
||||
|
@ -868,7 +951,7 @@ func adminInitDatabase(app *App) error {
|
|||
} else {
|
||||
log.Info("Creating table ??? (Weird query) No match in: %v", parts)
|
||||
}
|
||||
_, err = app.db.Exec(q)
|
||||
_, err := app.db.Exec(q)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
} else {
|
||||
|
@ -878,7 +961,7 @@ func adminInitDatabase(app *App) error {
|
|||
|
||||
// Set up migrations 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 {
|
||||
return fmt.Errorf("Unable to set initial migrations: %v", err)
|
||||
}
|
||||
|
|
2
auth.go
2
auth.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,7 +11,8 @@
|
|||
package author
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
@ -113,10 +114,17 @@ func IsValidUsername(cfg *config.Config, username string) bool {
|
|||
// Username is invalid if page with the same name exists. So traverse
|
||||
// available pages, adding them to reservedUsernames map that'll be checked
|
||||
// later.
|
||||
filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error {
|
||||
err := 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
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[IMPORTANT WARNING]: Could not determine IsValidUsername! %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Username is invalid if it is reserved!
|
||||
if _, reserved := reservedUsernames[username]; reserved {
|
||||
|
|
105
bindata-lib.go
105
bindata-lib.go
|
@ -1,105 +0,0 @@
|
|||
// +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 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -15,11 +15,10 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -13,9 +13,8 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,11 +11,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
116
collections.go
116
collections.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2022 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -24,15 +24,17 @@ import (
|
|||
"unicode"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
stripmd "github.com/writeas/go-strip-markdown/v2"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/activitystreams"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/bots"
|
||||
"github.com/writeas/web-core/log"
|
||||
waposts "github.com/writeas/web-core/posts"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writeas/web-core/posts"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -56,7 +58,7 @@ type (
|
|||
PublicOwner bool `datastore:"public_owner" json:"-"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
MonetizationPointer string `json:"monetization_pointer,omitempty"`
|
||||
Monetization string `json:"monetization_pointer,omitempty"`
|
||||
|
||||
db *datastore
|
||||
hostName string
|
||||
|
@ -110,6 +112,8 @@ type (
|
|||
|
||||
// User-related fields
|
||||
isCollOwner bool
|
||||
|
||||
isAuthorized bool
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -180,6 +184,11 @@ func (c *Collection) NewFormat() *CollectionFormat {
|
|||
return cf
|
||||
}
|
||||
|
||||
func (c *Collection) IsInstanceColl() bool {
|
||||
ur, _ := url.Parse(c.hostName)
|
||||
return c.Alias == ur.Host
|
||||
}
|
||||
|
||||
func (c *Collection) IsUnlisted() bool {
|
||||
return c.Visibility == 0
|
||||
}
|
||||
|
@ -229,13 +238,17 @@ func (c *Collection) DisplayCanonicalURL() string {
|
|||
if p == "/" {
|
||||
p = ""
|
||||
}
|
||||
return u.Hostname() + p
|
||||
d := u.Hostname()
|
||||
d, _ = idna.ToUnicode(d)
|
||||
return d + p
|
||||
}
|
||||
|
||||
// RedirectingCanonicalURL returns the fully-qualified canonical URL for the Collection, with a trailing slash. The
|
||||
// hostName field needs to be populated for this to work correctly.
|
||||
func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
|
||||
if c.hostName == "" {
|
||||
// If this is true, the human programmers screwed up. So ask for a bug report and fail, fail, fail
|
||||
log.Error("[PROGRAMMER ERROR] WARNING: Collection.hostName is empty! Federation and many other things will fail! If you're seeing this in the wild, please report this bug and let us know what you were doing just before this: https://github.com/writeas/writefreely/issues/new?template=bug_report.md")
|
||||
log.Error("[PROGRAMMER ERROR] WARNING: Collection.hostName is empty! Federation and many other things will fail! If you're seeing this in the wild, please report this bug and let us know what you were doing just before this: https://github.com/writefreely/writefreely/issues/new?template=bug_report.md")
|
||||
}
|
||||
if isSingleUser {
|
||||
return c.hostName + "/"
|
||||
|
@ -343,6 +356,37 @@ func (c *Collection) RenderMathJax() bool {
|
|||
return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
|
||||
}
|
||||
|
||||
func (c *Collection) MonetizationURL() string {
|
||||
if c.Monetization == "" {
|
||||
return ""
|
||||
}
|
||||
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 {
|
||||
return displayMonetization(c.Monetization, c.Alias)
|
||||
}
|
||||
|
||||
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r)
|
||||
alias := r.FormValue("alias")
|
||||
|
@ -528,11 +572,11 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
}
|
||||
}
|
||||
|
||||
posts, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false)
|
||||
ps, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
coll := &CollectionObj{Collection: *c, Posts: posts}
|
||||
coll := &CollectionObj{Collection: *c, Posts: ps}
|
||||
app.db.GetPostsCount(coll, isCollOwner)
|
||||
// Strip non-public information
|
||||
coll.Collection.ForPublic()
|
||||
|
@ -540,7 +584,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
// Transform post bodies if needed
|
||||
if r.FormValue("body") == "html" {
|
||||
for _, p := range *coll.Posts {
|
||||
p.Content = waposts.ApplyMarkdown([]byte(p.Content))
|
||||
p.Content = posts.ApplyMarkdown([]byte(p.Content))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -553,6 +597,7 @@ type CollectionPage struct {
|
|||
IsCustomDomain bool
|
||||
IsWelcome bool
|
||||
IsOwner bool
|
||||
IsCollLoggedIn bool
|
||||
CanPin bool
|
||||
Username string
|
||||
Monetization string
|
||||
|
@ -560,6 +605,9 @@ type CollectionPage struct {
|
|||
PinnedPosts *[]PublicPost
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
|
||||
// Helper field for Chorus mode
|
||||
CollAlias string
|
||||
}
|
||||
|
||||
type TagCollectionPage struct {
|
||||
|
@ -696,9 +744,9 @@ func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.R
|
|||
}
|
||||
|
||||
// See if we've authorized this collection
|
||||
authd := isAuthorizedForCollection(app, c.Alias, r)
|
||||
cr.isAuthorized = isAuthorizedForCollection(app, c.Alias, r)
|
||||
|
||||
if !authd {
|
||||
if !cr.isAuthorized {
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
*CollectionObj
|
||||
|
@ -816,9 +864,11 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||
// Serve collection
|
||||
displayPage := CollectionPage{
|
||||
DisplayCollection: coll,
|
||||
IsCollLoggedIn: cr.isAuthorized,
|
||||
StaticPage: pageForReq(app, r),
|
||||
IsCustomDomain: cr.isCustomDomain,
|
||||
IsWelcome: r.FormValue("greeting") != "",
|
||||
CollAlias: c.Alias,
|
||||
}
|
||||
displayPage.IsAdmin = u != nil && u.IsAdmin()
|
||||
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin)
|
||||
|
@ -1188,3 +1238,43 @@ func isAuthorizedForCollection(app *App, alias string, r *http.Request) bool {
|
|||
}
|
||||
return authd
|
||||
}
|
||||
|
||||
func logOutCollection(app *App, alias string, w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := app.sessionStore.Get(r, blogPassCookieName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove this from map of blogs logged into
|
||||
delete(session.Values, alias)
|
||||
|
||||
// If not auth'd with any blog, delete entire cookie
|
||||
if len(session.Values) == 0 {
|
||||
session.Options.MaxAge = -1
|
||||
}
|
||||
return session.Save(r, w)
|
||||
}
|
||||
|
||||
func handleLogOutCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
alias := collectionAliasFromReq(r)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if !c.IsProtected() {
|
||||
// Invalid to log out of this collection
|
||||
return ErrCollectionPageNotFound
|
||||
}
|
||||
|
||||
err = logOutCollection(app, c.Alias, w, r)
|
||||
if err != nil {
|
||||
addSessionFlash(app, w, r, "Logging out failed. Try clearing cookies for this site, instead.", nil)
|
||||
}
|
||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,9 +12,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
"github.com/go-ini/ini"
|
||||
"github.com/writeas/web-core/log"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -110,6 +113,10 @@ type (
|
|||
AuthEndpoint string `ini:"auth_endpoint"`
|
||||
Scope string `ini:"scope"`
|
||||
AllowDisconnect bool `ini:"allow_disconnect"`
|
||||
MapUserID string `ini:"map_user_id"`
|
||||
MapUsername string `ini:"map_username"`
|
||||
MapDisplayName string `ini:"map_display_name"`
|
||||
MapEmail string `ini:"map_email"`
|
||||
}
|
||||
|
||||
// AppCfg holds values that affect how the application functions
|
||||
|
@ -135,6 +142,7 @@ type (
|
|||
// Users
|
||||
SingleUser bool `ini:"single_user"`
|
||||
OpenRegistration bool `ini:"open_registration"`
|
||||
OpenDeletion bool `ini:"open_deletion"`
|
||||
MinUsernameLen int `ini:"min_username_len"`
|
||||
MaxBlogs int `ini:"max_blogs"`
|
||||
|
||||
|
@ -143,6 +151,7 @@ type (
|
|||
Federation bool `ini:"federation"`
|
||||
PublicStats bool `ini:"public_stats"`
|
||||
Monetization bool `ini:"monetization"`
|
||||
NotesOnly bool `ini:"notes_only"`
|
||||
|
||||
// Access
|
||||
Private bool `ini:"private"`
|
||||
|
@ -252,6 +261,22 @@ func Load(fname string) (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do any transformations
|
||||
u, err := url.Parse(uc.App.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, err := idna.ToASCII(u.Hostname())
|
||||
if err != nil {
|
||||
log.Error("idna.ToASCII for %s: %s", u.Hostname(), err)
|
||||
return nil, err
|
||||
}
|
||||
uc.App.Host = u.Scheme + "://" + d
|
||||
if u.Port() != "" {
|
||||
uc.App.Host += ":" + u.Port()
|
||||
}
|
||||
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018, 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,14 +11,34 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"golang.org/x/net/idna"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FriendlyHost returns the app's Host sans any schema
|
||||
func (ac AppCfg) FriendlyHost() string {
|
||||
return ac.Host[strings.Index(ac.Host, "://")+len("://"):]
|
||||
rawHost := ac.Host[strings.Index(ac.Host, "://")+len("://"):]
|
||||
|
||||
u, err := url.Parse(ac.Host)
|
||||
if err != nil {
|
||||
log.Error("url.Parse failed on %s: %s", ac.Host, err)
|
||||
return rawHost
|
||||
}
|
||||
d, err := idna.ToUnicode(u.Hostname())
|
||||
if err != nil {
|
||||
log.Error("idna.ToUnicode failed on %s: %s", ac.Host, err)
|
||||
return rawHost
|
||||
}
|
||||
|
||||
res := d
|
||||
if u.Port() != "" {
|
||||
res += ":" + u.Port()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (ac AppCfg) CanCreateBlogs(currentlyUsed uint64) bool {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
//go:build wflib
|
||||
// +build wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
//go:build !sqlite && !wflib
|
||||
// +build !sqlite,!wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
//go:build sqlite && !wflib
|
||||
// +build sqlite,!wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
81
database.go
81
database.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -15,7 +15,7 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/writeas/web-core/silobridge"
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -25,16 +25,15 @@ import (
|
|||
uuid "github.com/nu7hatch/gouuid"
|
||||
"github.com/writeas/activityserve"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/web-core/activitypub"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/data"
|
||||
"github.com/writeas/web-core/id"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/query"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writefreely/writefreely/author"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -52,7 +51,7 @@ var (
|
|||
)
|
||||
|
||||
type writestore interface {
|
||||
CreateUser(*config.Config, *User, string) error
|
||||
CreateUser(*config.Config, *User, string, string) error
|
||||
UpdateUserEmail(keys *key.Keychain, userID int64, email string) error
|
||||
UpdateEncryptedUserEmail(int64, []byte) error
|
||||
GetUserByID(int64) (*User, error)
|
||||
|
@ -77,8 +76,8 @@ type writestore interface {
|
|||
GetMeStats(u *User) userMeStats
|
||||
GetTotalCollections() (int64, error)
|
||||
GetTotalPosts() (int64, error)
|
||||
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
||||
GetAnonymousPosts(u *User) (*[]PublicPost, error)
|
||||
GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error)
|
||||
GetAnonymousPosts(u *User, page int) (*[]PublicPost, error)
|
||||
GetUserPosts(u *User) (*[]PublicPost, error)
|
||||
|
||||
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error)
|
||||
|
@ -180,7 +179,7 @@ func (db *datastore) dateSub(l int, unit string) string {
|
|||
}
|
||||
|
||||
// CreateUser creates a new user in the database from the given User, UPDATING it in the process with the user's ID.
|
||||
func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string) error {
|
||||
func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string, collectionDesc string) error {
|
||||
if db.PostIDExists(u.Username) {
|
||||
return impart.HTTPError{http.StatusConflict, "Invalid collection name."}
|
||||
}
|
||||
|
@ -214,7 +213,7 @@ func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle str
|
|||
if collectionTitle == "" {
|
||||
collectionTitle = u.Username
|
||||
}
|
||||
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", defaultVisibility(cfg), u.ID, 0)
|
||||
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, collectionDesc, defaultVisibility(cfg), u.ID, 0)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
|
@ -333,7 +332,7 @@ func (db *datastore) IsUserSilenced(id int64) (bool, error) {
|
|||
err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return false, fmt.Errorf("is user silenced: %v", ErrUserNotFound)
|
||||
return false, ErrUserNotFound
|
||||
case err != nil:
|
||||
log.Error("Couldn't SELECT user status: %v", err)
|
||||
return false, fmt.Errorf("is user silenced: %v", err)
|
||||
|
@ -613,7 +612,7 @@ func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias
|
|||
|
||||
func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) {
|
||||
idLen := postIDLen
|
||||
friendlyID := store.GenerateFriendlyRandomString(idLen)
|
||||
friendlyID := id.GenerateFriendlyRandomString(idLen)
|
||||
|
||||
// Handle appearance / font face
|
||||
appearance := post.Font
|
||||
|
@ -638,6 +637,9 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
|
|||
ownerCollID.Int64 = collID
|
||||
ownerCollID.Valid = true
|
||||
var slugVal string
|
||||
if post.Slug != nil && *post.Slug != "" {
|
||||
slugVal = *post.Slug
|
||||
} else {
|
||||
if post.Title != nil && *post.Title != "" {
|
||||
slugVal = getSlug(*post.Title, post.Language.String)
|
||||
if slugVal == "" {
|
||||
|
@ -646,6 +648,7 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
|
|||
} else {
|
||||
slugVal = getSlug(*post.Content, post.Language.String)
|
||||
}
|
||||
}
|
||||
if slugVal == "" {
|
||||
slugVal = friendlyID
|
||||
}
|
||||
|
@ -658,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
|
||||
created = created.UTC()
|
||||
}
|
||||
if post.Created != nil {
|
||||
if post.Created != nil && *post.Created != "" {
|
||||
created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created)
|
||||
if err != nil {
|
||||
log.Error("Unable to parse Created time '%s': %v", *post.Created, err)
|
||||
|
@ -810,6 +813,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll
|
|||
c.Signature = signature.String
|
||||
c.Format = format.String
|
||||
c.Public = c.IsPublic()
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
c.db = db
|
||||
|
||||
|
@ -872,7 +876,7 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
// WHERE values
|
||||
q.Where("alias = ? AND owner_id = ?", alias, c.OwnerID)
|
||||
|
||||
if q.Updates == "" {
|
||||
if q.Updates == "" && c.Monetization == nil {
|
||||
return ErrPostNoUpdatableVals
|
||||
}
|
||||
|
||||
|
@ -920,7 +924,7 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
}
|
||||
}
|
||||
if !skipUpdate {
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization)
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization)
|
||||
if err != nil {
|
||||
log.Error("Unable to insert monetization_pointer value: %v", err)
|
||||
return err
|
||||
|
@ -929,11 +933,13 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
}
|
||||
|
||||
// Update rest of the collection data
|
||||
if q.Updates != "" {
|
||||
res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...)
|
||||
if err != nil {
|
||||
log.Error("Unable to update collection: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rowsAffected, _ = res.RowsAffected()
|
||||
if !changed || rowsAffected == 0 {
|
||||
|
@ -1177,7 +1183,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu
|
|||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture, false)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1242,7 +1248,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin
|
|||
}
|
||||
p.extractData()
|
||||
p.augmentContent(c)
|
||||
p.formatContent(cfg, c, includeFuture)
|
||||
p.formatContent(cfg, c, includeFuture, false)
|
||||
|
||||
posts = append(posts, p.processPost())
|
||||
}
|
||||
|
@ -1647,6 +1653,14 @@ func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, er
|
|||
c.URL = c.CanonicalURL()
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
/*
|
||||
// NOTE: future functionality
|
||||
if visibility != nil { // TODO: && visibility == CollPublic {
|
||||
// Add Monetization info when retrieving all public collections
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
}
|
||||
*/
|
||||
|
||||
colls = append(colls, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
|
@ -1674,7 +1688,7 @@ func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error
|
|||
FROM collections c
|
||||
LEFT JOIN users u ON u.id = c.owner_id
|
||||
WHERE c.privacy = 1 AND u.status = 0
|
||||
ORDER BY id ASC`)
|
||||
ORDER BY title ASC`)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting public collections: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."}
|
||||
|
@ -1693,6 +1707,9 @@ func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error
|
|||
c.URL = c.CanonicalURL()
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
// Add Monetization information
|
||||
c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer")
|
||||
|
||||
colls = append(colls, c)
|
||||
}
|
||||
err = rows.Err()
|
||||
|
@ -1750,14 +1767,14 @@ func (db *datastore) GetTotalPosts() (postCount int64, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
||||
func (db *datastore) GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error) {
|
||||
params := []interface{}{u.ID}
|
||||
where := ""
|
||||
if alias != "" {
|
||||
where = " AND alias = ?"
|
||||
params = append(params, alias)
|
||||
}
|
||||
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...)
|
||||
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...)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user top posts."}
|
||||
|
@ -1771,7 +1788,7 @@ func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
|||
c := Collection{}
|
||||
var alias, title, description sql.NullString
|
||||
var views sql.NullInt64
|
||||
err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &alias, &title, &description, &views)
|
||||
err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &p.Content, &alias, &title, &description, &views)
|
||||
if err != nil {
|
||||
log.Error("Failed scanning User.getPosts() row: %v", err)
|
||||
gotErr = true
|
||||
|
@ -1785,6 +1802,7 @@ func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
|||
c.Title = title.String
|
||||
c.Description = description.String
|
||||
c.Views = views.Int64
|
||||
c.hostName = hostName
|
||||
pubPost.Collection = &CollectionObj{Collection: c}
|
||||
}
|
||||
|
||||
|
@ -1803,8 +1821,19 @@ func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
|||
return &posts, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetAnonymousPosts(u *User) (*[]PublicPost, error) {
|
||||
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", u.ID)
|
||||
func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) {
|
||||
pagePosts := 10
|
||||
start := page*pagePosts - pagePosts
|
||||
if page == 0 {
|
||||
start = 0
|
||||
pagePosts = 1000
|
||||
}
|
||||
|
||||
limitStr := ""
|
||||
if page > 0 {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}
|
||||
|
@ -1814,7 +1843,7 @@ func (db *datastore) GetAnonymousPosts(u *User) (*[]PublicPost, error) {
|
|||
posts := []PublicPost{}
|
||||
for rows.Next() {
|
||||
p := Post{}
|
||||
err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Created, &p.Updated, &p.Content)
|
||||
err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Language, &p.Created, &p.Updated, &p.Content)
|
||||
if err != nil {
|
||||
log.Error("Failed scanning row: %v", err)
|
||||
break
|
||||
|
@ -2603,7 +2632,7 @@ func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
|
|||
}
|
||||
|
||||
func (db *datastore) GenerateOAuthState(ctx context.Context, provider string, clientID string, attachUser int64, inviteCode string) (string, error) {
|
||||
state := store.Generate62RandomString(24)
|
||||
state := id.Generate62RandomString(24)
|
||||
attachUserVal := sql.NullInt64{Valid: attachUser > 0, Int64: attachUser}
|
||||
inviteCodeVal := sql.NullString{Valid: inviteCode != "", String: inviteCode}
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at, attach_user_id, invite_code) VALUES (?, ?, ?, FALSE, "+db.now()+", ?, ?)", state, provider, clientID, attachUserVal, inviteCodeVal)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
1
db/tx.go
1
db/tx.go
|
@ -23,4 +23,3 @@ func RunTransactionWithOptions(ctx context.Context, db *sql.DB, txOpts *sql.TxOp
|
|||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ networks:
|
|||
services:
|
||||
writefreely-web:
|
||||
container_name: "writefreely-web"
|
||||
image: "writefreely:latest"
|
||||
image: "writeas/writefreely:latest"
|
||||
|
||||
volumes:
|
||||
- "web-keys:/go/keys"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -110,7 +110,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
|
|||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
|
||||
posts, err := app.db.GetAnonymousPosts(u)
|
||||
posts, err := app.db.GetAnonymousPosts(u, 0)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch anon posts: %v", err)
|
||||
}
|
||||
|
|
24
feed.go
24
feed.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -15,9 +15,9 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/gorilla/feeds"
|
||||
"github.com/gorilla/feeds"
|
||||
"github.com/gorilla/mux"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
stripmd "github.com/writeas/go-strip-markdown/v2"
|
||||
"github.com/writeas/web-core/log"
|
||||
)
|
||||
|
||||
|
@ -87,25 +87,29 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
|||
siteURL += "tag:" + tag
|
||||
}
|
||||
|
||||
feed := &Feed{
|
||||
feed := &feeds.Feed{
|
||||
Title: collectionTitle,
|
||||
Link: &Link{Href: siteURL},
|
||||
Link: &feeds.Link{Href: siteURL},
|
||||
Description: coll.Description,
|
||||
Author: &Author{author, ""},
|
||||
Author: &feeds.Author{author, ""},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
var title, permalink string
|
||||
for _, p := range *coll.Posts {
|
||||
// Add necessary path back to the web browser for Web Monetization if needed
|
||||
p.Collection = coll.CollectionObj // augmentReadingDestination requires a populated Collection field
|
||||
p.augmentReadingDestination()
|
||||
// Create the item for the feed
|
||||
title = p.PlainDisplayTitle()
|
||||
permalink = fmt.Sprintf("%s%s", baseUrl, p.Slug.String)
|
||||
feed.Items = append(feed.Items, &Item{
|
||||
feed.Items = append(feed.Items, &feeds.Item{
|
||||
Id: fmt.Sprintf("%s%s", basePermalinkUrl, p.Slug.String),
|
||||
Title: title,
|
||||
Link: &Link{Href: permalink},
|
||||
Link: &feeds.Link{Href: permalink},
|
||||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>",
|
||||
Content: applyMarkdown([]byte(p.Content), "", app.cfg),
|
||||
Author: &Author{author, ""},
|
||||
Content: string(p.HTMLContent),
|
||||
Author: &feeds.Author{author, ""},
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
})
|
||||
|
|
101
go.mod
101
go.mod
|
@ -1,61 +1,82 @@
|
|||
module github.com/writeas/writefreely
|
||||
module github.com/writefreely/writefreely
|
||||
|
||||
require (
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-test/deep v1.0.1 // indirect
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/go-ini/ini v1.67.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/gorilla/csrf v1.7.1
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/guregu/null v3.5.0+incompatible
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/guregu/null v4.0.0+incompatible
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/manifoldco/promptui v0.8.0
|
||||
github.com/mattn/go-sqlite3 v1.14.4
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/microcosm-cc/bluemonday v1.0.24
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/nicksnyder/go-i18n v1.10.0 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||
github.com/pelletier/go-toml v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
github.com/writeas/activity v0.1.2
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1
|
||||
github.com/writeas/go-webfinger v1.1.0
|
||||
github.com/writeas/httpsig v1.0.0
|
||||
github.com/writeas/impart v1.1.1
|
||||
github.com/writeas/import v0.2.1
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219
|
||||
github.com/writeas/nerds v1.0.0
|
||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320
|
||||
github.com/writeas/slug v1.2.0
|
||||
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c
|
||||
github.com/writeas/web-core v1.4.1
|
||||
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b
|
||||
github.com/writefreely/go-nodeinfo v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
|
||||
gopkg.in/ini.v1 v1.61.0
|
||||
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/net v0.11.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
require (
|
||||
code.as/core/socks v1.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // 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/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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/go-test/deep v1.0.1 // indirect
|
||||
github.com/gofrs/uuid v3.3.0+incompatible // indirect
|
||||
github.com/gologme/log v1.2.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.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.17 // 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/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // 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/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible // indirect
|
||||
github.com/writeas/go-writeas/v2 v2.0.2 // indirect
|
||||
github.com/writeas/openssl-go v1.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
go 1.19
|
||||
|
|
239
go.sum
239
go.sum
|
@ -1,21 +1,11 @@
|
|||
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs=
|
||||
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU=
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
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/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
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/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/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
|
@ -26,260 +16,171 @@ 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.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-fed/httpsig v0.1.0 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY=
|
||||
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
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/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
|
||||
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
|
||||
github.com/gorilla/csrf v1.7.1/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/feeds v1.1.0 h1:pcgLJhbdYgaUESnj3AmXPcB7cS3vy63+jC/TI14AGXk=
|
||||
github.com/gorilla/feeds v1.1.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA=
|
||||
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM=
|
||||
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
|
||||
github.com/guregu/null v3.5.0+incompatible h1:fSdvRTQtmBA4B4YDZXhLtxTIJZYuUxBFTTHS4B9djG4=
|
||||
github.com/guregu/null v3.5.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
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/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
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/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/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts=
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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/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/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8=
|
||||
github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw=
|
||||
github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=
|
||||
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA=
|
||||
github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
|
||||
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/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q=
|
||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||
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/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44 h1:q5sit1FpzEt59aM2Fd2lSBKF+nxcY1o0StRCiJa/pWo=
|
||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44/go.mod h1:a97DSBRiRljeRVd5CRZL5bYCIeeGjSEngGf+QMR2evA=
|
||||
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/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/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/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c=
|
||||
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
|
||||
github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0=
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
||||
github.com/writeas/go-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-webfinger v1.1.0 h1:MzNyt0ry/GMsRmJGftn2o9mPwqK1Q5MLdh4VuJCfb1Q=
|
||||
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/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk=
|
||||
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
|
||||
github.com/writeas/httpsig v1.0.0 h1:peIAoIA3DmlP8IG8tMNZqI4YD1uEnWBmkcC9OFPjt3A=
|
||||
github.com/writeas/httpsig v1.0.0/go.mod h1:7ClMGSrSVXJbmiLa17bZ1LrG1oibGZmUMlh3402flPY=
|
||||
github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE=
|
||||
github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
|
||||
github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o=
|
||||
github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
|
||||
github.com/writeas/import v0.2.0 h1:Ov23JW9Rnjxk06rki1Spar45bNX647HhwhAZj3flJiY=
|
||||
github.com/writeas/import v0.2.0/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
|
||||
github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg=
|
||||
github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c=
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ=
|
||||
github.com/writeas/nerds v1.0.0 h1:ZzRcCN+Sr3MWID7o/x1cr1ZbLvdpej9Y1/Ho+JKlqxo=
|
||||
github.com/writeas/nerds v1.0.0/go.mod h1:Gn2bHy1EwRcpXeB7ZhVmuUwiweK0e+JllNf66gvNLdU=
|
||||
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/saturday v1.6.0/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
|
||||
github.com/writeas/saturday v1.7.1 h1:lYo1EH6CYyrFObQoA9RNWHVlpZA5iYL5Opxo7PYAnZE=
|
||||
github.com/writeas/saturday v1.7.1/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/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
|
||||
github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
||||
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
||||
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
||||
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c h1:/aPb8WKtC+Ga/xUEcME0iX3VKBeeJ02kXCaROaZ21SE=
|
||||
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||
github.com/writeas/web-core v1.4.1 h1:mdDwZepEyQb76j8gNUIPblV7SUIXi4WQ0h3Xl0ZwKT4=
|
||||
github.com/writeas/web-core v1.4.1/go.mod h1:MTWDZWikeG063S9IrI6ekvu3N2tJEVRpZuU4kAWg1DY=
|
||||
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/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
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-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0=
|
||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 h1:rJm0LuqUjoDhSk2zO9ISMSToQxGz7Os2jRiOL8AWu4c=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 h1:bPP/rGuN1LUM0eaEwo6vnP6OfIWJzJBulzGUiKLjjSY=
|
||||
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y=
|
||||
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||
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/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
|
||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
25
gopher.go
25
gopher.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,10 +14,12 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/prologic/go-gopher"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writefreely/go-gopher"
|
||||
)
|
||||
|
||||
func initGopher(apper Apper) {
|
||||
|
@ -28,6 +30,16 @@ func initGopher(apper Apper) {
|
|||
gopher.ListenAndServe(fmt.Sprintf(":%d", apper.App().Config().Server.GopherPort), nil)
|
||||
}
|
||||
|
||||
// Utility function to strip the URL from the hostname provided by app.cfg.App.Host
|
||||
func stripHostProtocol(app *App) string {
|
||||
u, err := url.Parse(app.cfg.App.Host)
|
||||
if err != nil {
|
||||
// Fall back to host, with scheme stripped
|
||||
return string(regexp.MustCompile("^.*://").ReplaceAll([]byte(app.cfg.App.Host), []byte("")))
|
||||
}
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error {
|
||||
parts := strings.Split(r.Selector, "/")
|
||||
if app.cfg.App.SingleUser {
|
||||
|
@ -51,6 +63,8 @@ func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error {
|
|||
|
||||
for _, c := range *colls {
|
||||
w.WriteItem(&gopher.Item{
|
||||
Host: stripHostProtocol(app),
|
||||
Port: app.cfg.Server.GopherPort,
|
||||
Type: gopher.DIRECTORY,
|
||||
Description: c.DisplayTitle(),
|
||||
Selector: "/" + c.Alias + "/",
|
||||
|
@ -92,6 +106,11 @@ func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request
|
|||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
w.WriteInfo(c.DisplayTitle())
|
||||
if c.Description != "" {
|
||||
w.WriteInfo(c.Description)
|
||||
}
|
||||
|
||||
posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -99,6 +118,8 @@ func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request
|
|||
|
||||
for _, p := range *posts {
|
||||
w.WriteItem(&gopher.Item{
|
||||
Port: app.cfg.Server.GopherPort,
|
||||
Host: stripHostProtocol(app),
|
||||
Type: gopher.FILE,
|
||||
Description: p.CreatedDate() + " - " + p.DisplayTitle(),
|
||||
Selector: baseSel + p.Slug.String,
|
||||
|
|
92
handle.go
92
handle.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -21,11 +21,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/prologic/go-gopher"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/go-gopher"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
// UserLevel represents the required user level for accessing an endpoint
|
||||
|
@ -155,8 +155,14 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
|
|||
err := f(h.app.App(), u, w, r)
|
||||
if err == nil {
|
||||
status = http.StatusOK
|
||||
} else if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
} else if impErr, ok := err.(impart.HTTPError); ok {
|
||||
status = impErr.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 {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
@ -287,6 +293,26 @@ func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
|
|||
return h.UserAll(false, f, apiAuth)
|
||||
}
|
||||
|
||||
// UserWebAPI handles endpoints that accept a user authorized either via the web (cookies) or an Authorization header.
|
||||
func (h *Handler) UserWebAPI(f userHandlerFunc) http.HandlerFunc {
|
||||
return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
|
||||
// Authorize user via cookies
|
||||
u := getUserSession(app, r)
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Fall back to access token, since user isn't logged in via web
|
||||
var err error
|
||||
u, err = apiAuth(app, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
handleFunc := func() error {
|
||||
|
@ -554,6 +580,38 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) PlainTextAPI(f handlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleTextError(w, r, func() error {
|
||||
// TODO: return correct "success" status
|
||||
status := 200
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s:\n%s", e, debug.Stack())
|
||||
status = http.StatusInternalServerError
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintf(w, "Something didn't work quite right. The robots have alerted the humans.")
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host))
|
||||
}()
|
||||
|
||||
err := f(h.app.App(), w, r)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) OAuth(f handlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleOAuthError(w, r, func() error {
|
||||
|
@ -602,7 +660,7 @@ func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
|
|||
}()
|
||||
|
||||
// Allow any origin, as public endpoints are handled in here
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*");
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if h.app.App().cfg.App.Private {
|
||||
// This instance is private, so ensure it's being accessed by a valid user
|
||||
|
@ -822,6 +880,26 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
|
|||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
}
|
||||
|
||||
func (h *Handler) handleTextError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
if err.Status >= 300 && err.Status < 400 {
|
||||
sendRedirect(w, err.Status, err.Message)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(err.Status)
|
||||
fmt.Fprintf(w, http.StatusText(err.Status))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "This is an unhelpful error message for a miscellaneous internal error.")
|
||||
}
|
||||
|
||||
func (h *Handler) handleOAuthError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
11
invites.go
11
invites.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -19,9 +19,9 @@ import (
|
|||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/web-core/id"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
type Invite struct {
|
||||
|
@ -78,6 +78,9 @@ func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Req
|
|||
|
||||
p.Silenced, err = app.db.IsUserSilenced(u.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("view invites: %v", err)
|
||||
}
|
||||
|
||||
|
@ -121,7 +124,7 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
|
|||
expDate = &ed
|
||||
}
|
||||
|
||||
inviteID := store.GenerateRandomString("0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", 6)
|
||||
inviteID := id.GenerateRandomString("0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", 6)
|
||||
err = app.db.CreateUserInvite(inviteID, u.ID, maxUses, expDate)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
10
key/key.go
10
key/key.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019, 2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -20,7 +20,7 @@ const (
|
|||
)
|
||||
|
||||
type Keychain struct {
|
||||
EmailKey, CookieAuthKey, CookieKey []byte
|
||||
EmailKey, CookieAuthKey, CookieKey, CSRFKey []byte
|
||||
}
|
||||
|
||||
// GenerateKeys generates necessary keys for the app on the given Keychain,
|
||||
|
@ -47,6 +47,12 @@ func (keys *Keychain) GenerateKeys() error {
|
|||
keyErrs = err
|
||||
}
|
||||
}
|
||||
if len(keys.CSRFKey) == 0 {
|
||||
keys.CSRFKey, err = GenerateBytes(EncKeysBytes)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
}
|
||||
|
||||
return keyErrs
|
||||
}
|
||||
|
|
6
keys.go
6
keys.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@ package writefreely
|
|||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writefreely/writefreely/key"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -26,6 +26,7 @@ var (
|
|||
emailKeyPath = filepath.Join(keysDir, "email.aes256")
|
||||
cookieAuthKeyPath = filepath.Join(keysDir, "cookies_auth.aes256")
|
||||
cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256")
|
||||
csrfKeyPath = filepath.Join(keysDir, "csrf.aes256")
|
||||
)
|
||||
|
||||
// InitKeys loads encryption keys into memory via the given Apper interface
|
||||
|
@ -42,6 +43,7 @@ func initKeyPaths(app *App) {
|
|||
emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath)
|
||||
cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath)
|
||||
cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath)
|
||||
csrfKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, csrfKeyPath)
|
||||
}
|
||||
|
||||
// generateKey generates a key at the given path used for the encryption of
|
||||
|
|
|
@ -5,6 +5,7 @@ all :
|
|||
lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css
|
||||
lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css
|
||||
lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css
|
||||
lessc prose.less --clean-css="--s1 --advanced" $(CSSDIR)prose.css
|
||||
|
||||
install :
|
||||
./install-less.sh
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
@import "admin";
|
||||
@import "login";
|
||||
@import "pages/error";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
||||
@import "lib/material";
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
||||
|
||||
body {
|
||||
font-family: @serifFont;
|
||||
font-size-adjust: 0.5;
|
||||
|
@ -407,6 +393,14 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
img {
|
||||
&.paid {
|
||||
height: 0.86em;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
nav#full-nav {
|
||||
margin: 0;
|
||||
|
||||
|
@ -529,7 +523,7 @@ pre, body#post article, #post .alert, #subpage .alert, body#collection article,
|
|||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article p {
|
||||
textarea, input#title, pre, body#post article, body#collection article p {
|
||||
&.norm, &.sans, &.wrap {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
|
@ -539,7 +533,7 @@ textarea, pre, body#post article, body#collection article p {
|
|||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
textarea, input#title, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
&.norm {
|
||||
font-family: @serifFont;
|
||||
}
|
||||
|
@ -701,6 +695,7 @@ table.downloads {
|
|||
|
||||
select.inputform, textarea.inputform {
|
||||
border: 1px solid #999;
|
||||
background: white;
|
||||
}
|
||||
|
||||
input, button, select.inputform, textarea.inputform, a.btn {
|
||||
|
@ -757,6 +752,19 @@ input, button, select.inputform, textarea.inputform, a.btn {
|
|||
}
|
||||
}
|
||||
|
||||
.btn.cta.secondary, input[type=submit].secondary {
|
||||
background: transparent;
|
||||
color: @primary;
|
||||
&:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.cta.disabled {
|
||||
background-color: desaturate(@primary, 100%) !important;
|
||||
border-color: desaturate(@primary, 100%) !important;
|
||||
}
|
||||
|
||||
div.flat-select {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
@ -1058,6 +1066,19 @@ li {
|
|||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
&.danger {
|
||||
border-color: #856404;
|
||||
background-color: white;
|
||||
h3 {
|
||||
margin: 0 0 0.5em 0;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
color: black !important;
|
||||
}
|
||||
h3 + p, button {
|
||||
font-size: 0.86em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
@ -1529,6 +1550,11 @@ div.row {
|
|||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
article {
|
||||
.hidden {
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Install Less via npm
|
||||
if [ ! -e "$(which lessc)" ]; then
|
||||
sudo npm install -g less
|
||||
sudo npm install -g less@3.5.3
|
||||
sudo npm install -g less-plugin-clean-css
|
||||
else
|
||||
echo LESS $(npm view less version 2>&1 | grep -v WARN) is installed
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -188,18 +188,18 @@ body#pad, body#pad-sub {
|
|||
body#pad {
|
||||
.pad-theme-transition;
|
||||
|
||||
textarea {
|
||||
textarea, #title {
|
||||
.pad-theme-transition;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @darkBG;
|
||||
color: @darkTextColor;
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @lightBG;
|
||||
color: @lightTextColor;
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ body#pad {
|
|||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
textarea {
|
||||
textarea, #title {
|
||||
position: fixed !important;
|
||||
top: 3em;
|
||||
right: 0;
|
||||
|
@ -340,6 +340,15 @@ body#pad {
|
|||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
line-height: 1.5;
|
||||
|
||||
input[type=text].confirm {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.short {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -385,6 +394,14 @@ body#pad .alert {
|
|||
top: 2.25em;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
&.classic {
|
||||
#editor {
|
||||
top: 5.25em;
|
||||
}
|
||||
#title {
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
#tools {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
@ -438,8 +455,8 @@ body#pad .alert {
|
|||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
@ -450,8 +467,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 60em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
|
@ -462,8 +479,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 70em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
|
@ -474,8 +491,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 85em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 25%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
@ -486,8 +503,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 105em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 30%;
|
||||
padding-right: 30%;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,25 @@ body#post article, pre, .hljs {
|
|||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
p.split {
|
||||
color: #6161FF;
|
||||
font-style: italic;
|
||||
font-size: 0.86em;
|
||||
}
|
||||
|
||||
#readmore-sell {
|
||||
padding: 1em 1em 2em;
|
||||
background-color: #fafafa;
|
||||
p.split {
|
||||
color: black;
|
||||
font-style: normal;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.cta + .cta {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Post mixins */
|
||||
.article-code() {
|
||||
background-color: #f8f8f8;
|
||||
|
|
|
@ -0,0 +1,490 @@
|
|||
@classicHorizMargin: 2rem;
|
||||
|
||||
body#pad.classic {
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#editor {
|
||||
top: 4em;
|
||||
bottom: 1em;
|
||||
}
|
||||
#title {
|
||||
top: 4.25rem;
|
||||
bottom: unset;
|
||||
height: auto;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
#tools {
|
||||
#belt {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
#target {
|
||||
ul {
|
||||
a {
|
||||
padding: 0 0.5em !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
margin-left: @classicHorizMargin;
|
||||
margin-right: @classicHorizMargin;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
height: calc(~"100% - 1.6em");
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 1.2em;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
padding: 0.5em @classicHorizMargin;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
div {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
font-family: @sansFont;
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
margin-left: @classicHorizMargin;
|
||||
margin-right: @classicHorizMargin;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: .8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: #ccc;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul, .ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 4px solid #ddd;
|
||||
color: #767676;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 0.25em;
|
||||
z-index: 11;
|
||||
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0 0 0.75em;
|
||||
font-family: @sansFont;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
color: #666;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor, .editor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
padding: 5px 0;
|
||||
margin: 4em auto 23px auto;
|
||||
}
|
||||
|
||||
.dark #editor {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 123px;
|
||||
border: 1px solid silver;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
padding: 3px 10px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper, #markdown textarea {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.editorreadmore {
|
||||
color: @textLinkColor;
|
||||
text-decoration: underline;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
#photo-upload label {
|
||||
display: inline;
|
||||
}
|
||||
.ProseMirror-menubar, #title, #photo-upload {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
.ProseMirror {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 60em) {
|
||||
.ProseMirror-menubar, #title, #photo-upload {
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
}
|
||||
.ProseMirror {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 70em) {
|
||||
.ProseMirror-menubar, #title, #photo-upload {
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
}
|
||||
.ProseMirror {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 85em) {
|
||||
.ProseMirror-menubar, #title, #photo-upload {
|
||||
margin-left: 25%;
|
||||
margin-right: 25%;
|
||||
}
|
||||
.ProseMirror {
|
||||
padding-left: 25%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 105em) {
|
||||
.ProseMirror-menubar, #title, #photo-upload {
|
||||
margin-left: 30%;
|
||||
margin-right: 30%;
|
||||
}
|
||||
.ProseMirror {
|
||||
padding-left: 30%;
|
||||
padding-right: 30%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
@import "prose-editor";
|
||||
@import "pad-theme";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
|
@ -0,0 +1,13 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -87,6 +87,9 @@ func Migrate(db *datastore) error {
|
|||
var err error
|
||||
if db.tableExists("appmigrations") {
|
||||
err = db.QueryRow("SELECT MAX(version) FROM appmigrations").Scan(&version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("Initializing appmigrations table...")
|
||||
version = 0
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,6 +12,9 @@ package migrations
|
|||
|
||||
func supportUserInvites(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = t.Exec(`CREATE TABLE userinvites (
|
||||
id ` + db.typeChar(6) + ` NOT NULL ,
|
||||
owner_id ` + db.typeInt() + ` NOT NULL ,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,6 +12,9 @@ package migrations
|
|||
|
||||
func supportInstancePages(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE appcontent ADD COLUMN title ` + db.typeVarChar(255) + db.collateMultiByte() + ` NULL`)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,6 +12,9 @@ package migrations
|
|||
|
||||
func supportUserStatus(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauth(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthSlack(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,6 +12,9 @@ package migrations
|
|||
|
||||
func supportActivityPubMentions(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` NULL`)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
/*
|
||||
* Copyright © 2020-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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthAttach(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
wf_db "github.com/writefreely/writefreely/db"
|
||||
)
|
||||
|
||||
func oauthInvites(db *datastore) error {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright © 2020-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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func displayMonetization(monetization, alias string) string {
|
||||
if monetization == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
ptrURL, err := url.Parse(strings.Replace(monetization, "$", "https://", 1))
|
||||
if err == nil {
|
||||
if strings.HasSuffix(ptrURL.Host, ".xrptipbot.com") {
|
||||
// xrp tip bot doesn't support stream receipts, so return plain pointer
|
||||
return monetization
|
||||
}
|
||||
}
|
||||
|
||||
u := os.Getenv("PAYMENT_HOST")
|
||||
if u == "" {
|
||||
return "$webmonetization.org/api/receipts/" + url.PathEscape(monetization)
|
||||
}
|
||||
u += "/" + alias
|
||||
return u
|
||||
}
|
||||
|
||||
func handleSPSPEndpoint(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
idStr := r.FormValue("id")
|
||||
id, err := url.QueryUnescape(idStr)
|
||||
if err != nil {
|
||||
log.Error("Unable to unescape: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var c *Collection
|
||||
if strings.IndexRune(id, '.') > 0 && app.cfg.App.SingleUser {
|
||||
c, err = app.db.GetCollectionByID(1)
|
||||
} else {
|
||||
c, err = app.db.GetCollection(id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pointer := c.Monetization
|
||||
if pointer == "" {
|
||||
err := impart.HTTPError{http.StatusNotFound, "No monetization pointer."}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, pointer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetSplitContent(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var collID int64
|
||||
var collLookupID string
|
||||
var coll *Collection
|
||||
var err error
|
||||
vars := mux.Vars(r)
|
||||
if collAlias := vars["alias"]; collAlias != "" {
|
||||
// Fetch collection information, since an alias is provided
|
||||
coll, err = app.db.GetCollection(collAlias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collID = coll.ID
|
||||
collLookupID = coll.Alias
|
||||
}
|
||||
|
||||
p, err := app.db.GetPost(vars["post"], collID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receipt := r.FormValue("receipt")
|
||||
if receipt == "" {
|
||||
return impart.HTTPError{http.StatusBadRequest, "No `receipt` given."}
|
||||
}
|
||||
err = verifyReceipt(receipt, collLookupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := struct {
|
||||
Content string `json:"body"`
|
||||
HTMLContent string `json:"html_body"`
|
||||
}{}
|
||||
|
||||
if exc := strings.Index(p.Content, shortCodePaid); exc > -1 {
|
||||
baseURL := ""
|
||||
if coll != nil {
|
||||
baseURL = coll.CanonicalURL()
|
||||
}
|
||||
|
||||
d.Content = p.Content[exc+len(shortCodePaid):]
|
||||
d.HTMLContent = applyMarkdown([]byte(d.Content), baseURL, app.cfg)
|
||||
}
|
||||
|
||||
return impart.WriteSuccess(w, d, http.StatusOK)
|
||||
}
|
||||
|
||||
func verifyReceipt(receipt, id string) error {
|
||||
receiptsHost := os.Getenv("RECEIPTS_HOST")
|
||||
if receiptsHost == "" {
|
||||
receiptsHost = "https://webmonetization.org/api/receipts/verify?id=" + id
|
||||
} else {
|
||||
receiptsHost = fmt.Sprintf("%s/receipts?id=%s", receiptsHost, id)
|
||||
}
|
||||
|
||||
log.Info("Verifying receipt %s at %s", receipt, receiptsHost)
|
||||
r, err := http.NewRequest("POST", receiptsHost, bytes.NewBufferString(receipt))
|
||||
if err != nil {
|
||||
log.Error("Unable to create new request to %s: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
log.Error("Unable to Do() request to %s: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error("Unable to read %s response body: %s", receiptsHost, err)
|
||||
return err
|
||||
}
|
||||
log.Info("Status : %s", resp.Status)
|
||||
log.Info("Response: %s", body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Error("Bad response from %s:\nStatus: %d\n%s", receiptsHost, resp.StatusCode, string(body))
|
||||
return impart.HTTPError{resp.StatusCode, string(body)}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,8 +12,8 @@ package writefreely
|
|||
|
||||
import (
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/go-nodeinfo"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -45,7 +45,7 @@ func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config {
|
|||
Private: cfg.App.Private,
|
||||
Software: nodeinfo.SoftwareMeta{
|
||||
HomePage: softwareURL,
|
||||
GitHub: "https://github.com/writeas/writefreely",
|
||||
GitHub: "https://github.com/writefreely/writefreely",
|
||||
Follow: "https://writing.exchange/@write_as",
|
||||
},
|
||||
MaxBlogs: cfg.App.MaxBlogs,
|
||||
|
|
20
oauth.go
20
oauth.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/gorilla/sessions"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
)
|
||||
|
||||
// OAuthButtons holds display information for different OAuth providers we support.
|
||||
|
@ -99,7 +99,7 @@ type OAuthDatastore interface {
|
|||
ValidateOAuthState(context.Context, string) (string, string, int64, string, error)
|
||||
GenerateOAuthState(context.Context, string, string, int64, string) (string, error)
|
||||
|
||||
CreateUser(*config.Config, *User, string) error
|
||||
CreateUser(*config.Config, *User, string, string) error
|
||||
GetUserByID(int64) (*User, error)
|
||||
}
|
||||
|
||||
|
@ -266,6 +266,10 @@ func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
|||
HttpClient: config.DefaultHTTPClient(),
|
||||
CallbackLocation: callbackLocation,
|
||||
Scope: config.OrDefaultString(app.Config().GenericOauth.Scope, "read_user"),
|
||||
MapUserID: config.OrDefaultString(app.Config().GenericOauth.MapUserID, "user_id"),
|
||||
MapUsername: config.OrDefaultString(app.Config().GenericOauth.MapUsername, "username"),
|
||||
MapDisplayName: config.OrDefaultString(app.Config().GenericOauth.MapDisplayName, "-"),
|
||||
MapEmail: config.OrDefaultString(app.Config().GenericOauth.MapEmail, "email"),
|
||||
}
|
||||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||
}
|
||||
|
@ -289,10 +293,15 @@ func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) {
|
|||
ClientID: app.Config().GiteaOauth.ClientID,
|
||||
ClientSecret: app.Config().GiteaOauth.ClientSecret,
|
||||
ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token",
|
||||
InspectLocation: app.Config().GiteaOauth.Host + "/api/v1/user",
|
||||
InspectLocation: app.Config().GiteaOauth.Host + "/login/oauth/userinfo",
|
||||
AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize",
|
||||
HttpClient: config.DefaultHTTPClient(),
|
||||
CallbackLocation: callbackLocation,
|
||||
Scope: "openid profile email",
|
||||
MapUserID: "sub",
|
||||
MapUsername: "login",
|
||||
MapDisplayName: "full_name",
|
||||
MapEmail: "email",
|
||||
}
|
||||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||
}
|
||||
|
@ -351,7 +360,7 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
|
|||
}
|
||||
|
||||
if localUserID != -1 && attachUserID > 0 {
|
||||
if err = addSessionFlash(app, w, r, "This Slack account is already attached to another user.", nil); err != nil {
|
||||
if err = addSessionFlash(app, w, r, "This OAuth account is already attached to another user.", nil); err != nil {
|
||||
return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
|
||||
}
|
||||
return impart.HTTPError{http.StatusFound, "/me/settings"}
|
||||
|
@ -372,6 +381,7 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
|
|||
}
|
||||
if attachUserID > 0 {
|
||||
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)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
|
||||
|
|
|
@ -7,4 +7,3 @@ type ClientStateStore interface {
|
|||
Generate(ctx context.Context) (string, error)
|
||||
Validate(ctx context.Context, state string) error
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
/*
|
||||
* Copyright © 2020-2021 Musing Studio LLC and respective authors.
|
||||
*
|
||||
* 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/writeas/web-core/log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -16,6 +28,10 @@ type genericOauthClient struct {
|
|||
InspectLocation string
|
||||
CallbackLocation string
|
||||
Scope string
|
||||
MapUserID string
|
||||
MapUsername string
|
||||
MapDisplayName string
|
||||
MapEmail string
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
|
@ -104,13 +120,23 @@ func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessT
|
|||
return nil, errors.New("unable to inspect access token")
|
||||
}
|
||||
|
||||
var inspectResponse InspectResponse
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
|
||||
// since we don't know what the JSON from the server will look like, we create a
|
||||
// generic interface and then map manually to values set in the config
|
||||
var genericInterface map[string]interface{}
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inspectResponse.Error != "" {
|
||||
return nil, errors.New(inspectResponse.Error)
|
||||
|
||||
// map each relevant field in inspectResponse to the mapped field from the config
|
||||
var inspectResponse InspectResponse
|
||||
inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string)
|
||||
if inspectResponse.UserID == "" {
|
||||
log.Error("[CONFIGURATION ERROR] Generic 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
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package writefreely
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/writeas/web-core/log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -15,6 +17,11 @@ type giteaOauthClient struct {
|
|||
ExchangeLocation string
|
||||
InspectLocation string
|
||||
CallbackLocation string
|
||||
Scope string
|
||||
MapUserID string
|
||||
MapUsername string
|
||||
MapDisplayName string
|
||||
MapEmail string
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
|
@ -46,7 +53,7 @@ func (c giteaOauthClient) buildLoginURL(state string) (string, error) {
|
|||
q.Set("redirect_uri", c.CallbackLocation)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("state", state)
|
||||
// q.Set("scope", "read_user")
|
||||
q.Set("scope", c.Scope)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
@ -55,7 +62,7 @@ func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*
|
|||
form := url.Values{}
|
||||
form.Add("grant_type", "authorization_code")
|
||||
form.Add("redirect_uri", c.CallbackLocation)
|
||||
// form.Add("scope", "read_user")
|
||||
form.Add("scope", c.Scope)
|
||||
form.Add("code", code)
|
||||
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
|
@ -103,12 +110,24 @@ func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
|
|||
return nil, errors.New("unable to inspect access token")
|
||||
}
|
||||
|
||||
var inspectResponse InspectResponse
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
|
||||
// since we don't know what the JSON from the server will look like, we create a
|
||||
// generic interface and then map manually to values set in the config
|
||||
var genericInterface map[string]interface{}
|
||||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inspectResponse.Error != "" {
|
||||
return nil, errors.New(inspectResponse.Error)
|
||||
|
||||
// map each relevant field in inspectResponse to the mapped field from the config
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2020 A Bunch Tell LLC.
|
||||
* Copyright © 2020-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -127,7 +127,7 @@ func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.R
|
|||
displayName = r.FormValue(oauthParamUsername)
|
||||
}
|
||||
|
||||
err = h.DB.CreateUser(h.Config, newUser, displayName)
|
||||
err = h.DB.CreateUser(h.Config, newUser, displayName, "")
|
||||
if err != nil {
|
||||
return h.showOauthSignupPage(app, w, r, tp, err)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
/*
|
||||
* 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 (
|
||||
|
@ -6,8 +16,8 @@ import (
|
|||
"github.com/gorilla/sessions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/nerds/store"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/web-core/id"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -100,7 +110,7 @@ func (m *MockOAuthDatastore) GetIDForRemoteUser(ctx context.Context, remoteUserI
|
|||
return -1, nil
|
||||
}
|
||||
|
||||
func (m *MockOAuthDatastore) CreateUser(cfg *config.Config, u *User, username string) error {
|
||||
func (m *MockOAuthDatastore) CreateUser(cfg *config.Config, u *User, username, description string) error {
|
||||
if m.DoCreateUser != nil {
|
||||
return m.DoCreateUser(cfg, u, username)
|
||||
}
|
||||
|
@ -127,7 +137,7 @@ func (m *MockOAuthDatastore) GenerateOAuthState(ctx context.Context, provider st
|
|||
if m.DoGenerateOAuthState != nil {
|
||||
return m.DoGenerateOAuthState(ctx, provider, clientID, attachUserID, inviteCode)
|
||||
}
|
||||
return store.Generate62RandomString(14), nil
|
||||
return id.Generate62RandomString(14), nil
|
||||
}
|
||||
|
||||
func TestViewOauthInit(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
[provider_sect]
|
||||
default = default_sect
|
||||
legacy = legacy_sect
|
||||
|
||||
[default_sect]
|
||||
activate = 1
|
||||
|
||||
[legacy_sect]
|
||||
activate = 1
|
11
pad.go
11
pad.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
)
|
||||
|
||||
func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
|
@ -55,6 +55,9 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID)
|
||||
if err != nil {
|
||||
if err == ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
log.Error("Unable to get user status for Pad: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +93,7 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
appData.EditCollection, err = app.db.GetCollectionForPad(collAlias)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetCollectionForPad: %s", err)
|
||||
return err
|
||||
}
|
||||
appData.EditCollection.hostName = app.cfg.App.Host
|
||||
|
@ -101,9 +105,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
if appData.Post.Gone {
|
||||
return ErrPostUnpublished
|
||||
} else if appData.Post.Found && appData.Post.Content != "" {
|
||||
} else if appData.Post.Found && (appData.Post.Title != "" || appData.Post.Content != "") {
|
||||
// Got the post
|
||||
} else if appData.Post.Found {
|
||||
log.Error("Found post, but other conditions failed.")
|
||||
return ErrPostFetchError
|
||||
} else {
|
||||
return ErrPostNotFound
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -21,6 +21,7 @@ type StaticPage struct {
|
|||
config.AppCfg
|
||||
Version string
|
||||
HeaderNav bool
|
||||
CustomCSS bool
|
||||
|
||||
// Request values
|
||||
Path string
|
||||
|
|
4
pages.go
4
pages.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019, 2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,7 +12,7 @@ package writefreely
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{{define "head"}}<title>Page not found — {{.SiteName}}</title>{{end}}
|
||||
{{define "content"}}
|
||||
<div class="error-page">
|
||||
<p class="msg">This page is missing.</p>
|
||||
<p>Are you sure it was ever here?</p>
|
||||
<p class="msg">Page not found.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{define "content"}}
|
||||
<div class="content-container tight">
|
||||
<h1>Server error 😵</h1>
|
||||
<p>Please <a href="https://github.com/writeas/writefreely/issues/new">contact the human authors</a> of this software and remind them of their many shortcomings.</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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,6 +11,7 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
|
@ -23,13 +24,13 @@ import (
|
|||
"unicode/utf8"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
stripmd "github.com/writeas/go-strip-markdown/v2"
|
||||
"github.com/writeas/impart"
|
||||
blackfriday "github.com/writeas/saturday"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/stringmanip"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
"github.com/writefreely/writefreely/config"
|
||||
"github.com/writefreely/writefreely/parse"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -42,12 +43,46 @@ var (
|
|||
mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`)
|
||||
)
|
||||
|
||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
||||
func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) {
|
||||
if c.Monetization != "" {
|
||||
// User has Web Monetization enabled, so split content if it exists
|
||||
spl := strings.Index(p.Content, shortCodePaid)
|
||||
p.IsPaid = spl > -1
|
||||
if postPage {
|
||||
// We're viewing the individual post
|
||||
if isOwner {
|
||||
p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`<p class="split">Your subscriber content begins here.</p>`+"\n\n", 1)
|
||||
} else {
|
||||
if spl > -1 {
|
||||
p.Content = p.Content[:spl+len(shortCodePaid)]
|
||||
p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`<p class="split">Continue reading with a <strong>Coil</strong> membership.</p>`+"\n\n", 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We've viewing the post on the collection landing
|
||||
if spl > -1 {
|
||||
baseURL := c.CanonicalURL()
|
||||
if isOwner {
|
||||
baseURL = "/" + c.Alias + "/"
|
||||
}
|
||||
|
||||
p.Content = p.Content[:spl+len(shortCodePaid)]
|
||||
p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) {
|
||||
baseURL := c.CanonicalURL()
|
||||
// TODO: redundant
|
||||
if !isSingleUser {
|
||||
baseURL = "/" + c.Alias + "/"
|
||||
}
|
||||
|
||||
p.handlePremiumContent(c, isOwner, isPostPage, cfg)
|
||||
p.Content = strings.Replace(p.Content, "<!--paid-->", "<!--paid-->", 1)
|
||||
|
||||
p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String)))
|
||||
p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg))
|
||||
if exc := strings.Index(string(p.Content), "<!--more-->"); exc > -1 {
|
||||
|
@ -55,11 +90,19 @@ func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) {
|
||||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner)
|
||||
func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) {
|
||||
p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage)
|
||||
}
|
||||
|
||||
func (p *Post) augmentContent(c *Collection) {
|
||||
if p.PinnedPosition.Valid {
|
||||
// Don't augment posts that are pinned
|
||||
return
|
||||
}
|
||||
if strings.Index(p.Content, "<!--nosig-->") > -1 {
|
||||
// Don't augment posts with the special "nosig" shortcode
|
||||
return
|
||||
}
|
||||
// Add post signatures
|
||||
if c.Signature != "" {
|
||||
p.Content += "\n\n" + c.Signature
|
||||
|
@ -70,6 +113,12 @@ func (p *PublicPost) augmentContent() {
|
|||
p.Post.augmentContent(&p.Collection.Collection)
|
||||
}
|
||||
|
||||
func (p *PublicPost) augmentReadingDestination() {
|
||||
if p.IsPaid {
|
||||
p.HTMLContent += template.HTML("\n\n" + `<p><a class="read-more" href="` + p.Collection.CanonicalURL() + p.Slug.String + `">` + localStr("Read more...", p.Language.String) + `</a> ($)</p>`)
|
||||
}
|
||||
}
|
||||
|
||||
func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string {
|
||||
return applyMarkdownSpecial(data, false, baseURL, cfg)
|
||||
}
|
||||
|
@ -133,6 +182,10 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
|
|||
}
|
||||
|
||||
func applyBasicMarkdown(data []byte) string {
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
mdExtensions := 0 |
|
||||
blackfriday.EXTENSION_STRIKETHROUGH |
|
||||
blackfriday.EXTENSION_SPACE_HEADERS |
|
||||
|
@ -143,7 +196,15 @@ func applyBasicMarkdown(data []byte) string {
|
|||
blackfriday.HTML_SMARTYPANTS_DASHES
|
||||
|
||||
// Generate Markdown
|
||||
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
|
||||
// This passes the supplied title into blackfriday.Markdown() as an H1 header, so we only render HTML that
|
||||
// belongs in an H1.
|
||||
md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions)
|
||||
// Remove H1 markup
|
||||
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>")]
|
||||
// Strip out bad HTML
|
||||
policy := bluemonday.UGCPolicy()
|
||||
policy.AllowAttrs("class", "id").Globally()
|
||||
|
@ -209,6 +270,7 @@ func getSanitizationPolicy() *bluemonday.Policy {
|
|||
policy.AllowAttrs("target").OnElements("a")
|
||||
policy.AllowAttrs("title").OnElements("abbr")
|
||||
policy.AllowAttrs("style", "class", "id").Globally()
|
||||
policy.AllowAttrs("alt").OnElements("img")
|
||||
policy.AllowElements("header", "footer")
|
||||
policy.AllowURLSchemes("http", "https", "mailto", "xmpp")
|
||||
return policy
|
||||
|
@ -223,6 +285,7 @@ func sanitizePost(content string) string {
|
|||
// 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
|
||||
// title, like so:
|
||||
//
|
||||
// var desc string
|
||||
// if title == "" {
|
||||
// desc = postDescription(content, title, friendlyId)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 writefreely
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyBasicMarkdown(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
result string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"empty spaces", " ", ""},
|
||||
{"empty tabs", "\t", ""},
|
||||
{"empty newline", "\n", ""},
|
||||
{"nums", "123", "123"},
|
||||
{"dot", ".", "."},
|
||||
{"dash", "-", "-"},
|
||||
{"plain", "Hello, World!", "Hello, World!"},
|
||||
{"multibyte", "こんにちは", `こんにちは`},
|
||||
{"bold", "**안녕하세요**", `<strong>안녕하세요</strong>`},
|
||||
{"link", "[WriteFreely](https://writefreely.org)", `<a href="https://writefreely.org" rel="nofollow">WriteFreely</a>`},
|
||||
{"date", "12. April", `12. April`},
|
||||
{"table", "| Hi | There |", `| Hi | There |`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res := applyBasicMarkdown([]byte(test.in))
|
||||
if res != test.result {
|
||||
t.Errorf("%s: wanted %s, got %s", test.name, test.result, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
114
posts.go
114
posts.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2021 Musing Studio LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/guregu/null/zero"
|
||||
"github.com/kylemcc/twitter-text-go/extract"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
stripmd "github.com/writeas/go-strip-markdown/v2"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/monday"
|
||||
"github.com/writeas/slug"
|
||||
|
@ -36,8 +36,8 @@ import (
|
|||
"github.com/writeas/web-core/i18n"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/tags"
|
||||
"github.com/writeas/writefreely/page"
|
||||
"github.com/writeas/writefreely/parse"
|
||||
"github.com/writefreely/writefreely/page"
|
||||
"github.com/writefreely/writefreely/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -48,6 +48,8 @@ const (
|
|||
postIDLen = 10
|
||||
|
||||
postMetaDateFormat = "2006-01-02 15:04:05"
|
||||
|
||||
shortCodePaid = "<!--paid-->"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -109,6 +111,7 @@ type (
|
|||
HTMLExcerpt template.HTML `db:"content" json:"-"`
|
||||
Tags []string `json:"tags"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
IsPaid bool `json:"paid"`
|
||||
|
||||
OwnerName string `json:"owner,omitempty"`
|
||||
}
|
||||
|
@ -125,9 +128,27 @@ type (
|
|||
Views int64 `json:"views"`
|
||||
Owner *PublicUser `json:"-"`
|
||||
IsOwner bool `json:"-"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Collection *CollectionObj `json:"collection,omitempty"`
|
||||
}
|
||||
|
||||
CollectionPostPage struct {
|
||||
*PublicPost
|
||||
page.StaticPage
|
||||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
Silenced bool
|
||||
|
||||
// Helper field for Chorus mode
|
||||
CollAlias string
|
||||
}
|
||||
|
||||
RawPost struct {
|
||||
Id, Slug string
|
||||
Title string
|
||||
|
@ -213,7 +234,7 @@ func (p Post) Summary() string {
|
|||
}
|
||||
p.Content = stripHTMLWithoutEscaping(p.Content)
|
||||
// and Markdown
|
||||
p.Content = stripmd.Strip(p.Content)
|
||||
p.Content = stripmd.StripOptions(p.Content, stripmd.Options{SkipImages: true})
|
||||
|
||||
title := p.Title.String
|
||||
var desc string
|
||||
|
@ -268,6 +289,14 @@ func (p *Post) HasTitleLink() bool {
|
|||
return hasLink
|
||||
}
|
||||
|
||||
func (c CollectionPostPage) DisplayMonetization() string {
|
||||
if c.Collection == nil {
|
||||
log.Info("CollectionPostPage.DisplayMonetization: c.Collection is nil")
|
||||
return ""
|
||||
}
|
||||
return displayMonetization(c.Monetization, c.Collection.Alias)
|
||||
}
|
||||
|
||||
func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
friendlyID := vars["post"]
|
||||
|
@ -396,7 +425,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
// Check if post has been unpublished
|
||||
if content == "" {
|
||||
if title == "" && content == "" {
|
||||
gone = true
|
||||
|
||||
if isJSON {
|
||||
|
@ -545,9 +574,14 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
t := ""
|
||||
p.Title = &t
|
||||
}
|
||||
if strings.TrimSpace(*(p.Content)) == "" {
|
||||
if strings.TrimSpace(*(p.Title)) == "" && (p.Content == nil || strings.TrimSpace(*(p.Content)) == "") {
|
||||
return ErrNoPublishableContent
|
||||
}
|
||||
if p.Content == nil {
|
||||
c := ""
|
||||
p.Content = &c
|
||||
}
|
||||
|
||||
} else {
|
||||
post := r.FormValue("body")
|
||||
appearance := r.FormValue("font")
|
||||
|
@ -612,6 +646,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
newPost.extractData()
|
||||
newPost.OwnerName = username
|
||||
newPost.URL = newPost.CanonicalURL(app.cfg.App.Host)
|
||||
|
||||
// Write success now
|
||||
response := impart.WriteSuccess(w, newPost, http.StatusCreated)
|
||||
|
@ -1124,14 +1159,19 @@ func (p *Post) processPost() PublicPost {
|
|||
|
||||
func (p *PublicPost) CanonicalURL(hostName string) string {
|
||||
if p.Collection == nil || p.Collection.Alias == "" {
|
||||
return hostName + "/" + p.ID
|
||||
return hostName + "/" + p.ID + ".md"
|
||||
}
|
||||
return p.Collection.CanonicalURL() + p.Slug.String
|
||||
}
|
||||
|
||||
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
||||
cfg := app.cfg
|
||||
o := activitystreams.NewArticleObject()
|
||||
var o *activitystreams.Object
|
||||
if cfg.App.NotesOnly || strings.Index(p.Content, "\n\n") == -1 {
|
||||
o = activitystreams.NewNoteObject()
|
||||
} else {
|
||||
o = activitystreams.NewArticleObject()
|
||||
}
|
||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||
o.Published = p.Created
|
||||
o.URL = p.CanonicalURL(cfg.App.Host)
|
||||
|
@ -1142,7 +1182,8 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
|||
o.Name = p.DisplayTitle()
|
||||
p.augmentContent()
|
||||
if p.HTMLContent == template.HTML("") {
|
||||
p.formatContent(cfg, false)
|
||||
p.formatContent(cfg, false, false)
|
||||
p.augmentReadingDestination()
|
||||
}
|
||||
o.Content = string(p.HTMLContent)
|
||||
if p.Language.Valid {
|
||||
|
@ -1171,6 +1212,11 @@ func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
|
|||
})
|
||||
}
|
||||
}
|
||||
if len(p.Images) > 0 {
|
||||
for _, i := range p.Images {
|
||||
o.Attachment = append(o.Attachment, activitystreams.NewImageAttachment(i))
|
||||
}
|
||||
}
|
||||
// Find mentioned users
|
||||
mentionedUsers := make(map[string]string)
|
||||
|
||||
|
@ -1201,6 +1247,8 @@ func getSlug(title, lang string) string {
|
|||
|
||||
func getSlugFromPost(title, body, lang string) string {
|
||||
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 = parse.PostLede(title, false)
|
||||
|
@ -1248,10 +1296,22 @@ func getRawPost(app *App, friendlyID string) *RawPost {
|
|||
case err == sql.ErrNoRows:
|
||||
return &RawPost{Content: "", Found: false, Gone: false}
|
||||
case err != nil:
|
||||
log.Error("Unable to fetch getRawPost: %s", err)
|
||||
return &RawPost{Content: "", Found: true, Gone: false}
|
||||
}
|
||||
|
||||
return &RawPost{Title: title, Content: content, Font: font, Created: created, Updated: updated, IsRTL: isRTL, Language: lang, OwnerID: ownerID.Int64, Found: true, Gone: content == ""}
|
||||
return &RawPost{
|
||||
Title: title,
|
||||
Content: content,
|
||||
Font: font,
|
||||
Created: created,
|
||||
Updated: updated,
|
||||
IsRTL: isRTL,
|
||||
Language: lang,
|
||||
OwnerID: ownerID.Int64,
|
||||
Found: true,
|
||||
Gone: content == "" && title == "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1274,6 +1334,7 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
|
|||
case err == sql.ErrNoRows:
|
||||
return &RawPost{Content: "", Found: false, Gone: false}
|
||||
case err != nil:
|
||||
log.Error("Unable to fetch getRawCollectionPost: %s", err)
|
||||
return &RawPost{Content: "", Found: true, Gone: false}
|
||||
}
|
||||
|
||||
|
@ -1289,7 +1350,7 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
|
|||
Language: lang,
|
||||
OwnerID: ownerID.Int64,
|
||||
Found: true,
|
||||
Gone: content == "",
|
||||
Gone: content == "" && title == "",
|
||||
Views: views,
|
||||
}
|
||||
}
|
||||
|
@ -1402,7 +1463,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
|
|||
if slug == "feed" {
|
||||
// User tried to access blog feed without a trailing slash, and
|
||||
// there's no post with a slug "feed"
|
||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/feed/"}
|
||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "feed/"}
|
||||
}
|
||||
|
||||
po := &Post{
|
||||
|
@ -1420,13 +1481,17 @@ Are you sure it was ever here?`,
|
|||
return err
|
||||
}
|
||||
}
|
||||
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64
|
||||
|
||||
// Check if the authenticated user is the post owner
|
||||
p.IsOwner = u != nil && u.ID == p.OwnerID.Int64
|
||||
p.Collection = coll
|
||||
p.IsTopLevel = app.cfg.App.SingleUser
|
||||
|
||||
if !p.IsOwner && silenced {
|
||||
// Only allow a post owner or admin to view a post for silenced collections
|
||||
if silenced && !p.IsOwner && (u == nil || !u.IsAdmin()) {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
|
||||
// Check if post has been unpublished
|
||||
if p.Content == "" && p.Title.String == "" {
|
||||
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
|
||||
|
@ -1468,26 +1533,15 @@ Are you sure it was ever here?`,
|
|||
p.extractData()
|
||||
p.Content = strings.Replace(p.Content, "<!--more-->", "", 1)
|
||||
// TODO: move this to function
|
||||
p.formatContent(app.cfg, cr.isCollOwner)
|
||||
tp := struct {
|
||||
*PublicPost
|
||||
page.StaticPage
|
||||
IsOwner bool
|
||||
IsPinned bool
|
||||
IsCustomDomain bool
|
||||
Monetization string
|
||||
PinnedPosts *[]PublicPost
|
||||
IsFound bool
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
Silenced bool
|
||||
}{
|
||||
p.formatContent(app.cfg, cr.isCollOwner, true)
|
||||
tp := CollectionPostPage{
|
||||
PublicPost: p,
|
||||
StaticPage: pageForReq(app, r),
|
||||
IsOwner: cr.isCollOwner,
|
||||
IsCustomDomain: cr.isCustomDomain,
|
||||
IsFound: postFound,
|
||||
Silenced: silenced,
|
||||
CollAlias: c.Alias,
|
||||
}
|
||||
tp.IsAdmin = u != nil && u.IsAdmin()
|
||||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
||||
|
@ -1503,7 +1557,7 @@ Are you sure it was ever here?`,
|
|||
postTmpl = "chorus-collection-post"
|
||||
}
|
||||
if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil {
|
||||
log.Error("Error in collection-post template: %v", err)
|
||||
log.Error("Error in %s template: %v", postTmpl, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
/*
|
||||
* Copyright © 2020-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_test
|
||||
|
||||
import (
|
||||
|
@ -5,7 +15,7 @@ import (
|
|||
|
||||
"github.com/guregu/null/zero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/writeas/writefreely"
|
||||
"github.com/writefreely/writefreely"
|
||||
)
|
||||
|
||||
func TestPostSummary(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
all :
|
||||
npm install
|
||||
npm run-script build
|
|
@ -0,0 +1,7 @@
|
|||
# Building
|
||||
|
||||
* Run `npm install` to download dependencies.
|
||||
* Run `npm run-script build` to build a production script in `../static/js/` or run
|
||||
`npm run develop` to build and watch for changes. You can use `prose.html`
|
||||
to test your development changes.
|
||||
* Manually copy the file `prose.bundle.js` to `static/js/`. _To be automated_
|
|
@ -0,0 +1,57 @@
|
|||
import { MarkdownParser } from "prosemirror-markdown";
|
||||
import markdownit from "markdown-it";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
export const writeFreelyMarkdownParser = new MarkdownParser(
|
||||
writeFreelySchema,
|
||||
markdownit("commonmark", { html: true }),
|
||||
{
|
||||
blockquote: { block: "blockquote" },
|
||||
paragraph: { block: "paragraph" },
|
||||
list_item: { block: "list_item" },
|
||||
bullet_list: { block: "bullet_list" },
|
||||
ordered_list: {
|
||||
block: "ordered_list",
|
||||
getAttrs: (tok) => ({ order: +tok.attrGet("start") || 1 }),
|
||||
},
|
||||
heading: {
|
||||
block: "heading",
|
||||
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
|
||||
},
|
||||
code_block: { block: "code_block", noCloseToken: true },
|
||||
fence: {
|
||||
block: "code_block",
|
||||
getAttrs: (tok) => ({ params: tok.info || "" }),
|
||||
noCloseToken: true,
|
||||
},
|
||||
hr: { node: "horizontal_rule" },
|
||||
image: {
|
||||
node: "image",
|
||||
getAttrs: (tok) => ({
|
||||
src: tok.attrGet("src"),
|
||||
title: tok.attrGet("title") || null,
|
||||
alt: (tok.children !== null && typeof tok.children[0] !== 'undefined' ? tok.children[0].content : null),
|
||||
}),
|
||||
},
|
||||
hardbreak: { node: "hard_break" },
|
||||
|
||||
em: { mark: "em" },
|
||||
strong: { mark: "strong" },
|
||||
link: {
|
||||
mark: "link",
|
||||
getAttrs: (tok) => ({
|
||||
href: tok.attrGet("href"),
|
||||
title: tok.attrGet("title") || null,
|
||||
}),
|
||||
},
|
||||
code_inline: { mark: "code", noCloseToken: true },
|
||||
html_block: {
|
||||
node: "readmore",
|
||||
getAttrs(token) {
|
||||
// TODO: Give different attributes depending on the token content
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
|
@ -0,0 +1,128 @@
|
|||
import { MarkdownSerializer } from "prosemirror-markdown";
|
||||
|
||||
function backticksFor(node, side) {
|
||||
const ticks = /`+/g;
|
||||
let m;
|
||||
let len = 0;
|
||||
if (node.isText)
|
||||
while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length);
|
||||
let result = len > 0 && side > 0 ? " `" : "`";
|
||||
for (let i = 0; i < len; i++) result += "`";
|
||||
if (len > 0 && side < 0) result += " ";
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (
|
||||
!content.isText ||
|
||||
content.text != link.attrs.href ||
|
||||
content.marks[content.marks.length - 1] != link
|
||||
)
|
||||
return false;
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) return true;
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks);
|
||||
}
|
||||
|
||||
export const writeFreelyMarkdownSerializer = new MarkdownSerializer(
|
||||
{
|
||||
readmore(state, node) {
|
||||
state.write("<!--more-->\n");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
blockquote(state, node) {
|
||||
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
||||
},
|
||||
code_block(state, node) {
|
||||
state.write(`\`\`\`${node.attrs.params || ""}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write("```");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(`${state.repeat("#", node.attrs.level)} `);
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
horizontal_rule: function horizontal_rule(state, node) {
|
||||
state.write(node.attrs.markup || "---");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
node.attrs.tight = true;
|
||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
const start = node.attrs.order || 1;
|
||||
const maxW = String(start + node.childCount - 1).length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
state.renderList(node, space, (i) => {
|
||||
const nStr = String(start + i);
|
||||
return `${state.repeat(" ", maxW - nStr.length) + nStr}. `;
|
||||
});
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node);
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
state.write(
|
||||
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
|
||||
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
|
||||
})`
|
||||
);
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i += 1)
|
||||
if (parent.child(i).type !== node.type) {
|
||||
state.write("\\\n");
|
||||
return;
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
em: {
|
||||
open: "_",
|
||||
close: "_",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strong: {
|
||||
open: "**",
|
||||
close: "**",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? ">"
|
||||
: `](${state.esc(mark.attrs.href)}${
|
||||
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ""
|
||||
})`;
|
||||
},
|
||||
},
|
||||
code: {
|
||||
open(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index), -1);
|
||||
},
|
||||
close(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index - 1), 1);
|
||||
},
|
||||
escape: false,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
import { MenuItem } from "prosemirror-menu";
|
||||
import { buildMenuItems } from "prosemirror-example-setup";
|
||||
|
||||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
function canInsert(state, nodeType, attrs) {
|
||||
let $from = state.selection.$from;
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
let index = $from.index(d);
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ReadMoreItem = new MenuItem({
|
||||
label: "Read more",
|
||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
|
||||
run(state, dispatch) {
|
||||
dispatch(
|
||||
state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getMenu = () => {
|
||||
const menuContent = [
|
||||
...buildMenuItems(writeFreelySchema).fullMenu,
|
||||
[ReadMoreItem],
|
||||
];
|
||||
return menuContent;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue