Skip to content
Open
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
30 changes: 27 additions & 3 deletions ee/packages/media-calls/src/server/CastDirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import type { IUser, MediaCallActor, MediaCallActorType, MediaCallContact, Media
import type { CallRole } from '@rocket.chat/media-signaling';
import { Users } from '@rocket.chat/models';

import { BroadcastActorAgent } from './BroadcastAgent';
import type { IMediaCallAgent } from '../definition/IMediaCallAgent';
import type { IMediaCallCastDirector } from '../definition/IMediaCallCastDirector';
import type { GetActorContactOptions, MinimalUserData, MediaCallHeader } from '../definition/common';
import { UserActorAgent } from '../internal/agents/UserActorAgent';
import { logger } from '../logger';
import { BroadcastActorAgent } from './BroadcastAgent';

type ContactList = Record<MediaCallActorType, MediaCallContact | null>;

Expand Down Expand Up @@ -89,11 +89,28 @@ export class MediaCallCastDirector implements IMediaCallCastDirector {

const list = user
? this.buildContactListForUser(user, defaultContactInfo)
: this.buildContactListForExtension(sipExtension, defaultContactInfo);
: await this.buildContactListForExtension(sipExtension, defaultContactInfo);

return this.getContactFromList(list, options);
}

private async findUserByPhone(phoneNumber: string): Promise<Pick<IUser, '_id' | 'name' | 'username' | 'freeSwitchExtension'> | null> {
const users = await Users.findByPhone<Pick<IUser, '_id' | 'name' | 'username' | 'freeSwitchExtension'>>(phoneNumber, {
projection: { name: 1, username: 1, freeSwitchExtension: 1 },
}).toArray();
Comment on lines +98 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate CastDirector.ts =="
fd -a 'CastDirector\.ts$' . || true

echo "== file outline =="
ast-grep outline ee/packages/media-calls/src/server/CastDirector.ts || true

echo "== relevant lines =="
sed -n '1,180p' ee/packages/media-calls/src/server/CastDirector.ts

echo "== Users.findByPhone definitions/usages =="
rg -n "findByPhone|findUserByPhone|phones\.number|limit\(|\.toArray\(\)" ee/packages/media-calls/src -S || true

Repository: RocketChat/Rocket.Chat

Length of output: 7434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate Users model files =="
fd -a '.*User.*|users' ee packages -t f 2>/dev/null | sed -n '1,120p' || true

echo "== findByPhone definitions/usages across repo (focused) =="
rg -n "findByPhone|phones\.number|createdAt_1|_id|limit\(|toArray\(\)" ee packages -S --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' | sed -n '1,240p'

echo "== inspect MediaCallCastDirector methods that use findUserByPhone =="
sed -n '150,175p' ee/packages/media-calls/src/server/CastDirector.ts

echo "== behavioral probe: query chain shape in source text =="
python3 - <<'PY'
from pathlib import Path
p=Path('ee/packages/media-calls/src/server/CastDirector.ts')
s=p.read_text()
start=s.index("private async findUserByPhone")
end=s.index("public async getAgentForActorAndRole")
block=s[start:end]
print("contains findByPhone:", "Users.findByPhone" in block)
print("chain includes limit before toArray:", ".limit(2)" in block or ".limit(1)" in block)
print("returns null if more than one:", "users.length > 1" in block and "return null" in block)
PY

Repository: RocketChat/Rocket.Chat

Length of output: 40603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect Users model relevant methods =="
wc -l packages/models/src/models/Users.ts
sed -n '1,260p' packages/models/src/models/Users.ts

echo "== inspect Users model typing relevant fields =="
wc -l packages/model-typings/src/models/IUsersModel.ts
sed -n '1,220p' packages/model-typings/src/models/IUsersModel.ts

echo "== search for phone index definitions/usages =="
rg -n "phones:|phones\.number|phones|phonesIndex|index" ee packages -S --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' | sed -n '1,260p'

Repository: RocketChat/Rocket.Chat

Length of output: 42065


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact findByPhone occurrences with context =="
rg -n -C 8 "findByPhone|findByPhoneNumber|find.*Phone|Phones|phones" ee packages models --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' -S || true

echo "== Users.ts methods around phone-related names =="
rg -n "^\s*(find|findOne).*[Pp]hone|^\s*.*find.*Phone" packages/models/src/models/Users.ts packages/model-typings/src/models/IUsersModel.ts -S || true

Repository: RocketChat/Rocket.Chat

Length of output: 50378


Limit the phone lookup to two results.

Users.findByPhone() is non-unique because the phones.number index is not unique. This lookup only needs zero, one, or multiple results, but .toArray() fetches every matching user before deciding. Add .limit(2) before .toArray().

Proposed change
-		}).toArray();
+		}).limit(2).toArray();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const users = await Users.findByPhone<Pick<IUser, '_id' | 'name' | 'username' | 'freeSwitchExtension'>>(phoneNumber, {
projection: { name: 1, username: 1, freeSwitchExtension: 1 },
}).toArray();
const users = await Users.findByPhone<Pick<IUser, '_id' | 'name' | 'username' | 'freeSwitchExtension'>>(phoneNumber, {
projection: { name: 1, username: 1, freeSwitchExtension: 1 },
}).limit(2).toArray();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/packages/media-calls/src/server/CastDirector.ts` around lines 98 - 100,
Update the Users.findByPhone call in CastDirector to apply a limit of 2 before
toArray(), preserving the existing projection and result handling.


if (!users.length) {
return null;
}

if (users.length > 1) {
logger.warn({ msg: 'Multiple users found for phone number, identity cannot be resolved', phoneNumber });
return null;
Comment on lines +106 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate CastDirector.ts and logger/redaction helpers"
fd -a 'CastDirector\.ts$' . || true
echo "--- logger/redaction search ---"
rg -n "redact|hash\\(|phoneNumber|personal |PII|logger\\.(warn|error|info)" --glob '!node_modules' --glob '!dist' --glob '!build' | head -200
echo "--- target file outline ---"
target="$(fd 'CastDirector\.ts$' . | head -1 || true)"
if [ -n "${target:-}" ]; then
  wc -l "$target"
  ast-grep outline "$target" --view expanded || true
  echo "--- target relevant lines ---"
  sed -n '1,180p' "$target" | cat -n
fi

Repository: RocketChat/Rocket.Chat

Length of output: 398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral check: parse JS/TS logger calls and print whether a logger method call includes a spread/string/variable,
# and whether any nearby identifier could represent redaction/hashing.
python3 - <<'PY'
from pathlib import Path
import re
target = next(Path('.').rglob('ee/packages/media-calls/src/server/CastDirector.ts'), None)
if not target:
    print("TARGET_NOT_FOUND")
    raise SystemExit
text = target.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if re.search(r'\blogger\.\w+\s*\(', line):
        print(f"LINE{i}: {line.strip()}")
PY

Repository: RocketChat/Rocket.Chat

Length of output: 334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="$(fd 'CastDirector\.ts$' . | grep '/ee/packages/media-calls/src/server/CastDirector.ts$' | head -1 || true)"
if [ -z "${target:-}" ]; then
  echo "CastDirector.ts not found"
  exit 1
fi

echo "--- target file outline ---"
ast-grep outline "$target" --view expanded || true
echo "--- relevant lines 1-180 ---"
sed -n '1,180p' "$target" | cat -n

echo "--- repository-wide logger/redaction calls/phrases (compact) ---"
rg -n "(redact|hash\\(|phoneNumber|personal data|PII|logger\\.(warn|error|info|debug))" --glob '!node_modules' --glob '!dist' --glob '!build' | head -300 || true

Repository: RocketChat/Rocket.Chat

Length of output: 8776


Do not log the raw phone number.

findUserByPhone passes phoneNumber into logger.warn, so the server log captures personal data. Remove the field from the warning payload or replace it with the repository’s approved redacted/hashed value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/packages/media-calls/src/server/CastDirector.ts` around lines 106 - 108,
Update the warning payload in findUserByPhone so it no longer logs the raw
phoneNumber; remove that field or replace it with the repository-approved
redacted or hashed representation while preserving the existing warning and null
return behavior.

}

