feat(push): ring Android calls and harden the relay (#2915)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-24 00:07:15 +02:00
1 parent b16989d567
commit 5fde6eb484
25 files changed
+1050 -120

No files matched your search

@@ -3,7 +3,7 @@
import {Config} from '@app/api/Config';
import {GatewayRpcMethodError, GatewayRpcMethodErrorCodes} from '@app/api/infrastructure/GatewayRpcError';
import type {IGatewayRpcTransport} from '@app/api/infrastructure/IGatewayRpcTransport';
import type {CallData} from '@app/api/infrastructure/IGatewayService';
import {type CallCaller, type CallData, callCallerRpcParams} from '@app/api/infrastructure/IGatewayService';
import {NatsGatewayRpcTransport} from '@app/api/infrastructure/NatsGatewayRpcTransport';
import {Logger} from '@app/api/Logger';
import {NatsConnectionManager} from '@pkgs/nats/src/NatsConnectionManager';
@@ -128,6 +128,7 @@ export class GatewayRpcClient {
region: string,
ringing: Array<string>,
recipients: Array<string>,
caller?: CallCaller,
): Promise<CallData> {
return this.call<CallData>('call.create', {
channel_id: channelId,
@@ -135,6 +136,7 @@ export class GatewayRpcClient {
region,
ringing,
recipients,
...callCallerRpcParams(caller),
});
}
@@ -142,8 +144,8 @@ export class GatewayRpcClient {
return this.call('call.update_region', {channel_id: channelId, region});
}
async ringCallRecipients(channelId: string, recipients: Array<string>): Promise<boolean> {
return this.call('call.ring', {channel_id: channelId, recipients});
async ringCallRecipients(channelId: string, recipients: Array<string>, caller?: CallCaller): Promise<boolean> {
return this.call('call.ring', {channel_id: channelId, recipients, ...callCallerRpcParams(caller)});
}
async stopRingingCallRecipients(channelId: string, recipients: Array<string>): Promise<boolean> {
@@ -6,16 +6,18 @@ import {SYSTEM_USER_ID} from '@app/api/constants/Core';
import type {GatewayDispatchEvent} from '@app/api/constants/Gateway';
import {GatewayRpcClient} from '@app/api/infrastructure/GatewayRpcClient';
import {GatewayRpcMethodError, GatewayRpcMethodErrorCodes} from '@app/api/infrastructure/GatewayRpcError';
import type {
CallData,
GatewayChannelMention,
GatewayGuildMemoryStats,
GatewayMentionSources,
GatewayMentionSourcesPage,
GatewayNodeStats,
GatewayVoiceStateCounts,
GatewayVoiceStateEntry,
GuildChannelAuthContext,
import {
type CallCaller,
type CallData,
callCallerRpcParams,
type GatewayChannelMention,
type GatewayGuildMemoryStats,
type GatewayMentionSources,
type GatewayMentionSourcesPage,
type GatewayNodeStats,
type GatewayVoiceStateCounts,
type GatewayVoiceStateEntry,
type GuildChannelAuthContext,
} from '@app/api/infrastructure/IGatewayService';
import {Logger} from '@app/api/Logger';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
@@ -1695,6 +1697,7 @@ export class GatewayService {
region: string,
ringing: Array<string>,
recipients: Array<string>,
caller?: CallCaller,
): Promise<CallData> {
return this.call<CallData>('call.create', {
channel_id: channelId.toString(),
@@ -1702,6 +1705,7 @@ export class GatewayService {
region,
ringing,
recipients,
...callCallerRpcParams(caller),
});
}
@@ -1709,8 +1713,12 @@ export class GatewayService {
return this.call<boolean>('call.update_region', {channel_id: channelId.toString(), region});
}
async ringCallRecipients(channelId: ChannelID, recipients: Array<string>): Promise<boolean> {
return this.call<boolean>('call.ring', {channel_id: channelId.toString(), recipients});
async ringCallRecipients(channelId: ChannelID, recipients: Array<string>, caller?: CallCaller): Promise<boolean> {
return this.call<boolean>('call.ring', {
channel_id: channelId.toString(),
recipients,
...callCallerRpcParams(caller),
});
}
async stopRingingCallRecipients(channelId: ChannelID, recipients: Array<string>): Promise<boolean> {
@@ -24,6 +24,19 @@ export interface CallData {
voice_states: Array<VoiceState>;
}
export interface CallCaller {
id: string;
name: string;
avatar: string | null;
}
export function callCallerRpcParams(caller: CallCaller | undefined): Record<string, unknown> {
if (!caller) {
return {};
}
return {caller_id: caller.id, caller_name: caller.name, caller_avatar: caller.avatar};
}
export interface GatewayGuildMemoryStatsEntry {
node_id: string;
guild_id: string | null;
@@ -381,11 +394,12 @@ export abstract class IGatewayService {
region: string,
ringing: Array<string>,
recipients: Array<string>,
caller?: CallCaller,
): Promise<CallData>;
abstract updateCallRegion(channelId: ChannelID, region: string | null): Promise<boolean>;
abstract ringCallRecipients(channelId: ChannelID, recipients: Array<string>): Promise<boolean>;
abstract ringCallRecipients(channelId: ChannelID, recipients: Array<string>, caller?: CallCaller): Promise<boolean>;
abstract stopRingingCallRecipients(channelId: ChannelID, recipients: Array<string>): Promise<boolean>;
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createChannelID} from '@app/api/BrandedTypes';
import {GatewayRpcClient} from '@app/api/infrastructure/GatewayRpcClient';
import {GatewayService} from '@app/api/infrastructure/GatewayService';
import type {IGatewayRpcTransport} from '@app/api/infrastructure/IGatewayRpcTransport';
import {afterEach, describe, expect, it} from 'vitest';
const CHANNEL_ID = createChannelID(12n);
interface RecordedCall {
method: string;
params: Record<string, unknown>;
}
function recordingService(recorded: Array<RecordedCall>): GatewayService {
const transport: IGatewayRpcTransport = {
async call(method: string, params: Record<string, unknown>): Promise<unknown> {
recorded.push({method, params});
return null;
},
async destroy(): Promise<void> {},
};
GatewayRpcClient.createForTests(transport);
return new GatewayService();
}
describe('call rpc caller params', () => {
afterEach(async () => {
await GatewayRpcClient.resetForTests();
});
it('sends the caller to call.create as caller_id, caller_name and caller_avatar', async () => {
const recorded: Array<RecordedCall> = [];
const service = recordingService(recorded);
await service.createCall(CHANNEL_ID, '99', 'automatic', ['2'], ['1', '2'], {
id: '1',
name: 'Elias',
avatar: 'a1b2c3d4',
});
expect(recorded).toHaveLength(1);
expect(recorded[0].method).toBe('call.create');
expect(recorded[0].params.caller_id).toBe('1');
expect(recorded[0].params.caller_name).toBe('Elias');
expect(recorded[0].params.caller_avatar).toBe('a1b2c3d4');
});
it('sends the caller to call.ring as caller_id, caller_name and caller_avatar', async () => {
const recorded: Array<RecordedCall> = [];
const service = recordingService(recorded);
await service.ringCallRecipients(CHANNEL_ID, ['2'], {id: '1', name: 'Elias', avatar: 'a1b2c3d4'});
expect(recorded).toHaveLength(1);
expect(recorded[0].method).toBe('call.ring');
expect(recorded[0].params.caller_id).toBe('1');
expect(recorded[0].params.caller_name).toBe('Elias');
expect(recorded[0].params.caller_avatar).toBe('a1b2c3d4');
});
it('sends caller_avatar as null when the caller has no avatar', async () => {
const recorded: Array<RecordedCall> = [];
const service = recordingService(recorded);
await service.ringCallRecipients(CHANNEL_ID, ['2'], {id: '1', name: 'Elias', avatar: null});
expect(recorded[0].params.caller_avatar).toBeNull();
expect(Object.hasOwn(recorded[0].params, 'caller_avatar')).toBe(true);
});
it('omits every caller key from call.create when no caller was resolved', async () => {
const recorded: Array<RecordedCall> = [];
const service = recordingService(recorded);
await service.createCall(CHANNEL_ID, '99', 'automatic', ['2'], ['1', '2']);
expect(recorded[0].params).toEqual({
channel_id: '12',
message_id: '99',
region: 'automatic',
ringing: ['2'],
recipients: ['1', '2'],
});
});
it('omits every caller key from call.ring when no caller was resolved', async () => {
const recorded: Array<RecordedCall> = [];
const service = recordingService(recorded);
await service.ringCallRecipients(CHANNEL_ID, ['2']);
expect(recorded[0].params).toEqual({channel_id: '12', recipients: ['2']});
});
});