Skip to content

Commit

Permalink
feat: Get profiles from catalyst and map to friends
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinszuchet committed Jan 22, 2025
1 parent c1b68ac commit 77fbe7e
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 32 deletions.
7 changes: 4 additions & 3 deletions src/adapters/rpc-server/rpc-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ export async function createRpcServerComponent({
pubsub,
config,
server,
archipelagoStats
archipelagoStats,
catalystClient
}: Pick<
AppComponents,
'logs' | 'db' | 'pubsub' | 'config' | 'server' | 'nats' | 'archipelagoStats' | 'redis'
'logs' | 'db' | 'pubsub' | 'config' | 'server' | 'nats' | 'archipelagoStats' | 'redis' | 'catalystClient'
>): Promise<IRPCServerComponent> {
// TODO: this should be a redis if we want to have more than one instance of the server
const SHARED_CONTEXT: Pick<RpcServerContext, 'subscribers'> = {
Expand All @@ -36,7 +37,7 @@ export async function createRpcServerComponent({

const rpcServerPort = (await config.getNumber('RPC_SERVER_PORT')) || 8085

const getFriends = getFriendsService({ components: { logs, db } })
const getFriends = await getFriendsService({ components: { logs, db, catalystClient, config } })
const getMutualFriends = getMutualFriendsService({ components: { logs, db } })
const getPendingFriendshipRequests = getPendingFriendshipRequestsService({ components: { logs, db } })
const getSentFriendshipRequests = getSentFriendshipRequestsService({ components: { logs, db } })
Expand Down
12 changes: 9 additions & 3 deletions src/adapters/rpc-server/services/get-friends.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { parseProfilesToFriends } from '../../../logic/friends'
import { RpcServerContext, RPCServiceContext } from '../../../types'
import { getPage } from '../../../utils/pagination'
import { FRIENDSHIPS_PER_PAGE, INTERNAL_SERVER_ERROR } from '../constants'
import { FRIENDSHIPS_PER_PAGE } from '../constants'
import {
GetFriendsPayload,
PaginatedUsersResponse
} from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen'

export function getFriendsService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) {
export async function getFriendsService({
components: { logs, db, catalystClient, config }
}: RPCServiceContext<'logs' | 'db' | 'catalystClient' | 'config'>) {
const logger = logs.getLogger('get-friends-service')
const contentServerUrl = await config.requireString('CONTENT_SERVER_URL')

return async function (request: GetFriendsPayload, context: RpcServerContext): Promise<PaginatedUsersResponse> {
const { pagination } = request
Expand All @@ -20,8 +24,10 @@ export function getFriendsService({ components: { logs, db } }: RPCServiceContex
db.getFriendsCount(loggedUserAddress)
])

const profiles = await catalystClient.getEntitiesByPointers(friends.map((friend) => friend.address))

return {
users: friends,
users: parseProfilesToFriends(profiles, contentServerUrl),
paginationData: {
total,
page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset)
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/rpc-server/services/get-mutual-friends.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RpcServerContext, RPCServiceContext } from '../../../types'
import { INTERNAL_SERVER_ERROR, FRIENDSHIPS_PER_PAGE } from '../constants'
import { FRIENDSHIPS_PER_PAGE } from '../constants'
import {
GetMutualFriendsPayload,
PaginatedUsersResponse
Expand Down
3 changes: 2 additions & 1 deletion src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function initComponents(): Promise<AppComponents> {
archipelagoStats,
peersSynchronizer,
nats,
peerTracking
peerTracking,
catalystClient
}
}
17 changes: 17 additions & 0 deletions src/logic/friends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { User } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen'
import { Entity } from '@dcl/schemas'
import { getProfileAvatar, getProfilePictureUrl } from './profiles'
import { normalizeAddress } from '../utils/address'

export function parseProfilesToFriends(profiles: Entity[], contentServerUrl: string): User[] {
return profiles.map((profile) => {
const { userId, name, hasClaimedName } = getProfileAvatar(profile)

return {
address: normalizeAddress(userId),
name,
hasClaimedName,
profilePictureUrl: getProfilePictureUrl(contentServerUrl, profile)
}
})
}
29 changes: 29 additions & 0 deletions src/logic/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity } from '@dcl/schemas'

type Avatar = {
userId: string
name: string
hasClaimedName: boolean
snapshots: {
face256: string
}
}

export function getProfileAvatar(profile: Entity): Avatar {
const [avatar] = profile.metadata.avatars

if (!avatar) throw new Error('Missing profile avatar')

return avatar
}

export function getProfilePictureUrl(baseUrl: string, profile: Entity): string {
if (!baseUrl) throw new Error('Missing baseUrl for profile picture')

const avatar = getProfileAvatar(profile)
const hash = avatar?.snapshots.face256

if (!hash) throw new Error('Missing snapshot hash for profile picture')

return `${baseUrl}/contents/${hash}`
}
43 changes: 43 additions & 0 deletions test/mocks/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Entity, EntityType } from '@dcl/schemas'

export const mockProfile: Entity = {
version: '1',
id: 'profile-id',
type: EntityType.PROFILE,
metadata: {
avatars: [
{
userId: '0x123',
name: 'TestUser',
hasClaimedName: true,
snapshots: {
face256: 'bafybeiasdfqwer'
}
}
]
},
pointers: ['0x123'],
timestamp: new Date().getTime(),
content: [
{
file: 'face256',
hash: 'bafybeiasdfqwer'
}
]
}

export const createMockProfile = (address: string): Entity => ({
...mockProfile,
pointers: [address],
metadata: {
...mockProfile.metadata,
avatars: [
{
...mockProfile.metadata.avatars[0],
userId: address,
name: `Profile name ${address}`,
hasClaimedName: true
}
]
}
})
62 changes: 38 additions & 24 deletions test/unit/adapters/rpc-server/services/get-friends.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,52 @@
import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components'
import { mockCatalystClient, mockConfig, mockDb, mockLogs } from '../../../../mocks/components'
import { getFriendsService } from '../../../../../src/adapters/rpc-server/services/get-friends'
import { FRIENDSHIPS_PER_PAGE } from '../../../../../src/adapters/rpc-server/constants'
import { RpcServerContext, AppComponents, Friend } from '../../../../../src/types'
import { RpcServerContext, Friend } from '../../../../../src/types'
import { createMockProfile } from '../../../../mocks/profile'

describe('getFriendsService', () => {
let getFriends: ReturnType<typeof getFriendsService>
let getFriends: Awaited<ReturnType<typeof getFriendsService>>

const contentServerUrl = 'https://peer.decentraland.org/content'

const rpcContext: RpcServerContext = {
address: '0x123',
subscribers: undefined
}

beforeEach(() => {
getFriends = getFriendsService({ components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } })
beforeEach(async () => {
mockConfig.requireString.mockResolvedValueOnce(contentServerUrl)

getFriends = await getFriendsService({
components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient, config: mockConfig }
})
})

it('should return the correct list of friends with pagination data', async () => {
const mockFriends = [createMockFriend('0x456'), createMockFriend('0x789'), createMockFriend('0x987')]
const addresses = ['0x456', '0x789', '0x987']
const mockFriends = addresses.map(createMockFriend)
const mockProfiles = addresses.map(createMockProfile)
const totalFriends = 2

mockDb.getFriends.mockResolvedValueOnce(mockFriends)
mockDb.getFriendsCount.mockResolvedValueOnce(totalFriends)
mockCatalystClient.getEntitiesByPointers.mockResolvedValueOnce(mockProfiles)

const response = await getFriends({ pagination: { limit: 10, offset: 0 } }, rpcContext)

expect(response).toEqual({
users: [{ address: '0x456' }, { address: '0x789' }, { address: '0x987' }],
users: addresses.map((address) => ({
address,
name: `Profile name ${address}`,
hasClaimedName: true,
profilePictureUrl: `${contentServerUrl}/contents/bafybeiasdfqwer`
})),
paginationData: {
total: totalFriends,
page: 1
}
})
})

it('should respect the pagination limit', async () => {
const mockFriends = Array.from({ length: FRIENDSHIPS_PER_PAGE }, (_, i) => createMockFriend(`0x${i + 1}`))
const totalFriends = FRIENDSHIPS_PER_PAGE + 5

mockDb.getFriends.mockResolvedValueOnce(mockFriends)
mockDb.getFriendsCount.mockResolvedValueOnce(totalFriends)

const response = await getFriends({ pagination: { limit: FRIENDSHIPS_PER_PAGE, offset: 0 } }, rpcContext)

expect(response.users).toHaveLength(FRIENDSHIPS_PER_PAGE)
expect(response.paginationData).toEqual({
total: totalFriends,
page: 1
})
})

it('should return an empty list if no friends are found', async () => {
mockDb.getFriends.mockResolvedValueOnce([])
mockDb.getFriendsCount.mockResolvedValueOnce(0)
Expand Down Expand Up @@ -80,6 +78,22 @@ describe('getFriendsService', () => {
})
})

it('should handle errors from the catalyst gracefully', async () => {
mockCatalystClient.getEntitiesByPointers.mockImplementationOnce(() => {
throw new Error('Catalyst error')
})

const response = await getFriends({ pagination: { limit: 10, offset: 0 } }, rpcContext)

expect(response).toEqual({
users: [],
paginationData: {
total: 0,
page: 1
}
})
})

// Helper to create a mock friendship object
const createMockFriend = (address): Friend => ({
address
Expand Down
42 changes: 42 additions & 0 deletions test/unit/logic/friends.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Entity } from '@dcl/schemas'
import { parseProfilesToFriends } from '../../../src/logic/friends'
import { normalizeAddress } from '../../../src/utils/address'
import { mockProfile } from '../../mocks/profile'

describe('parseProfilesToFriends', () => {
it('should convert profile entities to friend users', () => {
const contentServerUrl = 'https://peer.decentraland.org'
const anotherProfile = {
...mockProfile,
metadata: {
...mockProfile.metadata,
avatars: [
{
...mockProfile.metadata.avatars[0],
userId: '0x123aBcDE',
name: 'TestUser2',
hasClaimedName: false
}
]
}
}
const profiles = [mockProfile, anotherProfile]

const result = parseProfilesToFriends(profiles, contentServerUrl)

expect(result).toEqual([
{
address: '0x123',
name: 'TestUser',
hasClaimedName: true,
profilePictureUrl: 'https://peer.decentraland.org/contents/bafybeiasdfqwer'
},
{
address: '0x123abcde',
name: 'TestUser2',
hasClaimedName: false,
profilePictureUrl: 'https://peer.decentraland.org/contents/bafybeiasdfqwer'
}
])
})
})
74 changes: 74 additions & 0 deletions test/unit/logic/profiles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Entity } from '@dcl/schemas'
import { getProfileAvatar, getProfilePictureUrl } from '../../../src/logic/profiles'
import { mockProfile } from '../../mocks/profile'

describe('getProfileAvatar', () => {
it('should extract avatar information from profile entity', () => {
const avatar = getProfileAvatar(mockProfile)

expect(avatar).toEqual({
userId: '0x123',
name: 'TestUser',
hasClaimedName: true,
snapshots: {
face256: 'bafybeiasdfqwer'
}
})
})

it('should handle profile without avatars gracefully', () => {
const emptyProfile: Entity = {
...mockProfile,
metadata: {
avatars: []
}
}

expect(() => getProfileAvatar(emptyProfile)).toThrow('Missing profile avatar')
})
})

describe('getProfilePictureUrl', () => {
const baseUrl = 'https://peer.dcl.local/content'

it('should construct correct profile picture URL', () => {
const url = getProfilePictureUrl(baseUrl, mockProfile)

expect(url).toBe('https://peer.dcl.local/content/contents/bafybeiasdfqwer')
})

it('should handle missing avatar data gracefully', () => {
const emptyProfile: Entity = {
...mockProfile,
metadata: {
avatars: []
}
}

expect(() => getProfilePictureUrl(baseUrl, emptyProfile)).toThrow('Missing profile avatar')
})

it('should handle missing snapshots data', () => {
const profileWithoutSnapshot: Entity = {
...mockProfile,
metadata: {
avatars: [
{
userId: '0x123',
name: 'TestUser',
hasClaimedName: true,
snapshots: {}
}
]
}
}

expect(() => getProfilePictureUrl(baseUrl, profileWithoutSnapshot)).toThrow(
'Missing snapshot hash for profile picture'
)
})

it('should throw on empty baseUrl', () => {
expect(() => getProfilePictureUrl('', mockProfile)).toThrow('Missing baseUrl')
})
})

0 comments on commit 77fbe7e

Please sign in to comment.