Full Code of nekobin/nekobin for AI

master 79db149263aa cached
24 files
55.5 KB
16.5k tokens
58 symbols
1 requests
Download .txt
Repository: nekobin/nekobin
Branch: master
Commit: 79db149263aa
Files: 24
Total size: 55.5 KB

Directory structure:
gitextract_ljhha_s3/

├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── assets/
│   ├── static/
│   │   ├── css/
│   │   │   └── app.css
│   │   └── js/
│   │       └── app.js
│   └── templates/
│       └── app.html
├── config/
│   └── config.go
├── config-sample.yaml
├── database/
│   ├── database.go
│   ├── document.go
│   └── schema.sql
├── go.mod
├── go.sum
├── handlers/
│   ├── api.go
│   ├── raw.go
│   └── root.go
├── keygen/
│   ├── keygen.go
│   └── phonetic.go
├── limiter/
│   └── limiter.go
├── middleware/
│   └── middleware.go
├── nekobin.go
└── response/
    ├── error.go
    └── result.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = tab
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8

[*.{html, js, yaml}]
indent_style = space
indent_size = 2


================================================
FILE: .gitignore
================================================
.idea
config.yaml
nekobin


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Dan <https://github.com/delivrance>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
    <a href="//nekobin.com">
        <img src="https://i.imgur.com/zbQTQBl.png" alt="nekobin" width="500"/>
    </a>
</p>

# Nekobin

> Elegant pastebin service written in Go

**Nekobin** is an elegant, free and open-source pastebin web application written from the ground up in Go and publicly
hosted at [**nekobin.com**](//nekobin.com). Paste, save and share the link of your text content using a
sleek and intuitive interface!

## Features

- Choose between Dark and Light themes.
- Syntax highlighting for source codes based on file extension.
- Keyboard shortcuts: save <kbd>Ctrl+S</kbd>, new <kbd>Ctrl+N</kbd>, raw <kbd>Shift+Ctrl+R</kbd>.
- Powerful API rate limiter to allow fine-grained control.
- One-click URL copy.

## Soon

Nekobin is brand new software and is currently in its [MVP-stage](https://en.wikipedia.org/wiki/Minimum_viable_product).
Although it is already publicly available and is perfectly usable, you can expect new features and information soon!

Current priority: frontend rework using a modern approach and a proper UI framework.

Feedbacks are very welcome, just [open an issue](../../issues/new) and let us discuss.

## License

MIT © 2020 [Dan](https://github.com/delivrance)


================================================
FILE: assets/static/css/app.css
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

:root {
	font-size: 16px;
	font-family: Hack, Menlo, Monaco, Consolas, "Courier New", monospace;

	--accent-color: #CC9C5A;

	/* Dark colors */
	--bg-dark-color: #2B2B2B;
	--bg2-dark-color: #3C3F41;
	--main-dark-color: #BABABA;

	--border-dark-color: #464646;
	--scrollbar-dark-color: #494949;
	--scrollbar-dark-active-color: #595959;

	--placeholder-dark-color: #767676;
	--linenumber-dark-color: #888888;

	/* Light colors */
	--bg-light-color: #FFFFFF;
	--bg2-light-color: #ECECEC;
	--main-light-color: #3C3F41;

	--border-light-color: #E4E4E4;
	--scrollbar-light-color: #C6C6C6;
	--scrollbar-light-active-color: #7F7F7F;

	--placeholder-light-color: #8C8C8C;
	--linenumber-light-color: #888888;

	/* Used colors */
	--bg-color: var(--bg-dark-color);
	--bg2-color: var(--bg2-dark-color);
	--main-color: var(--main-dark-color);

	--border-color: var(--border-dark-color);
	--scrollbar-color: var(--scrollbar-dark-color);
	--scrollbar-active-color: var(--scrollbar-dark-active-color);

	--placeholder-color: var(--placeholder-dark-color);
	--linenumber-color: var(--linenumber-dark-color);

	--bar-height: 1.6rem;
	--bar-height-2x: calc(var(--bar-height) * 2);
	--lr-padding: calc(var(--bar-height) / 4);
	--lr-padding-2x: calc(var(--lr-padding) * 2);

	--scrollbar-size: 1rem;
	--transistion-all: all 150ms;
}

body {
	margin: 0;
	background: var(--bg-color);
	transition: var(--transistion-all);
	/*text-shadow: 0 0 0 currentColor;*/
}

a {
	color: var(--main-color);
	transition: var(--transistion-all);
}

a:hover {
	filter: brightness(105%);
}

header {
	font-size: 2rem;
	height: var(--bar-height-2x);
	padding-left: var(--lr-padding-2x);
	padding-right: var(--lr-padding-2x);
	position: fixed;
	width: 100%;
	background: var(--bg2-color);
	color: var(--main-color);
	display: flex;
	flex-direction: row;
	align-items: center;
	border-bottom: 1px solid var(--border-color);
	box-sizing: border-box;
}

.divider {
	border-right: 2px solid var(--border-color);
	margin-right: var(--lr-padding-2x);
}

header .actions {
	margin-left: auto;
}

header #url {
	font-size: 1.25rem;
	margin-left: auto;
	cursor: copy;
	opacity: 0.8;
	transition: var(--transistion-all);
}

#url:hover {
	opacity: 1;
}

header button.action {
	color: inherit;
	font-size: 1.75rem;
	cursor: pointer;
	border: 0;
	background: none;
	outline: none;
	opacity: 0.8;
	transition: var(--transistion-all);
}

button.action:hover {
	opacity: 1;
}

button.action:disabled {
	opacity: 0.2;
	cursor: auto;
}

#content {
	position: absolute;
	line-height: 1.4em;
	top: var(--bar-height-2x);
	bottom: var(--bar-height);
	width: 100%;
}

