TBD
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

701 lines
21 KiB

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/rand"
  5. "encoding/base64"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "log"
  11. "math"
  12. "net/http"
  13. "net/url"
  14. "os"
  15. "path"
  16. "runtime/debug"
  17. "strconv"
  18. "strings"
  19. "text/template"
  20. "time"
  21. spatial "git.sequentialread.com/forest/modular-spatial-index"
  22. errors "git.sequentialread.com/forest/pkg-errors"
  23. "github.com/boltdb/bolt"
  24. base58 "github.com/shengdoushi/base58"
  25. )
  26. type PostType int
  27. const (
  28. PostTypeText PostType = 1
  29. PostTypeImage PostType = 2
  30. PostTypeVideo PostType = 3
  31. )
  32. type Post struct {
  33. Id string `json:"id,omitempty"`
  34. SpatialKey64 string `json:"id,omitempty"`
  35. X int `json:"x,omitempty"`
  36. Y int `json:"y,omitempty"`
  37. Angle int `json:"angle,omitempty"`
  38. Width int `json:"width,omitempty"`
  39. Height int `json:"height,omitempty"`
  40. Collider *PostCollider `json:"collider,omitempty"`
  41. AuthorId string `json:"authorId,omitempty"`
  42. AuthorDisplayName string `json:"authorDisplayName,omitempty"`
  43. RandomSeed int `json:"randomSeed,omitempty"`
  44. Type PostType `json:"type,omitempty"`
  45. TextContent string `json:"textContent,omitempty"`
  46. TransparentImage bool `json:"transparentImage,omitempty"`
  47. ThumbnailJPEGBase64 string `json:"thumbnailJPEGBase64,omitempty"`
  48. Filename string `json:"filename,omitempty"`
  49. ContentType string `json:"contentType,omitempty"`
  50. MillisecondsSinceUnixEpoch int64 `json:"millisecondsSinceUnixEpoch,omitempty"`
  51. }
  52. type PostCollider struct {
  53. R int `json:"r,omitempty"`
  54. X1 int `json:"x1,omitempty"`
  55. Y1 int `json:"y1,omitempty"`
  56. X2 int `json:"x2,omitempty"`
  57. Y2 int `json:"y2,omitempty"`
  58. }
  59. type IndexTemplateData struct {
  60. Title string
  61. AppStateJSON string
  62. PreloadCSS string
  63. }
  64. type FrontendState struct {
  65. Version string
  66. Invited bool
  67. IsAuthor bool
  68. DisplayName string
  69. TemporaryError string
  70. }
  71. type Author struct {
  72. Id string
  73. DisplayName string
  74. Ancestry []string
  75. }
  76. var db *bolt.DB
  77. var pendingUploadMap = map[string]Post{}
  78. var progressMap = map[string]int{}
  79. var cookieSecureHTTPSOnly = false
  80. var bpgencPath = "/home/forest/Desktop/git/bpg/bpgenc"
  81. var getPostsRadius = 2000
  82. var appVersion = "0.0.0"
  83. var appTitle = "pride 2021 graffiti by cyberia.club"
  84. var listenPort = 8080
  85. var indexTemplate *template.Template
  86. var indexCSSBytes []byte
  87. var spatialIndex *spatial.SpatialIndex2D
  88. // the spatial index has a worst-case (about 3x slower) performance near 0,0.
  89. // so we simply shift the universe over by 25 units so the home page query lands in a more performant area.
  90. var sillyHilbertOptimizationOffset = 50
  91. // we scale the pixel-based world down by this much before storing in the spatial index,
  92. // so that we don't run out of space on our 32 bit spatial index which is only 32766 units wide
  93. var spatialIndexUnitSizeInPixels = float64(64)
  94. var pixelsToSpatialIndexUnits = float64(1) / spatialIndexUnitSizeInPixels
  95. // TODO set these to default to false for prod
  96. // create xyz.bpg.png files (decoded from bpg) so it's easier to view them and compare the quality, etc.
  97. var debugBPG = true
  98. // verbose logging
  99. var debugLog = true
  100. func main() {
  101. // sane defaults for golang runtime 🤮
  102. debug.SetPanicOnFault(true)
  103. var err error
  104. // Force 32 bit index so it will always be compatible no matter what CPU arch.
  105. // normally you would do spatial.NewSpatialIndex2D(bits.UintSize))
  106. spatialIndex, err = spatial.NewSpatialIndex2D(32)
  107. if err != nil {
  108. log.Fatalf("can't start because can't create spatial index: %+v", err)
  109. }
  110. envPath := os.Getenv("BPGENC_PATH")
  111. if envPath != "" {
  112. bpgencPath = envPath
  113. }
  114. debugBPG = getBooleanEnvVar("DEBUG_BPG", debugBPG)
  115. debugLog = getBooleanEnvVar("DEBUG_LOG", debugLog)
  116. if os.Getenv("LOG_LEVEL") == "DEBUG" || os.Getenv("LOG_LEVEL") == "VERBOSE" {
  117. debugLog = true
  118. }
  119. db, err = bolt.Open("graffiti-bolt.db", 0600, nil)
  120. if err != nil {
  121. log.Fatalf("can't start because can't open/create database file graffiti-bolt.db: %s", err)
  122. }
  123. defer db.Close()
  124. err = db.Update(func(tx *bolt.Tx) error {
  125. _, err := tx.CreateBucketIfNotExists([]byte("invite_codes"))
  126. if err == nil {
  127. _, err = tx.CreateBucketIfNotExists([]byte("authors"))
  128. }
  129. if err == nil {
  130. _, err = tx.CreateBucketIfNotExists([]byte("posts_by_author"))
  131. }
  132. if err == nil {
  133. _, err = tx.CreateBucketIfNotExists([]byte("posts_spatial"))
  134. }
  135. if err == nil {
  136. _, err = tx.CreateBucketIfNotExists([]byte("bpg_encode_queue"))
  137. }
  138. return err
  139. })
  140. if err != nil {
  141. log.Fatalf("can't start because can't initialize the database: %s", err)
  142. }
  143. indexTemplateBytes, err := ioutil.ReadFile("index.gotemplate.html")
  144. if err == nil {
  145. indexTemplate, err = template.New("index.html").Parse(string(indexTemplateBytes))
  146. }
  147. if err != nil {
  148. log.Fatalf("can't start becasue can't read index.gotemplate.html: %s", err)
  149. }
  150. indexCSSBytes, err = ioutil.ReadFile("index.css")
  151. if err != nil {
  152. log.Fatalf("can't start becasue can't read index.css: %s", err)
  153. }
  154. mux := http.NewServeMux()
  155. setupRoutes(mux)
  156. mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
  157. mux.Handle("/user-content/", http.StripPrefix("/user-content/", http.FileServer(http.Dir("./user-content/"))))
  158. go runBPGEncodeQueueConsumer()
  159. log.Printf(" 🎨🖼️🖌️ Graffiti is listening on ':%d'\n", listenPort)
  160. err = http.ListenAndServe(fmt.Sprintf(":%d", listenPort), mux)
  161. // if it got this far it means the server crashed!
  162. panic(errors.Wrap(err, "http server crashed"))
  163. }
  164. func setupRoutes(mux *http.ServeMux) {
  165. mux.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {
  166. if request.URL.Path != "" && request.URL.Path != "/" {
  167. http.Error(response, "Not Found", http.StatusNotFound)
  168. return
  169. }
  170. author := getAuthor(db, request)
  171. displayName := ""
  172. if author != nil {
  173. displayName = author.DisplayName
  174. }
  175. temporaryError := getCookie(request, "graffiti_temporary_error")
  176. if temporaryError != "" {
  177. setCookie(response, "graffiti_temporary_error", "", 0)
  178. }
  179. stateJSONBytes, err := json.MarshalIndent(
  180. FrontendState{
  181. Version: appVersion,
  182. Invited: getCookie(request, "graffiti_invite_code") != "",
  183. IsAuthor: author != nil,
  184. DisplayName: displayName,
  185. TemporaryError: temporaryError,
  186. },
  187. " ", " ",
  188. )
  189. var buffer bytes.Buffer
  190. if err == nil {
  191. err = indexTemplate.Execute(
  192. &buffer,
  193. IndexTemplateData{
  194. Title: appTitle,
  195. AppStateJSON: strings.TrimSpace(string(stateJSONBytes)),
  196. PreloadCSS: string(indexCSSBytes),
  197. },
  198. )
  199. }
  200. if err != nil {
  201. log.Printf("internal server error: indexTemplate.Execute: %+v\n", err)
  202. http.Error(response, "internal server error", http.StatusInternalServerError)
  203. return
  204. }
  205. response.Header().Set("Content-Type", "text/html")
  206. response.Header().Set("Content-Length", strconv.Itoa(buffer.Len()))
  207. io.Copy(response, &buffer)
  208. })
  209. mux.HandleFunc("/invite-code/", func(response http.ResponseWriter, request *http.Request) {
  210. code := getLastElementInURLPath(request.URL)
  211. setCookie(response, "graffiti_invite_code", code, 60*60*24)
  212. http.Redirect(response, request, "/", http.StatusFound)
  213. })
  214. mux.HandleFunc("/register", func(response http.ResponseWriter, request *http.Request) {
  215. code := getCookie(request, "graffiti_invite_code")
  216. displayName := request.URL.Query().Get("displayName")
  217. err := db.Update(func(tx *bolt.Tx) error {
  218. bucket := tx.Bucket([]byte("invite_codes"))
  219. authorsBucket := tx.Bucket([]byte("authors"))
  220. isFirstAuthor := authorsBucket.Stats().KeyN == 0
  221. parentAuthorIdBytes := bucket.Get([]byte(code))
  222. parentAuthorId := ""
  223. if parentAuthorIdBytes != nil {
  224. parentAuthorId = string(parentAuthorIdBytes)
  225. }
  226. //log.Printf("parentAuthorId != \"\" || isFirstAuthor: %t || %t", parentAuthorId != "", isFirstAuthor)
  227. if parentAuthorId != "" || isFirstAuthor {
  228. newAuthorId, err := createAuthor(tx, parentAuthorId, displayName)
  229. if err != nil {
  230. return err
  231. }
  232. bucket.Delete([]byte(code))
  233. setCookie(response, "graffiti_author_id", newAuthorId, 60*60*24*365)
  234. } else {
  235. setCookie(response, "graffiti_temporary_error", "invite_code_missing_or_already_used", 60*60)
  236. }
  237. return nil
  238. })
  239. if err != nil {
  240. log.Printf("internal server error: registration transaction failed: %+v\n", err)
  241. http.Error(response, "internal server error", http.StatusInternalServerError)
  242. return
  243. }
  244. http.Redirect(response, request, "/", http.StatusFound)
  245. })
  246. mux.HandleFunc("/posts", func(response http.ResponseWriter, request *http.Request) {
  247. query := request.URL.Query()
  248. xString := query.Get("x")
  249. yString := query.Get("y")
  250. x, err := strconv.Atoi(xString)
  251. var y int
  252. if err == nil {
  253. y, err = strconv.Atoi(yString)
  254. }
  255. if err != nil {
  256. http.Error(response, "Bad Request, x and y are required", http.StatusBadRequest)
  257. }
  258. posts, err := getPosts(x, y, getPostsRadius)
  259. if err != nil {
  260. log.Printf("/posts: getPosts(%d, %d, %d): %+v\n", x, y, getPostsRadius, err)
  261. http.Error(response, "internal server error", http.StatusInternalServerError)
  262. }
  263. contentLength := 2 // two for the surrounding array square brackets []
  264. for _, post := range posts {
  265. contentLength += len(post) // all of the post json objects
  266. }
  267. if len(posts) > 1 {
  268. contentLength += len(posts) - 1 // one for each comma between the objects
  269. }
  270. response.Header().Add("Content-Length", strconv.Itoa(contentLength))
  271. response.Write([]byte("["))
  272. for i, post := range posts {
  273. response.Write(post)
  274. if i < len(posts)-1 {
  275. response.Write([]byte(","))
  276. }
  277. }
  278. response.Write([]byte("]"))
  279. })
  280. mux.HandleFunc("/upload-meta", func(response http.ResponseWriter, request *http.Request) {
  281. if request.Method == "POST" {
  282. author := getAuthor(db, request)
  283. if author == nil {
  284. log.Println("/upload-meta 401 unauthorized: graffiti_author_id cookie is missing or invalid")
  285. http.Error(response, "Unauthorized: graffiti_author_id cookie is missing or invalid", http.StatusUnauthorized)
  286. return
  287. }
  288. var newPost Post
  289. err := json.NewDecoder(request.Body).Decode(&newPost)
  290. if err != nil {
  291. log.Printf("/upload-meta 400 bad request: json.NewDecoder(request.Body).Decode(): %+v\n", err)
  292. http.Error(response, "Bad Request", http.StatusBadRequest)
  293. return
  294. }
  295. if newPost.Type != PostTypeText && newPost.Type != PostTypeImage && newPost.Type != PostTypeVideo {
  296. log.Println("/upload-meta 400 bad request: Unknown post type")
  297. http.Error(response, "Bad Request: Unknown post type", http.StatusBadRequest)
  298. return
  299. }
  300. postIdBuffer := make([]byte, 16)
  301. rand.Read(postIdBuffer)
  302. postId := base58.Encode(postIdBuffer, base58.BitcoinAlphabet)
  303. newPost.Id = postId
  304. newPost.AuthorId = author.Id
  305. //newPost.Filename = regexp.MustCompile(`[^a-zA-Z0-9._-]+`).ReplaceAllString(newPost.Filename, "_")
  306. newPost.Filename = postId
  307. if newPost.Type != PostTypeText {
  308. pendingUploadMap[postId] = newPost
  309. } else {
  310. // TODO add post to DB
  311. }
  312. response.Write([]byte(postId))
  313. } else {
  314. response.Header().Set("Allow", "POST")
  315. http.Error(response, "Try POSTing instead", http.StatusMethodNotAllowed)
  316. }
  317. })
  318. mux.HandleFunc("/upload/", func(response http.ResponseWriter, request *http.Request) {
  319. if request.Method == "POST" {
  320. uploadId := getLastElementInURLPath(request.URL)
  321. if uploadId == "" {
  322. log.Println("/upload 400: malformed url")
  323. http.Error(response, "malformed url", http.StatusBadRequest)
  324. return
  325. }
  326. post, hasPost := pendingUploadMap[uploadId]
  327. if !hasPost {
  328. log.Printf("/upload 404: upload '%s' not found\n", uploadId)
  329. http.Error(response, fmt.Sprintf("upload '%s' not found", uploadId), http.StatusNotFound)
  330. return
  331. }
  332. multipartReader, err := request.MultipartReader()
  333. if err != nil {
  334. log.Printf("/upload 400: failed to read multipart upload: %+v\n", err)
  335. http.Error(response, "failed to read multipart upload", http.StatusBadRequest)
  336. return
  337. }
  338. length := request.ContentLength
  339. filepath := path.Join("user-content", post.Filename)
  340. dstFile, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0644)
  341. if err != nil {
  342. log.Printf("/upload 500: os.OpenFile(%s, os.O_RDWR|os.O_CREATE, 0644): %+v\n", filepath, err)
  343. http.Error(response, "internal server error", http.StatusInternalServerError)
  344. return
  345. }
  346. defer dstFile.Close()
  347. // the image encoding also updates the progress.
  348. // 1 for decoding the image.
  349. // ---
  350. // 1 for each time it resizes the image
  351. // 4 for every bpg encoding
  352. // 1 for every jpeg encoding
  353. // = 1 + 6*4 = 25
  354. // so the upload has to only push the progress up to 75%
  355. const lastStepsCount = 25
  356. for {
  357. part, err := multipartReader.NextPart()
  358. if err == io.EOF {
  359. break
  360. }
  361. var read int64
  362. for {
  363. //time.Sleep(time.Millisecond * 10)
  364. buffer := make([]byte, 1000)
  365. cBytes, err := part.Read(buffer)
  366. if err != nil && err != io.EOF {
  367. http.Error(response, "an error occurred while uploading this file, please try again :(", http.StatusInternalServerError)
  368. fmt.Printf("an error occurred while reading the multipart request body: %s\n", err)
  369. return
  370. }
  371. if cBytes > 0 {
  372. read = read + int64(cBytes)
  373. progressMap[uploadId] = int(float32(read) / float32(length) * (100 - lastStepsCount))
  374. _, err := dstFile.Write(buffer[0:cBytes])
  375. if err != nil {
  376. http.Error(response, "an error occurred while uploading this file, please try again :(", http.StatusInternalServerError)
  377. fmt.Printf("an error occurred while writing to %s: %s\n", filepath, err)
  378. return
  379. }
  380. }
  381. if err == io.EOF {
  382. break
  383. }
  384. }
  385. }
  386. _, err = dstFile.Seek(0, 0)
  387. if err != nil {
  388. log.Printf("/upload 500: dstFile.Seek(0,0): %+v\n", err)
  389. http.Error(response, "internal server error", http.StatusInternalServerError)
  390. return
  391. }
  392. determineContentTypeBuffer := make([]byte, 512)
  393. _, err = dstFile.Read(determineContentTypeBuffer)
  394. if err != nil {
  395. log.Printf("/upload 500: dstFile.Read(determineContentTypeBuffer): %+v\n", err)
  396. http.Error(response, "internal server error", http.StatusInternalServerError)
  397. return
  398. }
  399. contentType := http.DetectContentType(determineContentTypeBuffer)
  400. _, err = dstFile.Seek(0, 0)
  401. if err != nil {
  402. log.Printf("/upload 500: dstFile.Seek(0,0): %+v\n", err)
  403. http.Error(response, "internal server error", http.StatusInternalServerError)
  404. return
  405. }
  406. // handleImageEncodingOrDieTrying will respond with an http error if it fails
  407. // -- so we dont handle errors, we continue *only* if there are no errors.
  408. thumbnailJPEGBase64, err := handleImageEncodingOrDieTrying(response, dstFile, contentType, post.Filename, uploadId)
  409. if err == nil {
  410. post.ThumbnailJPEGBase64 = thumbnailJPEGBase64
  411. err := createPost(&post)
  412. if err != nil {
  413. log.Printf("/upload 500: createPost(): %+v\n", err)
  414. http.Error(response, "internal server error", http.StatusInternalServerError)
  415. return
  416. }
  417. }
  418. } else {
  419. response.Header().Set("Allow", "POST")
  420. http.Error(response, "Try POSTing instead", http.StatusMethodNotAllowed)
  421. }
  422. })
  423. mux.HandleFunc("/progress/", func(response http.ResponseWriter, request *http.Request) {
  424. uploadId := getLastElementInURLPath(request.URL)
  425. if uploadId == "" {
  426. http.Error(response, "malformed url", http.StatusNotFound)
  427. return
  428. }
  429. progress, hasProgress := progressMap[uploadId]
  430. if !hasProgress {
  431. http.Error(response, fmt.Sprintf("upload '%s' not found", uploadId), http.StatusNotFound)
  432. return
  433. }
  434. response.Header().Add("Content-Type", "text/plain; charset=utf-8")
  435. response.Write([]byte(strconv.Itoa(progress)))
  436. })
  437. }
  438. func createPost(post *Post) error {
  439. return db.Update(func(tx *bolt.Tx) error {
  440. spatialX := int(math.Round(float64(post.X)*pixelsToSpatialIndexUnits)) + sillyHilbertOptimizationOffset
  441. spatialY := int(math.Round(float64(post.Y)*pixelsToSpatialIndexUnits)) + sillyHilbertOptimizationOffset
  442. spatialKey, err := spatialIndex.GetIndexedPoint(spatialX, spatialY)
  443. if err != nil {
  444. return err
  445. }
  446. byAuthorBucket := tx.Bucket([]byte("posts_by_author"))
  447. byAuthorKey := []byte(fmt.Sprintf("%s-%s", post.AuthorId, post.Id))
  448. postByAuthorBytes, err := json.Marshal(&Post{
  449. Id: post.Id,
  450. SpatialKey64: base64.StdEncoding.EncodeToString(spatialKey),
  451. X: post.X,
  452. Y: post.Y,
  453. })
  454. if err != nil {
  455. return err
  456. }
  457. err = byAuthorBucket.Put(byAuthorKey, postByAuthorBytes)
  458. if err != nil {
  459. return err
  460. }
  461. postsBucket := tx.Bucket([]byte("posts_spatial"))
  462. post.MillisecondsSinceUnixEpoch = MillisecondsSinceUnixEpoch()
  463. postBytes, err := json.Marshal(post)
  464. if err != nil {
  465. return err
  466. }
  467. err = postsBucket.Put(spatialKey, postBytes)
  468. if err != nil {
  469. return err
  470. }
  471. return nil
  472. })
  473. }
  474. func getPosts(x, y, radius int) ([][]byte, error) {
  475. spatialXMin := int(math.Floor(float64(x-radius)*pixelsToSpatialIndexUnits)) + sillyHilbertOptimizationOffset
  476. spatialYMin := int(math.Floor(float64(x-radius)*pixelsToSpatialIndexUnits)) + sillyHilbertOptimizationOffset
  477. spatialWidth := int(math.Ceil(float64(radius*2) * pixelsToSpatialIndexUnits))
  478. spatialHeight := int(math.Ceil(float64(radius*2) * pixelsToSpatialIndexUnits))
  479. spatialXMax := spatialXMin + spatialWidth
  480. spatialYMax := spatialYMin + spatialHeight
  481. if debugLog {
  482. log.Printf("getPosts(): RectangleToIndexedRanges(%d, %d, %d, %d, 1)\n", spatialXMin, spatialYMin, spatialWidth, spatialHeight)
  483. }
  484. ranges, err := spatialIndex.RectangleToIndexedRanges(spatialXMin, spatialYMin, spatialWidth, spatialHeight, 1)
  485. if err != nil {
  486. return nil, err
  487. }
  488. if debugLog {
  489. log.Printf("getPosts(): spatialIndex range count: %d\n---\n", len(ranges))
  490. for _, rng := range ranges {
  491. fmt.Printf("%x\n%x\n---\n", rng.Start[0:8], rng.End[0:8])
  492. }
  493. }
  494. toReturn := [][]byte{}
  495. err = db.View(func(tx *bolt.Tx) error {
  496. bucket := tx.Bucket([]byte("posts_spatial"))
  497. for _, rng := range ranges {
  498. cursor := bucket.Cursor()
  499. key, value := cursor.Seek(rng.Start)
  500. for key != nil && bytes.Compare(key, rng.End) < 1 {
  501. spatialX, spatialY, err := spatialIndex.GetPositionFromIndexedPoint(key)
  502. if err != nil {
  503. return err
  504. }
  505. if spatialX > spatialXMin && spatialX < spatialXMax && spatialY > spatialYMin && spatialY < spatialYMax {
  506. toReturn = append(toReturn, value)
  507. }
  508. key, value = cursor.Next()
  509. }
  510. }
  511. return nil
  512. })
  513. return toReturn, err
  514. }
  515. func createAuthor(tx *bolt.Tx, parentAuthorId string, displayName string) (string, error) {
  516. bucket := tx.Bucket([]byte("authors"))
  517. newAuthorAncestry := []string{}
  518. parentAuthorBytes := bucket.Get([]byte(parentAuthorId))
  519. if parentAuthorBytes != nil {
  520. var parentAuthor Author
  521. err := json.Unmarshal(parentAuthorBytes, &parentAuthor)
  522. if err != nil {
  523. return "", err
  524. }
  525. newAuthorAncestry = append(parentAuthor.Ancestry, parentAuthorId)
  526. }
  527. newAuthorId := getRandomId(8)
  528. newAuthorBytes, err := json.Marshal(Author{
  529. Id: newAuthorId,
  530. DisplayName: displayName,
  531. Ancestry: newAuthorAncestry,
  532. })
  533. if err != nil {
  534. return "", err
  535. }
  536. err = bucket.Put([]byte(newAuthorId), newAuthorBytes)
  537. if err != nil {
  538. return "", err
  539. }
  540. return newAuthorId, nil
  541. }
  542. func getAuthor(db *bolt.DB, request *http.Request) *Author {
  543. var author *Author = nil
  544. err := db.View(func(tx *bolt.Tx) error {
  545. bucket := tx.Bucket([]byte("authors"))
  546. authorBytes := bucket.Get([]byte(getCookie(request, "graffiti_author_id")))
  547. if authorBytes != nil {
  548. var temp Author
  549. err := json.Unmarshal(authorBytes, &temp)
  550. if err != nil {
  551. return err
  552. }
  553. author = &temp
  554. }
  555. return nil
  556. })
  557. if err != nil {
  558. log.Printf("error in getAuthor(): %s\n", err)
  559. }
  560. return author
  561. }
  562. func getRandomId(bytesCount int) string {
  563. randomIdBuffer := make([]byte, bytesCount)
  564. rand.Read(randomIdBuffer)
  565. return base58.Encode(randomIdBuffer, base58.BitcoinAlphabet)
  566. }
  567. func setCookie(response http.ResponseWriter, key, value string, maxAge int) {
  568. http.SetCookie(response, &http.Cookie{
  569. Name: key,
  570. HttpOnly: true,
  571. Secure: cookieSecureHTTPSOnly,
  572. SameSite: http.SameSiteStrictMode,
  573. Path: "/",
  574. Value: value,
  575. MaxAge: maxAge,
  576. })
  577. }
  578. func getCookie(request *http.Request, name string) string {
  579. for _, cookie := range request.Cookies() {
  580. if cookie.Name == name {
  581. return cookie.Value
  582. }
  583. }
  584. return ""
  585. }
  586. func getLastElementInURLPath(url *url.URL) string {
  587. rawParts := strings.Split(url.Path, "/")
  588. pathParts := []string{}
  589. for _, p := range rawParts {
  590. if p != "" {
  591. pathParts = append(pathParts, p)
  592. }
  593. }
  594. if len(pathParts) > 0 {
  595. return pathParts[len(pathParts)-1]
  596. }
  597. return ""
  598. }
  599. func getBooleanEnvVar(envVar string, defaultValue bool) bool {
  600. str := strings.ToLower(os.Getenv("DEBUG_BPG"))
  601. if str != "" {
  602. return str == "1" || str == "t" || str == "y" || str == "true" || str == "yes"
  603. }
  604. return defaultValue
  605. }
  606. func TimeFromMillisecondsSinceUnixEpoch(timestamp int64) time.Time {
  607. return time.Unix(timestamp/int64(1000), 0)
  608. }
  609. func MillisecondsSinceUnixEpoch() int64 {
  610. return time.Now().UnixNano() / int64(time.Millisecond)
  611. }