Skip to content

Commit

Permalink
feat: Add GetFriendshipStatus RPC implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinszuchet committed Jan 17, 2025
1 parent de4e01b commit c4199a2
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 64 deletions.
11 changes: 5 additions & 6 deletions src/adapters/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface IDatabaseComponent {
getMutualFriendsCount(userAddress1: string, userAddress2: string): Promise<number>
getFriendship(userAddresses: [string, string]): Promise<Friendship | undefined>
getLastFriendshipAction(friendshipId: string): Promise<FriendshipAction | undefined>
getLastFriendshipActionByUsers(userAddresses: [string, string]): Promise<FriendshipAction | undefined>
getLastFriendshipActionByUsers(loggedUser: string, friendUser: string): Promise<FriendshipAction | undefined>
recordFriendshipAction(
friendshipId: string,
actingUser: string,
Expand Down Expand Up @@ -195,13 +195,12 @@ export function createDBComponent(components: Pick<AppComponents, 'pg' | 'logs'>

return results.rows[0]
},
async getLastFriendshipActionByUsers(users) {
const [userAddress1, userAddress2] = users
async getLastFriendshipActionByUsers(loggedUser: string, friendUser: string) {
const query = SQL`
SELECT fa.*
SELECT fa.action, fa.acting_user as by
FROM friendships f
INNER JOIN friendship_actions fa ON f.id = fa.friendship_id
WHERE WHERE (f.address_requester, f.address_requested) IN ((${userAddress1}, ${userAddress2}), (${userAddress2}, ${userAddress1}))
WHERE (f.address_requester, f.address_requested) IN ((${loggedUser}, ${friendUser}), (${friendUser}, ${loggedUser}))
ORDER BY fa.timestamp DESC LIMIT 1
`
const results = await pg.query<FriendshipAction>(query)
Expand Down Expand Up @@ -270,7 +269,7 @@ export function createDBComponent(components: Pick<AppComponents, 'pg' | 'logs'>
query.append(SQL` LIMIT ${limit}`)
}

if (offset) {
if (!!offset) {
query.append(SQL` OFFSET ${offset}`)
}

Expand Down
6 changes: 3 additions & 3 deletions src/adapters/rpc-server/rpc-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { upsertFriendshipService } from './services/upsert-friendship'
import { subscribeToFriendshipUpdatesService } from './services/subscribe-to-friendship-updates'
import { SocialServiceDefinition } from '@dcl/protocol/out-ts/decentraland/social_service/v3/social_service_v3.gen'
import { getSentFriendshipRequestsService } from './services/get-sent-friendship-requests'
import { getFriendshipStatusService } from './services/get-friendship-status'

export type IRPCServerComponent = IBaseComponent & {
attachUser(user: { transport: Transport; address: string }): void
Expand All @@ -31,22 +32,21 @@ export async function createRpcServerComponent(

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

// TODO: return pagination data too
const getFriends = getFriendsService({ components: { logs, db } })
const getMutualFriends = getMutualFriendsService({ components: { logs, db } })
const getPendingFriendshipRequests = getPendingFriendshipRequestsService({ components: { logs, db } })
const getSentFriendshipRequests = getSentFriendshipRequestsService({ components: { logs, db } })
const upsertFriendship = upsertFriendshipService({ components: { logs, db, pubsub } })
const subscribeToFriendshipUpdates = subscribeToFriendshipUpdatesService({ components: { logs } })
// const getFriendshipStatus = getFriendshipStatusService({ components: { logs } })
const getFriendshipStatus = getFriendshipStatusService({ components: { logs, db } })

rpcServer.setHandler(async function handler(port) {
registerService(port, SocialServiceDefinition, async () => ({
getFriends,
getMutualFriends,
getPendingFriendshipRequests,
getSentFriendshipRequests,
getFriendshipStatus: async () => ({ response: { $case: 'internalServerError', internalServerError: {} } }),
getFriendshipStatus,
upsertFriendship,
subscribeToFriendshipUpdates
}))
Expand Down
34 changes: 16 additions & 18 deletions src/adapters/rpc-server/services/get-friendship-status.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import { Action, RpcServerContext, RPCServiceContext } from '../../../types'
import { getFriendshipRequestStatus } from '../../../logic/friendships'
import { RpcServerContext, RPCServiceContext } from '../../../types'
import {
FriendshipStatus,
GetFriendshipStatusPayload,
GetFriendshipStatusResponse
} from '@dcl/protocol/out-ts/decentraland/social_service/v3/social_service_v3.gen'

const FRIENDSHIP_STATUS_BY_ACTION = {
[Action.ACCEPT]: FriendshipStatus.ACCEPTED,
[Action.CANCEL]: FriendshipStatus.CANCELED,
[Action.DELETE]: FriendshipStatus.DELETED,
[Action.REJECT]: FriendshipStatus.REJECTED,
[Action.REQUEST]: FriendshipStatus.REQUEST_SENT
// [Action.BLOCK]: FriendshipStatus.BLOCKED,
}

export function getFriendshipStatusService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) {
const logger = logs.getLogger('get-sent-friendship-requests-service')

const getStatusByAction = (action: Action): FriendshipStatus => {
// TODO: distinguish between REQUEST_SENT and REQUEST_RECEIVED
return FRIENDSHIP_STATUS_BY_ACTION[action] ?? FriendshipStatus.UNRECOGNIZED
}

return async function (
request: GetFriendshipStatusPayload,
context: RpcServerContext
): Promise<GetFriendshipStatusResponse> {
try {
const lastFriendshipAction = await db.getLastFriendshipActionByUsers([context.address, request.user!.address])
const { address: loggedUserAddress } = context
const userAddress = request.user?.address

if (!userAddress) {
return {
response: {
$case: 'internalServerError',
internalServerError: { message: 'User address is missing in the request payload' }
}
}
}

const lastFriendshipAction = await db.getLastFriendshipActionByUsers(loggedUserAddress, userAddress)

if (!lastFriendshipAction) {
return {
Expand All @@ -44,7 +42,7 @@ export function getFriendshipStatusService({ components: { logs, db } }: RPCServ
response: {
$case: 'accepted',
accepted: {
status: getStatusByAction(lastFriendshipAction.action)
status: getFriendshipRequestStatus(lastFriendshipAction, loggedUserAddress)
}
}
}
Expand Down
26 changes: 0 additions & 26 deletions src/adapters/ws.ts

This file was deleted.

24 changes: 23 additions & 1 deletion src/logic/friendships.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
FriendshipUpdate,
UpsertFriendshipPayload
UpsertFriendshipPayload,
FriendshipStatus as FriendshipRequestStatus
} from '@dcl/protocol/out-ts/decentraland/social_service/v3/social_service_v3.gen'
import {
Action,
Expand All @@ -11,6 +12,19 @@ import {
} from '../types'
import { normalizeAddress } from '../utils/address'

const FRIENDSHIP_STATUS_BY_ACTION: Record<
Action,
(actingUser: string, contextAddress: string) => FriendshipRequestStatus | undefined
> = {
[Action.ACCEPT]: () => FriendshipRequestStatus.ACCEPTED,
[Action.CANCEL]: () => FriendshipRequestStatus.CANCELED,
[Action.DELETE]: () => FriendshipRequestStatus.DELETED,
[Action.REJECT]: () => FriendshipRequestStatus.REJECTED,
[Action.REQUEST]: (actingUser, contextAddress) =>
actingUser === contextAddress ? FriendshipRequestStatus.REQUEST_SENT : FriendshipRequestStatus.REQUEST_RECEIVED
// TODO: [Action.BLOCK]: () => FriendshipRequestStatus.BLOCKED,
}

export function isFriendshipActionValid(from: Action | null, to: Action) {
return FRIENDSHIP_ACTION_TRANSITIONS[to].includes(from)
}
Expand Down Expand Up @@ -173,3 +187,11 @@ export function parseEmittedUpdateToFriendshipUpdate(
return null
}
}

export function getFriendshipRequestStatus(
{ action, acting_user }: FriendshipAction,
loggedUserAddress: string
): FriendshipRequestStatus {
const statusResolver = FRIENDSHIP_STATUS_BY_ACTION[action]
return statusResolver?.(acting_user, loggedUserAddress) ?? FriendshipRequestStatus.UNRECOGNIZED
}
74 changes: 66 additions & 8 deletions test/unit/adapters/db.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createDBComponent } from '../../../src/adapters/db'
import { Action } from '../../../src/types'
import SQL from 'sql-template-strings'
import { mockLogs, mockPg } from '../../mocks/components'
import { mockDb, mockLogs, mockPg } from '../../mocks/components'

describe('db', () => {
let dbComponent: ReturnType<typeof createDBComponent>
Expand Down Expand Up @@ -92,6 +92,30 @@ describe('db', () => {
})
})

describe('getLastFriendshipActionByUsers', () => {
it('should return the most recent friendship action between two users', async () => {
const mockAction = {
id: 'action-1',
friendship_id: 'friendship-1',
action: Action.REQUEST,
acting_user: '0x123',
metadata: null,
timestamp: '2025-01-01T00:00:00.000Z'
}
mockPg.query.mockResolvedValueOnce({ rows: [mockAction], rowCount: 1 })

const result = await dbComponent.getLastFriendshipActionByUsers('0x123', '0x456')

expect(result).toEqual(mockAction)
expect(mockPg.query).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('WHERE (f.address_requester, f.address_requested) IN'),
values: expect.arrayContaining(['0x123', '0x456', '0x456', '0x123'])
})
)
})
})

describe('createFriendship', () => {
it('should create a new friendship', async () => {
mockPg.query.mockResolvedValueOnce({
Expand Down Expand Up @@ -184,9 +208,17 @@ describe('db', () => {
]
mockPg.query.mockResolvedValueOnce({ rows: mockRequests, rowCount: mockRequests.length })

const result = await dbComponent.getReceivedFriendshipRequests('0x456')
const result = await dbComponent.getReceivedFriendshipRequests('0x456', { limit: 10, offset: 5 })

expect(result).toEqual(mockRequests)
expect(mockPg.query).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('f.address_requested ='),
values: expect.arrayContaining(['0x456'])
})
)

expectPaginatedQueryToHaveBeenCalledWithProperLimitAndOffset(10, 5)
})
})

Expand All @@ -201,20 +233,28 @@ describe('db', () => {
]
mockPg.query.mockResolvedValueOnce({ rows: mockRequests, rowCount: mockRequests.length })

const result = await dbComponent.getSentFriendshipRequests('0x123')
const result = await dbComponent.getSentFriendshipRequests('0x123', { limit: 10, offset: 5 })

expect(result).toEqual(mockRequests)
expectPaginatedQueryToHaveBeenCalledWithProperLimitAndOffset(10, 5)
})
})

describe('recordFriendshipAction', () => {
it('should record a friendship action', async () => {
const result = await dbComponent.recordFriendshipAction('friendship-id', '0x123', Action.REQUEST, {
message: 'Hi'
})
it.each([false, true])('should record a friendship action', async (withTxClient: boolean) => {
const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined
const result = await dbComponent.recordFriendshipAction(
'friendship-id',
'0x123',
Action.REQUEST,
{
message: 'Hi'
},
mockClient
)

expect(result).toBe(true)
expect(mockPg.query).toHaveBeenCalledWith(
expect(withTxClient ? mockClient.query : mockPg.query).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining(
'INSERT INTO friendship_actions (id, friendship_id, action, acting_user, metadata)'
Expand Down Expand Up @@ -259,4 +299,22 @@ describe('db', () => {
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK')
})
})

// Helpers

function expectPaginatedQueryToHaveBeenCalledWithProperLimitAndOffset(limit, offset) {
expect(mockPg.query).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('LIMIT'),
values: expect.arrayContaining([limit])
})
)

expect(mockPg.query).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('OFFSET'),
values: expect.arrayContaining([offset])
})
)
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('getFriendshipStatusService', () => {

const result: GetFriendshipStatusResponse = await getFriendshipStatus(mockRequest, rpcContext)

expect(mockDb.getLastFriendshipActionByUsers).toHaveBeenCalledWith(['0x123', '0x456'])
expect(mockDb.getLastFriendshipActionByUsers).toHaveBeenCalledWith('0x123', '0x456')
expect(result).toEqual({
response: {
$case: 'accepted',
Expand All @@ -56,7 +56,7 @@ describe('getFriendshipStatusService', () => {

const result: GetFriendshipStatusResponse = await getFriendshipStatus(mockRequest, rpcContext)

expect(mockDb.getLastFriendshipActionByUsers).toHaveBeenCalledWith(['0x123', '0x456'])
expect(mockDb.getLastFriendshipActionByUsers).toHaveBeenCalledWith('0x123', '0x456')
expect(result).toEqual({
response: {
$case: 'internalServerError',
Expand Down
31 changes: 31 additions & 0 deletions test/unit/logic/friendships.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
getFriendshipRequestStatus,
getNewFriendshipStatus,
isFriendshipActionValid,
isUserActionValid,
Expand All @@ -7,6 +8,7 @@ import {
validateNewFriendshipAction
} from '../../../src/logic/friendships'
import { Action, FriendshipStatus } from '../../../src/types'
import { FriendshipStatus as FriendshipRequestStatus } from '@dcl/protocol/out-ts/decentraland/social_service/v3/social_service_v3.gen'

describe('isFriendshipActionValid()', () => {
test('it should be valid if from is null and to is REQUEST ', () => {
Expand Down Expand Up @@ -509,3 +511,32 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => {
).toBe(null)
})
})

describe('getFriendshipRequestStatus()', () => {
const friendshipAction = {
id: '1111',
acting_user: '0x123',
friendship_id: '1111',
timestamp: new Date().toISOString()
}

test.each([
[Action.ACCEPT, 'accepted', FriendshipRequestStatus.ACCEPTED],
[Action.CANCEL, 'canceled', FriendshipRequestStatus.CANCELED],
[Action.DELETE, 'deleted', FriendshipRequestStatus.DELETED],
[Action.REJECT, 'rejected', FriendshipRequestStatus.REJECTED]
])('when the last action is %s it should return %s', (action, __, expected) => {
expect(getFriendshipRequestStatus({ ...friendshipAction, action }, '0x123')).toBe(expected)
})

test('when the last action is request and the acting user is the logged user it should return request sent', () => {
expect(getFriendshipRequestStatus({ ...friendshipAction, action: Action.REQUEST }, '0x123')).toBe(
FriendshipRequestStatus.REQUEST_SENT
)
})

test('when the last action is request and the acting user is not the logged user it should return request received', () => {
const requestMadeByAnotherUser = { ...friendshipAction, acting_user: '0x456', action: Action.REQUEST }
expect(getFriendshipRequestStatus(requestMadeByAnotherUser, '0x123')).toBe(FriendshipRequestStatus.REQUEST_RECEIVED)
})
})

0 comments on commit c4199a2

Please sign in to comment.