.CodeMirror {
	height: 100%;
	font-family: inherit;
}

/* Hide cursor in readonly */
.readonly .CodeMirror-cursor {
	display: none !important
}

.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
	padding-left: 6px;
}

.CodeMirror pre.CodeMirror-placeholder {
	color: var(--placeholder-color);
}

.cm-s-darcula .CodeMirror-gutters {
	border-right: 1px solid var(--border-color);
}

.CodeMirror-linenumber {
	color: var(--linenumber-color);
}

.unselectable {
	user-select: none;
}

.hidden {
	display: none;
}

.unclickable {
	pointer-events: none;
}

footer {
	height: var(--bar-height);
	padding-left: var(--lr-padding);
	padding-right: var(--lr-padding);
	position: fixed;
	width: 100%;
	bottom: 0;
	background: var(--bg2-color);
	color: var(--main-color);
	display: flex;
	align-items: center;
	border-top: 1px solid var(--border-color);
	box-sizing: border-box;
}

footer a {
	text-decoration: underline;
}

footer .links {
	display: flex;
	align-items: center;
	margin-left: auto;
}

.link {
	margin-left: 1em;
}

.CodeMirror-scrollbar-filler {
	background-color: var(--bg-color);
}

::-webkit-scrollbar {
	width: var(--scrollbar-size);
	height: var(--scrollbar-size);
}

::-webkit-scrollbar-track {
	background-color: var(--bg-color);
}

::-webkit-scrollbar-thumb {
	border-radius: calc(var(--scrollbar-size) / 2);
	background-color: var(--scrollbar-color);
	border: calc(var(--scrollbar-size) / 4) solid var(--bg-color);
}

::-webkit-scrollbar-thumb:hover {
	background-color: var(--scrollbar-active-color)
}

::-webkit-scrollbar-thumb:active {
	background-color: var(--scrollbar-active-color)
}

.cm-s-darcula span.cm-def,
.cm-s-darcula span.cm-comment,
.cm-s-darcula span.cm-tag {
	font-style: normal;
	text-decoration: none;
}

.cm-s-darcula span.cm-comment {
	color: var(--placeholder-color);
}


================================================
FILE: assets/static/js/app.js
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

class Nekobin {
  constructor() {
    this.theme = "dark"

    this.actions = {
      theme: document.getElementById("theme"),
      raw: document.getElementById("raw"),
      save: document.getElementById("save"),
      new: document.getElementById("new")
    }

    CodeMirror.modeURL = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/%N/%N.min.js"

    this.editor = CodeMirror(document.getElementById("content"), {
      placeholder: "Paste code, save and share the link!",
      lineNumbers: true
    })

    this.url = document.getElementById("url")
    this.editor.focus()
  }

  async animateURL() {
    let prevHTML = this.url.innerHTML

    this.url.classList.add("unclickable")
    this.url.style.opacity = "0"
    await sleep(150)

    this.url.innerHTML = `<i class="fas fa-check"></i> Copied!`
    this.url.style.opacity = "1"
    await sleep(1500)

    this.url.style.opacity = "0"
    await sleep(150)

    this.url.innerHTML = prevHTML
    this.url.style.opacity = null
    this.url.classList.remove("unclickable")
  }

  isContentEmpty() {
    return this.editor.getDoc().getValue().length === 0
  }

  async switchTheme() {
    let themeEl = this.actions.theme

    function getProp(prop) {
      return getComputedStyle(document.documentElement).getPropertyValue(prop)
    }

    function setProp(prop, value) {
      document.documentElement.style.setProperty(prop, value)
    }

    if (this.theme === "dark") {
      themeEl.classList.remove("fa-moon")
      themeEl.classList.add("fa-sun")

      setProp("--bg-color", getProp("--bg-light-color"))
      setProp("--bg2-color", getProp("--bg2-light-color"))
      setProp("--main-color", getProp("--main-light-color"))

      setProp("--border-color", getProp("--border-light-color"))
      setProp("--scrollbar-color", getProp("--scrollbar-light-color"))
      setProp("--scrollbar-active-color", getProp("--scrollbar-active-light-color"))

      setProp("--placeholder-color", getProp("--placeholder-light-color"))
      setProp("--linenumber-color", getProp("--linenumber-light-color"))

      this.editor.setOption("theme", "default")

      this.theme = "light"
    } else {
      themeEl.classList.remove("fa-sun")
      themeEl.classList.add("fa-moon")

      setProp("--bg-color", getProp("--bg-dark-color"))
      setProp("--bg2-color", getProp("--bg2-dark-color"))
      setProp("--main-color", getProp("--main-dark-color"))

      setProp("--border-color", getProp("--border-dark-color"))
      setProp("--scrollbar-color", getProp("--scrollbar-dark-color"))
      setProp("--scrollbar-active-color", getProp("--scrollbar-active-dark-color"))

      setProp("--placeholder-color", getProp("--placeholder-dark-color"))
      setProp("--linenumber-color", getProp("--linenumber-dark-color"))

      this.editor.setOption("theme", "darcula")

      this.theme = "dark"
    }

    document.cookie = `theme=${this.theme}`
  }

