mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(push): ring Android calls and harden the relay (#2915)
This commit is contained in:
1 parent
b16989d567
commit
5fde6eb484
25 files changed
+1050
-120
No files matched your search
@@ -0,0 +1,210 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createChannelID, createUserID, type UserID} from '@app/api/BrandedTypes';
|
||||
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
|
||||
import {CallService} from '@app/api/channel/services/CallService';
|
||||
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {CallCaller, CallData, IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
||||
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
|
||||
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
||||
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
|
||||
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '@app/api/models/Channel';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import type {ReadStateService} from '@app/api/read_state/ReadStateService';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const CALLER_ID = createUserID(1n);
|
||||
const RECIPIENT_ID = createUserID(2n);
|
||||
const CHANNEL_ID = createChannelID(12n);
|
||||
|
||||
interface CallerOverrides {
|
||||
username?: string;
|
||||
globalName?: string | null;
|
||||
avatar?: string | null;
|
||||
nickname?: string;
|
||||
userRowMissing?: boolean;
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
service: CallService;
|
||||
created: Array<CallCaller | undefined>;
|
||||
rung: Array<CallCaller | undefined>;
|
||||
}
|
||||
|
||||
const EXISTING_CALL: CallData = {
|
||||
channel_id: CHANNEL_ID.toString(),
|
||||
message_id: '99',
|
||||
region: 'automatic',
|
||||
ringing: [],
|
||||
recipients: [CALLER_ID.toString(), RECIPIENT_ID.toString()],
|
||||
voice_states: [],
|
||||
};
|
||||
|
||||
function harness(overrides: CallerOverrides, existingCall: CallData | null): Harness {
|
||||
const username = overrides.username ?? 'elias';
|
||||
const globalName = overrides.globalName === undefined ? 'Elias' : overrides.globalName;
|
||||
const avatar = overrides.avatar === undefined ? 'a1b2c3d4' : overrides.avatar;
|
||||
const nicknames = new Map<string, string>();
|
||||
if (overrides.nickname !== undefined) {
|
||||
nicknames.set(CALLER_ID.toString(), overrides.nickname);
|
||||
}
|
||||
const channel = {
|
||||
id: CHANNEL_ID,
|
||||
type: ChannelTypes.GROUP_DM,
|
||||
recipientIds: new Set<UserID>([CALLER_ID, RECIPIENT_ID]),
|
||||
nicknames,
|
||||
} as unknown as Channel;
|
||||
const created: Array<CallCaller | undefined> = [];
|
||||
const rung: Array<CallCaller | undefined> = [];
|
||||
const channelRepository = {
|
||||
findUnique: async () => channel,
|
||||
upsertMessage: async () => {},
|
||||
getMessage: async () => null,
|
||||
} as unknown as IChannelRepository;
|
||||
const userRepository = {
|
||||
findUnique: async () => (overrides.userRowMissing ? null : ({...callerUser(username, globalName, avatar)} as User)),
|
||||
listUsers: async () => [],
|
||||
findSettings: async () => null,
|
||||
isDmChannelOpen: async () => true,
|
||||
} as unknown as IUserRepository;
|
||||
const gatewayService = {
|
||||
getCall: async () => existingCall,
|
||||
createCall: async (
|
||||
_channelId: unknown,
|
||||
_messageId: string,
|
||||
_region: string,
|
||||
_ringing: Array<string>,
|
||||
_recipients: Array<string>,
|
||||
caller?: CallCaller,
|
||||
) => {
|
||||
created.push(caller);
|
||||
return EXISTING_CALL;
|
||||
},
|
||||
ringCallRecipients: async (_channelId: unknown, _recipients: Array<string>, caller?: CallCaller) => {
|
||||
rung.push(caller);
|
||||
return true;
|
||||
},
|
||||
} as unknown as IGatewayService;
|
||||
const userCacheService = {
|
||||
getUserPartialResponse: async (): Promise<UserPartialResponse> =>
|
||||
({
|
||||
id: CALLER_ID.toString(),
|
||||
username,
|
||||
discriminator: '0001',
|
||||
global_name: globalName,
|
||||
avatar,
|
||||
avatar_color: null,
|
||||
flags: 0,
|
||||
}) as unknown as UserPartialResponse,
|
||||
} as unknown as UserCacheService;
|
||||
const snowflakeService = {
|
||||
generateForChannel: async () => 7777n,
|
||||
} as unknown as ISnowflakeService;
|
||||
const readStateService = {
|
||||
ackMessage: async () => {},
|
||||
bulkIncrementMentionCounts: async () => {},
|
||||
} as unknown as ReadStateService;
|
||||
const service = new CallService(
|
||||
channelRepository,
|
||||
userRepository,
|
||||
{} as unknown as IGuildRepositoryAggregate,
|
||||
gatewayService,
|
||||
userCacheService,
|
||||
snowflakeService,
|
||||
readStateService,
|
||||
null,
|
||||
{} as unknown as IVoiceRoomStore,
|
||||
);
|
||||
return {service, created, rung};
|
||||
}
|
||||
|
||||
function callerUser(username: string, globalName: string | null, avatar: string | null): Partial<User> {
|
||||
return {
|
||||
id: CALLER_ID,
|
||||
username,
|
||||
globalName,
|
||||
avatarHash: avatar,
|
||||
isBot: false,
|
||||
};
|
||||
}
|
||||
|
||||
const requestCache = {
|
||||
userPartials: new Map(),
|
||||
} as unknown as RequestCache;
|
||||
|
||||
describe('CallService caller identity', () => {
|
||||
let harnessState: Harness;
|
||||
|
||||
const createCall = (overrides: CallerOverrides = {}) => {
|
||||
harnessState = harness(overrides, null);
|
||||
return harnessState.service.createOrGetCall({
|
||||
userId: CALLER_ID,
|
||||
channelId: CHANNEL_ID,
|
||||
ringing: [RECIPIENT_ID],
|
||||
requestCache,
|
||||
});
|
||||
};
|
||||
|
||||
const ringExistingCall = (overrides: CallerOverrides = {}) => {
|
||||
harnessState = harness(overrides, EXISTING_CALL);
|
||||
return harnessState.service.ringCallRecipients({
|
||||
userId: CALLER_ID,
|
||||
channelId: CHANNEL_ID,
|
||||
requestCache,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
requestCache.userPartials.clear();
|
||||
});
|
||||
|
||||
it('sends the caller id, display name and avatar hash to createCall', async () => {
|
||||
await createCall();
|
||||
expect(harnessState.created).toEqual([{id: '1', name: 'Elias', avatar: 'a1b2c3d4'}]);
|
||||
});
|
||||
|
||||
it('prefers the group dm nickname over the global name on createCall', async () => {
|
||||
await createCall({nickname: 'Eli'});
|
||||
expect(harnessState.created[0]?.name).toBe('Eli');
|
||||
});
|
||||
|
||||
it('falls back to the username when the caller has no nickname and no global name', async () => {
|
||||
await createCall({globalName: null});
|
||||
expect(harnessState.created[0]?.name).toBe('elias');
|
||||
});
|
||||
|
||||
it('sends a null avatar when the caller has no custom avatar', async () => {
|
||||
await createCall({avatar: null});
|
||||
expect(harnessState.created[0]).toEqual({id: '1', name: 'Elias', avatar: null});
|
||||
});
|
||||
|
||||
it('sends no caller at all when the caller user row is gone', async () => {
|
||||
await createCall({userRowMissing: true});
|
||||
expect(harnessState.created).toEqual([undefined]);
|
||||
});
|
||||
|
||||
it('sends the caller id, display name and avatar hash to ringCallRecipients', async () => {
|
||||
await ringExistingCall();
|
||||
expect(harnessState.rung).toEqual([{id: '1', name: 'Elias', avatar: 'a1b2c3d4'}]);
|
||||
});
|
||||
|
||||
it('prefers the group dm nickname over the global name on ringCallRecipients', async () => {
|
||||
await ringExistingCall({nickname: 'Eli'});
|
||||
expect(harnessState.rung[0]?.name).toBe('Eli');
|
||||
});
|
||||
|
||||
it('falls back to the username on ringCallRecipients', async () => {
|
||||
await ringExistingCall({globalName: null});
|
||||
expect(harnessState.rung[0]?.name).toBe('elias');
|
||||
});
|
||||
|
||||
it('resolves the caller on the ring branch and not on the create branch', async () => {
|
||||
await ringExistingCall();
|
||||
expect(harnessState.created).toEqual([]);
|
||||
expect(harnessState.rung).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'
|
||||
import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore';
|
||||
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
|
||||
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
||||
import type {Channel} from '@app/api/models/Channel';
|
||||
import type {ReadStateService} from '@app/api/read_state/ReadStateService';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import type {VoiceAccessContext, VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService';
|
||||
@@ -208,14 +209,26 @@ export class CallService {
|
||||
has_reaction: false,
|
||||
version: 1,
|
||||
});
|
||||
const author = await this.userRepository.findUnique(userId);
|
||||
const call = await this.gatewayService.createCall(
|
||||
channelId,
|
||||
messageId.toString(),
|
||||
selectedRegion,
|
||||
ringing.map((id) => id.toString()),
|
||||
allRecipients.map((id) => id.toString()),
|
||||
author
|
||||
? {
|
||||
id: userId.toString(),
|
||||
name: this.resolveCallerName({
|
||||
channel,
|
||||
userId,
|
||||
globalName: author.globalName,
|
||||
username: author.username,
|
||||
}),
|
||||
avatar: author.avatarHash,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const author = await this.userRepository.findUnique(userId);
|
||||
await incrementDmMentionCounts({
|
||||
readStateService: this.readStateService,
|
||||
userRepository: this.userRepository,
|
||||
@@ -390,13 +403,45 @@ export class CallService {
|
||||
longitude,
|
||||
});
|
||||
} else {
|
||||
const caller = await this.userCacheService.getUserPartialResponse(userId, requestCache);
|
||||
await this.gatewayService.ringCallRecipients(
|
||||
channelId,
|
||||
recipientsToRing.map((id) => id.toString()),
|
||||
{
|
||||
id: userId.toString(),
|
||||
name: this.resolveCallerName({
|
||||
channel,
|
||||
userId,
|
||||
globalName: caller.global_name,
|
||||
username: caller.username,
|
||||
}),
|
||||
avatar: caller.avatar,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCallerName({
|
||||
channel,
|
||||
userId,
|
||||
globalName,
|
||||
username,
|
||||
}: {
|
||||
channel: Channel;
|
||||
userId: UserID;
|
||||
globalName: string | null;
|
||||
username: string;
|
||||
}): string {
|
||||
const nickname = channel.nicknames.get(userId.toString());
|
||||
if (nickname) {
|
||||
return nickname;
|
||||
}
|
||||
if (globalName) {
|
||||
return globalName;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
async stopRingingCallRecipients({
|
||||
userId,
|
||||
channelId,
|
||||
|
||||
@@ -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']});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import {GuildMemberRepository} from '@app/api/guild/repositories/GuildMemberRepo
|
||||
import {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
|
||||
import {GuildRoleRepository} from '@app/api/guild/repositories/GuildRoleRepository';
|
||||
import {
|
||||
type CallCaller,
|
||||
type CallData,
|
||||
type GatewayChannelMention,
|
||||
type GatewayGuildMemoryStats,
|
||||
@@ -925,6 +926,7 @@ export class NoopGatewayService extends IGatewayService {
|
||||
_region: string,
|
||||
_ringing: Array<string>,
|
||||
_recipients: Array<string>,
|
||||
_caller?: CallCaller,
|
||||
): Promise<CallData> {
|
||||
return {
|
||||
channel_id: _channelId.toString(),
|
||||
@@ -940,7 +942,7 @@ export class NoopGatewayService extends IGatewayService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async ringCallRecipients(_channelId: ChannelID, _recipients: Array<string>): Promise<boolean> {
|
||||
async ringCallRecipients(_channelId: ChannelID, _recipients: Array<string>, _caller?: CallCaller): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user