Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🤞 Fix the channel avatar url #257

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"query-node-start": "NODE_ENV=production patch-package --patch-dir assets/patches && squid-graphql-server --subscriptions",
"auth-server-start": "node lib/auth-server/index.js",
"postinstall": "patch-package --patch-dir assets/patches",
"mail-scheduler": "npx ts-node ./src/mail-scheduler/index.ts",
"tests:codegen": "npx graphql-codegen -c ./src/tests/v1/codegen.yml && npx graphql-codegen -c ./src/tests/v2/codegen.yml",
"tests:compareState": "npx ts-node ./src/tests/compareState.ts",
"tests:benchmark": "npx ts-node ./src/tests/benchmarks/index.ts",
Expand Down
72 changes: 15 additions & 57 deletions src/server-extension/resolvers/AssetsResolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,18 @@ import {
import _ from 'lodash'
import { globalEm } from '../../../utils/globalEm'
import { performance } from 'perf_hooks'
import urljoin from 'url-join'
import { Context } from '@subsquid/openreader/lib/context'
import haversineDistance from 'haversine-distance'
import { createLogger, Logger } from '@subsquid/logger'
import { Logger } from '@subsquid/logger'

const rootLogger = createLogger('api:assets')

type Coordinates = {
lat: number
lon: number
}
type NodeData = {
location?: Coordinates
endpoint: string
}

type DistributionBucketCachedData = {
nodes: NodeData[]
}

type DistributionBucketIdsByBagId = Map<string, string[]>
type BucketsById = Map<string, DistributionBucketCachedData>

class DistributionBucketsCache {
import {
BucketsById,
Coordinates,
DistributionBucketCachedData,
DistributionBucketIdsByBagId,
} from './types'
import { getAssetUrls, locationLogger, rootLogger } from './utils'

export class DistributionBucketsCache {
protected bucketIdsByBagId: DistributionBucketIdsByBagId
protected bucketsById: BucketsById
protected em: EntityManager
Expand Down Expand Up @@ -172,11 +160,6 @@ class DistributionBucketsCache {
}
}

const distributionBucketsCache = new DistributionBucketsCache()
distributionBucketsCache.init(6000)

const locationLogger = rootLogger.child('location')

function isValidLat(lat: number | undefined): lat is number {
if (lat === undefined) {
return false
Expand All @@ -191,21 +174,6 @@ function isValidLon(lon: number | undefined): lon is number {
return !Number.isNaN(lon) && lon >= -180 && lon <= 180
}

function getDistance(node: NodeData, clientLoc: Coordinates) {
return node.location ? haversineDistance(clientLoc, node.location) : Infinity
}

function sortNodesByClosest(nodes: NodeData[], clientLoc: Coordinates): void {
nodes.sort((nodeA, nodeB) => getDistance(nodeA, clientLoc) - getDistance(nodeB, clientLoc))
nodes.forEach((n) => {
locationLogger.trace(
`Node: ${JSON.stringify(n)}, Client loc: ${JSON.stringify(
clientLoc
)}, Distance: ${getDistance(n, clientLoc)}`
)
})
}

function getClientLoc(ctx: Context): Coordinates | undefined {
const clientLoc = ctx.req.headers['x-client-loc']
if (typeof clientLoc !== 'string') {
Expand Down Expand Up @@ -255,23 +223,13 @@ export class AssetsResolver {
'incorrect query: to use resolvedUrls make sure to add storageBag.id into query for StorageDataObject'
)
}
const clientLoc = await getClientLoc(ctx)
const limit = await getResolvedUrlsLimit(ctx)

const clientLoc = getClientLoc(ctx)
const limit = getResolvedUrlsLimit(ctx)

// The resolvedUrl field is initially populated with the object ID
const [objectId] = object.resolvedUrls
if (!object.storageBag?.id || !objectId) {
return []
}
const buckets = await distributionBucketsCache.getBucketsByBagId(object.storageBag.id)
const nodes = buckets.flatMap((b) => b.nodes)
if (clientLoc) {
sortNodesByClosest(nodes, clientLoc)
} else {
nodes.sort(() => (_.random(0, 1) ? 1 : -1))
}

return nodes
.slice(0, limit || nodes.length)
.map((n) => urljoin(n.endpoint, 'api/v1/assets/', objectId))
return await getAssetUrls(objectId, object.storageBag.id, { clientLoc, limit })
}
}
15 changes: 15 additions & 0 deletions src/server-extension/resolvers/AssetsResolver/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type Coordinates = {
lat: number
lon: number
}
export type NodeData = {
location?: Coordinates
endpoint: string
}

export type DistributionBucketCachedData = {
nodes: NodeData[]
}

export type DistributionBucketIdsByBagId = Map<string, string[]>
export type BucketsById = Map<string, DistributionBucketCachedData>
53 changes: 53 additions & 0 deletions src/server-extension/resolvers/AssetsResolver/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createLogger } from '@subsquid/logger'
import haversineDistance from 'haversine-distance'
import { random } from 'lodash'
import urljoin from 'url-join'

import { Coordinates, NodeData } from './types'
import { DistributionBucketsCache } from '.'

export const rootLogger = createLogger('api:assets')
export const locationLogger = rootLogger.child('location')

let distributionBucketsCache: DistributionBucketsCache
export async function getAssetUrls(
objectId: string | undefined | null,
bagId: string | undefined | null,
{ clientLoc, limit }: { clientLoc?: Coordinates; limit: number }
): Promise<string[]> {
if (!objectId || !bagId) {
return []
}

if (!distributionBucketsCache) {
distributionBucketsCache = new DistributionBucketsCache()
distributionBucketsCache.init(6000)
}

const buckets = distributionBucketsCache.getBucketsByBagId(bagId)
const nodes = buckets.flatMap((b) => b.nodes)
if (clientLoc) {
sortNodesByClosest(nodes, clientLoc)
} else {
nodes.sort(() => (random(0, 1) ? 1 : -1))
}

return nodes
.slice(0, limit || nodes.length)
.map((n) => urljoin(n.endpoint, 'api/v1/assets/', objectId))
}

function getDistance(node: NodeData, clientLoc: Coordinates) {
return node.location ? haversineDistance(clientLoc, node.location) : Infinity
}

function sortNodesByClosest(nodes: NodeData[], clientLoc: Coordinates): void {
nodes.sort((nodeA, nodeB) => getDistance(nodeA, clientLoc) - getDistance(nodeB, clientLoc))
nodes.forEach((n) => {
locationLogger.trace(
`Node: ${JSON.stringify(n)}, Client loc: ${JSON.stringify(
clientLoc
)}, Distance: ${getDistance(n, clientLoc)}`
)
})
}
29 changes: 29 additions & 0 deletions src/utils/notification/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,32 @@ async function saveNextNotificationId(
})
await em.save(nextEntityId)
}