return users[0];
}

public async getAgentForActorAndRole(actor: MediaCallContact, role: CallRole): Promise<IMediaCallAgent | null> {
if (actor.type === 'user') {
return this.getAgentForUserActorAndRole(actor, role);
Expand Down Expand Up @@ -141,10 +158,17 @@ export class MediaCallCastDirector implements IMediaCallCastDirector {
};
}

protected buildContactListForExtension(sipExtension: string, defaultContactInfo?: MediaCallContactInformation): ContactList {
protected async buildContactListForExtension(
sipExtension: string,
defaultContactInfo?: MediaCallContactInformation,
): Promise<ContactList> {
const user = await this.findUserByPhone(sipExtension);

const data: Partial<MediaCallContact> = {
...defaultContactInfo,
...(sipExtension && { sipExtension }),
...(user?.username && { username: user.username }),
...(user?.name && { displayName: user.name }),
Comment on lines +165 to +171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the matched user contact for requiredType: 'user'.

Line 165 obtains a unique user, but the returned ContactList still sets user: null. ee/packages/media-calls/src/sip/providers/IncomingSipCall.ts calls getContactForExtensionNumber with { requiredType: 'user' }. getContactFromList therefore returns null, and the invite fails with SipErrorCodes.TEMPORARILY_UNAVAILABLE even for a unique phone match.

Populate the user contact from the matched user while retaining the original SIP contact.

Proposed change
 		const data: Partial<MediaCallContact> = {
 			...defaultContactInfo,
 			...(sipExtension && { sipExtension }),
 			...(user?.username && { username: user.username }),
 			...(user?.name && { displayName: user.name }),
 		};
+		const userContact = user ? this.buildContactListForUser(user, defaultContactInfo).user : null;

 		return {
-			user: null,
+			user: userContact,
 			sip: {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const user = await this.findUserByPhone(sipExtension);
const data: Partial<MediaCallContact> = {
...defaultContactInfo,
...(sipExtension && { sipExtension }),
...(user?.username && { username: user.username }),
...(user?.name && { displayName: user.name }),
const user = await this.findUserByPhone(sipExtension);
const data: Partial<MediaCallContact> = {
...defaultContactInfo,
...(sipExtension && { sipExtension }),
...(user?.username && { username: user.username }),
...(user?.name && { displayName: user.name }),
};
const userContact = user ? this.buildContactListForUser(user, defaultContactInfo).user : null;
return {
user: userContact,
sip: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/packages/media-calls/src/server/CastDirector.ts` around lines 165 - 171,
Update the ContactList construction in CastDirector’s
getContactForExtensionNumber flow to set its user field from the matched user
returned by findUserByPhone, while preserving the existing SIP contact. Ensure
requiredType: 'user' resolves the matched contact instead of returning null.

};

return {
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
freeSwitchExtension: string,
options?: O,
): Promise<DocumentWithProjection<T, O> | null>;
findByPhone<T extends Document = IUser>(phoneNumber: string, options?: FindOptions<IUser>): FindCursor<T>;
countUsersInRoles(roles: IRole['_id'][]): Promise<number>;
countAllUsersWithPendingAvatar(): Promise<number>;
findOneByIdAndRole<T extends Document = IUser, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
Expand Down
10 changes: 10 additions & 0 deletions packages/models/src/models/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class UsersRaw extends BaseRaw<IUser, DefaultFields<IUser>> implements IU
{ key: { openBusinessHours: 1 }, sparse: true },
{ key: { statusLivechat: 1 }, sparse: true },
{ key: { freeSwitchExtension: 1 }, sparse: true, unique: true },
{ key: { 'phones.number': 1 }, sparse: true },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 --glob '*.ts' --glob '*.tsx' --glob '*.js' '\bphones\b|\bphone\b' .

Repository: RocketChat/Rocket.Chat

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files of interest:\n'
git ls-files 'packages/models/src/models/Users.ts' 'packages/core-typings/src/IUser.ts' | sed -n '1,80p'

printf '\nUsers.ts index and findByPhone context:\n'
sed -n '60,85p' packages/models/src/models/Users.ts
sed -n '2520,2548p' packages/models/src/models/Users.ts

printf '\nIUser.ts:\n'
sed -n '1,140p' packages/core-typings/src/IUser.ts

printf '\nExact Users.ts/phone schemas and writes:\n'
rg -n -C 3 --glob '*.ts' --glob '*.tsx' '\bphones\.number\b|schema:\s*\{[^{}]*phone\b|phone\b.*\?(\{|\[[^]*\]?|}\)|phone:?\b|schemas/Users|Users' packages/server packages/applications packages/core services packages/models | sed -n '1,220p'

Repository: RocketChat/Rocket.Chat

Length of output: 5375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Git diff summary:\n'
git diff --stat || true

printf '\nRelevant tracked files for Users/phone schema:\n'
git ls-files | rg '(^|/)Users\.ts$|IUser\.ts$|Phone|phone|Users\.schema|users' | sed -n '1,240p'

printf '\nFocused schema/phone searches:\n'
rg -n --glob '*.ts' --glob '*.tsx' 'phones\.number|phones\s*:|phone\s*(\?|[:=])|Schema.*Users|users.*Schema|phone.*number|number.*phone' packages/server packages/models packages/settings packages/applications packages/core-typings packages/rest-typings 2>/dev/null | sed -n '1,260p'

printf '\nFindAll/phone writer candidates:\n'
rg -n --glob '*.ts' --glob '*.tsx' 'set\s*:\s*\{[^{}]*phone|\$set[^}]*phone|phone:|phoneNumber|createUser|register|set password|password' packages/services packages/applications packages/server | sed -n '1,260p'

Repository: RocketChat/Rocket.Chat

Length of output: 16281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Read relevant Users schema excerpt:\n'
sed -n '50,95p' packages/models/src/models/Users.ts

printf '\nRead core typings phone section:\n'
sed -n '200,225p' packages/core-typings/src/IUser.ts

printf '\nRead Users phone projection excerpt:\n'
sed -n '1958,1972p' packages/models/src/models/Users.ts

printf '\nRead REST user phone types:\n'
rg -n --glob '*.ts' 'phone.*string|phones.*string\[\]|phone?:|phone:' packages/rest-typings/src/v1/users packages/apps-engine/src/definition/users packages/core-typings/src/IUser.ts | sed -n '1,160p'

printf '\nCheck current index/query exact strings:\n'
python3 - <<'PY'
from pathlib import Path
users = Path('packages/models/src/models/Users.ts')
text = users.read_text()
for needle in ["'phones.number'", "\"phones.number\"", "findByPhone"]:
    print(needle, text.find(needle))
PY

Repository: RocketChat/Rocket.Chat

Length of output: 3341


Align findByPhone with the persisted user phone field.

findByPhone and the new index use phones.number, but user documents and IUser store a scalar phone?: string. This sparse index will not support the query path. Use the actual persisted field for the index, query, and shared type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/models/src/models/Users.ts` at line 72, Update the user phone
handling around findByPhone and its index to use the persisted scalar phone
field instead of phones.number. Change the index, query path, and shared IUser
type consistently to phone, preserving optionality and existing lookup behavior.

{ key: { language: 1 }, sparse: true },
{ key: { 'active': 1, 'services.email2fa.enabled': 1 }, sparse: true }, // used by statistics
{ key: { 'active': 1, 'services.totp.enabled': 1 }, sparse: true }, // used by statistics
Expand Down Expand Up @@ -2713,6 +2714,15 @@ export class UsersRaw extends BaseRaw<IUser, DefaultFields<IUser>> implements IU
);
}

findByPhone<T extends Document = IUser>(phoneNumber: string, options: FindOptions<IUser> = {}): FindCursor<T> {
return this.find<T>(
{
'phones.number': phoneNumber,
} as Filter<IUser>,
options,
);
}

// UPDATE
addImportIds(_id: IUser['_id'], importIds: string[]) {
importIds = ([] as string[]).concat(importIds);
Expand Down
Loading