Browse Source

OOPS that jpeg thing was not needed. first commit of app

main
forest 4 months ago
parent
commit
83b80613c5
12 changed files with 497 additions and 1229 deletions
  1. +1
    -0
      .gitignore
  2. +0
    -0
      README.md
  3. +11
    -0
      go.mod
  4. +10
    -0
      go.sum
  5. +54
    -0
      index.css
  6. +22
    -0
      index.gotemplate.html
  7. +0
    -11
      jpeg-encoder/build.sh
  8. +0
    -46
      jpeg-encoder/main.cpp
  9. +0
    -1172
      jpeg-encoder/tinyjpeg.h
  10. +398
    -0
      main.go
  11. BIN
      static/spray-paint.png
  12. +1
    -0
      user-content/README.md

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
graffiti-bolt.db

+ 0
- 0
README.md View File


+ 11
- 0
go.mod View File

@ -0,0 +1,11 @@
module git.sequentialread.com/forest/graffiti
go 1.16
require (
git.sequentialread.com/forest/pkg-errors v0.9.2 // indirect
github.com/boltdb/bolt v1.3.1 // indirect
github.com/shengdoushi/base58 v1.0.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect
)

+ 10
- 0
go.sum View File

@ -0,0 +1,10 @@
git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO6ZR26Nv5nest9g=
git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 54
- 0
index.css View File