  async setup() {
    let key = window.location.pathname

    this.theme = getCookie("theme") || "dark"
    // Call twice to set the theme got from cookies. Rework.
    await this.switchTheme()
    await this.switchTheme()

    this.url.onclick = async () => {
      copyToClipboard(window.location.href)
      await this.animateURL()
    }

    this.actions.theme.onclick = async () => {
      document.body.style.opacity = "0"
      await sleep(150)
      await this.switchTheme()
      document.body.style.opacity = null
    }

    this.actions.raw.onclick = () => {
      window.location.href = `/raw${key}`
    }

    this.actions.new.onclick = () => {
      window.location.href = "/"
    }

    this.actions.save.onclick = async () => {
      this.actions.save.disabled = true

      let content = this.editor.getDoc().getValue()

      let response = await fetch("/api/documents", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({content})
      })

      if (response.ok) {
        let {key} = (await response.json()).result
        window.location.href = `/${key}`
      } else {
        let {error} = await response.json()
        this.actions.save.disabled = false
        alert(`Error: ${error}`)
      }
    }

    this.editor.on("change", () => {
      this.actions.save.disabled = this.isContentEmpty()
    })

    this.editor.setOption("extraKeys", {
      "Ctrl-S": () => this.actions.save.click(),
      "Shift-Ctrl-R": () => this.actions.raw.click(),
      "Ctrl-N": () => this.actions.new.click()
    })
  }

  async load() {
    let path = window.location.pathname

    if (path === "/") {
      return
    }

    let response = await fetch(`/api/documents${path}`)

    if (response.ok) {
      let {key, content} = (await response.json()).result

      this.editor.getDoc().setValue(content)
      this.editor.setOption("readOnly", true)

      let mode = CodeMirror.findModeByFileName(path)

      if (mode !== undefined) {
        CodeMirror.autoLoadMode(this.editor, mode.mode)
        this.editor.setOption("mode", mode.mime)
      }

      document.getElementById("content").classList.add("readonly")
      document.title = `nekobin - ${key}`

      let url = document.getElementById("url")

      url.insertAdjacentText("afterbegin", path)
      url.classList.remove("hidden")

      this.actions.save.disabled = true

      if (key !== "about") {
        this.actions.raw.disabled = false
      }
    } else {
      if (response.status === 429) {
        let {error} = await response.json()
        alert(`Error: ${error}`)
      } else {
        window.location.replace("/")
      }
    }
  }
}

// https://www.w3schools.com/js/js_cookies.asp
function getCookie(cname) {
  let name = cname + "="
  let decodedCookie = decodeURIComponent(document.cookie)
  let ca = decodedCookie.split(";")

  for (let i = 0; i < ca.length; i++) {
    let c = ca[i]

    while (c.charAt(0) === " ") {
      c = c.substring(1)
    }

    if (c.indexOf(name) === 0) {
      return c.substring(name.length, c.length)
    }
  }
  return ""
}

// https://stackoverflow.com/questions/33855641/copy-output-of-a-javascript-variable-to-the-clipboard
const copyToClipboard = text => {
  let dummy = document.createElement("textarea")

  document.body.appendChild(dummy)

  dummy.value = text
  dummy.select()

  document.execCommand("copy")

  document.body.removeChild(dummy)
}

// https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep
const sleep = ms => new Promise(r => setTimeout(r, ms))

window.addEventListener("DOMContentLoaded", async () => {
  let nekobin = new Nekobin()

  await nekobin.setup()
  await nekobin.load()
})


================================================
FILE: assets/templates/app.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <title>nekobin</title>

  <meta charset="UTF-8">

  <meta content="Paste, save and share the link of your text content using a sleek and intuitive interface!"
        name="description">
  <meta content="#2B2B2B" name="theme-color">
  <meta content="pastebin,paste,paste tool,code,go,golang" name="keywords">

  <meta content="Nekobin.com &mdash; Elegant and open-source pastebin service" property="og:title">
  <meta content="website" property="og:type">
  <meta content="https://nekobin.com/static/img/nekobin.jpg" property="og:image">
  <meta content="https://nekobin.com/" property="og:url">
  <meta content="Paste, save and share the link of your text content using a sleek and intuitive interface!"
        property="og:description">
  <meta content="Nekobin" property="og:site_name">
  <meta content="en_US" property="og:locale">

  <link href="static/favicon.ico" rel="shortcut icon"/>
  <link href="https://nekobin.com/" rel="canonical"/>

  <link href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.003/web/hack.min.css" rel="stylesheet"/>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" rel="stylesheet"/>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css" rel="stylesheet"/>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/theme/darcula.min.css" rel="stylesheet"/>
  <link href="static/css/app.css" rel="stylesheet">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/addon/display/placeholder.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/addon/mode/loadmode.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/meta.min.js"></script>
  <script src="static/js/app.js"></script>
</head>

<body>
<header class="unselectable">
  <div class="title">
    <a href="/" style="color: #8A8A8A; text-decoration: none">
          <span>
            {<i><span style="color: var(--accent-color)"><b>neko</b></span></i>:<i><span
            style="color: #A8A8A8">bin</span></i>}
          </span>
    </a>
  </div>

  <div class="hidden" id="url">
    <i class="fas fa-copy"></i>
  </div>

  <div class="actions">
    <button class="fas fa-save action" disabled id="save"></button>
    <button class="fas fa-code action" disabled id="raw"></button>
    <button class="fas fa-plus action" id="new"></button>
    <span class="divider"></span>
    <button class="fas action" id="theme"></button>
  </div>
</header>

<div id="content"></div>

