├── .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 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 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 | 9 | 10 | 15 | 16 | 20 | 21 | 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 |
43 |
44 |
45 |
46 |
47 |
48 |

Login

49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | {#if error} 66 |
67 | 68 | {error} 69 | 70 |
71 | {/if} 72 | 75 |
76 |
77 |
78 |
79 | 86 |
87 |
88 |
89 | 90 | -------------------------------------------------------------------------------- /web/app/svelte/Signup.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |

Project Clips

37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |

Signup

49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | {#if error} 66 |
67 | 68 | {error} 69 | 70 |
71 | {/if} 72 | 75 |
76 |
77 |
78 |
79 | 86 |
87 |
88 |
89 | 90 | -------------------------------------------------------------------------------- /web/app/svelte/Upload.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 |
31 |
32 |
33 |
34 |

Project Clips

35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 |

Upload Clip

47 |
48 | 49 |
50 | 51 | 52 |
53 | 54 |
55 | 56 | 57 |
58 | 59 |
60 |
Select video clip to upload:
61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 | {#if error} 69 |
70 | 71 | {error} 72 | 73 |
74 | {/if} 75 |
76 |
77 |
78 |
79 | 86 |
87 |
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 | 4 | 5 | 6 | 7 | { 8 | "fontFamily": "lg", 9 | "majorVersion": 1, 10 | "minorVersion": 0, 11 | "fontURL": "https://github.com/sachinchoolur/lightGallery", 12 | "copyright": "sachin", 13 | "license": "MLT", 14 | "licenseURL": "http://opensource.org/licenses/MIT", 15 | "version": "Version 1.0", 16 | "fontId": "lg", 17 | "psName": "lg", 18 | "subFamily": "Regular", 19 | "fullName": "lg", 20 | "description": "Font generated by IcoMoon." 21 | } 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------