commit
8d86bdd33d
@ -0,0 +1,14 @@
|
||||
*.log
|
||||
*.swp
|
||||
.env
|
||||
.envrc
|
||||
build/*
|
||||
!build/.gitkeep
|
||||
ssh_data/*
|
||||
!ssh_data/.gitkeep
|
||||
caddy_data/*
|
||||
!caddy_data/.gitkeep
|
||||
caddy_config/*
|
||||
!caddy_config/.gitkeep
|
||||
.env.prod
|
||||
*.bak
|
@ -0,0 +1,8 @@
|
||||
*.pastes.sh, pastes.sh {
|
||||
reverse_proxy web:3000
|
||||
tls hello@pastes.sh
|
||||
tls {
|
||||
dns cloudflare {env.CF_API_TOKEN}
|
||||
}
|
||||
encode zstd gzip
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
FROM golang:1.18.1-alpine3.15 AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./build/ssh ./cmd/ssh
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./build/web ./cmd/web
|
||||
|
||||
FROM alpine:3.15 AS ssh
|
||||
WORKDIR /app
|
||||
COPY --from=0 /app/build/ssh ./
|
||||
CMD ["./ssh"]
|
||||
|
||||
FROM alpine:3.15 AS web
|
||||
WORKDIR /app
|
||||
COPY --from=0 /app/build/web ./
|
||||
COPY --from=0 /app/html ./html
|
||||
COPY --from=0 /app/public ./public
|
||||
CMD ["./web"]
|
@ -0,0 +1,8 @@
|
||||
FROM caddy:builder-alpine AS builder
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/caddy-dns/cloudflare
|
||||
|
||||
FROM caddy:alpine
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Eric Bower
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,76 @@
|
||||
PGDATABASE?="pastes"
|
||||
PGHOST?="db"
|
||||
PGUSER?="postgres"
|
||||
PORT?="5432"
|
||||
DB_CONTAINER?=pastessh_db_1
|
||||
|
||||
test:
|
||||
docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -E goimports -E godot
|
||||
.PHONY: test
|
||||
|
||||
build:
|
||||
go build -o build/web ./cmd/web
|
||||
go build -o build/ssh ./cmd/ssh
|
||||
.PHONY: build
|
||||
|
||||
format:
|
||||
go fmt ./...
|
||||
.PHONY: format
|
||||
|
||||
create:
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) < ./db/setup.sql
|
||||
.PHONY: create
|
||||
|
||||
teardown:
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/teardown.sql
|
||||
.PHONY: teardown
|
||||
|
||||
migrate:
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220310_init.sql
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220422_add_desc_to_user_and_post.sql
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220426_add_index_for_filename.sql
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220427_username_to_lower.sql
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220523_timestamp_with_tz.sql
|
||||
.PHONY: migrate
|
||||
|
||||
latest:
|
||||
docker exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./db/migrations/20220523_timestamp_with_tz.sql
|
||||
.PHONY: latest
|
||||
|
||||
psql:
|
||||
docker exec -it $(DB_CONTAINER) psql -U $(PGUSER)
|
||||
.PHONY: psql
|
||||
|
||||
dump:
|
||||
docker exec -it $(DB_CONTAINER) pg_dump -U $(PGUSER) $(PGDATABASE) > ./backup.sql
|
||||
.PHONY: dump
|
||||
|
||||
restore:
|
||||
docker cp ./backup.sql $(DB_CONTAINER):/backup.sql
|
||||
docker exec -it $(DB_CONTAINER) /bin/bash
|
||||
# psql postgres -U postgres < /backup.sql
|
||||
.PHONY: restore
|
||||
|
||||
bp-caddy:
|
||||
docker build -t neurosnap/pastes-caddy -f Dockerfile.caddy .
|
||||
docker push neurosnap/pastes-caddy
|
||||
.PHONY: bp-caddy
|
||||
|
||||
bp-ssh:
|
||||
docker build -t neurosnap/pastes-ssh --target ssh .
|
||||
docker push neurosnap/pastes-ssh
|
||||
.PHONY: bp-ssh
|
||||
|
||||
bp-web:
|
||||
docker build -t neurosnap/pastes-web --target web .
|
||||
docker push neurosnap/pastes-web
|
||||
.PHONY: bp-web
|
||||
|
||||
bp: bp-ssh bp-web bp-caddy
|
||||
.PHONY: bp
|
||||
|
||||
deploy:
|
||||
docker system prune -f
|
||||
docker-compose -f production.yml pull --ignore-pull-failures
|
||||
docker-compose -f production.yml up --no-deps -d
|
||||
.PHONY: deploy
|
@ -0,0 +1,108 @@
|
||||
# pastes.sh
|
||||
|
||||
A pastebin service for hackers.
|
||||
|
||||
## comms
|
||||
|
||||
- [website](https://pico.sh)
|
||||
- [irc #pico.sh](irc://irc.libera.chat/#pico.sh)
|
||||
- [mailing list](https://lists.sr.ht/~erock/pico.sh)
|
||||
- [ticket tracker](https://todo.sr.ht/~erock/pico.sh)
|
||||
- [email](mailto:hello@pico.sh)
|
||||
|
||||
## setup
|
||||
|
||||
- golang `v1.18`
|
||||
|
||||
You'll also need some environment variables
|
||||
|
||||
```
|
||||
export POSTGRES_PASSWORD="secret"
|
||||
export DATABASE_URL="postgresql://postgres:secret@db/pastes?sslmode=disable"
|
||||
export PASTES_SSH_PORT=2222
|
||||
export PASTES_WEB_PORT=3000
|
||||
export PASTES_DOMAIN="pastes.sh"
|
||||
export PASTES_EMAIL="hello@pastes.sh"
|
||||
```
|
||||
|
||||
I just use `direnv` which will load my `.env` file.
|
||||
|
||||
## development
|
||||
|
||||
### db
|
||||
|
||||
I use `docker-compose` to standup a postgresql server. If you already have a
|
||||
server running you can skip this step.
|
||||
|
||||
Copy example `.env`
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then run docker compose.
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Then create the database and migrate
|
||||
|
||||
```bash
|
||||
make create
|
||||
make migrate
|
||||
```
|
||||
|
||||
### build the apps
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### run the apps
|
||||
|
||||
There are two apps: an ssh and web server.
|
||||
|
||||
```bash
|
||||
./build/ssh
|
||||
```
|
||||
|
||||
Default port for ssh server is `2222`.
|
||||
|
||||
```bash
|
||||
./build/web
|
||||
```
|
||||
|
||||
Default port for web server is `3000`.
|
||||
|
||||
### subdomains
|
||||
|
||||
Since we use subdomains for blogs, you'll need to update your `/etc/hosts` file
|
||||
to accommodate.
|
||||
|
||||
```bash
|
||||
# /etc/hosts
|
||||
127.0.0.1 pastes.test
|
||||
127.0.0.1 erock.pastes.test
|
||||
```
|
||||
|
||||
Wildcards are not support in `/etc/hosts` so you'll have to add a subdomain for
|
||||
each blog in development. For this example you'll also want to change the domain
|
||||
env var to `PASTES_DOMAIN=pastes.test`.
|
||||
|
||||
## deployment
|
||||
|
||||
I use `docker-compose` for deployment. First you need `.env.prod`.
|
||||
|
||||
```bash
|
||||
cp .env.example .env.prod
|
||||
```
|
||||
|
||||
The `production.yml` file in this repo uses my docker hub images for deployment.
|
||||
|
||||
```bash
|
||||
docker-compose -f production.yml up -d
|
||||
```
|
||||
|
||||
If you want to deploy using your own domain then you'll need to edit the
|
||||
`Caddyfile` with your domain.
|
@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~erock/pastes.sh/internal"
|
||||
"git.sr.ht/~erock/wish/cms"
|
||||
"git.sr.ht/~erock/wish/cms/db/postgres"
|
||||
"git.sr.ht/~erock/wish/proxy"
|
||||
"git.sr.ht/~erock/wish/send/scp"
|
||||
"git.sr.ht/~erock/wish/send/sftp"
|
||||
"github.com/charmbracelet/wish"
|
||||
bm "github.com/charmbracelet/wish/bubbletea"
|
||||
lm "github.com/charmbracelet/wish/logging"
|
||||
"github.com/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
type SSHServer struct{}
|
||||
|
||||
func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func createRouter(handler *internal.DbHandler) proxy.Router {
|
||||
return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
|
||||
cmd := s.Command()
|
||||
mdw := []wish.Middleware{}
|
||||
|
||||
if len(cmd) == 0 {
|
||||
mdw = append(mdw,
|
||||
bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
|
||||
lm.Middleware(),
|
||||
)
|
||||
} else if cmd[0] == "scp" {
|
||||
mdw = append(mdw, scp.Middleware(handler))
|
||||
}
|
||||
|
||||
return mdw
|
||||
}
|
||||
}
|
||||
|
||||
func withProxy(handler *internal.DbHandler) ssh.Option {
|
||||
return func(server *ssh.Server) error {
|
||||
err := sftp.SSHOption(handler)(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return proxy.WithProxy(createRouter(handler))(server)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
host := internal.GetEnv("PASTES_HOST", "0.0.0.0")
|
||||
port := internal.GetEnv("PASTES_SSH_PORT", "2222")
|
||||
cfg := internal.NewConfigSite()
|
||||
logger := cfg.Logger
|
||||
dbh := postgres.NewDB(&cfg.ConfigCms)
|
||||
defer dbh.Close()
|
||||
handler := internal.NewDbHandler(dbh, cfg)
|
||||
|
||||
sshServer := &SSHServer{}
|
||||
s, err := wish.NewServer(
|
||||
wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
|
||||
wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
|
||||
wish.WithPublicKeyAuth(sshServer.authHandler),
|
||||
withProxy(handler),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan os.Signal, 1)
|
||||
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
logger.Infof("Starting SSH server on %s:%s", host, port)
|
||||
go func() {
|
||||
if err = s.ListenAndServe(); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
logger.Info("Stopping SSH server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer func() { cancel() }()
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "git.sr.ht/~erock/pastes.sh/internal"
|
||||
|
||||
func main() {
|
||||
internal.StartApiServer()
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_users (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(50),
|
||||
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT unique_name UNIQUE (name),
|
||||
CONSTRAINT app_user_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public_keys (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
user_id uuid NOT NULL,
|
||||
public_key varchar(2048) NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT user_public_keys_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT unique_key_for_user UNIQUE (user_id, public_key),
|
||||
CONSTRAINT fk_user_public_keys_owner
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES app_users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
user_id uuid NOT NULL,
|
||||
title character varying(255) NOT NULL,
|
||||
text text NOT NULL DEFAULT '',
|
||||
publish_at timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
created_at timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT posts_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT unique_title_for_user UNIQUE (user_id, title),
|
||||
CONSTRAINT fk_posts_app_users
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES app_users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
@ -0,0 +1,8 @@
|
||||
ALTER TABLE app_users ADD COLUMN bio character varying(150) NOT NULL DEFAULT '';
|
||||
ALTER TABLE posts ADD COLUMN description character varying(150) NOT NULL DEFAULT '';
|
||||
ALTER TABLE posts ADD COLUMN filename character varying(255);
|
||||
|
||||
UPDATE posts SET filename = title;
|
||||
|
||||
ALTER TABLE posts ADD CONSTRAINT unique_filename_for_user UNIQUE (user_id, filename);
|
||||
ALTER TABLE posts DROP CONSTRAINT unique_title_for_user;
|
@ -0,0 +1,2 @@
|
||||
CREATE INDEX posts_filename ON posts USING btree(filename);
|
||||
ALTER TABLE app_users DROP COLUMN bio;
|
@ -0,0 +1 @@
|
||||
UPDATE app_users SET name = LOWER(name) WHERE name != LOWER(name);
|
@ -0,0 +1,5 @@
|
||||
ALTER TABLE posts ALTER COLUMN updated_at TYPE timestamp WITH TIME ZONE USING updated_at AT TIME ZONE 'UTC';
|
||||
ALTER TABLE posts ALTER COLUMN publish_at TYPE timestamp WITH TIME ZONE USING publish_at AT TIME ZONE 'UTC';
|
||||
ALTER TABLE posts ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';
|
||||
ALTER TABLE app_users ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';
|
||||
ALTER TABLE public_keys ALTER COLUMN created_at TYPE timestamp WITH TIME ZONE USING created_at AT TIME ZONE 'UTC';
|
@ -0,0 +1 @@
|
||||
CREATE DATABASE "pastes" OWNER "postgres";
|
@ -0,0 +1,3 @@
|
||||
DROP TABLE posts CASCADE;
|
||||
DROP TABLE app_users CASCADE;
|
||||
DROP TABLE public_keys CASCADE;
|
@ -0,0 +1,9 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env_file:
|
||||
- .env
|
@ -0,0 +1,46 @@
|
||||
module git.sr.ht/~erock/pastes.sh
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/charmbracelet/wish v0.5.0
|
||||
github.com/gliderlabs/ssh v0.3.4
|
||||
github.com/yuin/goldmark v1.4.12
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
go.uber.org/zap v1.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/caarlos0/sshmarshal v0.1.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.12.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v0.22.0 // indirect
|
||||
github.com/charmbracelet/keygen v0.3.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.5.0 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/pkg/sftp v1.13.5 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
@ -0,0 +1,160 @@
|
||||
git.sr.ht/~erock/wish v0.0.0-20220707194507-66e938674d95 h1:q3G01ELBZt3EZEOiYWfwdiqZWtp6PKYueRHT2eeOlSU=
|
||||
git.sr.ht/~erock/wish v0.0.0-20220707194507-66e938674d95/go.mod h1:QZKk7m9jc9iXah90daPGhQkSfNfxSVvpb6nfVeI+MM0=
|
||||
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac h1:d+q9VPi+kaZpYpZOoXPSEx2KtZ3Gcmkz+x/lpb/V6bU=
|
||||
git.sr.ht/~erock/wish v0.0.0-20220713141740-64595ee518ac/go.mod h1:QZKk7m9jc9iXah90daPGhQkSfNfxSVvpb6nfVeI+MM0=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
|
||||
github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
|
||||
github.com/charmbracelet/bubbles v0.12.0 h1:fxb9U9yI60Hek3tcPmMTFya5NhvPrqpkpyMaNngFh7A=
|
||||
github.com/charmbracelet/bubbles v0.12.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
|
||||
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
|
||||
github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc=
|
||||
github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
|
||||
github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/charmbracelet/wish v0.5.0 h1:FkkdNBFqrLABR1ciNrAL2KCxoyWfKhXnIGZw6GfAtPg=
|
||||
github.com/charmbracelet/wish v0.5.0/go.mod h1:5GAn5SrDSZ7cgKjnC+3kDmiIo7I6k4/AYiRzC4+tpCk=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
|
||||
github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
|
||||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
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/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
|
||||
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg=
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -0,0 +1,18 @@
|
||||
{{define "base"}}
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="theme-dark">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{template "title" .}}</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
|
||||
<meta name="keywords" content="pastebin, paste, copy" />
|
||||
{{template "meta" .}}
|
||||
|
||||
<link rel="stylesheet" href="/main.css" />
|
||||
</head>
|
||||
<body>{{template "body" .}}</body>
|
||||
</html>
|
||||
{{end}}
|
@ -0,0 +1,44 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.PageTitle}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="{{.Site.Domain}}">
|
||||
<meta property="og:url" content="{{.URL}}">
|
||||
<meta property="og:title" content="{{.Header.Title}}">
|
||||
{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
|
||||
<meta property="og:image:width" content="300" />
|
||||
<meta property="og:image:height" content="300" />
|
||||
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
|
||||
<meta property="twitter:card" content="summary">
|
||||
<meta property="twitter:url" content="{{.URL}}">
|
||||
<meta property="twitter:title" content="{{.Header.Title}}">
|
||||
{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
|
||||
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header class="text-center">
|
||||
<h1 class="text-2xl font-bold">{{.Header.Title}}</h1>
|
||||
<hr />
|
||||
</header>
|
||||
<main>
|
||||
<section class="posts">
|
||||
{{range .Posts}}
|
||||
<article>
|
||||
<div class="flex items-center">
|
||||
<time datetime="{{.PublishAtISO}}" class="font-italic text-sm post-date">{{.PublishAt}}</time>
|
||||
<h2 class="font-bold flex-1"><a href="{{.URL}}">{{.Title}}</a></h2>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</section>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,6 @@
|
||||
{{define "footer"}}
|
||||
<footer>
|
||||
<hr />
|
||||
published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
|
||||
</footer>
|
||||
{{end}}
|
@ -0,0 +1,12 @@
|
||||
{{define "marketing-footer"}}
|
||||
<footer>
|
||||
<hr />
|
||||
<p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
|
||||
<div>
|
||||
<a href="/">home</a> |
|
||||
<a href="/ops">ops</a> |
|
||||
<a href="/help">help</a> |
|
||||
<a href="https://git.sr.ht/~erock/pastes.sh">source</a>
|
||||
</div>
|
||||
</footer>
|
||||
{{end}}
|
@ -0,0 +1,99 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Site.Domain}} -- a pastebin for hackers{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="a pastebin for hackers" />
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="{{.Site.Domain}}">
|
||||
<meta property="og:url" content="https://{{.Site.Domain}}">
|
||||
<meta property="og:title" content="{{.Site.Domain}}">
|
||||
<meta property="og:description" content="a pastebin for hackers">
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta property="twitter:url" content="https://{{.Site.Domain}}">
|
||||
<meta property="twitter:title" content="{{.Site.Domain}}">
|
||||
<meta property="twitter:description" content="a pastebin platform for hackers">
|
||||
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
|
||||
|
||||
<meta property="og:image:width" content="300" />
|
||||
<meta property="og:image:height" content="300" />
|
||||
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header class="text-center">
|
||||
<h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
|
||||
<p class="text-lg">a pastebin platform for hackers</p>
|
||||
<p class="text-lg"><a href="/read">discover</a> some interesting pastebins</p>
|
||||
<hr />
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2 class="text-lg font-bold">Create your account with Public-Key Cryptography</h2>
|
||||
<p>We don't want your email address.</p>
|
||||
<p>To get started, simply ssh into our content management system:</p>
|
||||
<pre>ssh new@{{.Site.Domain}}</pre>
|
||||
<div class="text-sm font-italic note">
|
||||
note: <code>new</code> is a special username that will always send you to account
|
||||
creation, even with multiple accounts associated with your key-pair.
|
||||
</div>
|
||||
<div class="text-sm font-italic note">
|
||||
note: getting permission denied? <a href="/help#permission-denied">read this</a>
|
||||
</div>
|
||||
<p>
|
||||
After that, just set a username and you're ready to start writing! When you SSH
|
||||
again, use your username that you set in the CMS.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-bold">Publish your pastes with one command</h2>
|
||||
<p>
|
||||
When your post is ready to be published, copy the file to our server with a familiar
|
||||
command:
|
||||
</p>
|
||||
<pre>scp ~/pastes/* {{.Site.Domain}}:/</pre>
|
||||
<p>We'll either create or update the posts for you.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-bold">Terminal workflow without installation</h2>
|
||||
<p>
|
||||
Since we are leveraging tools you already have on your computer
|
||||
(<code>ssh</code> and <code>scp</code>), there is nothing to install.
|
||||
</p>
|
||||
<p>
|
||||
This provides the convenience of a web app, but from inside your terminal!
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-bold">Features</h2>
|
||||
<ul>
|
||||
<li>Bring your own editor</li>
|
||||
<li>You control the source files</li>
|
||||
<li>Terminal workflow with no installation</li>
|
||||
<li>Public-key based authentication</li>
|
||||
<li>No ads, zero tracking</li>
|
||||
<li>No platform lock-in</li>
|
||||
<li>No javascript</li>
|
||||
<li>Minimalist design</li>
|
||||
<li>100% open source</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-bold">Roadmap</h2>
|
||||
<ol>
|
||||
<li>idk</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{template "marketing-footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,124 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}operations -- {{.Site.Domain}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="{{.Site.Domain}} operations" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header>
|
||||
<h1 class="text-2xl">Operations</h1>
|
||||
<ul>
|
||||
<li><a href="/privacy">privacy</a></li>
|
||||
<li><a href="/transparency">transparency</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2 class="text-xl">Purpose</h2>
|
||||
<p>
|
||||
{{.Site.Domain}} exists to allow people to create and share their thoughts
|
||||
without the need to set up their own server or be part of a platform
|
||||
that shows ads or tracks its users.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Ethics</h2>
|
||||
<p>We are committed to:</p>
|
||||
<ul>
|
||||
<li>No tracking of user or visitor behaviour.</li>
|
||||
<li>Never sell any user or visitor data.</li>
|
||||
<li>No ads โ ever.</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Code of Content Publication</h2>
|
||||
<p>
|
||||
Content in {{.Site.Domain}} pastes is unfiltered and unmonitored. Users are free to publish any
|
||||
combination of words and pixels except for: content of animosity or disparagement of an
|
||||
individual or a group on account of a group characteristic such as race, color, national
|
||||
origin, sex, disability, religion, or sexual orientation, which will be taken down
|
||||
immediately.
|
||||
</p>
|
||||
<p>
|
||||
If one notices something along those lines in a paste post please let us know at
|
||||
<a href="mailto:{{.Site.Email}}">{{.Site.Email}}</a>.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Liability</h2>
|
||||
<p>
|
||||
The user expressly understands and agrees that Eric Bower and Antonio Mika, the operator of this website
|
||||
shall not be liable, in law or in equity, to them or to any third party for any direct,
|
||||
indirect, incidental, lost profits, special, consequential, punitive or exemplary damages.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Account Terms</h2>
|
||||
<p>
|
||||
<ul>
|
||||
<li>
|
||||
The user is responsible for all content posted and all actions performed with
|
||||
their account.
|
||||
</li>
|
||||
<li>
|
||||
We reserve the right to disable or delete a user's account for any reason at
|
||||
any time. We have this clause because, statistically speaking, there will be
|
||||
people trying to do something nefarious.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Service Availability</h2>
|
||||
<p>
|
||||
We provide the {{.Site.Domain}} service on an "as is" and "as available" basis. We do not offer
|
||||
service-level agreements but do take uptime seriously.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Contact and Support</h2>
|
||||
<p>
|
||||
Email us at <a href="mailto:{{.Site.Email}}">{{.Site.Email}}</a>
|
||||
with any questions.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-xl">Acknowledgments</h2>
|
||||
<p>
|
||||
{{.Site.Domain}} was inspired by <a href="https://mataroa.blog">Mataroa Blog</a>
|
||||
and <a href="https://bearblog.dev/">Bear Blog</a>.
|
||||
</p>
|
||||
<p>
|
||||
{{.Site.Domain}} is built with many open source technologies.
|
||||
</p>
|
||||
<p>
|
||||
In particular we would like to thank:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<span>The </span>
|
||||
<a href="https://charm.sh">charm.sh</a>
|
||||
<span> community</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>The </span>
|
||||
<a href="https://go.dev">golang</a>
|
||||
<span> community</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>The </span>
|
||||
<a href="https://www.postgresql.org/">postgresql</a>
|
||||
<span> community</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>The </span>
|
||||
<a href="https://github.com/caddyserver/caddy">caddy</a>
|
||||
<span> community</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
{{template "marketing-footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,36 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.PageTitle}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="{{.Site.Domain}}">
|
||||
<meta property="og:url" content="{{.URL}}">
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:image:width" content="300" />
|
||||
<meta property="og:image:height" content="300" />
|
||||
<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
|
||||
<meta property="twitter:card" content="summary">
|
||||
<meta property="twitter:url" content="{{.URL}}">
|
||||
<meta property="twitter:title" content="{{.Title}}">
|
||||
<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
|
||||
<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header>
|
||||
<h1 class="text-2xl font-bold">{{.Title}}</h1>
|
||||
<p class="font-bold m-0">
|
||||
<time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
|
||||
<span> on </span>
|
||||
<a href="{{.BlogURL}}">{{.BlogName}}</a></p>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
{{.Contents}}
|
||||
</article>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,52 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}privacy -- {{.Site.Domain}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="{{.Site.Domain}} privacy policy" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header>
|
||||
<h1 class="text-2xl">Privacy</h1>
|
||||
<p>Details on our privacy and security approach.</p>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2 class="text-xl">Account Data</h2>
|
||||
<p>
|
||||
In order to have a functional account at {{.Site.Domain}}, we need to store
|
||||
your public key. That is the only piece of information we record for a user.
|
||||
</p>
|
||||
<p>
|
||||
Because we use public-key cryptography, our security posture is a battle-tested
|
||||
and proven technique for authentication.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl">Third parties</h2>
|
||||
<p>
|
||||
We have a strong commitment to never share any user data with any third-parties.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl">Service Providers</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span>We host our server on </span>
|
||||
<a href="https://digitalocean.com">digital ocean</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl">Cookies</h2>
|
||||
<p>
|
||||
We do not use any cookies, not even account authentication.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
{{template "marketing-footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,35 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}discover -- {{.Site.Domain}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="discover interesting posts" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header class="text-center">
|
||||
<h1 class="text-2xl font-bold">read</h1>
|
||||
<p class="text-lg">recent pastes</p>
|
||||
<hr />
|
||||
</header>
|
||||
<main>
|
||||
<div class="my">
|
||||
{{if .PrevPage}}<a href="{{.PrevPage}}">prev</a>{{else}}<span class="text-grey">prev</span>{{end}}
|
||||
{{if .NextPage}}<a href="{{.NextPage}}">next</a>{{else}}<span class="text-grey">next</span>{{end}}
|
||||
</div>
|
||||
{{range .Posts}}
|
||||
<article>
|
||||
<div class="flex items-center">
|
||||
<time datetime="{{.PublishAtISO}}" class="font-italic text-sm post-date">{{.PublishAt}}</time>
|
||||
<div class="flex-1">
|
||||
<h2 class="inline"><a href="{{.URL}}">{{.Title}}</a></h2>
|
||||
<address class="text-sm inline">
|
||||
<a href="{{.BlogURL}}" class="link-grey">({{.Username}})</a>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</main>
|
||||
{{template "marketing-footer" .}}
|
||||
{{end}}
|
@ -0,0 +1 @@
|
||||
{{.Contents}}
|
@ -0,0 +1,57 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}transparency -- {{.Site.Domain}}{{end}}
|
||||
|
||||
{{define "meta"}}
|
||||
<meta name="description" content="full transparency of analytics and cost at {{.Site.Domain}}" />
|
||||
{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<header>
|
||||
<h1 class="text-2xl">Transparency</h1>
|
||||
<hr />
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2 class="text-xl">Analytics</h2>
|
||||
<p>
|
||||
Here are some interesting stats on usage.
|
||||
</p>
|
||||
|
||||
<article>
|
||||
<h2 class="text-lg">Total users</h2>
|
||||
<div>{{.Analytics.TotalUsers}}</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-lg">New users in the last month</h2>
|
||||
<div>{{.Analytics.UsersLastMonth}}</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-lg">Total pastes</h2>
|
||||
<div>{{.Analytics.TotalPosts}}</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-lg">New pastes in the last month</h2>
|
||||
<div>{{.Analytics.PostsLastMonth}}</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-lg">Users with at least one paste</h2>
|
||||
<div>{{.Analytics.UsersWithPost}}</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl">Service maintenance costs</h2>
|
||||
<ul>
|
||||
<li>Server $5.00/mo</li>
|
||||
<li>Domain name $3.25/mo</li>
|
||||
<li>Programmer $0.00/mo</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
{{template "marketing-footer" .}}
|
||||
{{end}}
|
@ -0,0 +1,499 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~erock/wish/cms/db"
|
||||
"git.sr.ht/~erock/wish/cms/db/postgres"
|
||||
)
|
||||
|
||||
type PageData struct {
|
||||
Site SitePageData
|
||||
}
|
||||
|
||||
type PostItemData struct {
|
||||
URL template.URL
|
||||
BlogURL template.URL
|
||||
Username string
|
||||
Title string
|
||||
Description string
|
||||
PublishAtISO string
|
||||
PublishAt string
|
||||
UpdatedAtISO string
|
||||
UpdatedTimeAgo string
|
||||
Padding string
|
||||
}
|
||||
|
||||
type BlogPageData struct {
|
||||
Site SitePageData
|
||||
PageTitle string
|
||||
URL template.URL
|
||||
RSSURL template.URL
|
||||
Username string
|
||||
Header *HeaderTxt
|
||||
Posts []PostItemData
|
||||
}
|
||||
|
||||
type ReadPageData struct {
|
||||
Site SitePageData
|
||||
NextPage string
|
||||
PrevPage string
|
||||
Posts []PostItemData
|
||||
}
|
||||
|
||||
type PostPageData struct {
|
||||
Site SitePageData
|
||||
PageTitle string
|
||||
URL template.URL
|
||||
BlogURL template.URL
|
||||
Title string
|
||||
Description string
|
||||
Username string
|
||||
BlogName string
|
||||
Contents template.HTML
|
||||
PublishAtISO string
|
||||
PublishAt string
|
||||
}
|
||||
|
||||
type TransparencyPageData struct {
|
||||
Site SitePageData
|
||||
Analytics *db.Analytics
|
||||
}
|
||||
|
||||
func renderTemplate(templates []string) (*template.Template, error) {
|
||||
files := make([]string, len(templates))
|
||||
copy(files, templates)
|
||||
files = append(
|
||||
files,
|
||||
"./html/footer.partial.tmpl",
|
||||
"./html/marketing-footer.partial.tmpl",
|
||||
"./html/base.layout.tmpl",
|
||||
)
|
||||
|
||||
ts, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func createPageHandler(fname string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := GetLogger(r)
|
||||
cfg := GetCfg(r)
|
||||
ts, err := renderTemplate([]string{fname})
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := PageData{
|
||||
Site: *cfg.GetSiteData(),
|
||||
}
|
||||
err = ts.Execute(w, data)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
URL string
|
||||
Text string
|
||||
}
|
||||
|
||||
type HeaderTxt struct {
|
||||
Title string
|
||||
Bio string
|
||||
Nav []Link
|
||||
HasLinks bool
|
||||
}
|
||||
|
||||
type ReadmeTxt struct {
|
||||
HasText bool
|
||||
Contents template.HTML
|
||||
}
|
||||
|
||||
func GetUsernameFromRequest(r *http.Request) string {
|
||||
subdomain := GetSubdomain(r)
|
||||
cfg := GetCfg(r)
|
||||
|
||||
if !cfg.IsSubdomains() || subdomain == "" {
|
||||
return GetField(r, 0)
|
||||
}
|
||||
return subdomain
|
||||
}
|
||||
|
||||
func blogHandler(w http.ResponseWriter, r *http.Request) {
|
||||
username := GetUsernameFromRequest(r)
|
||||
dbpool := GetDB(r)
|
||||
logger := GetLogger(r)
|
||||
cfg := GetCfg(r)
|
||||
|
||||
user, err := dbpool.FindUserForName(username)
|
||||
if err != nil {
|
||||
logger.Infof("blog not found: %s", username)
|
||||
http.Error(w, "blog not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
posts, err := dbpool.FindPostsForUser(user.ID)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ts, err := renderTemplate([]string{
|
||||
"./html/blog.page.tmpl",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
headerTxt := &HeaderTxt{
|
||||
Title: GetBlogName(username),
|
||||
Bio: "",
|
||||
}
|
||||
|
||||
postCollection := make([]PostItemData, 0, len(posts))
|
||||
for _, post := range posts {
|
||||
p := PostItemData{
|
||||
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
|
||||
BlogURL: template.URL(cfg.BlogURL(post.Username)),
|
||||
Title: FilenameToTitle(post.Filename, post.Title),
|
||||
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
|
||||
PublishAtISO: post.PublishAt.Format(time.RFC3339),
|
||||
UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
|
||||
UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
postCollection = append(postCollection, p)
|
||||
}
|
||||
|
||||
data := BlogPageData{
|
||||
Site: *cfg.GetSiteData(),
|
||||
PageTitle: headerTxt.Title,
|
||||
URL: template.URL(cfg.BlogURL(username)),
|
||||
RSSURL: template.URL(cfg.RssBlogURL(username)),
|
||||
Header: headerTxt,
|
||||
Username: username,
|
||||
Posts: postCollection,
|
||||
}
|
||||
|
||||
err = ts.Execute(w, data)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPostTitle(post *db.Post) string {
|
||||
if post.Description == "" {
|
||||
return post.Title
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s", post.Title, post.Description)
|
||||
}
|
||||
|
||||
func GetBlogName(username string) string {
|
||||
return fmt.Sprintf("%s's blog", username)
|
||||
}
|
||||
|
||||
func postHandler(w http.ResponseWriter, r *http.Request) {
|
||||
username := GetUsernameFromRequest(r)
|
||||
subdomain := GetSubdomain(r)
|
||||
cfg := GetCfg(r)
|
||||
|
||||
var filename string
|
||||
if !cfg.IsSubdomains() || subdomain == "" {
|
||||
filename, _ = url.PathUnescape(GetField(r, 1))
|
||||
} else {
|
||||
filename, _ = url.PathUnescape(GetField(r, 0))
|
||||
}
|
||||
|
||||
dbpool := GetDB(r)
|
||||
logger := GetLogger(r)
|
||||
|
||||
user, err := dbpool.FindUserForName(username)
|
||||
if err != nil {
|
||||
logger.Infof("blog not found: %s", username)
|
||||
http.Error(w, "blog not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
blogName := GetBlogName(username)
|
||||
|
||||
post, err := dbpool.FindPostWithFilename(filename, user.ID)
|
||||
if err != nil {
|
||||
logger.Infof("post not found %s/%s", username, filename)
|
||||
http.Error(w, "post not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
parsedText, err := ParseText(post.Filename, post.Text)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
data := PostPageData{
|
||||
Site: *cfg.GetSiteData(),
|
||||
PageTitle: GetPostTitle(post),
|
||||
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
|
||||
BlogURL: template.URL(cfg.BlogURL(username)),
|
||||
Description: post.Description,
|
||||
Title: FilenameToTitle(post.Filename, post.Title),
|
||||
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
|
||||
PublishAtISO: post.PublishAt.Format(time.RFC3339),
|
||||
Username: username,
|
||||
BlogName: blogName,
|
||||
Contents: template.HTML(parsedText),
|
||||
}
|
||||
|
||||
ts, err := renderTemplate([]string{
|
||||
"./html/post.page.tmpl",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = ts.Execute(w, data)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
|
||||
username := GetUsernameFromRequest(r)
|
||||
subdomain := GetSubdomain(r)
|
||||
cfg := GetCfg(r)
|
||||
|
||||
var filename string |