<footer class="unselectable">
  <div id="copyright">
    Copyright <i class="far fa-copyright"></i> {{.year}} -
    <a class="fas fa-cat" href="static/img/robi.jpg" style="text-decoration: none" target="_blank"></a>
    <a href="https://github.com/delivrance" rel="noopener" target="_blank">Dan</a>
  </div>

  <div class="links">
    <div class="link">
      <i class="fas fa-address-card"></i>
      <a href="about.md">About</a>
    </div>

    <div class="link">
      <i class="fab fa-github"></i>
      <a href="https://github.com/nekobin/nekobin" rel="noopener" target="_blank">Source</a>
    </div>

    <div class="link">
      <i class="fab fa-telegram-plane"></i>
      <a href="https://t.me/haskell" rel="noopener" target="_blank">Contact</a>
    </div>
  </div>
</footer>
</body>
</html>


================================================
FILE: config/config.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package config

import (
	"io/ioutil"
	"log"
	"time"

	"gopkg.in/yaml.v2"

	"github.com/nekobin/nekobin/limiter"
)

type (
	Nekobin struct {
		Host string `yaml:"host"`
		Port string `yaml:"port"`

		MaxTitleLength   int `yaml:"max_title_length"`
		MaxAuthorLength  int `yaml:"max_author_length"`
		MaxContentLength int `yaml:"max_content_length"`
	}

	Database struct {
		URI string `yaml:"uri"`

		MaxIdleConns    int           `yaml:"max_idle_conns"`
		MaxOpenConns    int           `yaml:"max_open_conns"`
		ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
	}

	Documents struct {
		Get  []limiter.Limit `yaml:"get"`
		Post []limiter.Limit `yaml:"post"`
	}

	Limits struct {
		Documents Documents `yaml:"documents"`
	}

	Config struct {
		Nekobin  Nekobin  `yaml:"nekobin"`
		Database Database `yaml:"database"`
		Limits   Limits   `yaml:"limits"`
	}
)

func Load(path string) *Config {
	file, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}

	cfg := &Config{}

	err = yaml.UnmarshalStrict(file, cfg)
	if err != nil {
		log.Fatal(err)
	}

	// YAML time values are kept in seconds for convenience.
	// Convert them here to nanoseconds because that's what Limiter needs.
	{
		for i, get := 0, cfg.Limits.Documents.Get; i < len(get); i++ {
			get[i].Period *= time.Second
		}

		for i, post := 0, cfg.Limits.Documents.Post; i < len(post); i++ {
			post[i].Period *= time.Second
		}
	}

	return cfg
}


================================================
FILE: config-sample.yaml
================================================
# Main configuration
nekobin:
  # Host and port nekobin will bind to
  host: "0.0.0.0"
  port: 5555

  # Maximum length for title, author and content
  max_title_length: 32
  max_author_length: 32
  max_content_length: 65536

# Postgres database configuration
database:
  # Connection string
  uri: "postgres://user:pass@host:port/name"

  # Advanced connection pool settings
  max_idle_conns: 5
  max_open_conns: 20
  conn_max_lifetime: 1800

# Endpoints limits. Maximum requests over period (in seconds)
limits:
  documents:
    # GET /api/documents/:key and GET /raw/:key
    get:
      - amount: 20
        period: 5
      - amount: 10000
        period: 86400

    # POST /api/documents
    post:
      - amount: 10
        period: 60
      - amount: 20
        period: 3600
      - amount: 50
        period: 86400


================================================
FILE: database/database.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package database

import (
	"log"
	"time"

	"github.com/jmoiron/sqlx"

	"github.com/nekobin/nekobin/config"
)

type Database struct {
	Documents DocumentsQuery
}

func NewDatabase(cfg *config.Database) *Database {
	db := sqlx.MustConnect("postgres", cfg.URI)

	db.SetMaxIdleConns(cfg.MaxIdleConns)
	db.SetMaxOpenConns(cfg.MaxOpenConns)
	db.SetConnMaxLifetime(cfg.ConnMaxLifetime * time.Second)

	err := db.Ping()
	if err != nil {
		log.Fatal(err)
	}

	return &Database{
		Documents: NewDocuments(db),
	}
}


================================================
FILE: database/document.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package database

import (
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/jmoiron/sqlx"

	"github.com/nekobin/nekobin/keygen"
)

type Document struct {
	Key     string  `json:"key"`
	Title   *string `json:"title"`
	Author  *string `json:"author"`
	Date    int     `json:"date"`
	Views   int     `json:"views"`
	Length  int     `json:"length"`
	Content string  `json:"content"`
}

type DocumentsQuery interface {
	Select(key string) (doc *Document, err error)
	Insert(title, author *string, content string) (doc *Document, err error)
	Exists(key string) (exists bool, err error)
	IncrementViews(key, ip string)
}

type ViewIPsKey struct {
	documentKey string
	ipAddress   string
}

type Documents struct {
	*sqlx.DB

	keygen  keygen.Keygen
	viewIPs map[ViewIPsKey]time.Time
	mu      *sync.Mutex
}

func NewDocuments(db *sqlx.DB) *Documents {
	return &Documents{
		DB:      db,
		keygen:  keygen.NewPhoneticKeygen(),
		viewIPs: make(map[ViewIPsKey]time.Time),
		mu:      &sync.Mutex{},
	}
}

