owpengram-server/internal/rpc/inline_registry.go

785 lines
22 KiB
Go

package rpc
import (
"context"
"fmt"
"sync"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
type inlineRegistry struct {
mu sync.Mutex
pending map[int64]*pendingInlineQuery
cache map[inlineCacheKey]cachedInlineResults
prepared map[string]preparedInlineMessage
webDocuments map[inlineWebDocumentKey]registeredInlineWebDocument
ttl time.Duration
maxCacheTTL time.Duration
maxCacheEntries int
shared store.InlineRegistryStore
}
type pendingInlineQuery struct {
ch chan domain.BotInlineResults
botUserID int64
userID int64
peer domain.Peer
createdAt time.Time
cacheKey inlineCacheKey
results *domain.BotInlineResults
}
type inlineCacheKey struct {
botUserID int64
userID int64
peer domain.Peer
query string
offset string
hasGeo bool
geoLat float64
geoLong float64
geoAccuracy int
}
type cachedInlineResults struct {
results domain.BotInlineResults
expiresAt time.Time
storedAt time.Time
}
type preparedInlineMessage struct {
id string
botUserID int64
userID int64
results domain.BotInlineResults
expiresAt time.Time
}
type inlineWebDocumentKey struct {
url string
accessHash int64
}
type registeredInlineWebDocument struct {
document domain.BotInlineWebDocument
bytes []byte
mimeType string
expiresAt time.Time
}
func newInlineRegistry(ttl time.Duration, shared ...store.InlineRegistryStore) *inlineRegistry {
if ttl <= 0 {
ttl = botInlineQueryTTL
}
var sharedStore store.InlineRegistryStore
if len(shared) > 0 {
sharedStore = shared[0]
}
return &inlineRegistry{
pending: make(map[int64]*pendingInlineQuery),
cache: make(map[inlineCacheKey]cachedInlineResults),
prepared: make(map[string]preparedInlineMessage),
webDocuments: make(map[inlineWebDocumentKey]registeredInlineWebDocument),
ttl: ttl,
maxCacheTTL: botInlineCacheMaxTTL,
maxCacheEntries: botInlineCacheMaxEntries,
shared: sharedStore,
}
}
func (r *inlineRegistry) register(now time.Time, botUserID, userID int64, peer domain.Peer) (int64, *pendingInlineQuery) {
return r.registerWithCacheKeyContext(context.Background(), now, botUserID, userID, peer, inlineCacheKey{})
}
func (r *inlineRegistry) registerWithCacheKey(now time.Time, botUserID, userID int64, peer domain.Peer, key inlineCacheKey) (int64, *pendingInlineQuery) {
return r.registerWithCacheKeyContext(context.Background(), now, botUserID, userID, peer, key)
}
func (r *inlineRegistry) registerWithCacheKeyContext(ctx context.Context, now time.Time, botUserID, userID int64, peer domain.Peer, key inlineCacheKey) (int64, *pendingInlineQuery) {
p := &pendingInlineQuery{
ch: make(chan domain.BotInlineResults, 1),
botUserID: botUserID,
userID: userID,
peer: peer,
createdAt: now,
cacheKey: key,
}
r.mu.Lock()
r.pruneLocked(now)
var queryID int64
for {
queryID = randomNonZeroInt64()
if _, exists := r.pending[queryID]; !exists {
break
}
}
r.pending[queryID] = p
r.mu.Unlock()
r.putSharedPending(ctx, queryID, p)
return queryID, p
}
func (r *inlineRegistry) cachedContext(ctx context.Context, now time.Time, key inlineCacheKey) (domain.BotInlineResults, bool) {
if key.botUserID == 0 {
return domain.BotInlineResults{}, false
}
r.mu.Lock()
r.pruneLocked(now)
cached, ok := r.cache[key]
if ok {
if !cached.expiresAt.After(now) {
delete(r.cache, key)
} else {
out := cloneInlineResults(cached.results)
r.mu.Unlock()
clampInlineCacheTime(&out, cached.expiresAt.Sub(now))
return out, true
}
}
r.mu.Unlock()
if r.shared == nil {
return domain.BotInlineResults{}, false
}
out, ok, remaining, err := r.shared.GetInlineCache(ctx, storeInlineCacheKey(key))
if err != nil || !ok {
return domain.BotInlineResults{}, false
}
out = cloneInlineResults(out)
clampInlineCacheTime(&out, remaining)
return out, true
}
func (r *inlineRegistry) registerCachedContext(ctx context.Context, now time.Time, botUserID, userID int64, peer domain.Peer, results domain.BotInlineResults) domain.BotInlineResults {
clone := cloneInlineResults(results)
clone.BotUserID = botUserID
clone.UserID = userID
clone.Peer = peer
p := &pendingInlineQuery{
ch: make(chan domain.BotInlineResults, 1),
botUserID: botUserID,
userID: userID,
peer: peer,
createdAt: now,
results: &clone,
}
r.mu.Lock()
r.pruneLocked(now)
var queryID int64
for {
queryID = randomNonZeroInt64()
if _, exists := r.pending[queryID]; !exists {
break
}
}
clone.QueryID = queryID
r.pending[queryID] = p
r.storeWebDocumentsLocked(now, clone)
r.mu.Unlock()
r.putSharedResult(ctx, clone, r.ttl)
r.putSharedWebDocuments(ctx, now, clone)
return clone
}
func (r *inlineRegistry) deregisterIfUnansweredContext(ctx context.Context, queryID int64) {
deleteShared := false
r.mu.Lock()
if p, ok := r.pending[queryID]; ok && p.results == nil {
delete(r.pending, queryID)
deleteShared = true
}
r.mu.Unlock()
if deleteShared && r.shared != nil {
_ = r.shared.DeleteInlinePending(ctx, queryID)
}
}
func (r *inlineRegistry) resolve(now time.Time, callerBotID, queryID int64, results domain.BotInlineResults) bool {
return r.resolveContext(context.Background(), now, callerBotID, queryID, results)
}
func (r *inlineRegistry) resolveContext(ctx context.Context, now time.Time, callerBotID, queryID int64, results domain.BotInlineResults) bool {
r.mu.Lock()
r.pruneLocked(now)
p, ok := r.pending[queryID]
if ok && p.botUserID == callerBotID {
results.QueryID = queryID
results.BotUserID = p.botUserID
results.UserID = p.userID
results.Peer = p.peer
results.Query = p.cacheKey.query
if p.cacheKey.hasGeo {
results.Geo = &domain.MessageGeoPoint{
Lat: p.cacheKey.geoLat,
Long: p.cacheKey.geoLong,
AccuracyRadius: p.cacheKey.geoAccuracy,
}
}
clone := cloneInlineResults(results)
p.results = &clone
r.storeWebDocumentsLocked(now, clone)
r.storeCacheLocked(now, p.cacheKey, clone)
r.mu.Unlock()
r.putSharedResolved(ctx, now, p.cacheKey, clone)
select {
case p.ch <- clone:
default:
}
return true
}
r.mu.Unlock()
if r.shared == nil {
return false
}
sharedPending, found, err := r.shared.GetInlinePending(ctx, queryID)
if err != nil || !found || sharedPending.BotUserID != callerBotID {
return false
}
results.QueryID = queryID
results.BotUserID = sharedPending.BotUserID
results.UserID = sharedPending.UserID
results.Peer = sharedPending.Peer
results.Query = sharedPending.CacheKey.Query
if sharedPending.CacheKey.HasGeo {
results.Geo = &domain.MessageGeoPoint{
Lat: sharedPending.CacheKey.GeoLat,
Long: sharedPending.CacheKey.GeoLong,
AccuracyRadius: sharedPending.CacheKey.GeoAccuracy,
}
}
clone := cloneInlineResults(results)
r.putSharedResolved(ctx, now, inlineCacheKeyFromStore(sharedPending.CacheKey), clone)
return true
}
func (r *inlineRegistry) resultsForQueryContext(ctx context.Context, now time.Time, userID, queryID int64) (domain.BotInlineResults, bool) {
r.mu.Lock()
r.pruneLocked(now)
p, ok := r.pending[queryID]
if ok && p.userID == userID && p.results != nil {
results := cloneInlineResults(*p.results)
r.mu.Unlock()
return results, true
}
r.mu.Unlock()
if r.shared == nil {
return domain.BotInlineResults{}, false
}
results, found, err := r.shared.GetInlineResult(ctx, queryID)
if err != nil || !found || results.UserID != userID {
return domain.BotInlineResults{}, false
}
return cloneInlineResults(results), true
}
func (r *inlineRegistry) resultForSendContext(ctx context.Context, now time.Time, userID, queryID int64, id string) (domain.BotInlineResults, domain.BotInlineResult, bool) {
r.mu.Lock()
r.pruneLocked(now)
p, ok := r.pending[queryID]
if ok && p.userID == userID && p.results != nil {
results, result, found := inlineResultByID(*p.results, id)
r.mu.Unlock()
return results, result, found
}
r.mu.Unlock()
if r.shared == nil {
return domain.BotInlineResults{}, domain.BotInlineResult{}, false
}
results, found, err := r.shared.GetInlineResult(ctx, queryID)
if err != nil || !found || results.UserID != userID {
return domain.BotInlineResults{}, domain.BotInlineResult{}, false
}
return inlineResultByID(results, id)
}
func (r *inlineRegistry) savePreparedInlineContext(ctx context.Context, now time.Time, botUserID, userID int64, result domain.BotInlineResult, peerTypes []string) (string, int) {
ttl := r.ttl
if ttl <= 0 {
ttl = botInlineQueryTTL
}
results := domain.BotInlineResults{
BotUserID: botUserID,
UserID: userID,
Results: []domain.BotInlineResult{result},
CacheTime: int(ttl.Seconds()),
PeerTypes: append([]string(nil), peerTypes...),
}
expiresAt := now.Add(ttl)
r.mu.Lock()
r.pruneLocked(now)
id := ""
for i := 0; i < 16; i++ {
candidate := randomPreparedInlineID()
if _, exists := r.prepared[candidate]; !exists {
id = candidate
break
}
}
if id == "" {
id = fmt.Sprintf("%016x%016x", uint64(now.UnixNano()), uint64(randomNonZeroInt64()))
}
msg := preparedInlineMessage{
id: id,
botUserID: botUserID,
userID: userID,
results: cloneInlineResults(results),
expiresAt: expiresAt,
}
r.prepared[id] = msg
r.storeWebDocumentsLocked(now, msg.results)
r.mu.Unlock()
r.putSharedPrepared(ctx, msg, ttl)
r.putSharedWebDocuments(ctx, now, msg.results)
return id, int(expiresAt.Unix())
}
func (r *inlineRegistry) preparedInlineContext(ctx context.Context, now time.Time, userID, botUserID int64, id string) (domain.BotInlineResults, bool) {
var results domain.BotInlineResults
found := false
r.mu.Lock()
r.pruneLocked(now)
if msg, ok := r.prepared[id]; ok {
if msg.expiresAt.After(now) && msg.userID == userID && msg.botUserID == botUserID {
results = cloneInlineResults(msg.results)
found = true
}
}
r.mu.Unlock()
if found {
return r.registerCachedContext(ctx, now, botUserID, userID, domain.Peer{}, results), true
}
if r.shared == nil {
return domain.BotInlineResults{}, false
}
shared, ok, err := r.shared.GetPreparedInlineMessage(ctx, id)
if err != nil || !ok || shared.UserID != userID || shared.BotUserID != botUserID || !shared.ExpiresAt.After(now) {
return domain.BotInlineResults{}, false
}
results = cloneInlineResults(shared.Results)
return r.registerCachedContext(ctx, now, botUserID, userID, domain.Peer{}, results), true
}
func (r *inlineRegistry) consume(queryID int64) {
r.consumeContext(context.Background(), queryID)
}
func (r *inlineRegistry) consumeContext(ctx context.Context, queryID int64) {
r.mu.Lock()
delete(r.pending, queryID)
r.mu.Unlock()
if r.shared != nil {
_ = r.shared.DeleteInlinePending(ctx, queryID)
_ = r.shared.DeleteInlineResult(ctx, queryID)
}
}
func (r *inlineRegistry) unansweredSize() int {
r.mu.Lock()
defer r.mu.Unlock()
count := 0
for _, p := range r.pending {
if p.results == nil {
count++
}
}
return count
}
func (r *inlineRegistry) pruneLocked(now time.Time) {
if r.ttl > 0 {
for queryID, p := range r.pending {
if now.Sub(p.createdAt) > r.ttl {
delete(r.pending, queryID)
}
}
}
for key, cached := range r.cache {
if !cached.expiresAt.After(now) {
delete(r.cache, key)
}
}
for id, msg := range r.prepared {
if !msg.expiresAt.After(now) {
delete(r.prepared, id)
}
}
for key, document := range r.webDocuments {
if !document.expiresAt.After(now) {
delete(r.webDocuments, key)
}
}
}
func (r *inlineRegistry) storeCacheLocked(now time.Time, key inlineCacheKey, results domain.BotInlineResults) {
ttl, ok := r.cacheTTL(results)
if key.botUserID == 0 || !ok || r.maxCacheEntries <= 0 {
return
}
cached := cloneInlineResults(results)
cached.QueryID = 0
r.dropOldestCacheEntryLocked(now)
r.cache[key] = cachedInlineResults{
results: cached,
expiresAt: now.Add(ttl),
storedAt: now,
}
}
func (r *inlineRegistry) dropOldestCacheEntryLocked(now time.Time) {
if len(r.cache) < r.maxCacheEntries {
return
}
var (
oldestKey inlineCacheKey
oldestAt time.Time
found bool
)
for key, cached := range r.cache {
if !cached.expiresAt.After(now) {
delete(r.cache, key)
continue
}
if !found || cached.storedAt.Before(oldestAt) {
oldestKey = key
oldestAt = cached.storedAt
found = true
}
}
if len(r.cache) >= r.maxCacheEntries && found {
delete(r.cache, oldestKey)
}
}
func (r *inlineRegistry) storeWebDocumentsLocked(now time.Time, results domain.BotInlineResults) {
expiresAt := now.Add(r.webDocumentTTL(results))
for _, item := range results.Results {
r.storeWebDocumentLocked(item.Thumb, expiresAt)
r.storeWebDocumentLocked(item.Content, expiresAt)
}
}
func (r *inlineRegistry) storeWebDocumentLocked(document *domain.BotInlineWebDocument, expiresAt time.Time) {
if document == nil || document.URL == "" || document.AccessHash == 0 {
return
}
key := inlineWebDocumentKey{url: document.URL, accessHash: document.AccessHash}
clone := cloneInlineWebDocument(*document)
existing := r.webDocuments[key]
r.webDocuments[key] = registeredInlineWebDocument{
document: clone,
bytes: append([]byte(nil), existing.bytes...),
mimeType: existing.mimeType,
expiresAt: expiresAt,
}
}
func (r *inlineRegistry) webDocumentForDownloadContext(ctx context.Context, now time.Time, url string, accessHash int64) (domain.BotInlineWebDocument, []byte, string, bool) {
r.mu.Lock()
r.pruneLocked(now)
registered, ok := r.webDocuments[inlineWebDocumentKey{url: url, accessHash: accessHash}]
if ok {
r.mu.Unlock()
return cloneInlineWebDocument(registered.document), append([]byte(nil), registered.bytes...), registered.mimeType, true
}
r.mu.Unlock()
if r.shared == nil {
return domain.BotInlineWebDocument{}, nil, "", false
}
shared, ok, err := r.shared.GetInlineWebDocument(ctx, store.InlineWebDocumentKey{URL: url, AccessHash: accessHash})
if err != nil || !ok {
return domain.BotInlineWebDocument{}, nil, "", false
}
return cloneInlineWebDocument(shared.Document), append([]byte(nil), shared.Bytes...), shared.MimeType, true
}
func (r *inlineRegistry) cacheWebDocumentBytesContext(ctx context.Context, now time.Time, url string, accessHash int64, data []byte, mimeType string) bool {
if len(data) > domain.MaxBotInlineWebSize {
return false
}
localUpdated := false
r.mu.Lock()
r.pruneLocked(now)
key := inlineWebDocumentKey{url: url, accessHash: accessHash}
registered, ok := r.webDocuments[key]
if ok {
registered.bytes = append([]byte(nil), data...)
registered.mimeType = mimeType
r.webDocuments[key] = registered
localUpdated = true
}
r.mu.Unlock()
if r.shared == nil {
return localUpdated
}
err := r.shared.PutInlineWebDocumentBytes(ctx, store.InlineWebDocumentKey{URL: url, AccessHash: accessHash}, append([]byte(nil), data...), mimeType, r.ttl)
return localUpdated || err == nil
}
func (r *inlineRegistry) registerWebDocumentContext(ctx context.Context, now time.Time, document domain.BotInlineWebDocument, ttl time.Duration) {
if document.URL == "" || document.AccessHash == 0 {
return
}
if ttl <= 0 {
ttl = r.ttl
}
r.mu.Lock()
r.pruneLocked(now)
r.storeWebDocumentLocked(&document, now.Add(ttl))
r.mu.Unlock()
r.putSharedWebDocument(ctx, &document, ttl)
}
func (r *inlineRegistry) putSharedPending(ctx context.Context, queryID int64, p *pendingInlineQuery) {
if r.shared == nil {
return
}
_ = r.shared.PutInlinePending(ctx, store.InlinePending{
QueryID: queryID,
BotUserID: p.botUserID,
UserID: p.userID,
Peer: p.peer,
CacheKey: storeInlineCacheKey(p.cacheKey),
CreatedAt: p.createdAt,
}, r.ttl)
}
func (r *inlineRegistry) putSharedResolved(ctx context.Context, now time.Time, key inlineCacheKey, results domain.BotInlineResults) {
r.putSharedResult(ctx, results, r.ttl)
r.putSharedCache(ctx, key, results)
r.putSharedWebDocuments(ctx, now, results)
}
func (r *inlineRegistry) putSharedResult(ctx context.Context, results domain.BotInlineResults, ttl time.Duration) {
if r.shared == nil || ttl <= 0 {
return
}
_ = r.shared.PutInlineResult(ctx, cloneInlineResults(results), ttl)
}
func (r *inlineRegistry) putSharedCache(ctx context.Context, key inlineCacheKey, results domain.BotInlineResults) {
if r.shared == nil || key.botUserID == 0 {
return
}
ttl, ok := r.cacheTTL(results)
if !ok {
return
}
cached := cloneInlineResults(results)
cached.QueryID = 0
_ = r.shared.PutInlineCache(ctx, storeInlineCacheKey(key), cached, ttl)
}
func (r *inlineRegistry) putSharedWebDocuments(ctx context.Context, now time.Time, results domain.BotInlineResults) {
if r.shared == nil {
return
}
ttl := r.webDocumentTTL(results)
if ttl <= 0 {
return
}
for _, item := range results.Results {
r.putSharedWebDocument(ctx, item.Thumb, ttl)
r.putSharedWebDocument(ctx, item.Content, ttl)
}
_ = now
}
func (r *inlineRegistry) putSharedWebDocument(ctx context.Context, document *domain.BotInlineWebDocument, ttl time.Duration) {
if r.shared == nil || document == nil || document.URL == "" || document.AccessHash == 0 || ttl <= 0 {
return
}
_ = r.shared.PutInlineWebDocument(ctx, cloneInlineWebDocument(*document), ttl)
}
func (r *inlineRegistry) putSharedPrepared(ctx context.Context, msg preparedInlineMessage, ttl time.Duration) {
if r.shared == nil || ttl <= 0 {
return
}
_ = r.shared.PutPreparedInlineMessage(ctx, store.PreparedInlineMessage{
ID: msg.id,
BotUserID: msg.botUserID,
UserID: msg.userID,
Results: cloneInlineResults(msg.results),
ExpiresAt: msg.expiresAt,
}, ttl)
}
func (r *inlineRegistry) cacheTTL(results domain.BotInlineResults) (time.Duration, bool) {
if results.Private || results.CacheTime <= 0 || r.maxCacheTTL <= 0 {
return 0, false
}
ttl := time.Duration(results.CacheTime) * time.Second
if ttl <= 0 {
return 0, false
}
if ttl > r.maxCacheTTL {
ttl = r.maxCacheTTL
}
return ttl, true
}
func (r *inlineRegistry) webDocumentTTL(results domain.BotInlineResults) time.Duration {
ttl := r.ttl
if cacheTTL, ok := r.cacheTTL(results); ok && cacheTTL > ttl {
ttl = cacheTTL
}
return ttl
}
func inlineResultByID(results domain.BotInlineResults, id string) (domain.BotInlineResults, domain.BotInlineResult, bool) {
for _, item := range results.Results {
if item.ID == id {
return cloneInlineResults(results), cloneInlineResult(item), true
}
}
return domain.BotInlineResults{}, domain.BotInlineResult{}, false
}
func randomPreparedInlineID() string {
return fmt.Sprintf("%016x%016x", uint64(randomNonZeroInt64()), uint64(randomNonZeroInt64()))
}
func clampInlineCacheTime(results *domain.BotInlineResults, remaining time.Duration) {
seconds := int(remaining.Seconds())
if seconds <= 0 {
seconds = 1
}
if results.CacheTime <= 0 || seconds < results.CacheTime {
results.CacheTime = seconds
}
}
func storeInlineCacheKey(key inlineCacheKey) store.InlineCacheKey {
return store.InlineCacheKey{
BotUserID: key.botUserID,
UserID: key.userID,
Peer: key.peer,
Query: key.query,
Offset: key.offset,
HasGeo: key.hasGeo,
GeoLat: key.geoLat,
GeoLong: key.geoLong,
GeoAccuracy: key.geoAccuracy,
}
}
func inlineCacheKeyFromStore(key store.InlineCacheKey) inlineCacheKey {
return inlineCacheKey{
botUserID: key.BotUserID,
userID: key.UserID,
peer: key.Peer,
query: key.Query,
offset: key.Offset,
hasGeo: key.HasGeo,
geoLat: key.GeoLat,
geoLong: key.GeoLong,
geoAccuracy: key.GeoAccuracy,
}
}
func cloneInlineResults(in domain.BotInlineResults) domain.BotInlineResults {
if in.Geo != nil {
geo := *in.Geo
in.Geo = &geo
}
if in.SwitchPM != nil {
switchPM := *in.SwitchPM
in.SwitchPM = &switchPM
}
if in.SwitchWeb != nil {
switchWeb := *in.SwitchWeb
in.SwitchWeb = &switchWeb
}
in.PeerTypes = append([]string(nil), in.PeerTypes...)
in.Results = append([]domain.BotInlineResult(nil), in.Results...)
for i := range in.Results {
in.Results[i] = cloneInlineResult(in.Results[i])
}
return in
}
func cloneInlineResult(in domain.BotInlineResult) domain.BotInlineResult {
in.Entities = append([]domain.MessageEntity(nil), in.Entities...)
if in.Thumb != nil {
thumb := cloneInlineWebDocument(*in.Thumb)
in.Thumb = &thumb
}
if in.Content != nil {
content := cloneInlineWebDocument(*in.Content)
in.Content = &content
}
in.ReplyMarkup = cloneInlineReplyMarkup(in.ReplyMarkup)
in.Media = cloneInlineMedia(in.Media)
return in
}
func cloneInlineWebDocument(in domain.BotInlineWebDocument) domain.BotInlineWebDocument {
in.Attributes = append([]domain.DocumentAttribute(nil), in.Attributes...)
return in
}
func cloneInlineReplyMarkup(in *domain.MessageReplyMarkup) *domain.MessageReplyMarkup {
if in == nil {
return nil
}
out := domain.MessageReplyMarkup{}
if in.Inline != nil {
out.Inline = make([][]domain.MarkupButton, len(in.Inline))
for i, row := range in.Inline {
out.Inline[i] = make([]domain.MarkupButton, len(row))
for j, button := range row {
out.Inline[i][j] = button
out.Inline[i][j].Data = append([]byte(nil), button.Data...)
}
}
}
return &out
}
func cloneInlineMedia(in *domain.MessageMedia) *domain.MessageMedia {
if in == nil {
return nil
}
out := *in
if in.Photo != nil {
photo := *in.Photo
photo.FileReference = append([]byte(nil), in.Photo.FileReference...)
photo.Sizes = cloneInlinePhotoSizes(in.Photo.Sizes)
out.Photo = &photo
}
if in.Document != nil {
doc := *in.Document
doc.FileReference = append([]byte(nil), in.Document.FileReference...)
doc.Attributes = append([]domain.DocumentAttribute(nil), in.Document.Attributes...)
doc.Thumbs = cloneInlinePhotoSizes(in.Document.Thumbs)
out.Document = &doc
}
if in.Geo != nil {
geo := *in.Geo
out.Geo = &geo
}
if in.Venue != nil {
venue := *in.Venue
out.Venue = &venue
}
if in.Contact != nil {
contact := *in.Contact
out.Contact = &contact
}
return &out
}
func cloneInlinePhotoSizes(in []domain.PhotoSize) []domain.PhotoSize {
if len(in) == 0 {
return nil
}
out := append([]domain.PhotoSize(nil), in...)
for i := range out {
out[i].Bytes = append([]byte(nil), in[i].Bytes...)
out[i].Sizes = append([]int(nil), in[i].Sizes...)
out[i].BackgroundColors = append([]int(nil), in[i].BackgroundColors...)
}
return out
}