Re-organise the code to be more modular (#3172)
This commit is contained in:
parent
42ee98e295
commit
6ac2308ee1
|
@ -51,3 +51,4 @@ exclude_patterns:
|
||||||
- "protocol/pushnotificationclient/migrations/migrations.go"
|
- "protocol/pushnotificationclient/migrations/migrations.go"
|
||||||
- "protocol/pushnotificationserver/migrations/migrations.go"
|
- "protocol/pushnotificationserver/migrations/migrations.go"
|
||||||
- "protocol/transport/migrations/migrations.go"
|
- "protocol/transport/migrations/migrations.go"
|
||||||
|
- "images/qr-assets.go"
|
||||||
|
|
BIN
_assets/tests/qr/QRWithLogo.png
Normal file
BIN
_assets/tests/qr/QRWithLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
_assets/tests/qr/defaultQR.png
Normal file
BIN
_assets/tests/qr/defaultQR.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
_assets/tests/qr/status.png
Normal file
BIN
_assets/tests/qr/status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -152,3 +152,16 @@ func isWebp(buf []byte) bool {
|
||||||
buf[8] == 0x57 && buf[9] == 0x45 &&
|
buf[8] == 0x57 && buf[9] == 0x45 &&
|
||||||
buf[10] == 0x42 && buf[11] == 0x50
|
buf[10] == 0x42 && buf[11] == 0x50
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetImageDimensions(imgBytes []byte) (int, int, error) {
|
||||||
|
// Decode image bytes
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
// Get the image dimensions
|
||||||
|
bounds := img.Bounds()
|
||||||
|
width := bounds.Max.X - bounds.Min.X
|
||||||
|
height := bounds.Max.Y - bounds.Min.Y
|
||||||
|
return width, height, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,41 @@
|
||||||
package images
|
package images
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/png"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/nfnt/resize"
|
"github.com/nfnt/resize"
|
||||||
"github.com/oliamb/cutter"
|
"github.com/oliamb/cutter"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
xdraw "golang.org/x/image/draw"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Circle struct {
|
||||||
|
X, Y, R int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circle) ColorModel() color.Model {
|
||||||
|
return color.AlphaModel
|
||||||
|
}
|
||||||
|
func (c *Circle) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R)
|
||||||
|
}
|
||||||
|
func (c *Circle) At(x, y int) color.Color {
|
||||||
|
xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R)
|
||||||
|
if xx*xx+yy*yy < rr*rr {
|
||||||
|
return color.Alpha{255}
|
||||||
|
}
|
||||||
|
return color.Alpha{0}
|
||||||
|
}
|
||||||
|
|
||||||
func Resize(size ResizeDimension, img image.Image) image.Image {
|
func Resize(size ResizeDimension, img image.Image) image.Image {
|
||||||
var width, height uint
|
var width, height uint
|
||||||
|
|
||||||
|
@ -84,3 +109,122 @@ func CropCenter(img image.Image) (image.Image, error) {
|
||||||
}
|
}
|
||||||
return Crop(img, cropRect)
|
return Crop(img, cropRect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ImageToBytes(imagePath string) ([]byte, error) {
|
||||||
|
// Open the image file
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Decode the image
|
||||||
|
img, _, err := image.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new buffer to hold the image data
|
||||||
|
var imgBuffer bytes.Buffer
|
||||||
|
|
||||||
|
// Encode the image to the desired format and save it in the buffer
|
||||||
|
err = png.Encode(&imgBuffer, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the image data as a byte slice
|
||||||
|
return imgBuffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPadding(img image.Image, padding int) *image.RGBA {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
|
||||||
|
paddedImg := image.NewRGBA(newBounds)
|
||||||
|
draw.Draw(paddedImg, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src)
|
||||||
|
|
||||||
|
return paddedImg
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePNG(img *image.RGBA) ([]byte, error) {
|
||||||
|
resultImg := &bytes.Buffer{}
|
||||||
|
err := png.Encode(resultImg, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resultImg.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCircle(img image.Image) *image.RGBA {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
circle := image.NewRGBA(bounds)
|
||||||
|
draw.DrawMask(circle, bounds, img, image.ZP, &Circle{
|
||||||
|
X: bounds.Dx() / 2,
|
||||||
|
Y: bounds.Dy() / 2,
|
||||||
|
R: bounds.Dx() / 2,
|
||||||
|
}, image.ZP, draw.Over)
|
||||||
|
return circle
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlaceCircleInCenter(paddedImg, circle *image.RGBA) *image.RGBA {
|
||||||
|
bounds := circle.Bounds()
|
||||||
|
centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2
|
||||||
|
centerY := (paddedImg.Bounds().Min.Y + paddedImg.Bounds().Max.Y) / 2
|
||||||
|
draw.Draw(paddedImg, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over)
|
||||||
|
return paddedImg
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) {
|
||||||
|
// Decode image bytes
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Create a new image with the desired dimensions
|
||||||
|
newImg := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||||
|
xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil)
|
||||||
|
// Encode the new image to bytes
|
||||||
|
var newImgBytes bytes.Buffer
|
||||||
|
if err = png.Encode(&newImgBytes, newImg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newImgBytes.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuperimposeLogoOnQRImage(imageBytes []byte, qrFilepath []byte) []byte {
|
||||||
|
// Read the two images from bytes
|
||||||
|
img1, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error decoding logo Image", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img2, _, err := image.Decode(bytes.NewReader(qrFilepath))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error decoding QR Image", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Create a new image with the dimensions of the first image
|
||||||
|
result := image.NewRGBA(img1.Bounds())
|
||||||
|
// Draw the first image on the new image
|
||||||
|
draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src)
|
||||||
|
// Get the dimensions of the second image
|
||||||
|
img2Bounds := img2.Bounds()
|
||||||
|
// Calculate the x and y coordinates to center the second image
|
||||||
|
x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2
|
||||||
|
y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2
|
||||||
|
// Draw the second image on top of the first image at the calculated coordinates
|
||||||
|
draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over)
|
||||||
|
// Encode the final image to a desired format
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = png.Encode(&b, result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error encoding final result Image to Buffer", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
327
images/qr-assets.go
Normal file
327
images/qr-assets.go
Normal file
File diff suppressed because one or more lines are too long
|
@ -7,6 +7,7 @@ var (
|
||||||
testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff}
|
testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff}
|
||||||
testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50}
|
testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50}
|
||||||
testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35}
|
testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35}
|
||||||
|
testLogoBytes, _ = Asset("_assets/tests/qr/status.png")
|
||||||
)
|
)
|
||||||
|
|
||||||
func SampleIdentityImages() []IdentityImage {
|
func SampleIdentityImages() []IdentityImage {
|
||||||
|
@ -31,3 +32,17 @@ func SampleIdentityImages() []IdentityImage {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SampleIdentityImageForQRCode() []IdentityImage {
|
||||||
|
return []IdentityImage{
|
||||||
|
{
|
||||||
|
Name: LargeDimName,
|
||||||
|
Payload: testLogoBytes,
|
||||||
|
Width: 240,
|
||||||
|
Height: 300,
|
||||||
|
FileSize: 1024,
|
||||||
|
ResizeTarget: 240,
|
||||||
|
Clock: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
182
qrcode/qrcode.go
182
qrcode/qrcode.go
|
@ -1,182 +0,0 @@
|
||||||
package qrcode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
"image/png"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
xdraw "golang.org/x/image/draw"
|
|
||||||
|
|
||||||
"github.com/status-im/status-go/multiaccounts"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultPadding = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetImageDimensions(imgBytes []byte) (int, int, error) {
|
|
||||||
// Decode image bytes
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
// Get the image dimensions
|
|
||||||
bounds := img.Bounds()
|
|
||||||
width := bounds.Max.X - bounds.Min.X
|
|
||||||
height := bounds.Max.Y - bounds.Min.Y
|
|
||||||
return width, height, nil
|
|
||||||
}
|
|
||||||
func ToLogoImageFromBytes(imageBytes []byte, padding int) []byte {
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
bounds := img.Bounds()
|
|
||||||
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
|
|
||||||
white := image.NewRGBA(newBounds)
|
|
||||||
draw.Draw(white, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src)
|
|
||||||
// Create a circular mask
|
|
||||||
circle := image.NewRGBA(bounds)
|
|
||||||
draw.DrawMask(circle, bounds, img, image.ZP, &Circle{
|
|
||||||
X: bounds.Dx() / 2,
|
|
||||||
Y: bounds.Dy() / 2,
|
|
||||||
R: bounds.Dx() / 2,
|
|
||||||
}, image.ZP, draw.Over)
|
|
||||||
// Calculate the center point of the new image
|
|
||||||
centerX := (newBounds.Min.X + newBounds.Max.X) / 2
|
|
||||||
centerY := (newBounds.Min.Y + newBounds.Max.Y) / 2
|
|
||||||
// Draw the circular image in the center of the new image
|
|
||||||
draw.Draw(white, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over)
|
|
||||||
// Encode image to png format and save in a bytes
|
|
||||||
var resultImg bytes.Buffer
|
|
||||||
err = png.Encode(&resultImg, white)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resultBytes := resultImg.Bytes()
|
|
||||||
return resultBytes
|
|
||||||
}
|
|
||||||
func SuperimposeImage(imageBytes []byte, qrFilepath []byte) []byte {
|
|
||||||
// Read the two images from bytes
|
|
||||||
img1, _, _ := image.Decode(bytes.NewReader(imageBytes))
|
|
||||||
img2, _, _ := image.Decode(bytes.NewReader(qrFilepath))
|
|
||||||
// Create a new image with the dimensions of the first image
|
|
||||||
result := image.NewRGBA(img1.Bounds())
|
|
||||||
// Draw the first image on the new image
|
|
||||||
draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src)
|
|
||||||
// Get the dimensions of the second image
|
|
||||||
img2Bounds := img2.Bounds()
|
|
||||||
// Calculate the x and y coordinates to center the second image
|
|
||||||
x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2
|
|
||||||
y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2
|
|
||||||
// Draw the second image on top of the first image at the calculated coordinates
|
|
||||||
draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over)
|
|
||||||
// Encode the final image to a desired format
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := png.Encode(&b, result); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
return b.Bytes()
|
|
||||||
}
|
|
||||||
func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) {
|
|
||||||
// Decode image bytes
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Create a new image with the desired dimensions
|
|
||||||
newImg := image.NewNRGBA(image.Rect(0, 0, width, height))
|
|
||||||
xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil)
|
|
||||||
// Encode the new image to bytes
|
|
||||||
var newImgBytes bytes.Buffer
|
|
||||||
if err = png.Encode(&newImgBytes, newImg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newImgBytes.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ImageToBytes(imagePath string) ([]byte, error) {
|
|
||||||
// Open the image file
|
|
||||||
file, err := os.Open(imagePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// Decode the image
|
|
||||||
img, _, err := image.Decode(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new buffer to hold the image data
|
|
||||||
var imgBuffer bytes.Buffer
|
|
||||||
|
|
||||||
// Encode the image to the desired format and save it in the buffer
|
|
||||||
err = png.Encode(&imgBuffer, img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the image data as a byte slice
|
|
||||||
return imgBuffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogoImage(multiaccountsDB *multiaccounts.Database, params url.Values) ([]byte, error) {
|
|
||||||
var imageFolderBasePath = "../_assets/tests/"
|
|
||||||
var logoFileStaticPath = imageFolderBasePath + "status.png"
|
|
||||||
|
|
||||||
keyUids, ok := params["keyUid"]
|
|
||||||
if !ok || len(keyUids) == 0 {
|
|
||||||
return nil, errors.New("no keyUid")
|
|
||||||
}
|
|
||||||
imageNames, ok := params["imageName"]
|
|
||||||
if !ok || len(imageNames) == 0 {
|
|
||||||
return nil, errors.New("no imageName")
|
|
||||||
}
|
|
||||||
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
staticLogoFileBytes, _ := ImageToBytes(logoFileStaticPath)
|
|
||||||
|
|
||||||
if identityImageObjectFromDB == nil {
|
|
||||||
return ToLogoImageFromBytes(staticLogoFileBytes, GetPadding(staticLogoFileBytes)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ToLogoImageFromBytes(identityImageObjectFromDB.Payload, GetPadding(identityImageObjectFromDB.Payload)), nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPadding(imgBytes []byte) int {
|
|
||||||
size, _, err := GetImageDimensions(imgBytes)
|
|
||||||
if err != nil {
|
|
||||||
return defaultPadding
|
|
||||||
}
|
|
||||||
return size / 5
|
|
||||||
}
|
|
||||||
|
|
||||||
type Circle struct {
|
|
||||||
X, Y, R int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Circle) ColorModel() color.Model {
|
|
||||||
return color.AlphaModel
|
|
||||||
}
|
|
||||||
func (c *Circle) Bounds() image.Rectangle {
|
|
||||||
return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R)
|
|
||||||
}
|
|
||||||
func (c *Circle) At(x, y int) color.Color {
|
|
||||||
xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R)
|
|
||||||
if xx*xx+yy*yy < rr*rr {
|
|
||||||
return color.Alpha{255}
|
|
||||||
}
|
|
||||||
return color.Alpha{0}
|
|
||||||
}
|
|
|
@ -3,15 +3,12 @@ package server
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
|
||||||
"image"
|
"image"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
qrcode "github.com/yeqown/go-qrcode/v2"
|
|
||||||
"github.com/yeqown/go-qrcode/writer/standard"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/status-im/status-go/images"
|
"github.com/status-im/status-go/images"
|
||||||
|
@ -20,7 +17,6 @@ import (
|
||||||
"github.com/status-im/status-go/protocol/identity/colorhash"
|
"github.com/status-im/status-go/protocol/identity/colorhash"
|
||||||
"github.com/status-im/status-go/protocol/identity/identicon"
|
"github.com/status-im/status-go/protocol/identity/identicon"
|
||||||
"github.com/status-im/status-go/protocol/identity/ring"
|
"github.com/status-im/status-go/protocol/identity/ring"
|
||||||
qrcodeutils "github.com/status-im/status-go/qrcode"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -40,25 +36,6 @@ const (
|
||||||
|
|
||||||
type HandlerPatternMap map[string]http.HandlerFunc
|
type HandlerPatternMap map[string]http.HandlerFunc
|
||||||
|
|
||||||
type QROptions struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
ErrorCorrectionLevel string `json:"errorCorrectionLevel"`
|
|
||||||
Capacity string `json:"capacity"`
|
|
||||||
AllowProfileImage bool `json:"withLogo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WriterCloserByteBuffer struct {
|
|
||||||
*bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc WriterCloserByteBuffer) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
|
|
||||||
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
params := r.URL.Query()
|
||||||
|
@ -412,62 +389,19 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun
|
||||||
func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
params := r.URL.Query()
|
||||||
qrURLBase64Encoded, ok := params["qrurl"]
|
|
||||||
if !ok || len(qrURLBase64Encoded) == 0 {
|
payload := generateQRBytes(params, logger, multiaccountsDB)
|
||||||
logger.Error("no qr url provided")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
qrURLBase64Decoded, err := base64.StdEncoding.DecodeString(qrURLBase64Encoded[0])
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error decoding string from base64", zap.Error(err))
|
|
||||||
}
|
|
||||||
level, ok := params["level"]
|
|
||||||
// Default error correction level
|
|
||||||
correctionLevel := qrcode.ErrorCorrectionMedium
|
|
||||||
if ok && len(level) == 1 {
|
|
||||||
switch level[0] {
|
|
||||||
case "4":
|
|
||||||
correctionLevel = qrcode.ErrorCorrectionHighest
|
|
||||||
case "1":
|
|
||||||
correctionLevel = qrcode.ErrorCorrectionLow
|
|
||||||
case "3":
|
|
||||||
correctionLevel = qrcode.ErrorCorrectionQuart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf := NewWriterCloserByteBuffer()
|
|
||||||
qrc, err := qrcode.NewWith(string(qrURLBase64Decoded),
|
|
||||||
qrcode.WithEncodingMode(qrcode.EncModeAuto),
|
|
||||||
qrcode.WithErrorCorrectionLevel(correctionLevel),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("could not generate QRCode", zap.Error(err))
|
|
||||||
}
|
|
||||||
nw := standard.NewWithWriter(buf)
|
|
||||||
if err = qrc.Save(nw); err != nil {
|
|
||||||
logger.Error("could not save image", zap.Error(err))
|
|
||||||
}
|
|
||||||
payload := buf.Bytes()
|
|
||||||
logo, err := qrcodeutils.GetLogoImage(multiaccountsDB, params)
|
|
||||||
if err == nil {
|
|
||||||
qrWidth, qrHeight, _ := qrcodeutils.GetImageDimensions(payload)
|
|
||||||
logo, _ = qrcodeutils.ResizeImage(logo, qrWidth/5, qrHeight/5)
|
|
||||||
payload = qrcodeutils.SuperimposeImage(payload, logo)
|
|
||||||
}
|
|
||||||
size, ok := params["size"]
|
|
||||||
if ok && len(size) == 1 {
|
|
||||||
size, err := strconv.Atoi(size[0])
|
|
||||||
if err == nil {
|
|
||||||
payload, _ = qrcodeutils.ResizeImage(payload, size, size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mime, err := images.GetProtobufImageMime(payload)
|
mime, err := images.GetProtobufImageMime(payload)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("could not generate image from payload", zap.Error(err))
|
logger.Error("could not generate image from payload", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", mime)
|
w.Header().Set("Content-Type", mime)
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
_, err = w.Write(payload)
|
_, err = w.Write(payload)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to write image", zap.Error(err))
|
logger.Error("failed to write image", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
306
server/qrops.go
Normal file
306
server/qrops.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/images"
|
||||||
|
"github.com/status-im/status-go/multiaccounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WriterCloserByteBuffer struct {
|
||||||
|
*bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc WriterCloserByteBuffer) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
|
||||||
|
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QRConfig struct {
|
||||||
|
DecodedQRURL string
|
||||||
|
WithLogo bool
|
||||||
|
CorrectionLevel qrcode.EncodeOption
|
||||||
|
KeyUID string
|
||||||
|
ImageName string
|
||||||
|
Size int
|
||||||
|
Params url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQRConfig(params url.Values, logger *zap.Logger) (*QRConfig, error) {
|
||||||
|
config := &QRConfig{}
|
||||||
|
config.Params = params
|
||||||
|
err := config.setQrURL()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[qrops-error] error in setting QRURL", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setAllowProfileImage()
|
||||||
|
config.setErrorCorrectionLevel()
|
||||||
|
err = config.setSize()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("[qrops-error] could not convert string to int for size param ", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.WithLogo {
|
||||||
|
err = config.setKeyUID()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setImageName()
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setQrURL() error {
|
||||||
|
qrURL, ok := q.Params["url"]
|
||||||
|
|
||||||
|
if !ok || len(qrURL) == 0 {
|
||||||
|
return errors.New("[qrops-error] no qr url provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedURL, err := base64.StdEncoding.DecodeString(qrURL[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.DecodedQRURL = string(decodedURL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setAllowProfileImage() {
|
||||||
|
allowProfileImage, ok := q.Params["allowProfileImage"]
|
||||||
|
|
||||||
|
if !ok || len(allowProfileImage) == 0 {
|
||||||
|
// we default to false when this flag was not provided
|
||||||
|
// so someone does not want to allowProfileImage on their QR Image
|
||||||
|
// fine then :)
|
||||||
|
q.WithLogo = false
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoOnImage, err := strconv.ParseBool(allowProfileImage[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// maybe for fun someone tries to send non-boolean values to this flag
|
||||||
|
// we also default to false in that case
|
||||||
|
q.WithLogo = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we reach here its most probably true
|
||||||
|
q.WithLogo = LogoOnImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setErrorCorrectionLevel() {
|
||||||
|
level, ok := q.Params["level"]
|
||||||
|
if !ok || len(level) == 0 {
|
||||||
|
// we default to MediumLevel of error correction when the level flag
|
||||||
|
// is not passed.
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
levelInt, err := strconv.Atoi(level[0])
|
||||||
|
if err != nil || levelInt < 0 {
|
||||||
|
// if there is any issue with string to int conversion
|
||||||
|
// we still default to MediumLevel of error correction
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch levelInt {
|
||||||
|
case 1:
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow)
|
||||||
|
case 2:
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
||||||
|
case 3:
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionQuart)
|
||||||
|
case 4:
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionHighest)
|
||||||
|
default:
|
||||||
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setSize() error {
|
||||||
|
size, ok := q.Params["size"]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
imageToBeResized, err := strconv.Atoi(size[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageToBeResized <= 0 {
|
||||||
|
return errors.New("[qrops-error] Got an invalid size parameter, it should be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
q.Size = imageToBeResized
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setKeyUID() error {
|
||||||
|
keyUID, ok := q.Params["keyUid"]
|
||||||
|
// the keyUID was not passed, which is a requirement to get the multiaccount image,
|
||||||
|
// so we log this error
|
||||||
|
if !ok || len(keyUID) == 0 {
|
||||||
|
return errors.New("[qrops-error] A keyUID is required to put logo on image and it was not passed in the parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
q.KeyUID = keyUID[0]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRConfig) setImageName() {
|
||||||
|
imageName, ok := q.Params["imageName"]
|
||||||
|
//if the imageName was not passed, we default to const images.LargeDimName
|
||||||
|
if !ok || len(imageName) == 0 {
|
||||||
|
q.ImageName = images.LargeDimName
|
||||||
|
}
|
||||||
|
|
||||||
|
q.ImageName = imageName[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLogoImageFromBytes(imageBytes []byte, padding int) ([]byte, error) {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding image failed: %v", err)
|
||||||
|
}
|
||||||
|
paddedImg := images.AddPadding(img, padding)
|
||||||
|
circle := images.CreateCircle(img)
|
||||||
|
centeredImg := images.PlaceCircleInCenter(paddedImg, circle)
|
||||||
|
resultBytes, err := images.EncodePNG(centeredImg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encoding PNG failed: %v", err)
|
||||||
|
}
|
||||||
|
return resultBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogoImage(multiaccountsDB *multiaccounts.Database, keyUID string, imageName string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
padding int
|
||||||
|
LogoBytes []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
staticImageData, err := images.Asset("_assets/tests/qr/status.png")
|
||||||
|
if err != nil { // Asset was not found.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUID, imageName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if identityImageObjectFromDB == nil {
|
||||||
|
padding = GetPadding(staticImageData)
|
||||||
|
LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
|
||||||
|
} else {
|
||||||
|
padding = GetPadding(identityImageObjectFromDB.Payload)
|
||||||
|
LogoBytes, err = ToLogoImageFromBytes(identityImageObjectFromDB.Payload, padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LogoBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPadding(imgBytes []byte) int {
|
||||||
|
const (
|
||||||
|
defaultPadding = 20
|
||||||
|
)
|
||||||
|
size, _, err := images.GetImageDimensions(imgBytes)
|
||||||
|
if err != nil {
|
||||||
|
return defaultPadding
|
||||||
|
}
|
||||||
|
return size / 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateQRBytes(params url.Values, logger *zap.Logger, multiaccountsDB *multiaccounts.Database) []byte {
|
||||||
|
|
||||||
|
qrGenerationConfig, err := NewQRConfig(params, logger)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not generate QRConfig please rectify the errors with input parameters", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qrc, err := qrcode.NewWith(qrGenerationConfig.DecodedQRURL,
|
||||||
|
qrcode.WithEncodingMode(qrcode.EncModeAuto),
|
||||||
|
qrGenerationConfig.CorrectionLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not generate QRCode with provided options", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := NewWriterCloserByteBuffer()
|
||||||
|
nw := standard.NewWithWriter(buf)
|
||||||
|
err = qrc.Save(nw)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not save image", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := buf.Bytes()
|
||||||
|
|
||||||
|
if qrGenerationConfig.WithLogo {
|
||||||
|
logo, err := GetLogoImage(multiaccountsDB, qrGenerationConfig.KeyUID, qrGenerationConfig.ImageName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not get logo image from multiaccountsDB", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qrWidth, qrHeight, err := images.GetImageDimensions(payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not get image dimensions from payload", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logo, err = images.ResizeImage(logo, qrWidth/5, qrHeight/5)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not resize logo image ", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = images.SuperimposeLogoOnQRImage(payload, logo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if qrGenerationConfig.Size > 0 {
|
||||||
|
|
||||||
|
payload, err = images.ResizeImage(payload, qrGenerationConfig.Size, qrGenerationConfig.Size)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not resize final logo image ", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
}
|
118
server/qrops_test.go
Normal file
118
server/qrops_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/images"
|
||||||
|
"github.com/status-im/status-go/multiaccounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QROpsTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
TestKeyComponents
|
||||||
|
TestLoggerComponents
|
||||||
|
|
||||||
|
server *MediaServer
|
||||||
|
serverNoPort *MediaServer
|
||||||
|
testStart time.Time
|
||||||
|
multiaccountsDB *multiaccounts.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
keyUID = "0xdeadbeef"
|
||||||
|
qrURL = "https://github.com/status-im/status-go/pull/3154"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQROpsTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(QROpsTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QROpsTestSuite) SetupTest() {
|
||||||
|
s.SetupKeyComponents(s.T())
|
||||||
|
s.SetupLoggerComponents()
|
||||||
|
|
||||||
|
mediaServer, err := NewMediaServer(nil, nil, nil)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.server = mediaServer
|
||||||
|
err = s.server.SetPort(customPortForTests)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
err = s.server.Start()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.serverNoPort = &MediaServer{Server: Server{
|
||||||
|
hostname: DefaultIP.String(),
|
||||||
|
portManger: newPortManager(s.Logger, nil),
|
||||||
|
}}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
s.serverNoPort.port = 80
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.testStart = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedTestDBWithIdentityImagesForQRCodeTests(s *QROpsTestSuite, db *multiaccounts.Database, keyUID string) {
|
||||||
|
iis := images.SampleIdentityImageForQRCode()
|
||||||
|
err := db.StoreIdentityImages(keyUID, iis, false)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestQROpsCodeWithoutSuperImposingLogo tests the QR code generation logic, it also allows us to debug in case
|
||||||
|
// things go wrong, here we compare generate a new QR code and compare its bytes with an already generated one.
|
||||||
|
func (s *QROpsTestSuite) TestQROpsCodeWithoutSuperImposingLogo() {
|
||||||
|
|
||||||
|
var params = url.Values{}
|
||||||
|
params.Set("url", base64.StdEncoding.EncodeToString([]byte(qrURL)))
|
||||||
|
params.Set("allowProfileImage", "false")
|
||||||
|
params.Set("level", "2")
|
||||||
|
params.Set("size", "200")
|
||||||
|
params.Set("imageName", "")
|
||||||
|
|
||||||
|
payload := generateQRBytes(params, s.Logger, s.multiaccountsDB)
|
||||||
|
expectedPayload, err := images.Asset("_assets/tests/qr/defaultQR.png")
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotEmpty(payload)
|
||||||
|
require.Equal(s.T(), payload, expectedPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QROpsTestSuite) TestQROpsCodeWithSuperImposingLogo() {
|
||||||
|
|
||||||
|
tmpfile, err := ioutil.TempFile("", "accounts-tests-")
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
db, err := multiaccounts.InitializeDB(tmpfile.Name())
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
seedTestDBWithIdentityImagesForQRCodeTests(s, db, keyUID)
|
||||||
|
|
||||||
|
var params = url.Values{}
|
||||||
|
params.Set("url", base64.StdEncoding.EncodeToString([]byte(qrURL)))
|
||||||
|
params.Set("allowProfileImage", "true")
|
||||||
|
params.Set("level", "2")
|
||||||
|
params.Set("size", "200")
|
||||||
|
params.Set("keyUid", keyUID)
|
||||||
|
params.Set("imageName", "large")
|
||||||
|
|
||||||
|
payload := generateQRBytes(params, s.Logger, db)
|
||||||
|
s.Require().NotEmpty(payload)
|
||||||
|
expectedPayload, err := images.Asset("_assets/tests/qr/QRWithLogo.png")
|
||||||
|
require.Equal(s.T(), payload, expectedPayload)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
err = os.Remove(tmpfile.Name())
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
|
@ -105,10 +105,20 @@ func (s *MediaServer) MakeStickerURL(stickerHash string) string {
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MediaServer) MakeQRURL(qurul string) string {
|
func (s *MediaServer) MakeQRURL(qurul string,
|
||||||
|
allowProfileImage string,
|
||||||
|
level string,
|
||||||
|
size string,
|
||||||
|
keyUID string,
|
||||||
|
imageName string) string {
|
||||||
u := s.MakeBaseURL()
|
u := s.MakeBaseURL()
|
||||||
u.Path = generateQRCode
|
u.Path = generateQRCode
|
||||||
u.RawQuery = url.Values{"qurul": {qurul}}.Encode()
|
u.RawQuery = url.Values{"url": {qurul},
|
||||||
|
"level": {level},
|
||||||
|
"allowProfileImage": {allowProfileImage},
|
||||||
|
"size": {size},
|
||||||
|
"keyUid": {keyUID},
|
||||||
|
"imageName": {imageName}}.Encode()
|
||||||
|
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,33 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/images"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
waitTime = 50 * time.Millisecond
|
waitTime = 50 * time.Millisecond
|
||||||
|
customPortForTests = 1337
|
||||||
|
defaultPortForTests = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseURL = "https://127.0.0.1"
|
||||||
|
baseURLWithCustomPort = fmt.Sprintf("%s:%d", baseURL, customPortForTests)
|
||||||
|
baseURLWithDefaultPort = fmt.Sprintf("%s:%d", baseURL, defaultPortForTests)
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerURLSuite(t *testing.T) {
|
func TestServerURLSuite(t *testing.T) {
|
||||||
|
@ -21,6 +40,7 @@ type ServerURLSuite struct {
|
||||||
TestLoggerComponents
|
TestLoggerComponents
|
||||||
|
|
||||||
server *MediaServer
|
server *MediaServer
|
||||||
|
serverForQR *MediaServer
|
||||||
serverNoPort *MediaServer
|
serverNoPort *MediaServer
|
||||||
testStart time.Time
|
testStart time.Time
|
||||||
}
|
}
|
||||||
|
@ -29,11 +49,19 @@ func (s *ServerURLSuite) SetupTest() {
|
||||||
s.SetupKeyComponents(s.T())
|
s.SetupKeyComponents(s.T())
|
||||||
s.SetupLoggerComponents()
|
s.SetupLoggerComponents()
|
||||||
|
|
||||||
|
mediaServer, err := NewMediaServer(nil, nil, nil)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.serverForQR = mediaServer
|
||||||
|
|
||||||
|
err = s.serverForQR.Start()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
s.server = &MediaServer{Server: Server{
|
s.server = &MediaServer{Server: Server{
|
||||||
hostname: DefaultIP.String(),
|
hostname: DefaultIP.String(),
|
||||||
portManger: newPortManager(s.Logger, nil),
|
portManger: newPortManager(s.Logger, nil),
|
||||||
}}
|
}}
|
||||||
err := s.server.SetPort(1337)
|
err = s.server.SetPort(customPortForTests)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
s.serverNoPort = &MediaServer{Server: Server{
|
s.serverNoPort = &MediaServer{Server: Server{
|
||||||
|
@ -42,7 +70,7 @@ func (s *ServerURLSuite) SetupTest() {
|
||||||
}}
|
}}
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(waitTime)
|
time.Sleep(waitTime)
|
||||||
s.serverNoPort.port = 80
|
s.serverNoPort.port = defaultPortForTests
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.testStart = time.Now()
|
s.testStart = time.Now()
|
||||||
|
@ -58,48 +86,130 @@ func (s *ServerURLSuite) testNoPort(expected string, actual string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeBaseURL() {
|
func (s *ServerURLSuite) TestServer_MakeBaseURL() {
|
||||||
s.Require().Equal("https://127.0.0.1:1337", s.server.MakeBaseURL().String())
|
s.Require().Equal(baseURLWithCustomPort, s.server.MakeBaseURL().String())
|
||||||
s.testNoPort("https://127.0.0.1:80", s.serverNoPort.MakeBaseURL().String())
|
s.testNoPort(baseURLWithDefaultPort, s.serverNoPort.MakeBaseURL().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeImageServerURL() {
|
func (s *ServerURLSuite) TestServer_MakeImageServerURL() {
|
||||||
s.Require().Equal("https://127.0.0.1:1337/messages/", s.server.MakeImageServerURL())
|
s.Require().Equal(baseURLWithCustomPort+"/messages/", s.server.MakeImageServerURL())
|
||||||
s.testNoPort("https://127.0.0.1:80/messages/", s.serverNoPort.MakeImageServerURL())
|
s.testNoPort(baseURLWithDefaultPort+"/messages/", s.serverNoPort.MakeImageServerURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeIdenticonURL() {
|
func (s *ServerURLSuite) TestServer_MakeIdenticonURL() {
|
||||||
s.Require().Equal(
|
s.Require().Equal(
|
||||||
"https://127.0.0.1:1337/messages/identicons?publicKey=0xdaff0d11decade",
|
baseURLWithCustomPort+"/messages/identicons?publicKey=0xdaff0d11decade",
|
||||||
s.server.MakeIdenticonURL("0xdaff0d11decade"))
|
s.server.MakeIdenticonURL("0xdaff0d11decade"))
|
||||||
s.testNoPort(
|
s.testNoPort(
|
||||||
"https://127.0.0.1:80/messages/identicons?publicKey=0xdaff0d11decade",
|
baseURLWithDefaultPort+"/messages/identicons?publicKey=0xdaff0d11decade",
|
||||||
s.serverNoPort.MakeIdenticonURL("0xdaff0d11decade"))
|
s.serverNoPort.MakeIdenticonURL("0xdaff0d11decade"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeImageURL() {
|
func (s *ServerURLSuite) TestServer_MakeImageURL() {
|
||||||
s.Require().Equal(
|
s.Require().Equal(
|
||||||
"https://127.0.0.1:1337/messages/images?messageId=0x10aded70ffee",
|
baseURLWithCustomPort+"/messages/images?messageId=0x10aded70ffee",
|
||||||
s.server.MakeImageURL("0x10aded70ffee"))
|
s.server.MakeImageURL("0x10aded70ffee"))
|
||||||
|
|
||||||
s.testNoPort(
|
s.testNoPort(
|
||||||
"https://127.0.0.1:80/messages/images?messageId=0x10aded70ffee",
|
baseURLWithDefaultPort+"/messages/images?messageId=0x10aded70ffee",
|
||||||
s.serverNoPort.MakeImageURL("0x10aded70ffee"))
|
s.serverNoPort.MakeImageURL("0x10aded70ffee"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeAudioURL() {
|
func (s *ServerURLSuite) TestServer_MakeAudioURL() {
|
||||||
s.Require().Equal(
|
s.Require().Equal(
|
||||||
"https://127.0.0.1:1337/messages/audio?messageId=0xde1e7ebee71e",
|
baseURLWithCustomPort+"/messages/audio?messageId=0xde1e7ebee71e",
|
||||||
s.server.MakeAudioURL("0xde1e7ebee71e"))
|
s.server.MakeAudioURL("0xde1e7ebee71e"))
|
||||||
s.testNoPort(
|
s.testNoPort(
|
||||||
"https://127.0.0.1:80/messages/audio?messageId=0xde1e7ebee71e",
|
baseURLWithDefaultPort+"/messages/audio?messageId=0xde1e7ebee71e",
|
||||||
s.serverNoPort.MakeAudioURL("0xde1e7ebee71e"))
|
s.serverNoPort.MakeAudioURL("0xde1e7ebee71e"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerURLSuite) TestServer_MakeStickerURL() {
|
func (s *ServerURLSuite) TestServer_MakeStickerURL() {
|
||||||
s.Require().Equal(
|
s.Require().Equal(
|
||||||
"https://127.0.0.1:1337/ipfs?hash=0xdeadbeef4ac0",
|
baseURLWithCustomPort+"/ipfs?hash=0xdeadbeef4ac0",
|
||||||
s.server.MakeStickerURL("0xdeadbeef4ac0"))
|
s.server.MakeStickerURL("0xdeadbeef4ac0"))
|
||||||
s.testNoPort(
|
s.testNoPort(
|
||||||
"https://127.0.0.1:80/ipfs?hash=0xdeadbeef4ac0",
|
baseURLWithDefaultPort+"/ipfs?hash=0xdeadbeef4ac0",
|
||||||
s.serverNoPort.MakeStickerURL("0xdeadbeef4ac0"))
|
s.serverNoPort.MakeStickerURL("0xdeadbeef4ac0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestQRCodeGeneration tests if we provide all the correct parameters to the media server
|
||||||
|
// do we get a valid QR code or not as part of the response payload.
|
||||||
|
// we have stored a generated QR code in tests folder, and we compare their bytes.
|
||||||
|
func (s *ServerURLSuite) TestQRCodeGeneration() {
|
||||||
|
|
||||||
|
qrURL := "https://github.com/status-im/status-go/pull/3154"
|
||||||
|
generatedURL := base64.StdEncoding.EncodeToString([]byte(qrURL))
|
||||||
|
generatedURL = s.serverForQR.MakeQRURL(generatedURL, "false", "2", "200", "", "")
|
||||||
|
|
||||||
|
u, err := url.Parse(generatedURL)
|
||||||
|
if err != nil {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "" || u.Host == "" {
|
||||||
|
s.Require().Failf("generatedURL is not a valid URL: %s", generatedURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCert := s.serverForQR.cert
|
||||||
|
serverCertBytes := serverCert.Certificate[0]
|
||||||
|
|
||||||
|
certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCertBytes})
|
||||||
|
|
||||||
|
rootCAs, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = rootCAs.AppendCertsFromPEM(certPem)
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
InsecureSkipVerify: true, //nolint:all // MUST BE FALSE
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, generatedURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
s.Require().Failf("Unexpected response status code: %d", fmt.Sprint(resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Require().NotEmpty(payload)
|
||||||
|
|
||||||
|
expectedPayload, err := images.Asset("_assets/tests/qr/defaultQR.png")
|
||||||
|
require.Equal(s.T(), payload, expectedPayload)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
//(siddarthkay) un-comment code block below to generate the file in tests folder
|
||||||
|
//f, err := os.Create("image.png")
|
||||||
|
//if err != nil {
|
||||||
|
// s.Require().NoError(err)
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
//defer f.Close()
|
||||||
|
//_, err = f.Write(payload)
|
||||||
|
//
|
||||||
|
//if err != nil {
|
||||||
|
// s.Require().NoError(err)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue