├── .eslintignore
├── web
├── app
│ ├── global.d.ts
│ ├── index.ts
│ ├── svelte
│ │ ├── App.svelte
│ │ ├── Login.svelte
│ │ ├── Signup.svelte
│ │ ├── Upload.svelte
│ │ ├── Clip.svelte
│ │ └── Home.svelte
│ ├── lib
│ │ ├── user.ts
│ │ └── clip.ts
│ ├── auth.ts
│ └── routes.ts
└── static
│ ├── fonts
│ ├── lg
│ │ ├── lg.eot
│ │ ├── lg.ttf
│ │ ├── lg.woff
│ │ └── lg.svg
│ ├── custom
│ │ ├── Custom.woff
│ │ ├── Custom.woff2
│ │ └── selection.json
│ ├── unicons
│ │ ├── Unicons.woff
│ │ └── Unicons.woff2
│ └── thicccboi
│ │ ├── THICCCBOI-Bold.woff
│ │ ├── THICCCBOI-Bold.woff2
│ │ ├── THICCCBOI-Medium.woff
│ │ ├── THICCCBOI-Medium.woff2
│ │ ├── THICCCBOI-Regular.woff
│ │ ├── THICCCBOI-Regular.woff2
│ │ └── thicccboi.css
│ └── index.html
├── .dockerignore
├── .gitignore
├── internal
├── data
│ ├── app.go
│ ├── user.go
│ └── session.go
├── services
│ ├── service.go
│ ├── user.go
│ └── auth.go
├── routes
│ ├── auth.go
│ └── api.go
├── db
│ └── db.go
├── api
│ ├── auth.go
│ ├── user.go
│ └── clip.go
├── controllers
│ ├── login.go
│ └── signup.go
├── app
│ └── app.go
└── config
│ └── config.go
├── main.go
├── nodemon.json
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── jsLinters
│ └── eslint.xml
├── .gitignore
├── modules.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── clips.iml
├── docker
├── clips
│ └── Dockerfile
└── docker-compose.yml
├── tsconfig.json
├── .env.docker.example
├── .env.dev.example
├── pkg
└── models
│ ├── models.go
│ ├── user.go
│ └── clip.go
├── .env.production.example
├── .eslintrc
├── cmd
└── clips-helper
│ ├── main.go
│ └── commands
│ └── new-user.go
├── .env.demo.example
├── go.mod
├── package.json
├── rollup.config.js
├── README.md
└── go.sum
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/build/
--------------------------------------------------------------------------------
/web/app/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.git
2 | **/build
3 | **/node_modules
4 | **/web/static/build
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | build/
3 | node_modules/
4 | web/**/*.gz
5 | web/static/build/
--------------------------------------------------------------------------------
/web/static/fonts/lg/lg.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/lg/lg.eot
--------------------------------------------------------------------------------
/web/static/fonts/lg/lg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/lg/lg.ttf
--------------------------------------------------------------------------------
/web/static/fonts/lg/lg.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/lg/lg.woff
--------------------------------------------------------------------------------
/internal/data/app.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import "github.com/gofiber/fiber/v2"
4 |
5 | var Application *fiber.App
6 |
--------------------------------------------------------------------------------
/web/static/fonts/custom/Custom.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/custom/Custom.woff
--------------------------------------------------------------------------------
/web/static/fonts/custom/Custom.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/custom/Custom.woff2
--------------------------------------------------------------------------------
/web/static/fonts/unicons/Unicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/unicons/Unicons.woff
--------------------------------------------------------------------------------
/web/static/fonts/unicons/Unicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/unicons/Unicons.woff2
--------------------------------------------------------------------------------
/web/app/index.ts:
--------------------------------------------------------------------------------
1 | import App from './svelte/App.svelte'
2 |
3 | const app = new App({target:document.body})
4 |
5 | export default app
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "clips/internal/app"
5 | )
6 |
7 | func main() {
8 | app.Setup()
9 | app.Start()
10 | }
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Bold.woff
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Bold.woff2
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Medium.woff
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Medium.woff2
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Regular.woff
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/THICCCBOI-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhsoj/clips/HEAD/web/static/fonts/thicccboi/THICCCBOI-Regular.woff2
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore":[
3 | "build/**/*",
4 | "public/**/*",
5 | "frontend/**/*",
6 | "node_modules/**/*"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/docker/clips/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | WORKDIR /opt/clips
4 |
5 | RUN apk add --update --no-cache nodejs npm go
6 |
7 | COPY . .
8 |
9 | RUN npm install
10 | RUN npm run build
11 |
12 | EXPOSE 3000
13 |
14 | CMD ["./build/clips"]
--------------------------------------------------------------------------------
/internal/data/user.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | // Login is a struct representing the login/signup data from the forms Login.svelte & Signup.svelte.
4 | type Login struct {
5 | Username string `json:"username"`
6 | Password string `json:"password"`
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 |
4 | "compilerOptions": {
5 | "isolatedModules": false
6 | },
7 |
8 | "include": ["web/app/**/*"],
9 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
10 | }
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/internal/data/session.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "clips/pkg/models"
5 | "github.com/gofiber/fiber/v2/middleware/session"
6 | )
7 |
8 | var Store *session.Store
9 |
10 | // Setup registers the needed types to the fiber middleware session.
11 | func Setup() {
12 | Store.RegisterType(models.User{})
13 | }
--------------------------------------------------------------------------------
/web/app/svelte/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/internal/services/service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "clips/pkg/models"
5 | )
6 |
7 | // Response is a structure used in all services representing the success & information returned from these services.
8 | type Response struct {
9 | Status int
10 | Success bool
11 | Message string
12 | User models.User
13 | }
14 |
--------------------------------------------------------------------------------
/.idea/clips.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.env.docker.example:
--------------------------------------------------------------------------------
1 | # Specifies what kind of build rollup creates (doesn't have to be production for docker)
2 | STAGING=production
3 |
4 | # Whether or not to use HTTPS, if HTTPS is enabled & TLS_CERT/TLS_KEY you will get an error
5 | HTTPS=false
6 |
7 | # Postgres connection url (if using docker, make sure the hostname is 'db' like below)
8 | POSTGRES_URL=postgres://postgres:postgres@db/clips?sslmode=disable
9 |
--------------------------------------------------------------------------------
/internal/routes/auth.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "clips/internal/controllers"
5 | "clips/internal/data"
6 | "github.com/gofiber/fiber/v2"
7 | )
8 |
9 | var AuthRouter fiber.Router
10 |
11 | // SetupAuthRoutes sets up all routing under /auth.
12 | func SetupAuthRoutes() {
13 | AuthRouter = data.Application.Group("/auth")
14 |
15 | AuthRouter.Post("/login",controllers.Login)
16 | AuthRouter.Post("/signup",controllers.Signup)
17 | }
--------------------------------------------------------------------------------
/internal/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "clips/internal/config"
5 | "github.com/go-pg/pg/v10"
6 | "log"
7 | )
8 |
9 | var Database *pg.DB
10 |
11 | // Setup tries to connect using the configured PostgreSQL connection URL.
12 | func Setup() {
13 | options,err := pg.ParseURL(config.PostgresURL)
14 |
15 | if err != nil {
16 | log.Printf("Failed to parse PG Connection URL: %s",err)
17 | }
18 |
19 | Database = pg.Connect(options)
20 | }
--------------------------------------------------------------------------------
/web/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Project Clips
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.env.dev.example:
--------------------------------------------------------------------------------
1 | # Decides what kind of build rollup will create
2 | STAGING=development
3 |
4 | # Whether or not to use HTTPS, if HTTPS is enabled & TLS_CERT/TLS_KEY you will get an error
5 | HTTPS=false
6 |
7 | # What address the server will bind on
8 | LISTEN_ADDRESS=localhost:3000
9 |
10 | # Postgres connection url
11 | POSTGRES_URL=postgres://postgres:postgres@localhost/clips?sslmode=disable
12 |
13 | # Where clips will be saved on disk
14 | CLIP_SAVE_PATH=/example/storage/clips
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | db:
4 | image: postgres
5 | restart: always
6 | environment:
7 | - POSTGRES_DB=clips
8 | - POSTGRES_USER=postgres
9 | - POSTGRES_PASSWORD=postgres
10 | expose:
11 | - 5432
12 | volumes:
13 | - clips-db:/var/lib/postgresql/data
14 | clips:
15 | build:
16 | context: ../
17 | dockerfile: docker/clips/Dockerfile
18 | depends_on:
19 | - db
20 | ports:
21 | - '3000:3000'
22 | volumes:
23 | clips-db:
--------------------------------------------------------------------------------
/pkg/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/go-pg/pg/v10"
5 | "github.com/go-pg/pg/v10/orm"
6 | )
7 |
8 | // Setup is used to set up & created the needed tables in the Postgres database with the specified models.
9 | func Setup(db *pg.DB) {
10 |
11 | models := []interface{}{
12 | (*User)(nil),
13 | (*Clip)(nil),
14 | }
15 |
16 | for _,model := range models {
17 | if err := db.Model(model).CreateTable(&orm.CreateTableOptions{
18 | IfNotExists: true,
19 | }); err != nil {
20 | panic(err)
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/pkg/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | // User represents the logical information of a user in the Postgres database.
6 | type User struct {
7 | UserID uint64 `pg:",pk" json:"user_id"`
8 | Username string `pg:"type:varchar(16),unique,notnull" json:"username"`
9 | Password string `pg:"type:varchar(60),notnull" json:"-"`
10 | APIKey string `pg:"type:varchar(60),default:gen_random_uuid(),notnull" json:"api_key"`
11 | Clips []*Clip `pg:"rel:has-many" json:"clips"`
12 | CreatedAt time.Time `pg:"default:CURRENT_TIMESTAMP" json:"created_at"`
13 | }
14 |
--------------------------------------------------------------------------------
/.env.production.example:
--------------------------------------------------------------------------------
1 | # Decides what kind of build rollup will create
2 | STAGING=production
3 |
4 | # Whether or not to use HTTPS, if HTTPS is enabled & TLS_CERT/TLS_KEY you will get an error
5 | HTTPS=true
6 |
7 | # The path to the TLS cert & key files (doesn't have to be in .pem format)
8 | TLS_CERT=/path/to/cert.pem
9 | TLS_KEY=/path/to/key.pem
10 |
11 | # What address the server will bind on
12 | LISTEN_ADDRESS=localhost:443
13 |
14 | # Postgres connection url
15 | POSTGRES_URL=postgres://postgres:postgres@localhost/clips
16 |
17 | # Where clips will be saved on disk
18 | CLIP_SAVE_PATH=/example/storage/clips
19 |
20 | # Max request body size in megabytes
21 | BODY_LIMIT_MB=250
22 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "browser": true
7 | },
8 | "root": true,
9 | "parser": "@typescript-eslint/parser",
10 | "plugins": [
11 | "@typescript-eslint",
12 | "svelte3"
13 | ],
14 | "overrides":[
15 | {
16 | "files": "*.svelte",
17 | "processor": "svelte3/svelte3"
18 | }
19 | ],
20 | "extends": [
21 | "eslint:recommended",
22 | "plugin:@typescript-eslint/eslint-recommended",
23 | "plugin:@typescript-eslint/recommended"
24 | ],
25 | "rules": {
26 | "quotes":["error","single"],
27 | "semi":["error","never"]
28 | }
29 | }
--------------------------------------------------------------------------------
/pkg/models/clip.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | // Clip represents the logical information of a user uploaded clip in the Postgres database.
6 | type Clip struct {
7 | ClipID uint64 `pg:",pk" json:"clip_id"`
8 | UserID uint64 `pg:",notnull" json:"user_id"`
9 | Creator string `pg:"type:varchar(16),notnull" json:"creator"`
10 | Source string `pg:"type:varchar(4096),notnull" json:"-"`
11 | Type string `pg:"type:varchar(255),notnull" json:"type"`
12 | Title string `pg:"type:varchar(128),notnull" json:"title"`
13 | Description string `pg:"type:varchar(512),notnull" json:"description"`
14 | Views uint64 `pg:"default:0,notnull" json:"views"`
15 | UploadedAt time.Time `pg:"default:CURRENT_TIMESTAMP" json:"uploaded_at"`
16 | }
17 |
--------------------------------------------------------------------------------
/web/static/fonts/thicccboi/thicccboi.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'THICCCBOI';
3 | src: url('THICCCBOI-Regular.woff2') format('woff2'),
4 | url('THICCCBOI-Regular.woff') format('woff');
5 | font-weight: normal;
6 | font-style: normal;
7 | font-display: block;
8 | }
9 | @font-face {
10 | font-family: 'THICCCBOI';
11 | src: url('THICCCBOI-Medium.woff2') format('woff2'),
12 | url('THICCCBOI-Medium.woff') format('woff');
13 | font-weight: 500;
14 | font-style: normal;
15 | font-display: block;
16 | }
17 | @font-face {
18 | font-family: 'THICCCBOI';
19 | src: url('THICCCBOI-Bold.woff2') format('woff2'),
20 | url('THICCCBOI-Bold.woff') format('woff');
21 | font-weight: bold;
22 | font-style: normal;
23 | font-display: block;
24 | }
--------------------------------------------------------------------------------
/cmd/clips-helper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "clips/cmd/clips-helper/commands"
5 | "clips/internal/config"
6 | "clips/internal/db"
7 | "clips/pkg/models"
8 | "fmt"
9 | "os"
10 | )
11 |
12 | func main() {
13 | if err := config.LoadConfig(); err != nil {
14 | panic(err)
15 | }
16 |
17 | db.Setup()
18 | models.Setup(db.Database)
19 |
20 | args := os.Args
21 |
22 | if len(args) < 2 {
23 | fmt.Println("usage: clips-helper [command:new-user]")
24 | os.Exit(1)
25 | }
26 |
27 | command := args[1]
28 | commandArgs := args[2:]
29 |
30 | switch command {
31 | case "new-user":
32 | if err := commands.NewUser(commandArgs); err != nil {
33 | fmt.Printf("Failed to run %s: %s\n", command, err.Error())
34 | os.Exit(1)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.env.demo.example:
--------------------------------------------------------------------------------
1 | # Decides what kind of build rollup will create
2 | STAGING=production
3 |
4 | # Whether or not to use HTTPS, if HTTPS is enabled & TLS_CERT/TLS_KEY you will get an error
5 | HTTPS=true
6 |
7 | # The path to the TLS cert & key files (doesn't have to be in .pem format)
8 | TLS_CERT=/path/to/cert.pem
9 | TLS_KEY=/path/to/key.pem
10 |
11 | # What address the server will bind on
12 | LISTEN_ADDRESS=localhost:3000
13 |
14 | # Postgres connection url
15 | POSTGRES_URL=postgres://postgres:postgres@localhost/clips
16 |
17 | # Where clips will be saved on disk
18 | CLIP_SAVE_PATH=/example/storage/clips
19 |
20 | # Max request body size in megabytes
21 | MAX_BODY_SIZE_MB=100
22 |
23 | # To allow signups:
24 | ALLOW_SIGNUP=false
25 |
26 | # To allow uploads (from anyone):
27 | ALLOW_UPLOAD=false
28 |
--------------------------------------------------------------------------------
/web/app/lib/user.ts:
--------------------------------------------------------------------------------
1 | import type {Clip} from './clip'
2 |
3 | interface User {
4 | user_id:number
5 | username:string
6 | clips:Clip[]
7 | created_at:Date
8 | }
9 |
10 | async function getMe():Promise {
11 | try {
12 | const res = await fetch('/api/user/me')
13 |
14 | if(res.status !== 200) {
15 |
16 | if(res.status === 401) {
17 | return Promise.resolve(null)
18 | }
19 |
20 | return Promise.reject('Failed to get user')
21 | }
22 |
23 | const user = (await res.json())
24 |
25 | return Promise.resolve(user)
26 | } catch(err) {
27 | console.trace(err)
28 |
29 | return Promise.reject('Unexpected Error')
30 | }
31 | }
32 |
33 | export {
34 | User,
35 | getMe
36 | }
--------------------------------------------------------------------------------
/web/app/auth.ts:
--------------------------------------------------------------------------------
1 | import {ConditionsFailedEvent, replace} from 'svelte-spa-router'
2 | import {getMe} from './lib/user'
3 |
4 | async function isAuthenticated() {
5 | try {
6 | const user = await getMe()
7 |
8 | if(!user) {
9 | return Promise.resolve(false)
10 | }
11 |
12 | return Promise.resolve(true)
13 | } catch(err) {
14 | console.trace(err)
15 |
16 | return Promise.reject(err)
17 | }
18 | }
19 |
20 | function conditionsFailed(event:ConditionsFailedEvent) {
21 | const authRoutes = [
22 | '/login',
23 | '/signup'
24 | ]
25 |
26 | if (authRoutes.includes( event.detail.route)) {
27 | return replace('/')
28 | }
29 |
30 | return replace('/login')
31 | }
32 |
33 | export {
34 | conditionsFailed,
35 | isAuthenticated
36 | }
--------------------------------------------------------------------------------
/internal/routes/api.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "clips/internal/api"
5 | "clips/internal/data"
6 | "github.com/gofiber/fiber/v2"
7 | )
8 |
9 | var APIRouter fiber.Router
10 | var UserRouter fiber.Router
11 | var ClipRouter fiber.Router
12 |
13 | // SetupAPIRoutes sets up all routing under /api.
14 | func SetupAPIRoutes() {
15 | // API index routing
16 | APIRouter = data.Application.Group("/api",api.Auth)
17 |
18 | // User API routing
19 | UserRouter = APIRouter.Group("/user")
20 |
21 | UserRouter.Get("/me",api.GetMe)
22 | UserRouter.Get("/:user_id",api.GetUser)
23 |
24 | // Clip API routing
25 | ClipRouter = APIRouter.Group("/clip")
26 |
27 | ClipRouter.Post("/new",api.NewClip)
28 | ClipRouter.Get("/get",api.GetClips)
29 | ClipRouter.Post("/views/:clip_id",api.IncrementViews)
30 | ClipRouter.Get("/view/:clip_id",api.ViewClip)
31 | ClipRouter.Get("/:clip_id",api.GetClip)
32 | }
--------------------------------------------------------------------------------
/internal/api/auth.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "clips/internal/data"
5 | "clips/internal/services"
6 | "github.com/gofiber/fiber/v2"
7 | "log"
8 | )
9 |
10 | // Auth is used to set the authenticated user for every request that hits /api/*
11 | func Auth(c *fiber.Ctx) error {
12 | sess,err := data.Store.Get(c)
13 |
14 | if err != nil {
15 | log.Printf("Failed to get session: %s",err)
16 |
17 | return c.Status(500).JSON(fiber.Map{
18 | "error":"Unknown Error",
19 | })
20 | }
21 |
22 | if sess.Get("active") == true {
23 | c.Locals("active",true)
24 | c.Locals("user",sess.Get("user"))
25 |
26 | return c.Next()
27 | }
28 |
29 | // If there is an API key in the header, then set the authenticated user with the specified API key
30 |
31 | apiKey := c.GetReqHeaders()["X-Api-Key"]
32 |
33 | if len(apiKey) != 0 {
34 | if res := services.APIKeyAuth(apiKey); res.Success {
35 | c.Locals("active",true)
36 | c.Locals("user",res.User)
37 | }
38 | }
39 |
40 | return c.Next()
41 | }
--------------------------------------------------------------------------------
/web/app/routes.ts:
--------------------------------------------------------------------------------
1 | import {wrap} from 'svelte-spa-router/wrap'
2 | import {isAuthenticated} from './auth'
3 |
4 | import Home from './svelte/Home.svelte'
5 | import Login from './svelte/Login.svelte'
6 | import Signup from './svelte/Signup.svelte'
7 | import Upload from './svelte/Upload.svelte'
8 | import Clip from './svelte/Clip.svelte'
9 |
10 | export default {
11 | '/':wrap({
12 | component:Home
13 | }),
14 | '/login':wrap({
15 | component:Login,
16 | conditions:[
17 | async () => {
18 | return !(await isAuthenticated())
19 | }
20 | ]
21 | }),
22 | '/signup':wrap({
23 | component:Signup,
24 | conditions:[
25 | async () => {
26 | return !(await isAuthenticated())
27 | }
28 | ]
29 | }),
30 | '/upload':wrap({
31 | component:Upload,
32 | conditions:[
33 | async () => {
34 | return await isAuthenticated()
35 | }
36 | ]
37 | }),
38 | '/clip/:clip_id':wrap({
39 | component:Clip
40 | })
41 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module clips
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/go-pg/pg/v10 v10.10.6
7 | github.com/gofiber/fiber/v2 v2.30.0
8 | github.com/joho/godotenv v1.4.0
9 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292
10 | )
11 |
12 | require (
13 | github.com/andybalholm/brotli v1.0.4 // indirect
14 | github.com/go-pg/zerochecker v0.2.0 // indirect
15 | github.com/jinzhu/inflection v1.0.0 // indirect
16 | github.com/klauspost/compress v1.15.0 // indirect
17 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
18 | github.com/valyala/bytebufferpool v1.0.0 // indirect
19 | github.com/valyala/fasthttp v1.34.0 // indirect
20 | github.com/valyala/tcplisten v1.0.0 // indirect
21 | github.com/vmihailenco/bufpool v0.1.11 // indirect
22 | github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
23 | github.com/vmihailenco/tagparser v0.1.2 // indirect
24 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
25 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
26 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
27 | mellium.im/sasl v0.2.1 // indirect
28 | )
29 |
--------------------------------------------------------------------------------
/cmd/clips-helper/commands/new-user.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "clips/internal/config"
5 | "clips/internal/db"
6 | "clips/pkg/models"
7 | "fmt"
8 | "golang.org/x/crypto/bcrypt"
9 | )
10 |
11 | // NewUser takes either two sys arguments or inputs (username and password), and creates a user in the database
12 | func NewUser(args []string) error {
13 |
14 | var username string
15 | var password string
16 |
17 | if len(args) != 2 {
18 | fmt.Print("Enter username: ")
19 | if _,err := fmt.Scanln(&username); err != nil {
20 | return err
21 | }
22 |
23 | fmt.Print("Enter password: ")
24 | if _,err := fmt.Scanln(&password); err != nil {
25 | return err
26 | }
27 | } else {
28 | username = args[0]
29 | password = args[1]
30 | }
31 |
32 | hashedPassword,err := bcrypt.GenerateFromPassword([]byte(password),config.BcryptCost)
33 |
34 | if err != nil {
35 | return err
36 | }
37 |
38 | user := models.User{
39 | Username: username,
40 | Password: string(hashedPassword),
41 | }
42 |
43 | if _,err = db.Database.Model(&user).Insert(); err != nil {
44 | return err
45 | }
46 |
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/internal/controllers/login.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "clips/internal/data"
5 | "clips/internal/services"
6 | "github.com/gofiber/fiber/v2"
7 | "log"
8 | )
9 |
10 | // Login is called whenever a user hits POST /auth/login. It's passed the credentials from the form in Login.svelte, and
11 | // tries to authenticate.
12 | func Login(c *fiber.Ctx) error {
13 |
14 | sess,err := data.Store.Get(c)
15 |
16 | if err != nil {
17 | log.Printf("Failed to get session: %s",err)
18 |
19 | return c.Status(500).SendString("Unexpected Error")
20 | }
21 |
22 | login := data.Login{}
23 |
24 | if err := c.BodyParser(&login); err != nil {
25 | log.Printf("Failed to parse login body: %s",err)
26 |
27 | return c.Status(500).SendString("Unexpected Error")
28 | }
29 |
30 | res := services.UserAuth(login)
31 |
32 | if !res.Success {
33 | return c.Status(res.Status).SendString(res.Message)
34 | }
35 |
36 | sess.Set("user",res.User)
37 | sess.Set("active",true)
38 |
39 | if err = sess.Save(); err != nil {
40 | log.Printf("Failed to save session: %s",err)
41 |
42 | return c.Status(500).SendString("Unexpected Error")
43 | }
44 |
45 | return c.Status(res.Status).SendString(res.Message)
46 | }
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clips",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "install": "go get",
7 | "build": "rollup -c && go build -o ./build/ ./...",
8 | "dev": "concurrently \"rollup -c -w\" \"nodemon --exec go run main.go\"",
9 | "start": "go run main.go",
10 | "check": "svelte-check frontend/ --tsconfig tsconfig.json"
11 | },
12 | "devDependencies": {
13 | "@rollup/plugin-commonjs": "^17.0.0",
14 | "@rollup/plugin-node-resolve": "^11.0.0",
15 | "@rollup/plugin-typescript": "^8.0.0",
16 | "@tsconfig/svelte": "^2.0.0",
17 | "@typescript-eslint/eslint-plugin": "^5.0.0",
18 | "@typescript-eslint/parser": "^5.0.0",
19 | "concurrently": "^7.0.0",
20 | "eslint": "^8.12.0",
21 | "eslint-plugin-svelte3": "^3.4.1",
22 | "nodemon": "^2.0.15",
23 | "rollup": "^2.3.4",
24 | "rollup-plugin-css-only": "^3.1.0",
25 | "rollup-plugin-livereload": "^2.0.0",
26 | "rollup-plugin-svelte": "^7.0.0",
27 | "rollup-plugin-terser": "^7.0.0",
28 | "svelte": "^3.0.0",
29 | "svelte-check": "^2.0.0",
30 | "svelte-preprocess": "^4.0.0",
31 | "svelte-spa-router": "^3.2.0",
32 | "tslib": "^2.0.0",
33 | "typescript": "^4.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import sveltePreprocess from 'svelte-preprocess'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import { terser } from 'rollup-plugin-terser'
6 | import svelte from 'rollup-plugin-svelte'
7 | import css from 'rollup-plugin-css-only'
8 |
9 | const production = process.env.STAGING === 'production'
10 |
11 | export default {
12 | input: 'web/app/index.ts',
13 | output: {
14 | sourcemap: true,
15 | format: 'iife',
16 | name: 'app',
17 | file: 'web/static/build/bundle.js'
18 | },
19 | plugins: [
20 | svelte({
21 | preprocess: sveltePreprocess({ sourceMap: !production }),
22 | compilerOptions: {
23 | // enable run-time checks when not in production
24 | dev: !production
25 | }
26 | }),
27 | css({ output: 'bundle.css' }),
28 | resolve({
29 | browser: true,
30 | dedupe: ['svelte']
31 | }),
32 | commonjs(),
33 | typescript({
34 | sourceMap: !production,
35 | inlineSources: !production
36 | }),
37 | production && terser()
38 | ],
39 | watch: {
40 | clearScreen: false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/internal/controllers/signup.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "clips/internal/config"
5 | "clips/internal/data"
6 | "clips/internal/services"
7 | "github.com/gofiber/fiber/v2"
8 | "log"
9 | )
10 |
11 | // Signup is called whenever a user hits POST /auth/signup. It's passed the information from the form in Signup.svelte,
12 | // and tries to create a user with the specified information.
13 | func Signup(c *fiber.Ctx) error {
14 |
15 | if !config.AllowSignup {
16 | return c.Status(401).SendString("Signup is disabled")
17 | }
18 |
19 | sess,err := data.Store.Get(c)
20 |
21 | if err != nil {
22 | log.Printf("Failed to get session: %s",err)
23 |
24 | return c.Status(500).SendString("Unexpected Error")
25 | }
26 |
27 | signup := data.Login{}
28 |
29 | if err := c.BodyParser(&signup); err != nil {
30 | return c.Status(500).SendString("Unexpected Error")
31 | }
32 |
33 | res,err := services.Signup(signup)
34 |
35 | if err != nil {
36 | log.Printf("Failed to signup: %s",err)
37 |
38 | return c.Status(500).SendString("Unexpected Error")
39 | }
40 |
41 | if !res.Success {
42 | return c.Status(res.Status).SendString(res.Message)
43 | }
44 |
45 | sess.Set("user",res.User)
46 | sess.Set("active",true)
47 |
48 | if err = sess.Save(); err != nil {
49 | log.Printf("Failed to save session: %s",err)
50 |
51 | return c.Status(500).SendString("Unexpected Error")
52 | }
53 |
54 | return c.Status(res.Status).SendString(res.Message)
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/internal/services/user.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "clips/internal/config"
5 | "clips/internal/data"
6 | "clips/internal/db"
7 | "clips/pkg/models"
8 | "golang.org/x/crypto/bcrypt"
9 | "log"
10 | )
11 |
12 | // Signup takes a data.Login and returns a Response based on the result of creating a user.
13 | func Signup(login data.Login) (Response,error) {
14 |
15 | hashedPassword,err := bcrypt.GenerateFromPassword([]byte(login.Password),config.BcryptCost)
16 |
17 | if err != nil {
18 | log.Printf("Failed to generate bcrypt hash: %s",err)
19 |
20 | return Response{Success: false, Status: 500, Message: "Unexpected Error"},err
21 | }
22 |
23 | user := models.User{}
24 |
25 | exists,err := db.Database.Model(&user).
26 | Where("username = ?",login.Username).
27 | Exists()
28 |
29 | if err != nil {
30 | log.Printf("Failed to find if user exists: %s",err)
31 |
32 | return Response{Success: false, Status: 500, Message: "Unexpected Error"},err
33 | }
34 |
35 | if exists {
36 | return Response{Success: false, Status: 409, Message: "User already exists!"},err
37 | }
38 |
39 | user.Username = login.Username
40 | user.Password = string(hashedPassword)
41 |
42 | _,err = db.Database.Model(&user).Insert()
43 |
44 | if err != nil {
45 | log.Printf("Failed to insert new user: %s",err)
46 |
47 | return Response{Success: false, Status: 500, Message: "Unexpected Error"},err
48 | }
49 |
50 | return Response{Success: true, Status: 200, Message: "Success", User: user},nil
51 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Clips
2 |
3 | Project Clips is a simple video hosting platform that enables people to share and view clips.
4 |
5 | ## Getting Started
6 |
7 | ### Using Docker
8 |
9 | **Note:** Make sure you have a valid .env that works with docker (See [.env.docker.example](https://github.com/devhsoj/clips/blob/master/.env.docker.example))
10 | ```bash
11 | git clone https://github.com/devhsoj/clips
12 | cd clips/
13 | docker-compose -f .\docker\docker-compose.yml up -d --build
14 | ```
15 |
16 | ---
17 |
18 | ### Without Docker
19 |
20 | **Requirements:** [Go](https://go.dev/dl/) - [npm](https://nodejs.org/en/download/) - [PostgreSQL](https://www.postgresql.org/download/)
21 |
22 | ```bash
23 | git clone https://github.com/devhsoj/clips
24 | cd clips/
25 | npm install
26 | npm run build
27 | ```
28 |
29 | **Postgres Setup:**
30 | ```postgresql
31 | CREATE DATABASE clips;
32 | \c clips;
33 | CREATE EXTENSION pgcrypto; -- Used for generating UUIDs
34 | ```
35 |
36 | ### Starting
37 |
38 | **Note:** Make sure you have run the command `npm run build` before-hand, and have a valid `.env`
39 |
40 | #### Run with npm
41 | ```bash
42 | npm start # or with pm2: pm2 start npm --name clips -- start
43 | ```
44 | or
45 |
46 | #### Run from built executable
47 | ```bash
48 | ./build/clips # or ./build/clips.exe
49 | ```
50 | ---
51 | ## Development
52 |
53 | #### Starting
54 | ```bash
55 | # uses concurrently to start rollup & nodemon in watch mode for the web/app/ & internal/ directories
56 | npm run dev
57 | ```
--------------------------------------------------------------------------------
/internal/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "clips/internal/config"
5 | "clips/internal/data"
6 | "clips/internal/db"
7 | "clips/internal/routes"
8 | "clips/pkg/models"
9 | "github.com/gofiber/fiber/v2"
10 | "github.com/gofiber/fiber/v2/middleware/session"
11 | "log"
12 | )
13 |
14 | func Setup() {
15 | log.SetFlags(log.LstdFlags | log.Lshortfile)
16 |
17 | if err := config.LoadConfig(); err != nil {
18 | log.Fatalf("Failed to load config: %s",err)
19 | }
20 |
21 | db.Setup()
22 | data.Setup()
23 | models.Setup(db.Database)
24 | }
25 |
26 | func Start() {
27 | data.Application = fiber.New(fiber.Config{
28 | BodyLimit: config.BodyLimit,
29 | DisableStartupMessage: true,
30 | })
31 |
32 | // Setup session store
33 | data.Store = session.New()
34 |
35 | // Setup static router for serving Svelte bundle and other static files
36 | data.Application.Static("/","./web/static/",fiber.Static{Compress: true})
37 |
38 | // Setup API routing
39 | routes.SetupAPIRoutes()
40 |
41 | // Setup auth routing
42 | routes.SetupAuthRoutes()
43 |
44 | var err error
45 |
46 | if config.HTTPS {
47 | log.Printf("Starting on https://%s",config.ListenAddress)
48 |
49 | err = data.Application.ListenTLS(config.ListenAddress,config.TLSCertPath,config.TLSKeyPath)
50 | } else {
51 | log.Printf("Starting on http://%s",config.ListenAddress)
52 |
53 | err = data.Application.Listen(config.ListenAddress)
54 | }
55 |
56 | if err != nil {
57 | log.Fatalf("Failed to start server: %s",err)
58 |
59 | db.Database.Close()
60 | }
61 | }
--------------------------------------------------------------------------------
/internal/api/user.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "clips/internal/db"
5 | "clips/pkg/models"
6 | "github.com/go-pg/pg/v10"
7 | "github.com/gofiber/fiber/v2"
8 | "log"
9 | "strconv"
10 | )
11 |
12 | // GetMe is called whenever a user hits GET /api/user/me. It responds with the User data of the active user in JSON
13 | // format.
14 | func GetMe(c *fiber.Ctx) error {
15 |
16 | includeClips := c.Query("includeClips","false") == "true"
17 |
18 | if c.Locals("active") != true {
19 | return c.JSON(nil)
20 | }
21 |
22 | user := c.Locals("user").(models.User)
23 |
24 | if !includeClips {
25 | return c.JSON(&user)
26 | }
27 |
28 | err := db.Database.Model(&user).
29 | Where("user_id = ?",user.UserID).
30 | Relation("Clips").
31 | Select(&user)
32 |
33 | if err != nil {
34 | log.Println(err)
35 |
36 | return c.Status(500).JSON(fiber.Map{
37 | "message":"Unexpected Error",
38 | })
39 | }
40 |
41 | return c.JSON(&user)
42 | }
43 |
44 | // GetUser is called whenever a user hits GET /api/user/:user_id. It responds with the specified User's data in JSON
45 | // format.
46 | func GetUser(c *fiber.Ctx) error {
47 | userID,_ := strconv.ParseInt(c.Params("user_id","0"),10,64)
48 |
49 | user := models.User{}
50 |
51 | err := db.Database.Model(&user).
52 | Where("user_id = ?",userID).
53 | Select()
54 |
55 | if err != nil {
56 |
57 | if err == pg.ErrNoRows {
58 | return c.Status(404).JSON(fiber.Map{
59 | "error":"User not found",
60 | })
61 | }
62 |
63 | log.Println(err)
64 |
65 | return c.Status(500).JSON(fiber.Map{
66 | "error":"Unknown Error",
67 | })
68 | }
69 |
70 | return c.JSON(&user)
71 | }
--------------------------------------------------------------------------------
/internal/services/auth.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "clips/internal/data"
5 | "clips/internal/db"
6 | "clips/pkg/models"
7 | "github.com/go-pg/pg/v10"
8 | "golang.org/x/crypto/bcrypt"
9 | "log"
10 | )
11 |
12 | // APIKeyAuth returns a Response generated from the results of trying to authenticate a user with the given apiKey.
13 | func APIKeyAuth(apiKey string) Response {
14 | user := models.User{}
15 |
16 | err := db.Database.Model(&user).
17 | Where("api_key = ?",apiKey).
18 | Select()
19 |
20 | if err != nil {
21 |
22 | if err == pg.ErrNoRows {
23 | return Response{Success: false, Status: 401, Message: "Invalid Credentials"}
24 | }
25 |
26 | log.Printf("Failed to authenticate using API Key: %s",err)
27 |
28 | return Response{Success: false, Status: 500, Message: "Unexpected Error"}
29 | }
30 |
31 | return Response{Success: true, Status: 200, Message: "Success", User: user}
32 | }
33 |
34 | // UserAuth returns a Response generated from the results of trying to authenticate with the given data.Login.
35 | func UserAuth(login data.Login) Response {
36 | user := models.User{}
37 |
38 | err := db.Database.Model(&user).
39 | Where("username = ?",login.Username).
40 | Select()
41 |
42 | if err != nil {
43 |
44 | if err == pg.ErrNoRows {
45 | return Response{Success: false, Status: 401, Message: "Invalid Credentials"}
46 | }
47 |
48 | log.Printf("Failed to query user: %s",err)
49 |
50 | return Response{Success: false, Status: 500, Message: "Unexpected Error"}
51 | }
52 |
53 | if err = bcrypt.CompareHashAndPassword([]byte(user.Password),[]byte(login.Password)); err != nil {
54 | return Response{Success: false, Status: 401, Message: "Invalid Credentials"}
55 | }
56 |
57 | return Response{Success: true, Status: 200, Message: "Success", User: user}
58 | }
--------------------------------------------------------------------------------
/web/app/lib/clip.ts:
--------------------------------------------------------------------------------
1 | interface Clip {
2 | clip_id:number
3 | user_id:number
4 | creator:string
5 | type:string
6 | title:string
7 | description:string
8 | views:number
9 | uploaded_at:Date
10 | }
11 |
12 | async function getClips(options:{page:number,amount:number}):Promise {
13 | try {
14 | const res = await fetch(`/api/clip/get?page=${options.page}&amount=${options.amount}`)
15 |
16 | if(res.status !== 200) {
17 | console.log(res.status,res.statusText)
18 |
19 | return Promise.reject('Failed to get clips')
20 | }
21 |
22 | const clips = (await res.json())
23 |
24 | return Promise.resolve(clips)
25 | } catch(err) {
26 | console.trace(err)
27 |
28 | return Promise.reject('Unknown Error')
29 | }
30 | }
31 |
32 | async function getClip(options:{clip_id:number}):Promise {
33 | try {
34 | const res = await fetch(`/api/clip/${ options.clip_id }`)
35 |
36 | if(res.status !== 200) {
37 | console.log(res.status,res.statusText)
38 |
39 | return Promise.reject('Failed to get clip')
40 | }
41 |
42 | const clip = (await res.json())
43 |
44 | return Promise.resolve(clip)
45 | } catch(err) {
46 | console.trace(err)
47 |
48 | return Promise.reject('Unknown Error')
49 | }
50 | }
51 |
52 | async function incrementViews(clipID:number):Promise {
53 | try {
54 | const res = await fetch(`/api/clip/views/${ clipID }`,{method:'POST'})
55 |
56 | if(res.status !== 200) {
57 | console.log(res.status,res.statusText)
58 |
59 | return Promise.reject(`Failed to add to views for clip ${ clipID }`)
60 | }
61 |
62 | return Promise.resolve()
63 | } catch(err) {
64 | console.trace(err)
65 |
66 | return Promise.reject('Unknown Error')
67 | }
68 | }
69 |
70 | export {
71 | Clip,
72 | getClip,
73 | getClips,
74 | incrementViews
75 | }
76 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/joho/godotenv"
5 | "os"
6 | "path"
7 | "strconv"
8 | )
9 |
10 | var HTTPS bool // Whether to use https.
11 | var TLSCertPath string // The path to the SSL/TLS certificate.
12 | var TLSKeyPath string // The path to the SSL/TLS key.
13 | var ListenAddress string // The address for the HTTP/HTTPS server to listen on.
14 | var PostgresURL string // The PostgreSQL connection URL used for all database operations.
15 |
16 | var ApplicationPath string // The current working directory of the running executable.
17 |
18 | var BcryptCost int = 12 // The cost factor used in Bcrypt. Default to 12.
19 | var BodyLimit int = 100 * 1024 * 1024 // The body limit of all HTTP/HTTPS requests. Default to 100 MB
20 |
21 | var ClipSavePath string // The path to the directory used to store user uploaded clips.
22 | var AllowSignup bool = true // Whether to allow signups.
23 | var AllowUpload bool = true // Whether to allow uploads.
24 |
25 | // LoadConfig loads the environment variables from .env into global config variables.
26 | func LoadConfig() error {
27 | var err error
28 |
29 | if err = godotenv.Load(); err != nil {
30 | return err
31 | }
32 |
33 | HTTPS = os.Getenv("HTTPS") == "true"
34 |
35 | if HTTPS {
36 | TLSCertPath = os.Getenv("TLS_CERT")
37 | TLSKeyPath = os.Getenv("TLS_KEY")
38 | }
39 |
40 | ListenAddress = os.Getenv("LISTEN_ADDRESS")
41 |
42 | if ListenAddress == "" {
43 | ListenAddress = "0.0.0.0:3000"
44 | }
45 |
46 | PostgresURL = os.Getenv("POSTGRES_URL")
47 |
48 | if PostgresURL == "" {
49 | PostgresURL = "postgres://postgres:postgres@localhost/clips?sslmode=disable"
50 | }
51 |
52 | if os.Getenv("BCRYPT_COST") != "" {
53 | BcryptCost,err = strconv.Atoi(os.Getenv("BCRYPT_COST"))
54 |
55 | if err != nil {
56 | return err
57 | }
58 | }
59 |
60 | if os.Getenv("BODY_LIMIT_MB") != "" {
61 | BodyLimit,err = strconv.Atoi(os.Getenv("BODY_LIMIT_MB"))
62 |
63 | BodyLimit = BodyLimit * 1024 * 1024
64 |
65 | if err != nil {
66 | return err
67 | }
68 | }
69 |
70 | ClipSavePath = os.Getenv("CLIP_SAVE_PATH")
71 |
72 | AllowSignup = os.Getenv("ALLOW_SIGNUP") == "true" || os.Getenv("ALLOW_SIGNUP") == ""
73 | AllowUpload = os.Getenv("ALLOW_UPLOAD") == "true" || os.Getenv("ALLOW_UPLOAD") == ""
74 |
75 | ApplicationPath,err = os.Getwd()
76 |
77 | if err != nil {
78 | return err
79 | }
80 |
81 | if ClipSavePath == "" {
82 | ClipSavePath,err = CreateClipStorageDirectory()
83 | }
84 |
85 | return err
86 | }
87 |
88 | // CreateClipStorageDirectory creates a directory used to store user uploads if no directory was already specified.
89 | func CreateClipStorageDirectory() (string,error) {
90 |
91 | storagePath := path.Join(ApplicationPath,"build/","uploads/")
92 |
93 | if err := os.Mkdir(storagePath,0777); err != nil {
94 | if os.IsExist(err) {
95 | return path.Join(ApplicationPath,"build/","uploads/"),nil
96 | }
97 |
98 | return "",err
99 | }
100 |
101 | return storagePath,nil
102 | }
--------------------------------------------------------------------------------
/web/static/fonts/custom/selection.json:
--------------------------------------------------------------------------------
1 | {"IcoMoonType":"selection","icons":[{"icon":{"paths":["M518.321 1024h-353.975c-90.708-0.144-164.202-73.638-164.346-164.332l-0-0.014v-859.654h25.284v859.654c0.144 76.744 62.318 138.918 139.048 139.062l0.014 0h353.975z"],"attrs":[{}],"width":518,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["line"]},"attrs":[{}],"properties":{"order":13470,"id":5,"name":"comment-line","prevSize":32,"code":59652},"setIdx":0,"setId":1,"iconIdx":0},{"icon":{"paths":["M860.16 956.416c-34.427 41.237-85.852 67.284-143.36 67.284s-108.932-26.047-143.119-66.988l-0.241-0.297-163.84-225.28-262.144-321.536-112.64-147.456c-20.682-27.133-33.137-61.509-33.137-98.794 0-90.486 73.354-163.84 163.84-163.84 4.454 0 8.867 0.178 13.232 0.527l-0.575-0.037h1077.248c3.789-0.312 8.202-0.49 12.657-0.49 90.486 0 163.84 73.354 163.84 163.84 0 37.285-12.454 71.66-33.429 99.194l0.293-0.401-112.64 147.456-251.904 321.536z"],"width":1434,"isMulticolor":false,"isMulticolor2":false,"tags":["caret-down"],"defaultCode":59648,"grid":0},"properties":{"id":1,"order":13465,"ligatures":"","prevSize":32,"code":59648,"name":"caret-down"},"setIdx":0,"setId":1,"iconIdx":1},{"icon":{"paths":["M48.274 614.4c-29.455-24.591-48.060-61.323-48.060-102.4s18.605-77.809 47.848-102.228l0.212-0.172 160.914-117.029 229.669-187.246 105.326-80.457c19.381-14.773 43.935-23.669 70.567-23.669 64.633 0 117.029 52.395 117.029 117.029 0 3.182-0.127 6.334-0.376 9.451l0.026-0.411v769.463c0.223 2.707 0.35 5.859 0.35 9.040 0 64.633-52.395 117.029-117.029 117.029-26.632 0-51.186-8.896-70.853-23.878l0.286 0.209-105.326-80.457-229.669-179.931z"],"width":731,"isMulticolor":false,"isMulticolor2":false,"tags":["caret-left"],"defaultCode":59649,"grid":0},"properties":{"id":2,"order":13466,"ligatures":"","prevSize":32,"code":59649,"name":"caret-left"},"setIdx":0,"setId":1,"iconIdx":2},{"icon":{"paths":["M683.154 409.6c29.455 24.591 48.060 61.323 48.060 102.4s-18.605 77.809-47.848 102.228l-0.212 0.172-160.914 124.343-229.669 179.931-103.863 80.457c-19.667 15.506-44.804 24.871-72.129 24.871-64.633 0-117.029-52.395-117.029-117.029 0-3.606 0.163-7.173 0.482-10.696l-0.033 0.454v-769.463c-0.223-2.707-0.35-5.859-0.35-9.040 0-64.633 52.395-117.029 117.029-117.029 26.632 0 51.186 8.896 70.853 23.878l-0.286-0.209 105.326 80.457 229.669 187.246z"],"width":731,"isMulticolor":false,"isMulticolor2":false,"tags":["caret-right"],"defaultCode":59650,"grid":0},"properties":{"id":3,"order":13467,"ligatures":"","prevSize":32,"code":59650,"name":"caret-right"},"setIdx":0,"setId":1,"iconIdx":3},{"icon":{"paths":["M573.44 67.584c34.427-41.237 85.852-67.284 143.36-67.284s108.932 26.047 143.119 66.988l0.241 0.297 174.080 225.28 251.904 321.536 112.64 145.408c21.708 27.533 34.819 62.726 34.819 100.981 0 90.486-73.354 163.84-163.84 163.84-5.048 0-10.043-0.228-14.974-0.675l0.635 0.046h-1077.248c-3.789 0.312-8.202 0.49-12.657 0.49-90.486 0-163.84-73.354-163.84-163.84 0-37.285 12.454-71.66 33.429-99.194l-0.293 0.401 112.64-147.456 262.144-321.536z"],"width":1434,"isMulticolor":false,"isMulticolor2":false,"tags":["caret-up"],"defaultCode":59651,"grid":0},"properties":{"id":4,"order":13468,"ligatures":"","prevSize":32,"code":59651,"name":"caret-up"},"setIdx":0,"setId":1,"iconIdx":4}],"height":1024,"metadata":{"name":"Custom"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icn-","metadata":{"fontFamily":"Custom","majorVersion":1,"minorVersion":0},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon"},"historySize":50,"showCodes":false,"gridSize":16,"showLiga":false}}
--------------------------------------------------------------------------------
/web/app/svelte/Login.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
Project Clips
37 |
38 |
39 |
40 |
41 |
42 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/web/app/svelte/Signup.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
Project Clips
37 |
38 |
39 |
40 |
41 |
42 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/web/app/svelte/Upload.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Project Clips
35 |
36 |
37 |
38 |
39 |
40 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/web/app/svelte/Clip.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
43 |
44 | {clip?.title} ({clip?.creator}) - Project Clips
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Project Clips
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {#if clip === null}
63 | Clip not found.
64 | {:else}
65 |
66 |
67 |
68 | {clip.description} - {clip.creator}
69 |
70 |
71 | {clip.views} Views
72 |
73 |
74 |
75 |
78 |
79 |
80 | {/if}
81 |
82 |
83 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/internal/api/clip.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "clips/internal/config"
5 | "clips/internal/db"
6 | "clips/pkg/models"
7 | "fmt"
8 | "github.com/go-pg/pg/v10"
9 | "github.com/gofiber/fiber/v2"
10 | "log"
11 | "strconv"
12 | "strings"
13 | "time"
14 | )
15 |
16 | // NewClip is called whenever a user hits POST /api/clip/new. It reads the options given in the form in Upload.svelte,
17 | // creates a Clip in the database, stores the uploaded video file in the configured clip storage path, and responds with
18 | // the JSON representation of the newly created Clip.
19 | func NewClip(c *fiber.Ctx) error {
20 | if c.Locals("active") != true {
21 | return c.Status(401).JSON(fiber.Map{
22 | "error":"Guests are not allowed to upload",
23 | })
24 | }
25 |
26 | if !config.AllowUpload {
27 | return c.Status(500).JSON(fiber.Map{
28 | "error":"Uploading is disabled",
29 | })
30 | }
31 |
32 | user := c.Locals("user").(models.User)
33 | clipFile,err := c.FormFile("clip")
34 |
35 | if err != nil {
36 | log.Println(err)
37 |
38 | return c.Status(500).JSON(fiber.Map{
39 | "error":"Unexpected Error",
40 | })
41 | }
42 |
43 | splitFilename := strings.Split(clipFile.Filename,".")
44 |
45 | if len(splitFilename) <= 1 {
46 | log.Println(err)
47 |
48 | return c.Status(400).JSON(fiber.Map{
49 | "error":"Invalid Filename",
50 | })
51 | }
52 |
53 | ext := splitFilename[len(splitFilename) - 1]
54 | filename := fmt.Sprintf("%s-%d.%s",user.Username,time.Now().UnixNano(),ext)
55 | clipFilePath := fmt.Sprintf("%s/%s",config.ClipSavePath,filename)
56 |
57 | if err = c.SaveFile(clipFile,clipFilePath); err != nil {
58 | log.Println(err)
59 | }
60 |
61 | clipTitle := c.FormValue("title","No Title")
62 | clipDescription := c.FormValue("description","No Description")
63 |
64 | clip := models.Clip{
65 | UserID: user.UserID,
66 | Creator: user.Username,
67 | Type: clipFile.Header.Get("Content-Type"),
68 | Title: clipTitle,
69 | Description: clipDescription,
70 | Source: clipFilePath,
71 | }
72 |
73 | _,err = db.Database.Model(&clip).
74 | Insert()
75 |
76 | if err != nil {
77 | log.Println(err)
78 |
79 | return c.Status(500).JSON(fiber.Map{
80 | "error":"Unexpected Error",
81 | })
82 | }
83 |
84 | return c.JSON(&clip)
85 | }
86 |
87 | // GetClip is called whenever a user hits GET /api/clip/:clip_id. It responds with information about the specified Clip
88 | // in JSON format.
89 | func GetClip(c *fiber.Ctx) error {
90 | clipID,_ := strconv.ParseInt(c.Params("clip_id","0"),10,64)
91 |
92 | clip := models.Clip{}
93 |
94 | err := db.Database.Model(&clip).
95 | Where("clip_id = ?",clipID).
96 | Select()
97 |
98 | if err != nil {
99 |
100 | if err == pg.ErrNoRows {
101 | return c.Status(404).JSON(fiber.Map{
102 | "error":"Clip not found",
103 | })
104 | }
105 |
106 | log.Println(err)
107 |
108 | return c.Status(500).JSON(fiber.Map{
109 | "error":"Unknown Error",
110 | })
111 | }
112 |
113 | return c.JSON(&clip)
114 | }
115 |
116 | // GetClips is called whenever a user hits GET /api/clip/get. It returns a JSON array of Clips, retrieved by pagination
117 | // from the db, specified by the query parameters (page,amount).
118 | func GetClips(c *fiber.Ctx) error {
119 | page,_ := strconv.Atoi(c.Query("page","0"))
120 | amount,_ := strconv.Atoi(c.Query("amount","10"))
121 |
122 | var clips []models.Clip
123 |
124 | err := db.Database.Model(&clips).
125 | Order("clip_id ASC").
126 | Offset(page * amount).
127 | Limit(amount).
128 | Select()
129 |
130 | if err != nil {
131 |
132 | if err == pg.ErrNoRows {
133 | return c.Status(404).JSON(fiber.Map{
134 | "error":"No clips found",
135 | })
136 | }
137 |
138 | log.Println(err)
139 |
140 | return c.Status(500).JSON(fiber.Map{
141 | "error":"Unknown Error",
142 | })
143 | }
144 |
145 | return c.JSON(&clips)
146 | }
147 |
148 | // ViewClip is called whenever a user hits GET /api/clip/view/:clip_id. It responds with the specified Clip's raw video
149 | // data stored in the configured clip storage path.
150 | func ViewClip(c *fiber.Ctx) error {
151 | clipID,_ := strconv.ParseInt(c.Params("clip_id","0"),10,64)
152 |
153 | var clipSource string
154 |
155 | err := db.Database.Model(&models.Clip{}).
156 | Where("clip_id = ?",clipID).
157 | Column("source").
158 | Select(&clipSource)
159 |
160 | if err != nil {
161 |
162 | if err == pg.ErrNoRows {
163 | if err == pg.ErrNoRows {
164 | return c.Status(404).JSON(fiber.Map{
165 | "error":"Clip not found",
166 | })
167 | }
168 |
169 | log.Println(err)
170 |
171 | return c.Status(500).JSON(fiber.Map{
172 | "error":"Unknown Error",
173 | })
174 | }
175 |
176 | }
177 |
178 | return c.SendFile(clipSource,true)
179 | }
180 |
181 | // IncrementViews is called whenever a user hits POST /api/clip/views/:clip_id. It increments the amount of views of
182 | // the specified Clip by the :clip_id parameter, and responds with an empty body & 200 status code.
183 | func IncrementViews(c *fiber.Ctx) error {
184 | clipID,_ := strconv.ParseInt(c.Params("clip_id","0"),10,64)
185 |
186 | _,err := db.Database.Model(&models.Clip{}).
187 | Set("views = views + 1").
188 | Where("clip_id = ?",clipID).
189 | Update()
190 |
191 | if err != nil {
192 | log.Println(err)
193 |
194 | return c.Status(500).JSON(fiber.Map{
195 | "error":"Unknown Error",
196 | })
197 | }
198 |
199 | return c.SendStatus(200)
200 | }
--------------------------------------------------------------------------------
/web/app/svelte/Home.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
Project Clips
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | Latest Clips
94 | Scroll down to see more clips!
95 |
96 |
97 | {#if clips?.length === 0}
98 | Loading latest clips...
99 | {:else if clips === null}
100 | No clips found.
101 | {:else}
102 | {#each clips as clip}
103 |
104 |
107 |
108 | {clip.description} - {clip.creator}
109 |
110 |
111 | {clip.views} Views
112 |
113 |
114 |
115 |
118 |
119 |
120 | {/each}
121 | {/if}
122 |
123 |
124 |
136 |
137 |
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/web/static/fonts/lg/lg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
4 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
11 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
14 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
15 | github.com/go-pg/pg/v10 v10.10.6 h1:1vNtPZ4Z9dWUw/TjJwOfFUbF5nEq1IkR6yG8Mq/Iwso=
16 | github.com/go-pg/pg/v10 v10.10.6/go.mod h1:GLmFXufrElQHf5uzM3BQlcfwV3nsgnHue5uzjQ6Nqxg=
17 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
18 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
19 | github.com/gofiber/fiber/v2 v2.30.0 h1:R928kgJICQkcfIzAjMIQ+U0uOpa0+vTCZLLODeo4M14=
20 | github.com/gofiber/fiber/v2 v2.30.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
21 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
22 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
24 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
25 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
26 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
27 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
28 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
29 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
30 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
31 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
32 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
33 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
35 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
36 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
37 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
39 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
40 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
41 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
42 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
43 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
44 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
45 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
46 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
47 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
48 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
49 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
50 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
51 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
52 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
53 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
54 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
55 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
56 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
57 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
58 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
59 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
60 | github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
61 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
64 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
66 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
67 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
68 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
69 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
70 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
71 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
72 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
73 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
74 | github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
75 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
76 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
77 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
78 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
79 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
80 | github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc=
81 | github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
82 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
83 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
84 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
85 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
86 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
87 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
88 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
89 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
90 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
91 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
92 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
93 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
94 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
95 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
96 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
97 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
98 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
99 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
100 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
102 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
103 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
104 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
105 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
106 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
107 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
108 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
109 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
110 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
111 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
112 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
113 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
114 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
115 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
116 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
117 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
118 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
119 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
120 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
121 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
122 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
123 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
125 | golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
127 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
128 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
129 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
130 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
131 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
132 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
133 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
134 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
135 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
136 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
138 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
139 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
140 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
141 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
143 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
144 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
145 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
146 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
147 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
148 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
149 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
150 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
151 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
152 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
153 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
154 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
155 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
156 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
157 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
158 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
159 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
160 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
161 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
162 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
163 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
164 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
165 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
166 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
167 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
168 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
169 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
170 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
171 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
172 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
173 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
174 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
175 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
176 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
177 | mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
178 | mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
179 |
--------------------------------------------------------------------------------