Merge pull request #365 from zjs81/rpt-guest

enh: make repeater admin guest aware
This commit is contained in:
zjs81 2026-04-14 20:44:14 -07:00 committed by GitHub
commit 33a8f34463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 403 additions and 134 deletions

View file

@ -1038,8 +1038,8 @@
"login_enterPassword": "Enter password",
"login_savePassword": "Save password",
"login_savePasswordSubtitle": "Password will be stored securely on this device",
"login_repeaterDescription": "Enter the repeater password to access settings and status.",
"login_roomDescription": "Enter the room password to access settings and status.",
"login_repeaterDescription": "Enter the repeater password for guest or admin access.",
"login_roomDescription": "Enter the room password for guest or admin access.",
"login_routing": "Routing",
"login_routingMode": "Routing mode",
"login_autoUseSavedPath": "Auto (use saved path)",
@ -1105,7 +1105,10 @@
"path_setPath": "Set Path",
"repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_guest": "Repeater Information",
"room_guest": "Room Server Information",
"repeater_managementTools": "Management Tools",
"repeater_guestTools": "Guest Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
"repeater_telemetry": "Telemetry",

View file

@ -3438,13 +3438,13 @@ abstract class AppLocalizations {
/// No description provided for @login_repeaterDescription.
///
/// In en, this message translates to:
/// **'Enter the repeater password to access settings and status.'**
/// **'Enter the repeater password for guest or admin access.'**
String get login_repeaterDescription;
/// No description provided for @login_roomDescription.
///
/// In en, this message translates to:
/// **'Enter the room password to access settings and status.'**
/// **'Enter the room password for guest or admin access.'**
String get login_roomDescription;
/// No description provided for @login_routing.
@ -3609,12 +3609,30 @@ abstract class AppLocalizations {
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_guest.
///
/// In en, this message translates to:
/// **'Repeater Information'**
String get repeater_guest;
/// No description provided for @room_guest.
///
/// In en, this message translates to:
/// **'Room Server Information'**
String get room_guest;
/// No description provided for @repeater_managementTools.
///
/// In en, this message translates to:
/// **'Management Tools'**
String get repeater_managementTools;
/// No description provided for @repeater_guestTools.
///
/// In en, this message translates to:
/// **'Guest Tools'**
String get repeater_guestTools;
/// No description provided for @repeater_status.
///
/// In en, this message translates to:

View file

@ -2019,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Инструменти за управление';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Статус';

View file

@ -2017,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Verwaltungs-Tools';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -1871,11 +1871,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Enter the repeater password to access settings and status.';
'Enter the repeater password for guest or admin access.';
@override
String get login_roomDescription =>
'Enter the room password to access settings and status.';
'Enter the room password for guest or admin access.';
@override
String get login_routing => 'Routing';
@ -1979,9 +1979,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get room_management => 'Room Server Management';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Management Tools';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2015,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Estado';

View file

@ -2026,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get room_management => 'Administrattion Room Server';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Outils de Gestion';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'État';

View file

@ -2030,9 +2030,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get room_management => 'Szoba-szerver kezelés';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Menedzsmentes eszközök';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Állapot';

View file

@ -2016,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Stato';

View file

@ -1932,9 +1932,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get room_management => 'ルームサーバーの管理';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => '管理ツール';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'ステータス';

View file

@ -1929,9 +1929,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get room_management => '방 서버 관리';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => '관리 도구';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => '상태';

View file

@ -2003,9 +2003,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2031,9 +2031,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2015,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2019,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get room_management => 'Управление сервером комнат';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Инструменты управления';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Статус';

View file

@ -2004,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Nástroje na správu';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2001,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Upravne orodje';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -1990,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';

View file

@ -2014,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get room_management => 'Адміністрування сервера кімнати';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Інструменти керування';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Статус';

View file

@ -1890,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get room_management => '房间服务器管理';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => '管理工具';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => '状态';

View file

@ -961,13 +961,16 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context,
builder: (context) => RepeaterLoginDialog(
repeater: repeater,
onLogin: (password) {
onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
isAdmin: isAdmin,
),
),
);
},
@ -984,14 +987,18 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
? RepeaterHubScreen(
repeater: room,
password: password,
isAdmin: isAdmin,
)
: ChatScreen(contact: room),
),
);

View file

