diff --git a/db.go b/db.go new file mode 100755 index 0000000..a06eb20 --- /dev/null +++ b/db.go @@ -0,0 +1,105 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "path" + "runtime" + "time" + + "github.com/boltdb/bolt" +) + +var db *bolt.DB +var open bool + +// Open to create the database and open +func Open() error { + var err error + _, filename, _, _ := runtime.Caller(0) // get full path of this file + dbfile := path.Join(path.Dir(filename), "data.db") + config := &bolt.Options{Timeout: 30 * time.Second} + db, err = bolt.Open(dbfile, 0600, config) + if err != nil { + fmt.Println("Opening BoltDB timed out") + log.Fatal(err) + } + open = true + return nil +} + +// Close database +func Close() { + open = false + db.Close() +} + +// Data for storing in DB +type CowyoData struct { + Title string + Text string +} + +func (p *CowyoData) load() error { + if !open { + return fmt.Errorf("db must be opened before saving!") + } + err := db.View(func(tx *bolt.Tx) error { + var err error + b := tx.Bucket([]byte("datas")) + if b == nil { + return nil + } + k := []byte(p.Title) + val := b.Get(k) + if val == nil { + return nil + } + err = p.decode(val) + if err != nil { + return err + } + return nil + }) + if err != nil { + fmt.Printf("Could not get CowyoData: %s", err) + return err + } + return nil +} + +func (p *CowyoData) save() error { + if !open { + return fmt.Errorf("db must be opened before saving") + } + err := db.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte("datas")) + if err != nil { + return fmt.Errorf("create bucket: %s", err) + } + enc, err := p.encode() + if err != nil { + return fmt.Errorf("could not encode CowyoData: %s", err) + } + err = bucket.Put([]byte(p.Title), enc) + return err + }) + return err +} + +func (p *CowyoData) encode() ([]byte, error) { + enc, err := json.Marshal(p) + if err != nil { + return nil, err + } + return enc, nil +} + +func (p *CowyoData) decode(data []byte) error { + err := json.Unmarshal(data, &p) + if err != nil { + return err + } + return nil +} diff --git a/main.go b/main.go index 22379d3..ff56cdf 100755 --- a/main.go +++ b/main.go @@ -1,120 +1,17 @@ package main import ( - "encoding/json" - "fmt" - "html/template" "log" - "net/http" "os" - "path" - "runtime" - "strings" - "time" - "github.com/boltdb/bolt" "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday" ) -var db *bolt.DB -var open bool var ExternalIP string var AllowedIPs string func init() { - AllowedIPs = "192.168.1.13,192.168.1.12" -} - -func Open() error { - var err error - _, filename, _, _ := runtime.Caller(0) // get full path of this file - dbfile := path.Join(path.Dir(filename), "data.db") - config := &bolt.Options{Timeout: 30 * time.Second} - db, err = bolt.Open(dbfile, 0600, config) - if err != nil { - fmt.Println("Opening BoltDB timed out") - log.Fatal(err) - } - open = true - return nil -} - -func Close() { - open = false - db.Close() -} - -type CowyoData struct { - Title string - Text string -} - -// Database functions - -func (p *CowyoData) load() error { - if !open { - return fmt.Errorf("db must be opened before saving!") - } - err := db.View(func(tx *bolt.Tx) error { - var err error - b := tx.Bucket([]byte("datas")) - if b == nil { - return nil - } - k := []byte(p.Title) - val := b.Get(k) - if val == nil { - return nil - } - err = p.decode(val) - if err != nil { - return err - } - return nil - }) - if err != nil { - fmt.Printf("Could not get CowyoData: %s", err) - return err - } - return nil -} - -func (p *CowyoData) save() error { - if !open { - return fmt.Errorf("db must be opened before saving!") - } - err := db.Update(func(tx *bolt.Tx) error { - bucket, err := tx.CreateBucketIfNotExists([]byte("datas")) - if err != nil { - return fmt.Errorf("create bucket: %s", err) - } - enc, err := p.encode() - if err != nil { - return fmt.Errorf("could not encode CowyoData: %s", err) - } - err = bucket.Put([]byte(p.Title), enc) - return err - }) - return err -} - -func (p *CowyoData) encode() ([]byte, error) { - enc, err := json.Marshal(p) - if err != nil { - return nil, err - } - return enc, nil -} - -func (p *CowyoData) decode(data []byte) error { - err := json.Unmarshal(data, &p) - if err != nil { - return err - } - return nil + AllowedIPs = "192.168.1.13,192.168.1.12,192.168.1.2" } func main() { @@ -126,101 +23,8 @@ func main() { defer Close() r := gin.Default() r.LoadHTMLGlob("templates/*") - - r.GET("/", func(c *gin.Context) { - title := randomAlliterateCombo() - c.Redirect(302, "/"+title) - - }) - r.GET("/:title", func(c *gin.Context) { - title := c.Param("title") - if title == "ws" { - wshandler(c.Writer, c.Request) - } else if strings.ToLower(title) == "about" && strings.Contains(AllowedIPs, c.ClientIP()) != true { - c.Redirect(302, "/about/view") - } else { - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "Title": title, - "ExternalIP": ExternalIP, - }) - } - }) - r.GET("/:title/*option", func(c *gin.Context) { - option := c.Param("option") - title := c.Param("title") - fmt.Println(title, "["+option+"]") - if option == "/view" { - p := CowyoData{strings.ToLower(title), ""} - err := p.load() - if err != nil { - panic(err) - } - - unsafe := blackfriday.MarkdownCommon([]byte(p.Text)) - html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) - c.HTML(http.StatusOK, "view.tmpl", gin.H{ - "Title": title, - "Body": template.HTML(html), - }) - - } else { - c.Redirect(302, "/"+title) - } - }) - + r.GET("/", newNote) + r.GET("/:title", editNote) + r.GET("/:title/*option", everythingElse) r.Run(":12312") } - -var wsupgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -func wshandler(w http.ResponseWriter, r *http.Request) { - conn, err := wsupgrader.Upgrade(w, r, nil) - if err != nil { - fmt.Println("Failed to set websocket upgrade: %+v", err) - return - } - - for { - t, msg, err := conn.ReadMessage() - if err != nil { - break - } - - type Message struct { - TextData string - Title string - UpdateServer bool - UpdateClient bool - } - - var m Message - err = json.Unmarshal(msg, &m) - if err != nil { - panic(err) - } - - if m.UpdateServer { - p := CowyoData{strings.ToLower(m.Title), m.TextData} - err := p.save() - if err != nil { - panic(err) - } - } - if m.UpdateClient { - p := CowyoData{strings.ToLower(m.Title), ""} - err := p.load() - if err != nil { - panic(err) - } - m.TextData = p.Text - } - newMsg, err := json.Marshal(m) - if err != nil { - panic(err) - } - conn.WriteMessage(t, newMsg) - } -} diff --git a/routes.go b/routes.go new file mode 100755 index 0000000..cea3e90 --- /dev/null +++ b/routes.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "html/template" + "io/ioutil" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" +) + +func newNote(c *gin.Context) { + title := randomAlliterateCombo() + c.Redirect(302, "/"+title) +} + +func editNote(c *gin.Context) { + title := c.Param("title") + if title == "ws" { + wshandler(c.Writer, c.Request) + } else if strings.ToLower(title) == "about" && strings.Contains(AllowedIPs, c.ClientIP()) != true { + c.Redirect(302, "/about/view") + } else { + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "Title": title, + "ExternalIP": ExternalIP, + }) + } +} + +func everythingElse(c *gin.Context) { + option := c.Param("option") + title := c.Param("title") + fmt.Println(title, "["+option+"]") + if option == "/view" { + renderMarkdown(c, title) + } else if option == "/list" { + renderList(c, title) + } else if title == "static" { + serveStaticFile(c, option) + } else { + c.Redirect(302, "/"+title) + } +} + +func serveStaticFile(c *gin.Context, option string) { + staticFile, err := ioutil.ReadFile("./static" + option) + if err != nil { + c.AbortWithStatus(404) + } else { + c.Data(200, contentType(option), []byte(staticFile)) + } +} + +func renderMarkdown(c *gin.Context, title string) { + p := CowyoData{strings.ToLower(title), ""} + err := p.load() + if err != nil { + panic(err) + } + unsafe := blackfriday.MarkdownCommon([]byte(p.Text)) + html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) + c.HTML(http.StatusOK, "view.tmpl", gin.H{ + "Title": title, + "Body": template.HTML(html), + }) +} + +func renderList(c *gin.Context, title string) { + p := CowyoData{strings.ToLower(title), ""} + err := p.load() + if err != nil { + panic(err) + } + listItems := []string{} + for _, line := range strings.Split(p.Text, "\n") { + if len(line) > 1 { + listItems = append(listItems, line) + } + } + fmt.Println(listItems) + c.HTML(http.StatusOK, "list.tmpl", gin.H{ + "Title": title, + "ListItems": listItems, + }) +} diff --git a/static/css/view.css b/static/css/view.css new file mode 100755 index 0000000..61b28a4 --- /dev/null +++ b/static/css/view.css @@ -0,0 +1,304 @@ +/** +* yue.css +* +* yue.css is designed for readable content. +* +* Copyright (c) 2013 - 2014 by Hsiaoming Yang. +*/ + +.yue { +font: 400 18px/1.62 "Georgia", "Xin Gothic", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif; +color: #444443; +} + +.windows .yue { +font-size: 16px; +font-family: "Georgia", "SimSun", sans-serif; +} + +.yue ::-moz-selection { +background-color: rgba(0,0,0,0.2); +} + +.yue ::selection { +background-color: rgba(0,0,0,0.2); +} + +.yue h1, +.yue h2, +.yue h3, +.yue h4, +.yue h5, +.yue h6 { +font-family: "Georgia", "Xin Gothic", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", "SimSun", sans-serif; +color: #222223; +} + +.yue h1 { +font-size: 1.8em; +margin: 0.67em 0; +} + +.yue > h1 { +margin-top: 0; +font-size: 2em; +} + +.yue h2 { +font-size: 1.5em; +margin: 0.83em 0; +} + +.yue h3 { +font-size: 1.17em; +margin: 1em 0; +} + +.yue h4, +.yue h5, +.yue h6 { +font-size: 1em; +margin: 1.6em 0 1em 0; +} + +.yue h6 { +font-weight: 500; +} + +.yue p { +margin-top: 0; +margin-bottom: 1.46em; +} + +.yue a { +color: #111; +word-wrap: break-word; +-moz-text-decoration-color: rgba(0, 0, 0, 0.4); +text-decoration-color: rgba(0, 0, 0, 0.4); +} + +.yue a:hover { +color: #555; +-moz-text-decoration-color: rgba(0, 0, 0, 0.6); +text-decoration-color: rgba(0, 0, 0, 0.6); +} + +.yue h1 a, +.yue h2 a, +.yue h3 a { +text-decoration: none; +} + +.yue strong, +.yue b { +font-weight: 700; +color: #222223; +} + +.yue em, +.yue i { +font-style: italic; +color: #222223; +} + +.yue img { +max-width: 100%; +height: auto; +margin: 0.2em 0; +} + +.yue a img { +/* Remove border on IE */ +border: none; +} + +.yue figure { +position: relative; +clear: both; +outline: 0; +margin: 10px 0 30px; +padding: 0; +min-height: 100px; +} + +.yue figure img { +display: block; +max-width: 100%; +margin: auto auto 4px; +box-sizing: border-box; +} + +.yue figure figcaption { +position: relative; +width: 100%; +text-align: center; +left: 0; +margin-top: 10px; +font-weight: 400; +font-size: 14px; +color: #666665; +} + +.yue figure figcaption a { +text-decoration: none; +color: #666665; +} + +.yue hr { +display: block; +width: 14%; +margin: 40px auto 34px; +border: 0 none; +border-top: 3px solid #dededc; +} + +.yue blockquote { +margin: 0 0 1.64em 0; +border-left: 3px solid #dadada; +padding-left: 12px; +color: #666664; +} + +.yue blockquote a { +color: #666664; +} + +.yue ul, +.yue ol { +margin: 0 0 24px 6px; +padding-left: 16px; +} + +.yue ul { +list-style-type: square; +} + +.yue ol { +list-style-type: decimal; +} + +.yue li { +margin-bottom: 0.2em; +} + +.yue li ul, +.yue li ol { +margin-top: 0; +margin-bottom: 0; +margin-left: 14px; +} + +.yue li ul { +list-style-type: disc; +} + +.yue li ul ul { +list-style-type: circle; +} + +.yue li p { +margin: 0.4em 0 0.6em; +} + +.yue .unstyled { +list-style-type: none; +margin: 0; +padding: 0; +} + +.yue code, +.yue tt { +color: #808080; +font-size: 0.96em; +background-color: #f9f9f7; +padding: 1px 2px; +border: 1px solid #dadada; +border-radius: 3px; +font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +word-wrap: break-word; +} + +.yue pre { +margin: 1.64em 0; +padding: 7px; +border: none; +border-left: 3px solid #dadada; +padding-left: 10px; +overflow: auto; +line-height: 1.5; +font-size: 0.96em; +font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +color: #4c4c4c; +background-color: #f9f9f7; +} + +.yue pre code, +.yue pre tt { +color: #4c4c4c; +border: none; +background: none; +padding: 0; +} + +.yue table { +width: 100%; +max-width: 100%; +border-collapse: collapse; +border-spacing: 0; +margin-bottom: 1.5em; +font-size: 0.96em; +box-sizing: border-box; +} + +.yue th, +.yue td { +text-align: left; +padding: 4px 8px 4px 10px; +border: 1px solid #dadada; +} + +.yue td { +vertical-align: top; +} + +.yue tr:nth-child(even) { +background-color: #efefee; +} + +.yue iframe { +display: block; +max-width: 100%; +margin-bottom: 30px; +} + +.yue figure iframe { +margin: auto; +} + +.yue table pre { +margin: 0; +padding: 0; +border: none; +background: none; +} + +@media (min-width: 1100px) { +.yue blockquote { + margin-left: -24px; + padding-left: 20px; + border-width: 4px; +} + +.yue blockquote blockquote { + margin-left: 0; +} +} +body { +margin: 0; +padding: 0.4em 1em 6em; +background: #fff; +} +.yue { +max-width: 650px; +margin: 0 auto; +} diff --git a/static/js/cowyo.js b/static/js/cowyo.js new file mode 100755 index 0000000..26059d7 --- /dev/null +++ b/static/js/cowyo.js @@ -0,0 +1,60 @@ +$(document).ready(function() { + var isTyping = false; + var typingTimer; //timer identifier + var updateInterval; + var doneTypingInterval = 1000; //time in ms, 5 second for example + + //on keyup, start the countdown + $('#emit').keyup(function() { + clearTimeout(typingTimer); + clearInterval(updateInterval); + typingTimer = setTimeout(doneTyping, doneTypingInterval); + }); + + //on keydown, clear the countdown + $('#emit').keydown(function() { + clearTimeout(typingTimer); + clearInterval(updateInterval); + document.title = "[UNSAVED] " + title_name; + }); + + //user is "finished typing," do something + function doneTyping() { + payload = JSON.stringify({ TextData: $('#emit_data').val(), Title: title_name, UpdateServer: true, UpdateClient: false }) + send(payload) + console.log("Done typing") + updateInterval = setInterval(updateText, doneTypingInterval); + document.title = "[SAVED] " + title_name; + } + + function updateText() { + console.log("Getting server's latest copy") + payload = JSON.stringify({ TextData: $('#emit_data').val(), Title: title_name, UpdateServer: false, UpdateClient: true }) + send(payload) + } + + // websockets + url = 'ws://'+external_ip+'/ws'; + c = new WebSocket(url); + + send = function(data){ + console.log("Sending: " + data) + c.send(data) + } + + c.onmessage = function(msg){ + console.log(msg) + data = JSON.parse(msg.data); + if (data.UpdateClient == true) { + console.log("Updating...") + $('#emit_data').val(data.TextData) + document.title = "[LOADED] " + title_name; + } + console.log(data) + } + + c.onopen = function(){ + updateText(); + updateInterval = setInterval(updateText, doneTypingInterval); + } +}); diff --git a/static/jquery-1.8.1.min.js b/static/js/jquery-1.8.1.min.js similarity index 100% rename from static/jquery-1.8.1.min.js rename to static/js/jquery-1.8.1.min.js diff --git a/static/jquery.autogrowtextarea.min.js b/static/js/jquery.autogrowtextarea.min.js similarity index 100% rename from static/jquery.autogrowtextarea.min.js rename to static/js/jquery.autogrowtextarea.min.js diff --git a/templates/base.tmpl b/templates/base.tmpl new file mode 100755 index 0000000..e69de29 diff --git a/templates/index.tmpl b/templates/index.tmpl index 50ccaff..14a4888 100755 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -23,100 +23,16 @@ - + + + - - - - - - -
- {{ .Body }} -
- - - - - + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ .Body }} +
+ + + + + diff --git a/utils.go b/utils.go index a2621eb..6f27e0c 100755 --- a/utils.go +++ b/utils.go @@ -35,3 +35,17 @@ func randomAlliterateCombo() (combo string) { } return } + +func contentType(filename string) string { + switch { + case strings.Contains(filename, ".css"): + return "text/css" + case strings.Contains(filename, ".jpg"): + return "image/jpeg" + case strings.Contains(filename, ".png"): + return "image/png" + case strings.Contains(filename, ".js"): + return "application/javascript" + } + return "text/html" +} diff --git a/websockets.go b/websockets.go new file mode 100755 index 0000000..af7033d --- /dev/null +++ b/websockets.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gorilla/websocket" +) + +var wsupgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func wshandler(w http.ResponseWriter, r *http.Request) { + conn, err := wsupgrader.Upgrade(w, r, nil) + if err != nil { + fmt.Println("Failed to set websocket upgrade: %+v", err) + return + } + + for { + t, msg, err := conn.ReadMessage() + if err != nil { + break + } + + type Message struct { + TextData string + Title string + UpdateServer bool + UpdateClient bool + } + + var m Message + err = json.Unmarshal(msg, &m) + if err != nil { + panic(err) + } + + if m.UpdateServer { + p := CowyoData{strings.ToLower(m.Title), m.TextData} + err := p.save() + if err != nil { + panic(err) + } + } + if m.UpdateClient { + p := CowyoData{strings.ToLower(m.Title), ""} + err := p.load() + if err != nil { + panic(err) + } + m.UpdateClient = len(m.TextData) != len(p.Text) + m.TextData = p.Text + } + newMsg, err := json.Marshal(m) + if err != nil { + panic(err) + } + conn.WriteMessage(t, newMsg) + } +}