func (docs *Documents) Select(key string) (doc *Document, err error) {
	row := docs.QueryRowx(`
		SELECT
			key, title, author,
			extract(EPOCH FROM date AT TIME ZONE 'utc')::INT date,
			views, length, content
		FROM documents
		WHERE key = $1
		LIMIT 1`,
		key,
	)

	doc = &Document{}
	err = row.StructScan(doc)

	return
}

func (docs *Documents) Insert(title, author *string, content string) (doc *Document, err error) {
	if title != nil && *title == "" {
		title = nil
	}

	if author != nil && *author == "" {
		author = nil
	}

	var key string
	for {
		key = docs.keygen.GenerateKey()
		exists, err := docs.Exists(key)

		if err != nil {
			log.Println(err)
			return nil, err
		}

		if !exists {
			break
		}
	}

	rows, err := docs.Query(
		"INSERT INTO documents (key, title, author, length, content) VALUES ($1, $2, $3, $4, $5)",
		key, title, author, len(content), content,
	)

	if err == nil {
		defer func() {
			err := rows.Close()

			if err != nil {
				log.Println(err)
			}
		}()
	}

	doc, err = docs.Select(key)

	return
}

func (docs *Documents) Exists(key string) (exists bool, err error) {
	row := docs.QueryRowx("SELECT EXISTS(SELECT 1 FROM documents WHERE key = $1)", key)
	err = row.Scan(&exists)

	return
}

func (docs *Documents) IncrementViews(key, ip string) {
	docs.mu.Lock()
	defer docs.mu.Unlock()

	viewIPsKey := ViewIPsKey{key, ip}
	value, exists := docs.viewIPs[viewIPsKey]

	if exists && time.Now().Sub(value).Minutes() < 30 {
		return
	}

	docs.viewIPs[viewIPsKey] = time.Now()

	rows, err := docs.Query("UPDATE documents SET views = views + 1 WHERE key = $1", key)

	if err != nil {
		fmt.Println(err)
		return
	}

	err = rows.Close()

	if err != nil {
		log.Println(err)
	}
}


================================================
FILE: database/schema.sql
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

CREATE TABLE documents
(
    key     TEXT PRIMARY KEY,
    title   TEXT               DEFAULT NULL,
    author  TEXT               DEFAULT NULL,
    date    TIMESTAMP NOT NULL DEFAULT now(),
    views   INTEGER   NOT NULL DEFAULT 0,
    length  INTEGER   NOT NULL,
    content TEXT      NOT NULL
)


================================================
FILE: go.mod
================================================
module github.com/nekobin/nekobin

go 1.14

require (
	github.com/jmoiron/sqlx v1.2.0
	github.com/labstack/echo/v4 v4.1.16
	github.com/lib/pq v1.3.0
	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
	gopkg.in/yaml.v2 v2.2.8
)


================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=


================================================
FILE: handlers/api.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package handlers

import (
	"net/http"
	"strings"

	"github.com/labstack/echo/v4"

	"github.com/nekobin/nekobin/config"
	"github.com/nekobin/nekobin/database"
	"github.com/nekobin/nekobin/response"
)

func GetAbout(ctx echo.Context) error {
	about := ctx.Get("about").(*database.Document)

	return ctx.JSON(
		http.StatusOK,
		response.NewResult(about),
	)
}

func GetDocument(ctx echo.Context) error {
	db := ctx.Get("db").(*database.Database)
	key := strings.Split(ctx.Param("key"), ".")[0]
	doc, err := db.Documents.Select(key)

	if err != nil {
		return ctx.JSON(
			http.StatusBadRequest,
			response.ErrorDocumentNotFound,
		)
	}

	go db.Documents.IncrementViews(key, ctx.RealIP())

	return ctx.JSON(
		http.StatusOK,
		response.NewResult(doc),
	)
}

func PostDocument(ctx echo.Context) error {
	doc := &database.Document{}

	if err := ctx.Bind(doc); err != nil {
		return ctx.JSON(
			http.StatusBadRequest,
			response.ErrorInvalidData,
		)
	}

	title, author, content := doc.Title, doc.Author, doc.Content

	cfg := ctx.Get("cfg").(*config.Config)

	if title != nil {
		switch length := len(*title); {
		case length == 0:
			title = nil
		case length > cfg.Nekobin.MaxTitleLength:
			return ctx.JSON(
				http.StatusBadRequest,
				response.ErrorTitleTooLong,
			)
		}
	}

	if author != nil {
		switch length := len(*author); {
		case length == 0:
			author = nil
		case length > cfg.Nekobin.MaxAuthorLength:
			return ctx.JSON(
				http.StatusBadRequest,
				response.ErrorAuthorTooLong,
			)
		}
	}

	if len(content) == 0 {
		return ctx.JSON(
			http.StatusBadRequest,
			response.ErrorContentEmpty,
		)
	}

	if len(content) > cfg.Nekobin.MaxContentLength {
		return ctx.JSON(
			http.StatusBadRequest,
			response.ErrorContentTooLong,
		)
	}

	db := ctx.Get("db").(*database.Database)
	doc, err := db.Documents.Insert(title, author, content)

	if err != nil {
		return err
	}

	go db.Documents.IncrementViews(doc.Key, ctx.RealIP())

	return ctx.JSON(
		http.StatusCreated,
		response.NewResult(doc),
	)
}

func Pong(ctx echo.Context) error {
	return ctx.JSON(
		http.StatusOK,
		response.NewResult("pong"),
	)
}


================================================
FILE: handlers/raw.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package handlers

import (
	"net/http"
	"strconv"
	"strings"

	"github.com/labstack/echo/v4"

	"github.com/nekobin/nekobin/database"
	"github.com/nekobin/nekobin/response"
)