@ -1367,13 +1367,16 @@ class _MapScreenState extends State<MapScreen> {
context: context,
builder: (context) => RepeaterLoginDialog(
repeater: repeater,
onLogin: (password) {
onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
isAdmin: isAdmin,
),
),
);
},
@ -1386,7 +1389,8 @@ class _MapScreenState extends State<MapScreen> {
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
// onLogin(password, isAdmin) isAdmin not used for room caht screen
onLogin: (password, _) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(

View file

@ -13,11 +13,13 @@ import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
final String password;
final bool isAdmin;
const RepeaterHubScreen({
super.key,
required this.repeater,
required this.password,
required this.isAdmin,
});
@override
@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
if (isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
if (!isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_guest
: l10n.room_guest,
),
Text(
repeater.name,
style: const TextStyle(
@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget {
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.battery_full),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n.appSettings_batteryChemistry,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
if (isAdmin)
Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.battery_full),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n.appSettings_batteryChemistry,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
],
),
onChanged: (value) {
if (value == null) return;
settingsService.setBatteryChemistryForRepeater(
repeater.publicKeyHex,
value,
);
},
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
onChanged: (value) {
if (value == null) return;
settingsService.setBatteryChemistryForRepeater(
repeater.publicKeyHex,
value,
);
},
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
),
),
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
isAdmin
? l10n.repeater_managementTools
: l10n.repeater_guestTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
@ -210,26 +222,27 @@ class RepeaterHubScreen extends StatelessWidget {
);
},
),
const SizedBox(height: 12),
if (isAdmin) const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
if (isAdmin)
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
),
);
},
),
);
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
@ -248,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget {
);
},
),
const SizedBox(height: 12),
if (isAdmin) const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
if (isAdmin)
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
),
);
},
),
);
},
),
],
),
),

View file

@ -14,7 +14,7 @@ import 'path_management_dialog.dart';
class RepeaterLoginDialog extends StatefulWidget {
final Contact repeater;
final Function(String password) onLogin;
final Function(String password, bool isAdmin) onLogin;
const RepeaterLoginDialog({
super.key,
@ -119,6 +119,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
: '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin');
bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return;
setState(() {
@ -131,7 +132,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
);
await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout);
(loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) {
appLogger.info(
'Login succeeded for ${repeater.name}',
@ -212,7 +213,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
if (mounted) {
Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password));
Future.microtask(() => widget.onLogin(password, isAdmin));
}
} catch (e) {
final repeater = _resolveRepeater(_connector);
@ -229,17 +230,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
}
Future<bool?> _awaitLoginResponse(Duration timeout) async {
// _awaitLoginResponse returns a record of bool, for success and if the client is an admin
Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>();
Timer? timer;
StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.repeater.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
if (frame.length < 8) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return;
@ -258,7 +263,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
final result = await completer.future;
timer.cancel();
await subscription.cancel();
return result;
return (result, isAdmin);
}
@override

View file

@ -15,7 +15,7 @@ import 'path_management_dialog.dart';
class RoomLoginDialog extends StatefulWidget {
final Contact room;
final Function(String password) onLogin;
final Function(String password, bool isAdmin) onLogin;
const RoomLoginDialog({super.key, required this.room, required this.onLogin});
@ -115,6 +115,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
: '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin');
bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return;
setState(() {
@ -127,7 +128,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
);
await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout);
(loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) {
appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin');
break;
@ -167,7 +168,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
if (mounted) {
Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password));
Future.microtask(() => widget.onLogin(password, isAdmin));
}
} catch (e) {
final room = _resolveRepeater(_connector);
@ -185,16 +186,20 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
}
}
Future<bool?> _awaitLoginResponse(Duration timeout) async {
Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>();
Timer? timer;
StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.room.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
if (frame.length < 8) return;
final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return;
@ -214,7 +219,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
final result = await completer.future;
timer.cancel();
await subscription.cancel();
return result;
return (result, isAdmin);
}
@override

View file

@ -1,69 +1,120 @@
{
"bg": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"de": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"es": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"fr": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"hu": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"it": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"ja": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"ko": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"nl": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"pl": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"pt": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"ru": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"sk": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"sl": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"sv": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"uk": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
],
"zh": [
"chat_sendMessage"
"chat_sendMessage",
"repeater_guest",
"room_guest",
"repeater_guestTools"
]
}