const JOY_DECIMAL = 10
export const formatJOY = (hapiAmount: bigint | number): string => {
const [intPart, decPart] = splitInt(String(hapiAmount), JOY_DECIMAL)

const formatedIntPart = chunkFromEnd(intPart, 3).join(' ') || '0'

const fractionDigits = (decPart.match(/[1-9]/)?.index ?? -1) + 1
const roundedDecPart =
fractionDigits === 0
? ''
: !intPart && fractionDigits > 2
? roundDecPart(decPart, fractionDigits).replace(/\.?0+$/, '')
: roundDecPart(decPart, 2).replace(/^\.00/, '')

return `${formatedIntPart}${roundedDecPart} $JOY`
}
const splitInt = (numStr: string, decimalSize: number): [string, string] => {
const intPart = numStr.slice(0, -decimalSize) ?? ''
const decPart = numStr.slice(-decimalSize).padStart(decimalSize, '0') || '0'
return [intPart, decPart]
}
const chunkFromEnd = (str: string, interval: number): string[] =>
Array.from({ length: Math.floor((str.length - 1) / interval) }).reduce(
([head, ...tail]: string[]) => [head.slice(0, -interval), head.slice(-interval), ...tail],
[str]
)
const roundDecPart = (decPart: string, fractionDigits: number): string =>
Number(`.${decPart}`).toFixed(fractionDigits).slice(1)
55 changes: 27 additions & 28 deletions src/utils/notification/notificationAvatars.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import { memoize } from 'lodash'
import { EntityManager } from 'typeorm'

import { Channel, MemberMetadata, StorageDataObject } from '../../model'
import { getAssetUrls } from '../../server-extension/resolvers/AssetsResolver/utils'
import { ConfigVariable, config } from '../config'

