github.com/status-im/status-go@v1.1.0/server/handlers.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"database/sql"
     6  	"errors"
     7  	"image"
     8  	"image/color"
     9  	"math/big"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"time"
    16  
    17  	"github.com/golang/protobuf/proto"
    18  
    19  	"github.com/status-im/status-go/eth-node/types"
    20  	"github.com/status-im/status-go/protocol/protobuf"
    21  
    22  	"go.uber.org/zap"
    23  
    24  	eth_common "github.com/ethereum/go-ethereum/common"
    25  
    26  	"github.com/status-im/status-go/images"
    27  	"github.com/status-im/status-go/ipfs"
    28  	"github.com/status-im/status-go/multiaccounts"
    29  	"github.com/status-im/status-go/protocol/identity/colorhash"
    30  	"github.com/status-im/status-go/protocol/identity/ring"
    31  	"github.com/status-im/status-go/services/wallet/bigint"
    32  )
    33  
    34  const (
    35  	basePath                            = "/messages"
    36  	imagesPath                          = basePath + "/images"
    37  	audioPath                           = basePath + "/audio"
    38  	ipfsPath                            = "/ipfs"
    39  	discordAuthorsPath                  = "/discord/authors"
    40  	discordAttachmentsPath              = basePath + "/discord/attachments"
    41  	LinkPreviewThumbnailPath            = "/link-preview/thumbnail"
    42  	LinkPreviewFaviconPath              = "/link-preview/favicon"
    43  	StatusLinkPreviewThumbnailPath      = "/status-link-preview/thumbnail"
    44  	communityTokenImagesPath            = "/communityTokenImages"
    45  	communityDescriptionImagesPath      = "/communityDescriptionImages"
    46  	communityDescriptionTokenImagesPath = "/communityDescriptionTokenImages"
    47  
    48  	walletBasePath              = "/wallet"
    49  	walletCommunityImagesPath   = walletBasePath + "/communityImages"
    50  	walletCollectionImagesPath  = walletBasePath + "/collectionImages"
    51  	walletCollectibleImagesPath = walletBasePath + "/collectibleImages"
    52  
    53  	// Handler routes for pairing
    54  	accountImagesPath   = "/accountImages"
    55  	accountInitialsPath = "/accountInitials"
    56  	contactImagesPath   = "/contactImages"
    57  	generateQRCode      = "/GenerateQRCode"
    58  )
    59  
    60  type HandlerPatternMap map[string]http.HandlerFunc
    61  
    62  func handleRequestDBMissing(logger *zap.Logger) http.HandlerFunc {
    63  	return func(w http.ResponseWriter, r *http.Request) {
    64  		logger.Error("can't handle media request without appdb")
    65  	}
    66  }
    67  
    68  func handleRequestDownloaderMissing(logger *zap.Logger) http.HandlerFunc {
    69  	return func(w http.ResponseWriter, r *http.Request) {
    70  		logger.Error("can't handle media request without ipfs downloader")
    71  	}
    72  }
    73  
    74  type ImageParams struct {
    75  	KeyUID                string
    76  	PublicKey             string
    77  	ImageName             string
    78  	ImagePath             string
    79  	FullName              string
    80  	InitialsLength        int
    81  	FontFile              string
    82  	FontSize              float64
    83  	Color                 color.Color
    84  	BgSize                int
    85  	BgColor               color.Color
    86  	UppercaseRatio        float64
    87  	Theme                 ring.Theme
    88  	Ring                  bool
    89  	RingWidth             float64
    90  	IndicatorSize         float64
    91  	IndicatorBorder       float64
    92  	IndicatorCenterToEdge float64
    93  	IndicatorColor        color.Color
    94  
    95  	AuthorID     string
    96  	URL          string
    97  	MessageID    string
    98  	AttachmentID string
    99  	ImageID      string
   100  
   101  	Hash     string
   102  	Download bool
   103  }
   104  
   105  func ParseImageParams(logger *zap.Logger, params url.Values) ImageParams {
   106  	parsed := ImageParams{}
   107  	parsed.Color = color.Transparent
   108  	parsed.BgColor = color.Transparent
   109  	parsed.IndicatorColor = color.Transparent
   110  	parsed.UppercaseRatio = 1.0
   111  
   112  	keyUids := params["keyUid"]
   113  	if len(keyUids) != 0 {
   114  		parsed.KeyUID = keyUids[0]
   115  	}
   116  
   117  	pks := params["publicKey"]
   118  	if len(pks) != 0 {
   119  		parsed.PublicKey = pks[0]
   120  	}
   121  
   122  	imageNames := params["imageName"]
   123  	if len(imageNames) != 0 {
   124  		if filepath.IsAbs(imageNames[0]) {
   125  			if _, err := os.Stat(imageNames[0]); err == nil {
   126  				parsed.ImagePath = imageNames[0]
   127  			} else if errors.Is(err, os.ErrNotExist) {
   128  				logger.Error("ParseParams: image not exit", zap.String("imageName", imageNames[0]))
   129  				return parsed
   130  			} else {
   131  				logger.Error("ParseParams: failed to read image", zap.String("imageName", imageNames[0]), zap.Error(err))
   132  				return parsed
   133  			}
   134  		} else {
   135  			parsed.ImageName = imageNames[0]
   136  		}
   137  	}
   138  
   139  	names := params["name"]
   140  	if len(names) != 0 {
   141  		parsed.FullName = names[0]
   142  	}
   143  
   144  	parsed.InitialsLength = 2
   145  	amountInitialsStr := params["length"]
   146  	if len(amountInitialsStr) != 0 {
   147  		amountInitials, err := strconv.Atoi(amountInitialsStr[0])
   148  		if err != nil {
   149  			logger.Error("ParseParams: invalid initials length")
   150  			return parsed
   151  		}
   152  		parsed.InitialsLength = amountInitials
   153  	}
   154  
   155  	fontFiles := params["fontFile"]
   156  	if len(fontFiles) != 0 {
   157  		if _, err := os.Stat(fontFiles[0]); err == nil {
   158  			parsed.FontFile = fontFiles[0]
   159  		} else if errors.Is(err, os.ErrNotExist) {
   160  			logger.Error("ParseParams: font file not exit", zap.String("FontFile", fontFiles[0]))
   161  			return parsed
   162  		} else {
   163  			logger.Error("ParseParams: font file not exit", zap.String("FontFile", fontFiles[0]), zap.Error(err))
   164  			return parsed
   165  		}
   166  	}
   167  
   168  	fontSizeStr := params["fontSize"]
   169  	if len(fontSizeStr) != 0 {
   170  		fontSize, err := strconv.ParseFloat(fontSizeStr[0], 64)
   171  		if err != nil {
   172  			logger.Error("ParseParams: invalid fontSize", zap.String("FontSize", fontSizeStr[0]))
   173  			return parsed
   174  		}
   175  		parsed.FontSize = fontSize
   176  	}
   177  
   178  	colors := params["color"]
   179  	if len(colors) != 0 {
   180  		textColor, err := images.ParseColor(colors[0])
   181  		if err != nil {
   182  			logger.Error("ParseParams: invalid color", zap.String("Color", colors[0]))
   183  			return parsed
   184  		}
   185  		parsed.Color = textColor
   186  	}
   187  
   188  	sizeStrs := params["size"]
   189  	if len(sizeStrs) != 0 {
   190  		size, err := strconv.Atoi(sizeStrs[0])
   191  		if err != nil {
   192  			logger.Error("ParseParams: invalid size", zap.String("size", sizeStrs[0]))
   193  			return parsed
   194  		}
   195  		parsed.BgSize = size
   196  	}
   197  
   198  	bgColors := params["bgColor"]
   199  	if len(bgColors) != 0 {
   200  		bgColor, err := images.ParseColor(bgColors[0])
   201  		if err != nil {
   202  			logger.Error("ParseParams: invalid bgColor", zap.String("BgColor", bgColors[0]))
   203  			return parsed
   204  		}
   205  		parsed.BgColor = bgColor
   206  	}
   207  
   208  	uppercaseRatioStr := params["uppercaseRatio"]
   209  	if len(uppercaseRatioStr) != 0 {
   210  		uppercaseRatio, err := strconv.ParseFloat(uppercaseRatioStr[0], 64)
   211  		if err != nil {
   212  			logger.Error("ParseParams: invalid uppercaseRatio", zap.String("uppercaseRatio", uppercaseRatioStr[0]))
   213  			return parsed
   214  		}
   215  		parsed.UppercaseRatio = uppercaseRatio
   216  	}
   217  
   218  	indicatorColors := params["indicatorColor"]
   219  	if len(indicatorColors) != 0 {
   220  		indicatorColor, err := images.ParseColor(indicatorColors[0])
   221  		if err != nil {
   222  			logger.Error("ParseParams: invalid indicatorColor", zap.String("IndicatorColor", indicatorColors[0]))
   223  			return parsed
   224  		}
   225  		parsed.IndicatorColor = indicatorColor
   226  	}
   227  
   228  	indicatorSizeStrs := params["indicatorSize"]
   229  	if len(indicatorSizeStrs) != 0 {
   230  		indicatorSize, err := strconv.ParseFloat(indicatorSizeStrs[0], 64)
   231  		if err != nil {
   232  			logger.Error("ParseParams: invalid indicatorSize", zap.String("indicatorSize", indicatorSizeStrs[0]))
   233  			indicatorSize = 0
   234  		}
   235  		parsed.IndicatorSize = indicatorSize
   236  	}
   237  
   238  	indicatorBorderStrs := params["indicatorBorder"]
   239  	if len(indicatorBorderStrs) != 0 {
   240  		indicatorBorder, err := strconv.ParseFloat(indicatorBorderStrs[0], 64)
   241  		if err != nil {
   242  			logger.Error("ParseParams: invalid indicatorBorder", zap.String("indicatorBorder", indicatorBorderStrs[0]))
   243  			indicatorBorder = 0
   244  		}
   245  		parsed.IndicatorBorder = indicatorBorder
   246  	}
   247  
   248  	indicatorCenterToEdgeStrs := params["indicatorCenterToEdge"]
   249  	if len(indicatorCenterToEdgeStrs) != 0 {
   250  		indicatorCenterToEdge, err := strconv.ParseFloat(indicatorCenterToEdgeStrs[0], 64)
   251  		if err != nil {
   252  			logger.Error("ParseParams: invalid indicatorCenterToEdge", zap.String("indicatorCenterToEdge", indicatorCenterToEdgeStrs[0]))
   253  			indicatorCenterToEdge = 0
   254  		}
   255  		parsed.IndicatorCenterToEdge = indicatorCenterToEdge
   256  	}
   257  
   258  	ringWidthStrs := params["ringWidth"]
   259  	if len(ringWidthStrs) != 0 {
   260  		ringWidth, err := strconv.ParseFloat(ringWidthStrs[0], 64)
   261  		if err != nil {
   262  			logger.Error("ParseParams: invalid indicatorSize", zap.String("ringWidth", ringWidthStrs[0]))
   263  			ringWidth = 0
   264  		}
   265  		parsed.RingWidth = ringWidth
   266  	}
   267  
   268  	parsed.Theme = getTheme(params, logger)
   269  	parsed.Ring = ringEnabled(params)
   270  
   271  	messageIDs := params["message-id"]
   272  	if len(messageIDs) != 0 {
   273  		parsed.MessageID = messageIDs[0]
   274  	}
   275  
   276  	messageIDs = params["messageId"]
   277  	if len(messageIDs) != 0 {
   278  		parsed.MessageID = messageIDs[0]
   279  	}
   280  
   281  	authorIds := params["authorId"]
   282  	if len(authorIds) != 0 {
   283  		parsed.AuthorID = authorIds[0]
   284  	}
   285  
   286  	if attachmentIDs := params["attachmentId"]; len(attachmentIDs) != 0 {
   287  		parsed.AttachmentID = attachmentIDs[0]
   288  	}
   289  
   290  	if imageIds := params["image-id"]; len(imageIds) != 0 {
   291  		parsed.ImageID = imageIds[0]
   292  	}
   293  
   294  	urls := params["url"]
   295  	if len(urls) != 0 {
   296  		parsed.URL = urls[0]
   297  	}
   298  
   299  	hash := params["hash"]
   300  	if len(hash) != 0 {
   301  		parsed.Hash = hash[0]
   302  	}
   303  
   304  	_, download := params["download"]
   305  	parsed.Download = download
   306  
   307  	return parsed
   308  }
   309  
   310  func handleAccountImagesImpl(multiaccountsDB *multiaccounts.Database, logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
   311  	if parsed.KeyUID == "" {
   312  		logger.Error("handleAccountImagesImpl: no keyUid")
   313  		return
   314  	}
   315  
   316  	if parsed.ImageName == "" {
   317  		logger.Error("handleAccountImagesImpl: no imageName")
   318  		return
   319  	}
   320  
   321  	identityImage, err := multiaccountsDB.GetIdentityImage(parsed.KeyUID, parsed.ImageName)
   322  	if err != nil {
   323  		logger.Error("handleAccountImagesImpl: failed to load image.", zap.String("keyUid", parsed.KeyUID), zap.String("imageName", parsed.ImageName), zap.Error(err))
   324  		return
   325  	}
   326  
   327  	if parsed.Ring && parsed.RingWidth == 0 {
   328  		logger.Error("handleAccountImagesImpl: no ringWidth.")
   329  		return
   330  	}
   331  
   332  	if parsed.BgSize == 0 {
   333  		parsed.BgSize = identityImage.Width
   334  	}
   335  
   336  	payload, err := images.RoundCrop(identityImage.Payload)
   337  	if err != nil {
   338  		logger.Error("handleAccountImagesImpl: failed to crop image.", zap.String("keyUid", parsed.KeyUID), zap.String("imageName", parsed.ImageName), zap.Error(err))
   339  		return
   340  	}
   341  
   342  	enlargeRatio := float64(identityImage.Width) / float64(parsed.BgSize)
   343  
   344  	if parsed.Ring {
   345  		account, err := multiaccountsDB.GetAccount(parsed.KeyUID)
   346  		if err != nil {
   347  			logger.Error("handleAccountImagesImpl: failed to GetAccount .", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   348  			return
   349  		}
   350  
   351  		accColorHash := account.ColorHash
   352  
   353  		if accColorHash == nil {
   354  			if parsed.PublicKey == "" {
   355  				logger.Error("handleAccountImagesImpl: no public key for color hash", zap.String("keyUid", parsed.KeyUID))
   356  			}
   357  
   358  			accColorHash, err = colorhash.GenerateFor(parsed.PublicKey)
   359  			if err != nil {
   360  				logger.Error("handleAccountImagesImpl: could not generate color hash", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   361  			}
   362  		}
   363  
   364  		if accColorHash != nil {
   365  			payload, err = ring.DrawRing(&ring.DrawRingParam{
   366  				Theme: parsed.Theme, ColorHash: accColorHash, ImageBytes: payload, Height: identityImage.Height, Width: identityImage.Width, RingWidth: parsed.RingWidth * enlargeRatio,
   367  			})
   368  			if err != nil {
   369  				logger.Error("handleAccountImagesImpl: failed to draw ring for account identity", zap.Error(err))
   370  				return
   371  			}
   372  		}
   373  	}
   374  
   375  	if parsed.IndicatorSize != 0 {
   376  		// enlarge indicator size based on identity image size / desired size
   377  		// or we get a bad quality identity image
   378  		payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeRatio, parsed.IndicatorBorder*enlargeRatio, parsed.IndicatorCenterToEdge*enlargeRatio)
   379  		if err != nil {
   380  			logger.Error("handleAccountImagesImpl: failed to draw status-indicator for initials", zap.Error(err))
   381  			return
   382  		}
   383  	}
   384  
   385  	if len(payload) == 0 {
   386  		logger.Error("handleAccountImagesImpl: empty image")
   387  		return
   388  	}
   389  
   390  	mime, err := images.GetProtobufImageMime(payload)
   391  	if err != nil {
   392  		logger.Error("failed to get mime", zap.Error(err))
   393  	}
   394  
   395  	w.Header().Set("Content-Type", mime)
   396  	w.Header().Set("Cache-Control", "no-store")
   397  
   398  	_, err = w.Write(payload)
   399  	if err != nil {
   400  		logger.Error("handleAccountImagesImpl: failed to write image", zap.Error(err))
   401  	}
   402  }
   403  
   404  func handleAccountImagesPlaceholder(logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
   405  	if parsed.ImagePath == "" {
   406  		logger.Error("handleAccountImagesPlaceholder: no imagePath")
   407  		return
   408  	}
   409  
   410  	payload, im, err := images.ImageToBytesAndImage(parsed.ImagePath)
   411  	if err != nil {
   412  		logger.Error("handleAccountImagesPlaceholder: failed to load image from disk", zap.String("imageName", parsed.ImagePath))
   413  		return
   414  	}
   415  	width := im.Bounds().Dx()
   416  	if parsed.BgSize == 0 {
   417  		parsed.BgSize = width
   418  	}
   419  
   420  	payload, err = images.RoundCrop(payload)
   421  	if err != nil {
   422  		logger.Error("handleAccountImagesPlaceholder: failed to crop image.", zap.String("imageName", parsed.ImagePath), zap.Error(err))
   423  		return
   424  	}
   425  
   426  	if parsed.IndicatorSize != 0 {
   427  		enlargeIndicatorRatio := float64(width / parsed.BgSize)
   428  		payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeIndicatorRatio, parsed.IndicatorBorder*enlargeIndicatorRatio, parsed.IndicatorCenterToEdge)
   429  		if err != nil {
   430  			logger.Error("handleAccountImagesPlaceholder: failed to draw status-indicator for initials", zap.Error(err))
   431  			return
   432  		}
   433  	}
   434  
   435  	if len(payload) == 0 {
   436  		logger.Error("handleAccountImagesPlaceholder: empty image")
   437  		return
   438  	}
   439  
   440  	mime, err := images.GetProtobufImageMime(payload)
   441  	if err != nil {
   442  		logger.Error("failed to get mime", zap.Error(err))
   443  	}
   444  
   445  	w.Header().Set("Content-Type", mime)
   446  	w.Header().Set("Cache-Control", "no-store")
   447  
   448  	_, err = w.Write(payload)
   449  	if err != nil {
   450  		logger.Error("handleAccountImagesPlaceholder: failed to write image", zap.Error(err))
   451  	}
   452  }
   453  
   454  // handleAccountImages render multiaccounts custom profile image
   455  func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
   456  	return func(w http.ResponseWriter, r *http.Request) {
   457  		params := r.URL.Query()
   458  		parsed := ParseImageParams(logger, params)
   459  
   460  		if parsed.KeyUID == "" {
   461  			handleAccountImagesPlaceholder(logger, w, parsed)
   462  		} else {
   463  			handleAccountImagesImpl(multiaccountsDB, logger, w, parsed)
   464  		}
   465  	}
   466  }
   467  
   468  func handleAccountInitialsImpl(multiaccountsDB *multiaccounts.Database, logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
   469  	var name = parsed.FullName
   470  	var accColorHash multiaccounts.ColorHash
   471  	var account *multiaccounts.Account
   472  
   473  	if parsed.Ring && parsed.RingWidth == 0 {
   474  		logger.Error("handleAccountInitialsImpl: no ringWidth.")
   475  		return
   476  	}
   477  
   478  	if parsed.KeyUID != "" {
   479  		account, err := multiaccountsDB.GetAccount(parsed.KeyUID)
   480  
   481  		if err != nil {
   482  			logger.Error("handleAccountInitialsImpl: failed to get account.", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   483  			return
   484  		}
   485  		name = account.Name
   486  		accColorHash = account.ColorHash
   487  	}
   488  
   489  	initials := images.ExtractInitials(name, parsed.InitialsLength)
   490  
   491  	payload, err := images.GenerateInitialsImage(initials, parsed.BgColor, parsed.Color, parsed.FontFile, parsed.BgSize, parsed.FontSize, parsed.UppercaseRatio)
   492  
   493  	if err != nil {
   494  		logger.Error("handleAccountInitialsImpl: failed to generate initials image.", zap.String("keyUid", parsed.KeyUID), zap.String("name", account.Name), zap.Error(err))
   495  		return
   496  	}
   497  
   498  	if parsed.Ring {
   499  		if accColorHash == nil {
   500  			if parsed.PublicKey == "" {
   501  				logger.Error("handleAccountInitialsImpl: no public key, can't draw ring", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   502  			}
   503  
   504  			accColorHash, err = colorhash.GenerateFor(parsed.PublicKey)
   505  			if err != nil {
   506  				logger.Error("handleAccountInitialsImpl: failed to generate color hash from pubkey", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   507  			}
   508  		}
   509  
   510  		if accColorHash != nil {
   511  			payload, err = ring.DrawRing(&ring.DrawRingParam{
   512  				Theme: parsed.Theme, ColorHash: accColorHash, ImageBytes: payload, Height: parsed.BgSize, Width: parsed.BgSize, RingWidth: parsed.RingWidth,
   513  			})
   514  
   515  			if err != nil {
   516  				logger.Error("handleAccountInitialsImpl: failed to draw ring for account identity", zap.Error(err))
   517  				return
   518  			}
   519  		}
   520  	}
   521  
   522  	if parsed.IndicatorSize != 0 {
   523  		payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize, parsed.IndicatorBorder, parsed.IndicatorCenterToEdge)
   524  		if err != nil {
   525  			logger.Error("failed to draw status-indicator for initials", zap.Error(err))
   526  			return
   527  		}
   528  	}
   529  
   530  	if len(payload) == 0 {
   531  		logger.Error("handleAccountInitialsImpl: empty image", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   532  		return
   533  	}
   534  	mime, err := images.GetProtobufImageMime(payload)
   535  	if err != nil {
   536  		logger.Error("failed to get mime", zap.Error(err))
   537  	}
   538  
   539  	w.Header().Set("Content-Type", mime)
   540  	w.Header().Set("Cache-Control", "no-store")
   541  
   542  	_, err = w.Write(payload)
   543  	if err != nil {
   544  		logger.Error("failed to write image", zap.Error(err))
   545  	}
   546  }
   547  
   548  func handleAccountInitialsPlaceholder(logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
   549  	if parsed.FullName == "" {
   550  		logger.Error("handleAccountInitialsPlaceholder: no full name")
   551  		return
   552  	}
   553  
   554  	initials := images.ExtractInitials(parsed.FullName, parsed.InitialsLength)
   555  
   556  	payload, err := images.GenerateInitialsImage(initials, parsed.BgColor, parsed.Color, parsed.FontFile, parsed.BgSize, parsed.FontSize, parsed.UppercaseRatio)
   557  
   558  	if err != nil {
   559  		logger.Error("handleAccountInitialsPlaceholder: failed to generate initials image.", zap.String("keyUid", parsed.KeyUID), zap.String("name", parsed.FullName), zap.Error(err))
   560  		return
   561  	}
   562  
   563  	if parsed.IndicatorSize != 0 {
   564  		payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize, parsed.IndicatorBorder, parsed.IndicatorCenterToEdge)
   565  		if err != nil {
   566  			logger.Error("failed to draw status-indicator for initials", zap.Error(err))
   567  			return
   568  		}
   569  	}
   570  
   571  	if len(payload) == 0 {
   572  		logger.Error("handleAccountInitialsPlaceholder: empty image", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
   573  		return
   574  	}
   575  	mime, err := images.GetProtobufImageMime(payload)
   576  	if err != nil {
   577  		logger.Error("failed to get mime", zap.Error(err))
   578  	}
   579  
   580  	w.Header().Set("Content-Type", mime)
   581  	w.Header().Set("Cache-Control", "no-store")
   582  
   583  	_, err = w.Write(payload)
   584  	if err != nil {
   585  		logger.Error("failed to write image", zap.Error(err))
   586  	}
   587  }
   588  
   589  // handleAccountInitials render multiaccounts/contacts initials avatar image
   590  func handleAccountInitials(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
   591  	return func(w http.ResponseWriter, r *http.Request) {
   592  		params := r.URL.Query()
   593  		parsed := ParseImageParams(logger, params)
   594  
   595  		if parsed.FontFile == "" {
   596  			logger.Error("handleAccountInitials: no fontFile")
   597  			return
   598  		}
   599  		if parsed.FontSize == 0 {
   600  			logger.Error("handleAccountInitials: no fontSize")
   601  			return
   602  		}
   603  		if parsed.Color == color.Transparent {
   604  			logger.Error("handleAccountInitials: no color")
   605  			return
   606  		}
   607  		if parsed.BgSize == 0 {
   608  			logger.Error("handleAccountInitials: no size")
   609  			return
   610  		}
   611  		if parsed.BgColor == color.Transparent {
   612  			logger.Error("handleAccountInitials: no bgColor")
   613  			return
   614  		}
   615  
   616  		if parsed.KeyUID == "" && parsed.PublicKey == "" {
   617  			handleAccountInitialsPlaceholder(logger, w, parsed)
   618  		} else {
   619  			handleAccountInitialsImpl(multiaccountsDB, logger, w, parsed)
   620  		}
   621  	}
   622  }
   623  
   624  // handleContactImages render contacts custom profile image
   625  func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   626  	if db == nil {
   627  		return handleRequestDBMissing(logger)
   628  	}
   629  
   630  	return func(w http.ResponseWriter, r *http.Request) {
   631  		params := r.URL.Query()
   632  		parsed := ParseImageParams(logger, params)
   633  
   634  		if parsed.PublicKey == "" {
   635  			logger.Error("no publicKey")
   636  			return
   637  		}
   638  
   639  		if parsed.ImageName == "" {
   640  			logger.Error("no imageName")
   641  			return
   642  		}
   643  
   644  		if parsed.Ring && parsed.RingWidth == 0 {
   645  			logger.Error("handleContactImages: no ringWidth.")
   646  			return
   647  		}
   648  
   649  		var payload []byte
   650  		err := db.QueryRow(`SELECT payload FROM chat_identity_contacts WHERE contact_id = ? and image_type = ?`, parsed.PublicKey, parsed.ImageName).Scan(&payload)
   651  		if err != nil {
   652  			logger.Error("failed to load image.", zap.String("contact id", parsed.PublicKey), zap.String("image type", parsed.ImageName), zap.Error(err))
   653  			return
   654  		}
   655  
   656  		img, _, err := image.Decode(bytes.NewReader(payload))
   657  		if err != nil {
   658  			logger.Error("failed to decode config.", zap.String("contact id", parsed.PublicKey), zap.String("image type", parsed.ImageName), zap.Error(err))
   659  			return
   660  		}
   661  		width := img.Bounds().Dx()
   662  
   663  		if parsed.BgSize == 0 {
   664  			parsed.BgSize = width
   665  		}
   666  
   667  		payload, err = images.RoundCrop(payload)
   668  		if err != nil {
   669  			logger.Error("handleContactImages: failed to crop image.", zap.Error(err))
   670  			return
   671  		}
   672  
   673  		enlargeRatio := float64(width) / float64(parsed.BgSize)
   674  
   675  		if parsed.Ring {
   676  			colorHash, err := colorhash.GenerateFor(parsed.PublicKey)
   677  			if err != nil {
   678  				logger.Error("could not generate color hash")
   679  				return
   680  			}
   681  
   682  			payload, err = ring.DrawRing(&ring.DrawRingParam{
   683  				Theme: parsed.Theme, ColorHash: colorHash, ImageBytes: payload, Height: width, Width: width, RingWidth: parsed.RingWidth * enlargeRatio,
   684  			})
   685  
   686  			if err != nil {
   687  				logger.Error("failed to draw ring for contact image.", zap.Error(err))
   688  				return
   689  			}
   690  		}
   691  
   692  		if parsed.IndicatorSize != 0 {
   693  			payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeRatio, parsed.IndicatorBorder*enlargeRatio, parsed.IndicatorCenterToEdge*enlargeRatio)
   694  			if err != nil {
   695  				logger.Error("handleContactImages: failed to draw status-indicator for initials", zap.Error(err))
   696  				return
   697  			}
   698  		}
   699  
   700  		if len(payload) == 0 {
   701  			logger.Error("empty image")
   702  			return
   703  		}
   704  		mime, err := images.GetProtobufImageMime(payload)
   705  		if err != nil {
   706  			logger.Error("failed to get mime", zap.Error(err))
   707  		}
   708  
   709  		w.Header().Set("Content-Type", mime)
   710  		w.Header().Set("Cache-Control", "no-store")
   711  
   712  		_, err = w.Write(payload)
   713  		if err != nil {
   714  			logger.Error("failed to write image", zap.Error(err))
   715  		}
   716  	}
   717  }
   718  
   719  func ringEnabled(params url.Values) bool {
   720  	addRings, ok := params["addRing"]
   721  	return ok && len(addRings) == 1 && addRings[0] == "1"
   722  }
   723  
   724  func getTheme(params url.Values, logger *zap.Logger) ring.Theme {
   725  	theme := ring.LightTheme // default
   726  	themes, ok := params["theme"]
   727  	if ok && len(themes) > 0 {
   728  		t, err := strconv.Atoi(themes[0])
   729  		if err != nil {
   730  			logger.Error("invalid param[theme], value: " + themes[0])
   731  		} else {
   732  			theme = ring.Theme(t)
   733  		}
   734  	}
   735  	return theme
   736  }
   737  
   738  func handleDiscordAuthorAvatar(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   739  	if db == nil {
   740  		return handleRequestDBMissing(logger)
   741  	}
   742  
   743  	return func(w http.ResponseWriter, r *http.Request) {
   744  		params := r.URL.Query()
   745  		parsed := ParseImageParams(logger, params)
   746  
   747  		if parsed.AuthorID == "" {
   748  			logger.Error("no authorIDs")
   749  			return
   750  		}
   751  
   752  		var image []byte
   753  		err := db.QueryRow(`SELECT avatar_image_payload FROM discord_message_authors WHERE id = ?`, parsed.AuthorID).Scan(&image)
   754  		if err != nil {
   755  			logger.Error("failed to find image", zap.Error(err))
   756  			return
   757  		}
   758  		if len(image) == 0 {
   759  			logger.Error("empty image")
   760  			return
   761  		}
   762  		mime, err := images.GetProtobufImageMime(image)
   763  		if err != nil {
   764  			logger.Error("failed to get mime", zap.Error(err))
   765  		}
   766  
   767  		w.Header().Set("Content-Type", mime)
   768  		w.Header().Set("Cache-Control", "no-store")
   769  
   770  		_, err = w.Write(image)
   771  		if err != nil {
   772  			logger.Error("failed to write image", zap.Error(err))
   773  		}
   774  	}
   775  }
   776  
   777  func handleDiscordAttachment(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   778  	if db == nil {
   779  		return handleRequestDBMissing(logger)
   780  	}
   781  
   782  	return func(w http.ResponseWriter, r *http.Request) {
   783  		params := r.URL.Query()
   784  		parsed := ParseImageParams(logger, params)
   785  
   786  		if parsed.MessageID == "" {
   787  			logger.Error("no messageID")
   788  			return
   789  		}
   790  		if parsed.AttachmentID == "" {
   791  			logger.Error("no attachmentID")
   792  			return
   793  		}
   794  
   795  		var image []byte
   796  		err := db.QueryRow(`SELECT payload FROM discord_message_attachments WHERE discord_message_id = ? AND id = ?`, parsed.MessageID, parsed.AttachmentID).Scan(&image)
   797  		if err != nil {
   798  			logger.Error("failed to find image", zap.Error(err))
   799  			return
   800  		}
   801  		if len(image) == 0 {
   802  			logger.Error("empty image")
   803  			return
   804  		}
   805  		mime, err := images.GetProtobufImageMime(image)
   806  		if err != nil {
   807  			logger.Error("failed to get mime", zap.Error(err))
   808  		}
   809  
   810  		w.Header().Set("Content-Type", mime)
   811  		w.Header().Set("Cache-Control", "no-store")
   812  
   813  		_, err = w.Write(image)
   814  		if err != nil {
   815  			logger.Error("failed to write image", zap.Error(err))
   816  		}
   817  	}
   818  }
   819  
   820  func handleImage(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   821  	if db == nil {
   822  		return handleRequestDBMissing(logger)
   823  	}
   824  
   825  	return func(w http.ResponseWriter, r *http.Request) {
   826  		params := r.URL.Query()
   827  		parsed := ParseImageParams(logger, params)
   828  
   829  		if parsed.MessageID == "" {
   830  			logger.Error("no messageID")
   831  			return
   832  		}
   833  
   834  		var image []byte
   835  		err := db.QueryRow(`SELECT image_payload FROM user_messages WHERE id = ?`, parsed.MessageID).Scan(&image)
   836  		if err != nil {
   837  			logger.Error("failed to find image", zap.Error(err))
   838  			return
   839  		}
   840  		if len(image) == 0 {
   841  			logger.Error("empty image")
   842  			return
   843  		}
   844  		mime, err := images.GetProtobufImageMime(image)
   845  		if err != nil {
   846  			logger.Error("failed to get mime", zap.Error(err))
   847  		}
   848  
   849  		w.Header().Set("Content-Type", mime)
   850  		w.Header().Set("Cache-Control", "no-store")
   851  
   852  		_, err = w.Write(image)
   853  		if err != nil {
   854  			logger.Error("failed to write image", zap.Error(err))
   855  		}
   856  	}
   857  }
   858  
   859  func handleAudio(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   860  	if db == nil {
   861  		return handleRequestDBMissing(logger)
   862  	}
   863  
   864  	return func(w http.ResponseWriter, r *http.Request) {
   865  		params := r.URL.Query()
   866  		parsed := ParseImageParams(logger, params)
   867  
   868  		if parsed.MessageID == "" {
   869  			logger.Error("no messageID")
   870  			return
   871  		}
   872  
   873  		var audio []byte
   874  		err := db.QueryRow(`SELECT audio_payload FROM user_messages WHERE id = ?`, parsed.MessageID).Scan(&audio)
   875  		if err != nil {
   876  			logger.Error("failed to find image", zap.Error(err))
   877  			return
   878  		}
   879  		if len(audio) == 0 {
   880  			logger.Error("empty audio")
   881  			return
   882  		}
   883  
   884  		w.Header().Set("Content-Type", "audio/aac")
   885  		w.Header().Set("Cache-Control", "no-store")
   886  
   887  		_, err = w.Write(audio)
   888  		if err != nil {
   889  			logger.Error("failed to write audio", zap.Error(err))
   890  		}
   891  	}
   892  }
   893  
   894  func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFunc {
   895  	if downloader == nil {
   896  		return handleRequestDownloaderMissing(logger)
   897  	}
   898  
   899  	return func(w http.ResponseWriter, r *http.Request) {
   900  		params := r.URL.Query()
   901  		parsed := ParseImageParams(logger, params)
   902  
   903  		if parsed.Hash == "" {
   904  			logger.Error("no hash")
   905  			return
   906  		}
   907  
   908  		content, err := downloader.Get(parsed.Hash, parsed.Download)
   909  		if err != nil {
   910  			logger.Error("could not download hash", zap.Error(err))
   911  			return
   912  		}
   913  
   914  		w.Header().Set("Cache-Control", "max-age:290304000, public")
   915  		w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat))
   916  
   917  		_, err = w.Write(content)
   918  		if err != nil {
   919  			logger.Error("failed to write ipfs resource", zap.Error(err))
   920  		}
   921  	}
   922  }
   923  
   924  func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
   925  	return func(w http.ResponseWriter, r *http.Request) {
   926  		params := r.URL.Query()
   927  
   928  		payload := generateQRBytes(params, logger, multiaccountsDB)
   929  		mime, err := images.GetProtobufImageMime(payload)
   930  
   931  		if err != nil {
   932  			logger.Error("could not generate image from payload", zap.Error(err))
   933  		}
   934  
   935  		w.Header().Set("Content-Type", mime)
   936  		w.Header().Set("Cache-Control", "no-store")
   937  
   938  		_, err = w.Write(payload)
   939  
   940  		if err != nil {
   941  			logger.Error("failed to write image", zap.Error(err))
   942  		}
   943  	}
   944  }
   945  
   946  func handleCommunityTokenImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
   947  	if db == nil {
   948  		return handleRequestDBMissing(logger)
   949  	}
   950  
   951  	return func(w http.ResponseWriter, r *http.Request) {
   952  		params := r.URL.Query()
   953  
   954  		if len(params["communityID"]) == 0 {
   955  			logger.Error("no communityID")
   956  			return
   957  		}
   958  		if len(params["chainID"]) == 0 {
   959  			logger.Error("no chainID")
   960  			return
   961  		}
   962  		if len(params["symbol"]) == 0 {
   963  			logger.Error("no symbol")
   964  			return
   965  		}
   966  
   967  		chainID, err := strconv.ParseUint(params["chainID"][0], 10, 64)
   968  		if err != nil {
   969  			logger.Error("invalid chainID in community token image", zap.Error(err))
   970  			return
   971  		}
   972  
   973  		var base64Image string
   974  		err = db.QueryRow("SELECT image_base64 FROM community_tokens WHERE community_id = ? AND chain_id = ? AND symbol = ?", params["communityID"][0], chainID, params["symbol"][0]).Scan(&base64Image)
   975  		if err != nil {
   976  			logger.Error("failed to find community token image", zap.Error(err))
   977  			return
   978  		}
   979  		if len(base64Image) == 0 {
   980  			logger.Error("empty community token image")
   981  			return
   982  		}
   983  		imagePayload, err := images.GetPayloadFromURI(base64Image)
   984  		if err != nil {
   985  			logger.Error("failed to get community token image payload", zap.Error(err))
   986  			return
   987  		}
   988  		mime, err := images.GetProtobufImageMime(imagePayload)
   989  		if err != nil {
   990  			logger.Error("failed to get community token image mime", zap.Error(err))
   991  		}
   992  
   993  		w.Header().Set("Content-Type", mime)
   994  		w.Header().Set("Cache-Control", "no-store")
   995  
   996  		_, err = w.Write(imagePayload)
   997  		if err != nil {
   998  			logger.Error("failed to write community token image", zap.Error(err))
   999  		}
  1000  	}
  1001  }
  1002  
  1003  func handleCommunityDescriptionImagesPath(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
  1004  	if db == nil {
  1005  		return handleRequestDBMissing(logger)
  1006  	}
  1007  
  1008  	return func(w http.ResponseWriter, r *http.Request) {
  1009  		params := r.URL.Query()
  1010  
  1011  		if len(params["communityID"]) == 0 {
  1012  			logger.Error("[handleCommunityDescriptionImagesPath] no communityID")
  1013  			return
  1014  		}
  1015  		communityID := params["communityID"][0]
  1016  
  1017  		name := ""
  1018  		if len(params["name"]) > 0 {
  1019  			name = params["name"][0]
  1020  		}
  1021  
  1022  		err, communityDescription := getCommunityDescription(db, communityID, logger)
  1023  		if err != nil {
  1024  			return
  1025  		}
  1026  		if communityDescription.Identity == nil {
  1027  			logger.Error("no identity in community description", zap.String("community id", communityID))
  1028  			return
  1029  		}
  1030  
  1031  		var imagePayload []byte
  1032  		for t, i := range communityDescription.Identity.Images {
  1033  			if t == name {
  1034  				imagePayload = i.Payload
  1035  			}
  1036  		}
  1037  		if imagePayload == nil {
  1038  			logger.Error("can't find community description image", zap.String("community id", communityID), zap.String("name", name))
  1039  			return
  1040  		}
  1041  
  1042  		mime, err := images.GetProtobufImageMime(imagePayload)
  1043  		if err != nil {
  1044  			logger.Error("failed to get community image mime", zap.String("community id", communityID), zap.Error(err))
  1045  		}
  1046  
  1047  		w.Header().Set("Content-Type", mime)
  1048  		w.Header().Set("Cache-Control", "no-store")
  1049  		_, err = w.Write(imagePayload)
  1050  		if err != nil {
  1051  			logger.Error("failed to write community image", zap.String("community id", communityID), zap.Error(err))
  1052  		}
  1053  	}
  1054  }
  1055  
  1056  func handleCommunityDescriptionTokenImagesPath(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
  1057  	if db == nil {
  1058  		return handleRequestDBMissing(logger)
  1059  	}
  1060  
  1061  	return func(w http.ResponseWriter, r *http.Request) {
  1062  		params := r.URL.Query()
  1063  
  1064  		if len(params["communityID"]) == 0 {
  1065  			logger.Error("[handleCommunityDescriptionTokenImagesPath] no communityID")
  1066  			return
  1067  		}
  1068  		communityID := params["communityID"][0]
  1069  
  1070  		if len(params["symbol"]) == 0 {
  1071  			logger.Error("[handleCommunityDescriptionTokenImagesPath] no symbol")
  1072  			return
  1073  		}
  1074  		symbol := params["symbol"][0]
  1075  
  1076  		err, communityDescription := getCommunityDescription(db, communityID, logger)
  1077  		if err != nil {
  1078  			return
  1079  		}
  1080  
  1081  		var foundToken *protobuf.CommunityTokenMetadata
  1082  		for _, m := range communityDescription.CommunityTokensMetadata {
  1083  			if m.GetSymbol() == symbol {
  1084  				foundToken = m
  1085  			}
  1086  		}
  1087  		if foundToken == nil {
  1088  			logger.Error("can't find community description token image", zap.String("community id", communityID), zap.String("symbol", symbol))
  1089  			return
  1090  		}
  1091  
  1092  		imagePayload, err := images.GetPayloadFromURI(foundToken.Image)
  1093  		if err != nil {
  1094  			logger.Error("failed to get community description token image payload", zap.Error(err))
  1095  			return
  1096  		}
  1097  		mime, err := images.GetProtobufImageMime(imagePayload)
  1098  		if err != nil {
  1099  			logger.Error("failed to get community description token image mime", zap.String("community id", communityID), zap.String("symbol", symbol), zap.Error(err))
  1100  		}
  1101  
  1102  		w.Header().Set("Content-Type", mime)
  1103  		w.Header().Set("Cache-Control", "no-store")
  1104  		_, err = w.Write(imagePayload)
  1105  		if err != nil {
  1106  			logger.Error("failed to write community description token image", zap.String("community id", communityID), zap.String("symbol", symbol), zap.Error(err))
  1107  		}
  1108  	}
  1109  }
  1110  
  1111  // getCommunityDescription returns the latest community description from the cache.
  1112  // NOTE: you should ensure preprocessDescription is called before this function.
  1113  func getCommunityDescription(db *sql.DB, communityID string, logger *zap.Logger) (error, *protobuf.CommunityDescription) {
  1114  	var descriptionBytes []byte
  1115  	err := db.QueryRow(`SELECT description FROM encrypted_community_description_cache WHERE community_id = ? ORDER BY clock DESC LIMIT 1`, types.Hex2Bytes(communityID)).Scan(&descriptionBytes)
  1116  	if err != nil {
  1117  		logger.Error("failed to find community description", zap.String("community id", communityID), zap.Error(err))
  1118  		return err, nil
  1119  	}
  1120  	communityDescription := new(protobuf.CommunityDescription)
  1121  	err = proto.Unmarshal(descriptionBytes, communityDescription)
  1122  	if err != nil {
  1123  		logger.Error("failed to unmarshal community description", zap.String("community id", communityID), zap.Error(err))
  1124  	}
  1125  	return err, communityDescription
  1126  }
  1127  
  1128  func handleWalletCommunityImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
  1129  	if db == nil {
  1130  		return handleRequestDBMissing(logger)
  1131  	}
  1132  
  1133  	return func(w http.ResponseWriter, r *http.Request) {
  1134  		params := r.URL.Query()
  1135  
  1136  		if len(params["communityID"]) == 0 {
  1137  			logger.Error("no communityID")
  1138  			return
  1139  		}
  1140  
  1141  		var image []byte
  1142  		err := db.QueryRow(`SELECT image_payload FROM community_data_cache WHERE id = ?`, params["communityID"][0]).Scan(&image)
  1143  		if err != nil {
  1144  			logger.Error("failed to find wallet community image", zap.Error(err))
  1145  			return
  1146  		}
  1147  		if len(image) == 0 {
  1148  			logger.Error("empty wallet community image")
  1149  			return
  1150  		}
  1151  		mime, err := images.GetProtobufImageMime(image)
  1152  		if err != nil {
  1153  			logger.Error("failed to get wallet community image mime", zap.Error(err))
  1154  		}
  1155  
  1156  		w.Header().Set("Content-Type", mime)
  1157  		w.Header().Set("Cache-Control", "no-store")
  1158  
  1159  		_, err = w.Write(image)
  1160  		if err != nil {
  1161  			logger.Error("failed to write wallet community image", zap.Error(err))
  1162  		}
  1163  	}
  1164  }
  1165  
  1166  func handleWalletCollectionImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
  1167  	if db == nil {
  1168  		return handleRequestDBMissing(logger)
  1169  	}
  1170  
  1171  	return func(w http.ResponseWriter, r *http.Request) {
  1172  		params := r.URL.Query()
  1173  
  1174  		if len(params["chainID"]) == 0 {
  1175  			logger.Error("no chainID")
  1176  			return
  1177  		}
  1178  
  1179  		if len(params["contractAddress"]) == 0 {
  1180  			logger.Error("no contractAddress")
  1181  			return
  1182  		}
  1183  
  1184  		chainID, err := strconv.ParseUint(params["chainID"][0], 10, 64)
  1185  		if err != nil {
  1186  			logger.Error("invalid chainID in wallet collectible image", zap.Error(err))
  1187  			return
  1188  		}
  1189  		contractAddress := eth_common.HexToAddress(params["contractAddress"][0])
  1190  		if len(contractAddress) == 0 {
  1191  			logger.Error("invalid contractAddress in wallet collectible image", zap.Error(err))
  1192  			return
  1193  		}
  1194  
  1195  		var image []byte
  1196  		err = db.QueryRow(`SELECT image_payload FROM collection_data_cache WHERE chain_id = ? AND contract_address = ?`,
  1197  			chainID,
  1198  			contractAddress).Scan(&image)
  1199  		if err != nil {
  1200  			logger.Error("failed to find wallet collection image", zap.Error(err))
  1201  			return
  1202  		}
  1203  		if len(image) == 0 {
  1204  			logger.Error("empty wallet collection image")
  1205  			return
  1206  		}
  1207  		mime, err := images.GetProtobufImageMime(image)
  1208  		if err != nil {
  1209  			logger.Error("failed to get wallet collection image mime", zap.Error(err))
  1210  		}
  1211  
  1212  		w.Header().Set("Content-Type", mime)
  1213  		w.Header().Set("Cache-Control", "no-store")
  1214  
  1215  		_, err = w.Write(image)
  1216  		if err != nil {
  1217  			logger.Error("failed to write wallet collection image", zap.Error(err))
  1218  		}
  1219  	}
  1220  }
  1221  
  1222  func handleWalletCollectibleImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
  1223  	if db == nil {
  1224  		return handleRequestDBMissing(logger)
  1225  	}
  1226  
  1227  	return func(w http.ResponseWriter, r *http.Request) {
  1228  		params := r.URL.Query()
  1229  
  1230  		if len(params["chainID"]) == 0 {
  1231  			logger.Error("no chainID")
  1232  			return
  1233  		}
  1234  
  1235  		if len(params["contractAddress"]) == 0 {
  1236  			logger.Error("no contractAddress")
  1237  			return
  1238  		}
  1239  
  1240  		if len(params["tokenID"]) == 0 {
  1241  			logger.Error("no tokenID")
  1242  			return
  1243  		}
  1244  
  1245  		chainID, err := strconv.ParseUint(params["chainID"][0], 10, 64)
  1246  		if err != nil {
  1247  			logger.Error("invalid chainID in wallet collectible image", zap.Error(err))
  1248  			return
  1249  		}
  1250  		contractAddress := eth_common.HexToAddress(params["contractAddress"][0])
  1251  		if len(contractAddress) == 0 {
  1252  			logger.Error("invalid contractAddress in wallet collectible image", zap.Error(err))
  1253  			return
  1254  		}
  1255  		tokenID, ok := big.NewInt(0).SetString(params["tokenID"][0], 10)
  1256  		if !ok {
  1257  			logger.Error("invalid tokenID in wallet collectible image", zap.Error(err))
  1258  			return
  1259  		}
  1260  
  1261  		var image []byte
  1262  		err = db.QueryRow(`SELECT image_payload FROM collectible_data_cache WHERE chain_id = ? AND contract_address = ? AND token_id = ?`,
  1263  			chainID,
  1264  			contractAddress,
  1265  			(*bigint.SQLBigIntBytes)(tokenID)).Scan(&image)
  1266  		if err != nil {
  1267  			logger.Error("failed to find wallet collectible image", zap.Error(err))
  1268  			return
  1269  		}
  1270  		if len(image) == 0 {
  1271  			logger.Error("empty image")
  1272  			return
  1273  		}
  1274  		mime, err := images.GetProtobufImageMime(image)
  1275  		if err != nil {
  1276  			logger.Error("failed to get wallet collectible image mime", zap.Error(err))
  1277  		}
  1278  
  1279  		w.Header().Set("Content-Type", mime)
  1280  		w.Header().Set("Cache-Control", "no-store")
  1281  
  1282  		_, err = w.Write(image)
  1283  		if err != nil {
  1284  			logger.Error("failed to write wallet collectible image", zap.Error(err))
  1285  		}
  1286  	}
  1287  }