@ -0,0 +1,54 @@
body {
background: rgb(11, 129, 138);
}
.modal-container {
position: fixed;
top:0;
left:0;
width:100%;
height:100%;
background: rgba(230,230,230,0.7);
z-index: 100;
}
.loader,
.loader:before,
.loader:after {
background: rgba(0,0,0,0.4);
animation: load1 1s infinite ease-in-out;
width: 1em;
height: 4em;
}
.loader:before,
.loader:after {
position: absolute;
top: 0;
content: '';
}
.loader:before {
left: -1.5em;
animation-delay: -0.32s;
}
.loader {
color: rgba(0,0,0,0.4);
text-indent: -9999em;
margin: 160px auto;
position: relative;
font-size: 11px;
transform: translateZ(0);
animation-delay: -0.16s;
}
.loader:after {
left: 1.5em;
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

+ 22
- 0
index.gotemplate.html View File

@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ .Title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="static/app.css" rel="stylesheet">
<script>
window.graffitiAppState = {{ .AppStateJSON }};
</script>
<style>
{{ .PreloadCSS }}
</style>
</head>
<body>
<div class="modal-container" id="progress-container">
<div class="loader">loading...</div>
</div>
</body>
</html>

+ 0
- 11
jpeg-encoder/build.sh View File

@ -1,11 +0,0 @@
#!/bin/bash
set -e
emcc --bind -O3 --memory-init-file 0 \
-s NO_FILESYSTEM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 \
-s 'EXPORT_NAME="TinyJpegEncoder"' \
--std=c++11 \
-o ./TinyJpegEncoder.js \
-x c++ \
main.cpp

+ 0
- 46
jpeg-encoder/main.cpp View File

@ -1,46 +0,0 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <string.h>
#include <exception>
#define TJE_IMPLEMENTATION
#include "tinyjpeg.h"
using namespace emscripten;
// one megabyte 🤭😅
uint8_t *resultBuffer[1000000];
int resultLength;
// 1: crap quality, small file
// 2: medium
// 3: nicer quality, bigger file
const int qualitySetting = 2;
// HTML canvas uses 4, RGBA order.
const int numberOfColorChannels = 4;
void write(void* context, void* data, int size)
{
printf("memcpy( &resultBuffer[%d], data, %d); \n", resultLength, size);
memcpy( &resultBuffer[resultLength], data, size);
resultLength += size;
printf("resultLength = %d \n", resultLength);
}
val encode(std::string rgb_data_in, int width, int height)
{
resultLength = 0;
printf("strlen(rgb_data_in) = %d \n", (int)rgb_data_in.length());
tje_encode_with_func(write, NULL, qualitySetting, width, height, numberOfColorChannels, (unsigned char*)rgb_data_in.c_str());
return val(typed_memory_view(resultLength, (uint8_t*)resultBuffer));
}
EMSCRIPTEN_BINDINGS(TinyJpegEncoder)
{
function("encode", &encode);
}

+ 0
- 1172
jpeg-encoder/tinyjpeg.h
File diff suppressed because it is too large
View File


+ 398
- 0
main.go View File

@ -0,0 +1,398 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"text/template"
errors "git.sequentialread.com/forest/pkg-errors"
"github.com/boltdb/bolt"
base58 "github.com/shengdoushi/base58"
)
var db *bolt.DB
type PostType int
const (
PostTypeText PostType = 1
PostTypeImage PostType = 2
PostTypeVideo PostType = 3
)
type Post struct {
Id string
X float64
Y float64
Angle float64
AuthorId string
AuthorDisplayName string
Type PostType
TextContent string
URL string
Filename string
ContentType string
}
type IndexTemplateData struct {
Title string
AppStateJSON string
PreloadCSS string
}
type FrontendState struct {
Version string
Invited bool
IsAuthor bool
DisplayName string
TemporaryError string
}
type Author struct {
Id string
DisplayName string
Ancestry []string
}
var pendingUploadMap = map[string]Post{}
var progressMap = map[string]int{}
func main() {
appVersion := "0.0.0"
appTitle := "pride 2021 graffiti by cyberia.club"
listenPort := 8080
db, err := bolt.Open("graffiti-bolt.db", 0600, nil)
if err != nil {
log.Fatalf("can't start because can't open/create database file graffiti-bolt.db: %s", err)
}
defer db.Close()
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("invite_codes"))
if err == nil {
_, err = tx.CreateBucketIfNotExists([]byte("authors"))
}
return err
})
if err != nil {
log.Fatalf("can't start because can't initialize the database: %s", err)
}
var indexTemplate *template.Template
indexTemplateBytes, err := ioutil.ReadFile("index.gotemplate.html")
if err == nil {
indexTemplate, err = template.New("index.html").Parse(string(indexTemplateBytes))
}
if err != nil {
log.Fatalf("can't start becasue can't read index.gotemplate.html: %s", err)
}
indexCSSBytes, err := ioutil.ReadFile("index.css")
if err != nil {
log.Fatalf("can't start becasue can't read index.css: %s", err)
}
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {
author := getAuthor(db, request)
displayName := ""
if author != nil {
displayName = author.DisplayName
}
temporaryError := getCookie(request, "graffiti_temporary_error")
if temporaryError != "" {
setCookie(response, "graffiti_temporary_error", "", 0)
}
stateJSONBytes, err := json.MarshalIndent(
FrontendState{
Version: appVersion,
Invited: getCookie(request, "graffiti_invite_code") != "",
IsAuthor: author != nil,
DisplayName: displayName,
TemporaryError: temporaryError,
},
" ", " ",
)
var buffer bytes.Buffer
if err == nil {
err = indexTemplate.Execute(
&buffer,
IndexTemplateData{
Title: appTitle,
AppStateJSON: strings.TrimSpace(string(stateJSONBytes)),
PreloadCSS: string(indexCSSBytes),
},
)
}
if err != nil {
log.Printf("internal server error: indexTemplate.Execute: %s\n", err)
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.Header().Set("Content-Type", "text/html")
response.Header().Set("Content-Length", strconv.Itoa(buffer.Len()))
io.Copy(response, &buffer)
})
http.HandleFunc("/invite-code/", func(response http.ResponseWriter, request *http.Request) {
code := getLastElementInURLPath(request.URL)
setCookie(response, "graffiti_invite_code", code, 60*60*24)
http.Redirect(response, request, "/", http.StatusFound)
})
http.HandleFunc("/register", func(response http.ResponseWriter, request *http.Request) {
code := getCookie(request, "graffiti_invite_code")
displayName := request.URL.Query().Get("displayName")
err = db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("invite_codes"))
authorsBucket := tx.Bucket([]byte("authors"))
isFirstAuthor := authorsBucket.Stats().KeyN == 0
idOfAuthorWhoCreatedThisInviteCode := bucket.Get([]byte(code))
if idOfAuthorWhoCreatedThisInviteCode != nil || isFirstAuthor {
newAuthorId, err := createAuthor(tx, string(idOfAuthorWhoCreatedThisInviteCode), displayName)
if err != nil {
return err
}
bucket.Delete([]byte(code))
setCookie(response, "graffiti_author_id", newAuthorId, 60*60*24*365)
} else {
setCookie(response, "graffiti_temporary_error", "invite_code_missing_or_already_used", 60*60)
}
return nil
})
if err != nil {
log.Printf("internal server error: registration transaction failed: %s\n", err)
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
http.Redirect(response, request, "/", http.StatusFound)
})
http.HandleFunc("/upload-meta/", func(response http.ResponseWriter, request *http.Request) {
if request.Method == "POST" {
author := getAuthor(db, request)
if author == nil {
http.Error(response, "Unauthorized: graffiti_author_id cookie is missing or invalid", http.StatusUnauthorized)
return
}
var newPost Post
err := json.NewDecoder(request.Body).Decode(&newPost)
if err != nil {
http.Error(response, "Bad Request", http.StatusBadRequest)
return
}
if newPost.Type != PostTypeText && newPost.Type != PostTypeImage && newPost.Type != PostTypeVideo {
http.Error(response, "Bad Request: Unknown post type", http.StatusBadRequest)
return
}
postIdBuffer := make([]byte, 16)
rand.Read(postIdBuffer)
postId := base58.Encode(postIdBuffer, base58.BitcoinAlphabet)
newPost.Id = postId
newPost.AuthorId = author.Id
newPost.Filename = regexp.MustCompile(`[^a-zA-Z0-9._-]+`).ReplaceAllString(newPost.Filename, "_")
newPost.Filename = fmt.Sprintf("%s_%s", postId, newPost.Filename)
if newPost.Type != PostTypeText {
pendingUploadMap[postId] = newPost
} else {
// TODO add post to DB
}
response.Write([]byte(postId))
} else {
response.Header().Set("Allow", "POST")
http.Error(response, "Try POSTing instead", http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/upload/", func(response http.ResponseWriter, request *http.Request) {
if request.Method == "POST" {
uploadId := getLastElementInURLPath(request.URL)
if uploadId == "" {
http.Error(response, "malformed url", http.StatusNotFound)
return
}
post, hasPost := pendingUploadMap[uploadId]
if !hasPost {
http.Error(response, fmt.Sprintf("upload '%s' not found", uploadId), http.StatusNotFound)
return
}
multipartReader, err := request.MultipartReader()
if err != nil {
http.Error(response, "failed to read multipart upload", http.StatusNotFound)
return
}
length := request.ContentLength
fileName := post.Filename
dstFile, err := os.OpenFile(path.Join("user-content", fileName), os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
for {
part, err := multipartReader.NextPart()
if err == io.EOF {
break
}
var read int64
for {
buffer := make([]byte, 1000000)
cBytes, err := part.Read(buffer)
if err != nil && err != io.EOF {
http.Error(response, "an error occurred while uploading this file, please try again :(", http.StatusInternalServerError)
fmt.Printf("an error occurred while reading the multipart request body: %s\n", err)
return
}
if cBytes > 0 {
read = read + int64(cBytes)
progressMap[uploadId] = int(float32(read) / float32(length) * 100)
_, err := dstFile.Write(buffer[0:cBytes])
if err != nil {
http.Error(response, "an error occurred while uploading this file, please try again :(", http.StatusInternalServerError)
fmt.Printf("an error occurred while writing to %s: %s\n", path.Join("user-content", fileName), err)
return
}
}
if err == io.EOF {
break
}
}
}
} else {
response.Header().Set("Allow", "POST")
http.Error(response, "Try POSTing instead", http.StatusMethodNotAllowed)
}
})
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
http.Handle("/user-content/", http.StripPrefix("/user-content/", http.FileServer(http.Dir("./user-content/"))))
log.Printf(" 🎨🖌️🖼️ Graffiti is listening on ':%d'\n", listenPort)
err = http.ListenAndServe(fmt.Sprintf(":%d", listenPort), nil)
// if it got this far it means the server crashed!
panic(errors.Wrap(err, "http server crashed"))
}
func createAuthor(tx *bolt.Tx, parentAuthorId string, displayName string) (string, error) {
bucket := tx.Bucket([]byte("authors"))
newAuthorAncestry := []string{}
parentAuthorBytes := bucket.Get([]byte(parentAuthorId))
if parentAuthorBytes != nil {
var parentAuthor Author
err := json.Unmarshal(parentAuthorBytes, &parentAuthor)
if err != nil {
return "", err
}
newAuthorAncestry = append(parentAuthor.Ancestry, parentAuthorId)
}
newAuthorId := getRandomId(8)
newAuthorBytes, err := json.Marshal(Author{
Id: newAuthorId,
DisplayName: displayName,
Ancestry: newAuthorAncestry,
})
if err != nil {
return "", err
}
err = bucket.Put([]byte(newAuthorId), newAuthorBytes)
if err != nil {
return "", err
}
return newAuthorId, nil
}
func getAuthor(db *bolt.DB, request *http.Request) *Author {
var author *Author = nil
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("authors"))
authorBytes := bucket.Get([]byte(getCookie(request, "graffiti_author_id")))
if authorBytes != nil {
var temp Author
err := json.Unmarshal(authorBytes, &temp)
if err != nil {
return err
}
author = &temp
}
return nil
})
if err != nil {
log.Printf("error in getAuthor(): %s\n", err)
}
return author
}
func getRandomId(bytesCount int) string {
randomIdBuffer := make([]byte, bytesCount)
rand.Read(randomIdBuffer)
return base58.Encode(randomIdBuffer, base58.BitcoinAlphabet)
}
func setCookie(response http.ResponseWriter, key, value string, maxAge int) {
http.SetCookie(response, &http.Cookie{
Name: key,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
Value: value,
MaxAge: maxAge,
})
}
func getCookie(request *http.Request, name string) string {
for _, cookie := range request.Cookies() {
if cookie.Name == name {
return cookie.Value
}
}
return ""
}
func getLastElementInURLPath(url *url.URL) string {
rawParts := strings.Split(url.Path, "/")
pathParts := []string{}
for _, p := range rawParts {
if p != "" {
pathParts = append(pathParts, p)
}
}
if len(pathParts) > 0 {
return pathParts[len(pathParts)-1]
}
return ""
}

BIN
static/spray-paint.png View File

Before After
Width: 128  |  Height: 128  |  Size: 12 KiB

+ 1
- 0
user-content/README.md View File

@ -0,0 +1 @@
folder where user content will be uploaded

Loading…
Cancel
Save