export const getNotificationAvatar = async (
em: EntityManager,
type: 'channelId' | 'membershipId',
param: string
): Promise<string> => {
switch (type) {
case 'channelId': {
const channel = await em.getRepository(Channel).findOneBy({ id: param })

if (!channel?.avatarPhotoId) break

const avatar = await em
.getRepository(StorageDataObject)
.findOneBy({ id: channel.avatarPhotoId })
export const getNotificationAvatar = memoize(
async (em: EntityManager, type: 'channelId' | 'membershipId', param: string): Promise<string> => {
switch (type) {
case 'channelId': {
const channel = await em.getRepository(Channel).findOneBy({ id: param })
const objectId = channel?.avatarPhotoId
if (!objectId) break

if (!avatar || !avatar.isAccepted || !avatar.resolvedUrls[0]) break
const object = await em.getRepository(StorageDataObject).findOneBy({ id: objectId })
const avatarUrls = await getAssetUrls(objectId, object?.storageBagId, { limit: 1 })
if (!avatarUrls?.[0]) break

return avatar.resolvedUrls[0]
}
return avatarUrls[0]
}

case 'membershipId': {
const member = await em.getRepository(MemberMetadata).findOneBy({ id: param })
const avatar = member?.avatar
case 'membershipId': {
const member = await em.getRepository(MemberMetadata).findOneBy({ id: param })
const avatar = member?.avatar

// AvatarObject is not yet supported
if (!avatar || avatar.isTypeOf !== 'AvatarUri') break
// AvatarObject is not yet supported
if (!avatar || avatar.isTypeOf !== 'AvatarUri') break

return avatar.avatarUri
return avatar.avatarUri
}
}
}

// Fallback to a placeholder
const notificationAssetRoot = await config.get(ConfigVariable.AppAssetStorage, em)
return `${notificationAssetRoot}/placeholder/avatar.png`
}
// Fallback to a placeholder
const notificationAssetRoot = await config.get(ConfigVariable.AppAssetStorage, em)
return `${notificationAssetRoot}/placeholder/avatar.png`
}
)
32 changes: 2 additions & 30 deletions src/utils/notification/notificationsData.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EntityManager } from 'typeorm'
import { Channel, Notification } from '../../model'
import { Notification } from '../../model'
import { getNotificationAvatar } from './notificationAvatars'
import { getNotificationIcon } from './notificationIcons'
import { getNotificationLink } from './notificationLinks'
import { formatJOY } from './helpers'

export type NotificationData = {
icon: string
Expand Down Expand Up @@ -253,32 +254,3 @@ export const getNotificationData = async (
}
}
}

const JOY_DECIMAL = 10
export const formatJOY = (hapiAmount: bigint | number): string => {
const [intPart, decPart] = splitInt(String(hapiAmount), JOY_DECIMAL)

const formatedIntPart = chunkFromEnd(intPart, 3).join(' ') || '0'

const fractionDigits = (decPart.match(/[1-9]/)?.index ?? -1) + 1
const roundedDecPart =
fractionDigits === 0
? ''
: !intPart && fractionDigits > 2
? roundDecPart(decPart, fractionDigits).replace(/\.?0+$/, '')
: roundDecPart(decPart, 2).replace(/^\.00/, '')

return `${formatedIntPart}${roundedDecPart} $JOY`
}
const splitInt = (numStr: string, decimalSize: number): [string, string] => {
const intPart = numStr.slice(0, -decimalSize) ?? ''
const decPart = numStr.slice(-decimalSize).padStart(decimalSize, '0') || '0'
return [intPart, decPart]
}
const chunkFromEnd = (str: string, interval: number): string[] =>
Array.from({ length: Math.floor((str.length - 1) / interval) }).reduce(
([head, ...tail]: string[]) => [head.slice(0, -interval), head.slice(-interval), ...tail],
[str]
)
const roundDecPart = (decPart: string, fractionDigits: number): string =>
Number(`.${decPart}`).toFixed(fractionDigits).slice(1)
2 changes: 1 addition & 1 deletion src/utils/notification/testing/formatJOY.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { formatJOY } from '../notificationsData'
import { formatJOY } from '../helpers'

describe('formatJOY', () => {
it('Parse HAPI amounts into JOY values', () => {
Expand Down
Loading