func GetRawDocument(ctx echo.Context) error {
	db := ctx.Get("db").(*database.Database)
	key := strings.Split(ctx.Param("key"), ".")[0]
	doc, err := db.Documents.Select(key)

	if err != nil {
		return ctx.String(
			http.StatusBadRequest,
			response.ErrorDocumentNotFound.Error,
		)
	}

	go db.Documents.IncrementViews(key, ctx.RealIP())

	if doc.Title != nil {
		ctx.Response().Header().Set("Document-Title", *doc.Title)
	}

	if doc.Author != nil {
		ctx.Response().Header().Set("Document-Author", *doc.Author)
	}

	ctx.Response().Header().Set("Document-Date", strconv.Itoa(doc.Date))
	ctx.Response().Header().Set("Document-Views", strconv.Itoa(doc.Views))
	ctx.Response().Header().Set("Document-length", strconv.Itoa(doc.Length))

	return ctx.String(
		http.StatusOK,
		doc.Content,
	)
}


================================================
FILE: handlers/root.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package handlers

import (
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
)

func GetRoot(ctx echo.Context) error {
	return ctx.Render(
		http.StatusOK,
		"app.html",
		echo.Map{
			"year": time.Now().Year(),
		},
	)
}


================================================
FILE: keygen/keygen.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package keygen

import (
	"math/rand"
	"time"
)

type Keygen interface {
	GenerateKey() string
}

func newRand() *rand.Rand {
	seed := time.Now().UnixNano()
	source := rand.NewSource(seed)

	return rand.New(source)
}


================================================
FILE: keygen/phonetic.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package keygen

import "math/rand"

const (
	vowels            = "aeiou"
	consonants        = "bcdfghjklmnpqrstvwxyz"
	phoneticKeyLength = 10
)

type PhoneticKeygen struct {
	rand *rand.Rand
}

func NewPhoneticKeygen() *PhoneticKeygen {
	return &PhoneticKeygen{
		rand: newRand(),
	}
}

func (pk *PhoneticKeygen) GenerateKey() string {
	key := ""

	for i := 0; i < phoneticKeyLength; i++ {
		if i%2 == 0 {
			key += pk.getRandomFrom(consonants)
		} else {
			key += pk.getRandomFrom(vowels)
		}
	}

	return key
}

func (pk *PhoneticKeygen) getRandomFrom(s string) string {
	return string(s[pk.rand.Intn(len(s))])
}


================================================
FILE: limiter/limiter.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package limiter

import (
	"sort"
	"sync"
	"time"

	"golang.org/x/time/rate"
)

type Limit struct {
	Amount int
	Period time.Duration
}

type Limiter struct {
	limiters map[string][]*rate.Limiter
	limits   []Limit
	mu       *sync.Mutex
}

func NewLimiter(limits ...Limit) *Limiter {
	sort.SliceStable(limits, func(i, j int) bool {
		a := limits[i].Period * time.Duration(limits[i].Amount)
		b := limits[j].Period * time.Duration(limits[j].Amount)

		return a < b
	})

	return &Limiter{
		limiters: make(map[string][]*rate.Limiter),
		limits:   limits,
		mu:       &sync.Mutex{},
	}
}

func (lim *Limiter) add(key string) {
	for _, limit := range lim.limits {
		lim.limiters[key] = append(lim.limiters[key], rate.NewLimiter(
			rate.Limit(float64(limit.Amount)/float64(limit.Period)*float64(time.Second)),
			limit.Amount,
		))
	}
}

func (lim *Limiter) check(key string) bool {
	for _, keyLim := range lim.limiters[key] {
		if !keyLim.Allow() {
			return false
		}
	}

	return true
}

func (lim *Limiter) IsAllowed(key string) bool {
	lim.mu.Lock()
	defer lim.mu.Unlock()

	_, exists := lim.limiters[key]

	if !exists {
		lim.add(key)
	}

	return lim.check(key)
}


================================================
FILE: middleware/middleware.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package middleware

import (
	"io/ioutil"
	"log"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/nekobin/nekobin/config"
	"github.com/nekobin/nekobin/database"
	"github.com/nekobin/nekobin/limiter"
	"github.com/nekobin/nekobin/response"
)

// Middleware to add the configuration in handlers
func Config(cfg *config.Config) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) error {
			ctx.Set("cfg", cfg)
			return next(ctx)
		}
	}
}

// Middleware to add Database context in handlers
func Database(db *database.Database) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) error {
			ctx.Set("db", db)
			return next(ctx)
		}
	}
}

// Middleware to make the About document available in handlers
func About() echo.MiddlewareFunc {
	file, err := ioutil.ReadFile("./README.md")
	if err != nil {
		log.Fatal(err)
	}

	about := &database.Document{
		Key:     "about",
		Content: string(file),
	}

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) error {
			ctx.Set("about", about)
			return next(ctx)
		}
	}
}

// Middleware to limit requests
func Limiter(limits []limiter.Limit) echo.MiddlewareFunc {
	lim := limiter.NewLimiter(limits...)

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) error {
			if !lim.IsAllowed(ctx.RealIP()) {
				return ctx.JSON(
					http.StatusTooManyRequests,
					response.ErrorTooFast,
				)
			}

			return next(ctx)
		}
	}
}


================================================
FILE: nekobin.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package main

