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 — 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,
}
}
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
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.