├── .gitignore ├── go.mod ├── README.md ├── broadcast.go ├── LICENSE.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /imgboard 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/donatj/imgboard 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imgboard 2 | 3 | Super Fun JavaScript-Free Drawing Board 4 | -------------------------------------------------------------------------------- /broadcast.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | type imgdata []byte 6 | 7 | type broadcast struct { 8 | channels map[chan imgdata]bool 9 | 10 | sync.Mutex 11 | } 12 | 13 | func newBroadcast() *broadcast { 14 | return &broadcast{ 15 | channels: make(map[chan imgdata]bool), 16 | } 17 | } 18 | 19 | func (b *broadcast) Register() chan imgdata { 20 | b.Lock() 21 | defer b.Unlock() 22 | 23 | c := make(chan imgdata, 10) 24 | b.channels[c] = true 25 | 26 | return c 27 | } 28 | 29 | func (b *broadcast) Clear(c chan imgdata) { 30 | b.Lock() 31 | defer b.Unlock() 32 | 33 | delete(b.channels, c) 34 | close(c) 35 | } 36 | 37 | func (b *broadcast) Broadcast(data []byte) { 38 | b.Lock() 39 | defer b.Unlock() 40 | 41 | for c := range b.channels { 42 | c <- data 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2016 Jesse G. Donat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/jpeg" 10 | "log" 11 | "net/http" 12 | "strconv" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | ) 17 | 18 | var ( 19 | httpPort = flag.Uint("http-port", 8080, "http port to run on") 20 | ) 21 | 22 | var ( 23 | boundry = "spiderman" 24 | m = image.NewRGBA(image.Rect(0, 0, 800, 800)) 25 | mut = sync.RWMutex{} 26 | 27 | numOnline int64 28 | 29 | bc = newBroadcast() 30 | ) 31 | 32 | func init() { 33 | flag.Parse() 34 | } 35 | 36 | func indexHandler(w http.ResponseWriter, r *http.Request) { 37 | w.Header().Set("Content-Type", "text/html") 38 | fmt.Fprint(w, ` 39 |
`) 51 | } 52 | 53 | func mjpegHandler(w http.ResponseWriter, r *http.Request) { 54 | atomic.AddInt64(&numOnline, 1) 55 | 56 | n, ok := w.(http.CloseNotifier) 57 | if !ok { 58 | http.Error(w, "cannot stream - no closer", http.StatusInternalServerError) 59 | return 60 | } 61 | 62 | f, ok := w.(http.Flusher) 63 | if !ok { 64 | http.Error(w, "cannot stream - no flush", http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | w.Header().Set("Cache-Control", "no-cache") 69 | w.Header().Set("Cache-Control", "private") 70 | w.Header().Set("Pragma", "no-cache") 71 | w.Header().Set("Content-type", "multipart/x-mixed-replace; boundary="+boundry) 72 | 73 | datac := bc.Register() 74 | 75 | data := imgbytes() 76 | for i := 0; i <= 10; i++ { 77 | writeFrame(w, data) 78 | f.Flush() 79 | } 80 | fmt.Println("...") 81 | 82 | for { 83 | 84 | t := time.NewTimer(5 * time.Second) 85 | 86 | select { 87 | case data = <-datac: 88 | for i := 0; i <= 1; i++ { 89 | writeFrame(w, data) 90 | f.Flush() 91 | } 92 | case <-n.CloseNotify(): 93 | atomic.AddInt64(&numOnline, -1) 94 | fmt.Println("...closed") 95 | 96 | bc.Clear(datac) 97 | 98 | return 99 | case <-t.C: 100 | writeFrame(w, data) 101 | f.Flush() 102 | } 103 | 104 | } 105 | } 106 | 107 | func writeFrame(w http.ResponseWriter, data []byte) { 108 | fmt.Fprintf(w, "--%s\n", boundry) 109 | fmt.Fprint(w, "Content-Type: image/jpeg\n\n") 110 | w.Write(data) 111 | fmt.Fprint(w, "\n\n") 112 | } 113 | 114 | func clickHandler(w http.ResponseWriter, r *http.Request) { 115 | x, err := strconv.Atoi(r.FormValue("imgbtn.x")) 116 | if err != nil { 117 | http.Error(w, "invalid int for x", 400) 118 | return 119 | } 120 | 121 | y, err := strconv.Atoi(r.FormValue("imgbtn.y")) 122 | if err != nil { 123 | http.Error(w, "invalid int for y", 400) 124 | return 125 | } 126 | 127 | c := color.RGBA{255, 255, 0, 255} 128 | 129 | s, err := strconv.Atoi(r.FormValue("size")) 130 | if err != nil { 131 | http.Error(w, "invalid int for size", 400) 132 | return 133 | } 134 | 135 | if s < 0 || s > 100 { 136 | http.Error(w, "invalid size", 400) 137 | return 138 | } 139 | 140 | fc := r.FormValue("color") 141 | if fc != "" { 142 | if len(fc) != 7 || fc[0] != '#' { 143 | http.Error(w, "invalid color", 400) 144 | return 145 | } 146 | 147 | cr, err1 := strconv.ParseUint(fc[1:3], 16, 8) 148 | cg, err2 := strconv.ParseUint(fc[3:5], 16, 8) 149 | cb, err3 := strconv.ParseUint(fc[5:7], 16, 8) 150 | 151 | if err1 != nil || err2 != nil || err3 != nil { 152 | http.Error(w, "invalid color", 400) 153 | return 154 | } 155 | 156 | c = color.RGBA{uint8(cr), uint8(cg), uint8(cb), 255} 157 | } 158 | 159 | mut.Lock() 160 | for xx := 0 - s; xx <= s; xx++ { 161 | for yy := 0 - s; yy <= s; yy++ { 162 | m.Set(x+xx, y+yy, c) 163 | } 164 | } 165 | mut.Unlock() 166 | 167 | bc.Broadcast(imgbytes()) 168 | 169 | fmt.Println(x, y) 170 | w.WriteHeader(http.StatusNoContent) 171 | 172 | } 173 | 174 | func imgbytes() []byte { 175 | mut.Lock() 176 | defer mut.Unlock() 177 | 178 | var bb bytes.Buffer 179 | jpeg.Encode(&bb, m, &jpeg.Options{Quality: 70}) 180 | return bb.Bytes() 181 | } 182 | 183 | func main() { 184 | http.HandleFunc("/", indexHandler) 185 | http.HandleFunc("/click", clickHandler) 186 | http.HandleFunc("/image", mjpegHandler) 187 | 188 | go func() { 189 | var lastOnline int64 190 | for { 191 | online := atomic.LoadInt64(&numOnline) 192 | if online != lastOnline { 193 | log.Println(online, " users online") 194 | } 195 | time.Sleep(time.Second / 2) 196 | lastOnline = online 197 | } 198 | }() 199 | 200 | err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil) 201 | if err != nil { 202 | panic(err) 203 | } 204 | } 205 | --------------------------------------------------------------------------------