import (
	"fmt"
	"html/template"
	"io"
	"os"

	"github.com/labstack/echo/v4"
	mw "github.com/labstack/echo/v4/middleware"
	_ "github.com/lib/pq"

	"github.com/nekobin/nekobin/config"
	"github.com/nekobin/nekobin/database"
	"github.com/nekobin/nekobin/handlers"
	"github.com/nekobin/nekobin/middleware"
)

type Template struct {
	templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
	e := echo.New()

	e.HideBanner = true
	e.Renderer = &Template{
		templates: template.Must(
			template.ParseGlob("./assets/templates/*"),
		),
	}

	cfg := config.Load("config.yaml")
	db := database.NewDatabase(&cfg.Database)

	e.Use(
		mw.LoggerWithConfig(
			mw.LoggerConfig{
				Format: "[${time_rfc3339}] ${status} ${method} ${path} (${remote_ip}) ${latency_human}\n",
				Output: os.Stdout,
			},
		),
		mw.Recover(),
		middleware.Config(cfg),
		middleware.Database(db),
		middleware.About(),
	)

	e.Static("/static", "./assets/static")

	root := e.Group("")
	{
		root.GET("/", handlers.GetRoot)
		root.GET("/:key", handlers.GetRoot)

		getLimiter := middleware.Limiter(cfg.Limits.Documents.Get)
		postLimiter := middleware.Limiter(cfg.Limits.Documents.Post)

		api := root.Group("/api")
		{
			documents := api.Group("/documents")
			{
				documents.GET("/about.md", handlers.GetAbout)
				documents.GET("/:key", handlers.GetDocument, getLimiter)
				documents.POST("", handlers.PostDocument, postLimiter)
			}

			api.GET("/ping", handlers.Pong)
		}

		raw := root.Group("/raw")
		{
			raw.GET("/:key", handlers.GetRawDocument, getLimiter)
		}
	}

	e.Logger.Fatal(e.Start(fmt.Sprintf("%v:%v", cfg.Nekobin.Host, cfg.Nekobin.Port)))
}


================================================
FILE: response/error.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package response

type Error struct {
	Ok    bool   `json:"ok"`
	Error string `json:"error"`
}

func NewError(error string) *Error {
	return &Error{
		Ok:    false,
		Error: error,
	}
}

var (
	ErrorDocumentNotFound = NewError("DOCUMENT_NOT_FOUND")
	ErrorInvalidData      = NewError("INVALID_DATA")
	ErrorTitleTooLong     = NewError("TITLE_TOO_LONG")
	ErrorAuthorTooLong    = NewError("AUTHOR_TOO_LONG")
	ErrorContentEmpty     = NewError("CONTENT_EMPTY")
	ErrorContentTooLong   = NewError("CONTENT_TOO_LONG")
	ErrorTooFast          = NewError("TOO_FAST")
)


