A URL shortener service that uses ATProto to allow self hosting and ensuring the user owns their data

Getting ready to deploy

+4
.dockerignore
···
···
+
at-shorter
+
database.db
+
.env
+
example.env
+19
Dockerfile
···
···
+
FROM golang:latest AS builder
+
+
WORKDIR /app
+
+
COPY . .
+
RUN go mod download
+
+
COPY . .
+
+
RUN CGO_ENABLED=0 go build -o at-shorter .
+
+
FROM alpine:latest
+
+
RUN apk --no-cache add ca-certificates
+
+
WORKDIR /root/
+
COPY --from=builder /app/at-shorter .
+
+
CMD ["./at-shorter"]
+14 -4
cmd/atshorter/main.go
···
defaultServerAddr = "wss://jetstream.atproto.tools/subscribe"
httpClientTimeoutDuration = time.Second * 5
transportIdleConnTimeoutDuration = time.Second * 90
)
func main() {
-
err := godotenv.Load(".env")
if err != nil {
if !os.IsNotExist(err) {
log.Fatal("Error loading .env file")
···
defer db.Close()
var config oauth.ClientConfig
-
bind := ":8080"
scopes := []string{
"atproto",
"repo:com.atshorter.shorturl?action=create",
···
}
if host == "" {
config = oauth.NewLocalhostConfig(
-
fmt.Sprintf("http://127.0.0.1%s/oauth-callback", bind),
scopes,
)
slog.Info("configuring localhost OAuth client", "CallbackURL", config.CallbackURL)
···
},
}
-
server, err := atshorter.NewServer(host, 8080, db, oauthClient, httpClient)
if err != nil {
slog.Error("create new server", "error", err)
return
···
defaultServerAddr = "wss://jetstream.atproto.tools/subscribe"
httpClientTimeoutDuration = time.Second * 5
transportIdleConnTimeoutDuration = time.Second * 90
+
defaultPort = "8080"
)
func main() {
+
envLocation := os.Getenv("ENV_LOCATION")
+
if envLocation == "" {
+
envLocation = ".env"
+
}
+
+
err := godotenv.Load(envLocation)
if err != nil {
if !os.IsNotExist(err) {
log.Fatal("Error loading .env file")
···
defer db.Close()
var config oauth.ClientConfig
+
port := os.Getenv("PORT")
+
if port == "" {
+
port = defaultPort
+
}
+
scopes := []string{
"atproto",
"repo:com.atshorter.shorturl?action=create",
···
}
if host == "" {
config = oauth.NewLocalhostConfig(
+
fmt.Sprintf("http://127.0.0.1:%s/oauth-callback", port),
scopes,
)
slog.Info("configuring localhost OAuth client", "CallbackURL", config.CallbackURL)
···
},
}
+
server, err := atshorter.NewServer(host, port, db, oauthClient, httpClient)
if err != nil {
slog.Error("create new server", "error", err)
return
+11
docker-compose.yaml
···
···
+
services:
+
knot:
+
container_name: at-shorter
+
image: willdot/at-shorter:latest
+
environment:
+
ENV_LOCATION: "/data/at-shorter.env"
+
volumes:
+
- ./data:/app/data
+
ports:
+
- "3005:3005"
+
restart: always
+1
example.env
···
HOST="the host of the service such as https://my-url-shortner.com"
DATABASE_PATH="./"
JS_SERVER_ADDR="set to a different Jetstream instance"
···
HOST="the host of the service such as https://my-url-shortner.com"
DATABASE_PATH="./"
JS_SERVER_ADDR="set to a different Jetstream instance"
+
PORT="3002"
+2 -2
server.go
···
mu sync.Mutex
}
-
func NewServer(host string, port int, store Store, oauthClient *oauth.ClientApp, httpClient *http.Client) (*Server, error) {
sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
homeTemplate, err := template.ParseFiles("./html/home.html")
···
mux.HandleFunc("GET /oauth-client-metadata.json", srv.serveClientMetadata)
mux.HandleFunc("GET /oauth-callback", srv.handleOauthCallback)
-
addr := fmt.Sprintf("0.0.0.0:%d", port)
srv.httpserver = &http.Server{
Addr: addr,
Handler: mux,
···
mu sync.Mutex
}
+
func NewServer(host string, port string, store Store, oauthClient *oauth.ClientApp, httpClient *http.Client) (*Server, error) {
sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
homeTemplate, err := template.ParseFiles("./html/home.html")
···
mux.HandleFunc("GET /oauth-client-metadata.json", srv.serveClientMetadata)
mux.HandleFunc("GET /oauth-callback", srv.handleOauthCallback)
+
addr := fmt.Sprintf("0.0.0.0:%s", port)
srv.httpserver = &http.Server{
Addr: addr,
Handler: mux,