================================================
FILE: response/result.go
================================================
/*
 * MIT License
 *
 * Copyright (c) 2020 Dan <https://github.com/delivrance>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package response

type Result struct {
	Ok     bool        `json:"ok"`
	Result interface{} `json:"result"`
}

func NewResult(result interface{}) *Result {
	return &Result{
		Ok:     true,
		Result: result,
	}
}
Download .txt
gitextract_ljhha_s3/

├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── assets/
│   ├── static/
│   │   ├── css/
│   │   │   └── app.css
│   │   └── js/
│   │       └── app.js
│   └── templates/
│       └── app.html
├── config/
│   └── config.go
├── config-sample.yaml
├── database/
│   ├── database.go
│   ├── document.go
│   └── schema.sql
├── go.mod
├── go.sum
├── handlers/
│   ├── api.go
│   ├── raw.go
│   └── root.go
├── keygen/
│   ├── keygen.go
│   └── phonetic.go
├── limiter/
│   └── limiter.go
├── middleware/
│   └── middleware.go
├── nekobin.go
└── response/
    ├── error.go
    └── result.go
Download .txt
SYMBOL INDEX (58 symbols across 15 files)

FILE: assets/static/js/app.js
  class Nekobin (line 25) | class Nekobin {
    method constructor (line 26) | constructor() {
    method animateURL (line 47) | async animateURL() {
    method isContentEmpty (line 66) | isContentEmpty() {
    method switchTheme (line 70) | async switchTheme() {
    method setup (line 122) | async setup() {
    method load (line 182) | async load() {
  function getCookie (line 229) | function getCookie(cname) {

FILE: config/config.go
  type Nekobin (line 38) | type Nekobin struct
  type Database (line 47) | type Database struct
  type Documents (line 55) | type Documents struct
  type Limits (line 60) | type Limits struct
  type Config (line 64) | type Config struct
  function Load (line 71) | func Load(path string) *Config {

FILE: database/database.go
  type Database (line 36) | type Database struct
  function NewDatabase (line 40) | func NewDatabase(cfg *config.Database) *Database {

FILE: database/document.go
  type Document (line 38) | type Document struct
  type DocumentsQuery (line 48) | type DocumentsQuery interface
  type ViewIPsKey (line 55) | type ViewIPsKey struct
  type Documents (line 60) | type Documents struct
    method Select (line 77) | func (docs *Documents) Select(key string) (doc *Document, err error) {
    method Insert (line 95) | func (docs *Documents) Insert(title, author *string, content string) (...
    method Exists (line 139) | func (docs *Documents) Exists(key string) (exists bool, err error) {
    method IncrementViews (line 146) | func (docs *Documents) IncrementViews(key, ip string) {
  function NewDocuments (line 68) | func NewDocuments(db *sqlx.DB) *Documents {

FILE: database/schema.sql
  type documents (line 25) | CREATE TABLE documents

FILE: handlers/api.go
  function GetAbout (line 38) | func GetAbout(ctx echo.Context) error {
  function GetDocument (line 47) | func GetDocument(ctx echo.Context) error {
  function PostDocument (line 67) | func PostDocument(ctx echo.Context) error {
  function Pong (line 134) | func Pong(ctx echo.Context) error {

FILE: handlers/raw.go
  function GetRawDocument (line 38) | func GetRawDocument(ctx echo.Context) error {

FILE: handlers/root.go
  function GetRoot (line 34) | func GetRoot(ctx echo.Context) error {

FILE: keygen/keygen.go
  type Keygen (line 32) | type Keygen interface
  function newRand (line 36) | func newRand() *rand.Rand {

FILE: keygen/phonetic.go
  constant vowels (line 30) | vowels            = "aeiou"
  constant consonants (line 31) | consonants        = "bcdfghjklmnpqrstvwxyz"
  constant phoneticKeyLength (line 32) | phoneticKeyLength = 10
  type PhoneticKeygen (line 35) | type PhoneticKeygen struct
    method GenerateKey (line 45) | func (pk *PhoneticKeygen) GenerateKey() string {
    method getRandomFrom (line 59) | func (pk *PhoneticKeygen) getRandomFrom(s string) string {
  function NewPhoneticKeygen (line 39) | func NewPhoneticKeygen() *PhoneticKeygen {

FILE: limiter/limiter.go
  type Limit (line 35) | type Limit struct
  type Limiter (line 40) | type Limiter struct
    method add (line 61) | func (lim *Limiter) add(key string) {
    method check (line 70) | func (lim *Limiter) check(key string) bool {
    method IsAllowed (line 80) | func (lim *Limiter) IsAllowed(key string) bool {
  function NewLimiter (line 46) | func NewLimiter(limits ...Limit) *Limiter {

FILE: middleware/middleware.go
  function Config (line 41) | func Config(cfg *config.Config) echo.MiddlewareFunc {
  function Database (line 51) | func Database(db *database.Database) echo.MiddlewareFunc {
  function About (line 61) | func About() echo.MiddlewareFunc {
  function Limiter (line 81) | func Limiter(limits []limiter.Limit) echo.MiddlewareFunc {

FILE: nekobin.go
  type Template (line 43) | type Template struct
    method Render (line 47) | func (t *Template) Render(w io.Writer, name string, data interface{}, ...
  function main (line 51) | func main() {

FILE: response/error.go
  type Error (line 27) | type Error struct
  function NewError (line 32) | func NewError(error string) *Error {

FILE: response/result.go
  type Result (line 27) | type Result struct
  function NewResult (line 32) | func NewResult(result interface{}) *Result {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (61K chars).
[
  {
    "path": ".editorconfig",
    "chars": 204,
    "preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 4\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newlin"
  },
  {
    "path": ".gitignore",
    "chars": 26,
    "preview": ".idea\nconfig.yaml\nnekobin\n"
  },
  {
    "path": "LICENSE",
    "chars": 1092,
    "preview": "MIT License\n\nCopyright (c) 2020 Dan <https://github.com/delivrance>\n\nPermission is hereby granted, free of charge, to an"
  },
  {
    "path": "README.md",
    "chars": 1229,
    "preview": "<p align=\"center\">\n    <a href=\"//nekobin.com\">\n        <img src=\"https://i.imgur.com/zbQTQBl.png\" alt=\"nekobin\" width=\""
  },
  {
    "path": "assets/static/css/app.css",
    "chars": 5591,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "assets/static/js/app.js",
    "chars": 7809,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "assets/templates/app.html",
    "chars": 3500,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>nekobin</title>\n\n  <meta charset=\"UTF-8\">\n\n  <meta content=\"Paste, save"
  },
  {
    "path": "config/config.go",
    "chars": 2586,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "config-sample.yaml",
    "chars": 821,
    "preview": "# Main configuration\nnekobin:\n  # Host and port nekobin will bind to\n  host: \"0.0.0.0\"\n  port: 5555\n\n  # Maximum length "
  },
  {
    "path": "database/database.go",
    "chars": 1665,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "database/document.go",
    "chars": 3860,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "database/schema.sql",
    "chars": 1457,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "go.mod",
    "chars": 230,
    "preview": "module github.com/nekobin/nekobin\n\ngo 1.14\n\nrequire (\n\tgithub.com/jmoiron/sqlx v1.2.0\n\tgithub.com/labstack/echo/v4 v4.1."
  },
  {
    "path": "go.sum",
    "chars": 5721,
    "preview": "github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.m"
  },
  {
    "path": "handlers/api.go",
    "chars": 3282,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "handlers/raw.go",
    "chars": 2125,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "handlers/root.go",
    "chars": 1383,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "keygen/keygen.go",
    "chars": 1376,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "keygen/phonetic.go",
    "chars": 1774,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "limiter/limiter.go",
    "chars": 2323,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "middleware/middleware.go",
    "chars": 2714,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "nekobin.go",
    "chars": 2936,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "response/error.go",
    "chars": 1716,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  },
  {
    "path": "response/result.go",
    "chars": 1370,
    "preview": "/*\n * MIT License\n *\n * Copyright (c) 2020 Dan <https://github.com/delivrance>\n *\n * Permission is hereby granted, free "
  }
]

About this extraction

This page contains the full source code of the nekobin/nekobin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (55.5 KB), approximately 16.5k tokens, and a symbol index with 58 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!