Compare commits

...

20 Commits

Author SHA1 Message Date
damencho
732754c566 feat: Module to expose turn credentials via http endpoint. 2023-04-18 11:07:35 -05:00
Saúl Ibarra Corretgé
b360a9e572 fix(build) drop unused file
Also generalize the lib-jitsi-meet files rule so it can also work with
development (unminimized) builds.
2023-04-18 17:19:59 +02:00
Saúl Ibarra Corretgé
f88fa81616 deps(rn) update react-native-webrtc to version 111.0.0
Adapt to changes in the Android plugin initialization.

Leverage the new module initialization to simplify enabling WebRTC
logging.
2023-04-18 17:11:04 +02:00
Calin-Teodor
e0e66119f5 fix(google-api): sign out button label on smaller devices 2023-04-18 17:03:19 +03:00
Calinteodor
ba4784f149 feat(subtitles): rework feature (#12484)
* feat(subtitles): separated web from native and created native subtitles screen
2023-04-18 16:01:34 +03:00
Saúl Ibarra Corretgé
7fb7c3de9c chore(deps) update xmldom to the latest version 2023-04-18 13:46:37 +02:00
Saúl Ibarra Corretgé
3d2d449d31 chore(deps) react-native-google-signin@latest
Updates the Google SignIn SDK to the latest.
2023-04-18 13:16:09 +02:00
Saúl Ibarra Corretgé
ca60c33dda fix(ios) disable CallKit when running in the simulator
It doesn't actually work on the simulator but it never caused trouble...
until iOS 16.4 (or maybe earlier). Disable it.
2023-04-18 10:58:42 +02:00
damencho
162512496a feat: Module to rate limit based on sent stanzas via ip. 2023-04-17 18:09:16 -05:00
damencho
7819c97839 feat: Module to kick jigasi from a meeting via http endpoint. 2023-04-17 18:09:16 -05:00
damencho
e9c8603c3c feat: Module to invite jigasi to a meeting via http endpoint. 2023-04-17 18:09:16 -05:00
damencho
9e165c337a feat: Module to ban users based on external service. 2023-04-17 18:09:16 -05:00
damencho
58af1b98c0 feat: Module to provide http endpoint for ending a meeting. 2023-04-17 18:09:16 -05:00
damencho
6a077333c6 feat: Module to hide rooms for some queries. 2023-04-17 18:09:16 -05:00
damencho
cb234e6b1b feat: Module to flip devices. 2023-04-17 18:09:16 -05:00
damencho
67a9f35176 feat: Module to restrict muc access. 2023-04-17 18:09:16 -05:00
Jaya Allamsetty
9396e8b0c0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1615.0.0+a23a8c7c...v1617.0.0+faeff49a
2023-04-17 16:46:13 -04:00
dependabot[bot]
200d857012 chore(deps-dev): bump webpack from 5.57.1 to 5.76.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.57.1 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.57.1...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-17 09:50:18 +02:00
Christoph Settgast
1a22b7d0dd fix(desktop-picker): Populate list of desktop app windows to share
Fix bug introduced via #12994 where id was changed and "-tab" was appended. Due to this the selected tab was not anymore working and empty object was returned here: 05da37b56d/react/features/desktop-picker/components/DesktopPicker.tsx (L270)

Originally part of #13096 by @dudumanbogdan, but pulled ahead to fix application
window sharing via Electron desktop app.

Fixes: jitsi/jitsi-meet-electron#857
2023-04-13 23:12:37 -05:00
Calin-Teodor
035cccb97b fix(toolbox): imports 2023-04-14 00:05:25 +03:00
52 changed files with 1799 additions and 641 deletions

3
.npmrc
View File

@@ -1,3 +1,6 @@
package-lock=true
; FIXME Set legacy-peer-deps=false when we upgrade RN.
legacy-peer-deps=true
; Omit optional dependencies
omit=optional

View File

@@ -63,10 +63,7 @@ deploy-appbundle:
deploy-lib-jitsi-meet:
cp \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.e2ee-worker.js \
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.* \
$(DEPLOY_DIR)
deploy-olm:

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.jitsi.meet.sdk;
import org.webrtc.VideoCodecInfo;
import java.util.Map;
import java.util.HashMap;
/** Container for static helper functions related to dealing with H264 codecs. */
class H264Utils {
public static final String H264_FMTP_PROFILE_LEVEL_ID = "profile-level-id";
public static final String H264_FMTP_LEVEL_ASYMMETRY_ALLOWED = "level-asymmetry-allowed";
public static final String H264_FMTP_PACKETIZATION_MODE = "packetization-mode";
public static final String H264_PROFILE_CONSTRAINED_BASELINE = "42e0";
public static final String H264_PROFILE_CONSTRAINED_HIGH = "640c";
public static final String H264_LEVEL_3_1 = "1f"; // 31 in hex.
public static final String H264_CONSTRAINED_HIGH_3_1 =
H264_PROFILE_CONSTRAINED_HIGH + H264_LEVEL_3_1;
public static final String H264_CONSTRAINED_BASELINE_3_1 =
H264_PROFILE_CONSTRAINED_BASELINE + H264_LEVEL_3_1;
public static Map<String, String> getDefaultH264Params(boolean isHighProfile) {
final Map<String, String> params = new HashMap<>();
params.put(VideoCodecInfo.H264_FMTP_LEVEL_ASYMMETRY_ALLOWED, "1");
params.put(VideoCodecInfo.H264_FMTP_PACKETIZATION_MODE, "1");
params.put(VideoCodecInfo.H264_FMTP_PROFILE_LEVEL_ID,
isHighProfile ? VideoCodecInfo.H264_CONSTRAINED_HIGH_3_1
: VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1);
return params;
}
public static VideoCodecInfo DEFAULT_H264_BASELINE_PROFILE_CODEC =
new VideoCodecInfo("H264", getDefaultH264Params(/* isHighProfile= */ false));
public static VideoCodecInfo DEFAULT_H264_HIGH_PROFILE_CODEC =
new VideoCodecInfo("H264", getDefaultH264Params(/* isHighProfile= */ true));
}

View File

@@ -32,17 +32,17 @@ import com.facebook.react.jscexecutor.JSCExecutorFactory;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.oney.WebRTCModule.EglUtils;
import com.oney.WebRTCModule.RTCVideoViewManager;
import com.oney.WebRTCModule.WebRTCModule;
import com.oney.WebRTCModule.WebRTCModuleOptions;
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoDecoderFactory;
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
import org.devio.rn.splashscreen.SplashScreenModule;
import org.webrtc.EglBase;
import org.webrtc.audio.AudioDeviceModule;
import org.webrtc.audio.JavaAudioDeviceModule;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
class ReactInstanceManagerHolder {
@@ -79,30 +79,11 @@ class ReactInstanceManagerHolder {
nativeModules.add(new RNConnectionService(reactContext));
}
// Initialize the WebRTC module by hand, since we want to override some
// initialization options.
WebRTCModule.Options options = new WebRTCModule.Options();
AudioDeviceModule adm = JavaAudioDeviceModule.builder(reactContext)
.setEnableVolumeLogger(false)
.createAudioDeviceModule();
options.setAudioDeviceModule(adm);
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
options.setVideoDecoderFactory(new WebRTCVideoDecoderFactory(eglContext));
options.setVideoEncoderFactory(new WebRTCVideoEncoderFactory(eglContext));
nativeModules.add(new WebRTCModule(reactContext, options));
return nativeModules;
}
private static List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
// WebRTC, see createNativeModules for details.
new RTCVideoViewManager()
);
return Collections.emptyList();
}
static List<ReactPackage> getReactNativePackages() {
@@ -122,6 +103,7 @@ class ReactInstanceManagerHolder {
new com.reactnativecommunity.webview.RNCWebViewPackage(),
new com.kevinresol.react_native_default_preference.RNDefaultPreferencePackage(),
new com.learnium.RNDeviceInfo.RNDeviceInfo(),
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
new org.linusu.RNGetRandomValuesPackage(),
new com.rnimmersive.RNImmersivePackage(),
@@ -254,6 +236,14 @@ class ReactInstanceManagerHolder {
return;
}
// Initialize the WebRTC module options.
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
options.videoDecoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
options.videoEncoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
Log.d(TAG, "initializing RN with Application");
reactInstanceManager

View File

@@ -1,19 +0,0 @@
package org.jitsi.meet.sdk;
/** Enumeration of supported video codec types. */
public enum VideoCodecMimeType {
VP8("video/x-vnd.on2.vp8"),
VP9("video/x-vnd.on2.vp9"),
H264("video/avc"),
AV1("video/av01");
private final String mimeType;
private VideoCodecMimeType(String mimeType) {
this.mimeType = mimeType;
}
String mimeType() {
return mimeType;
}
}

View File

@@ -1,52 +0,0 @@
package org.jitsi.meet.sdk;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.HardwareVideoDecoderFactory;
import org.webrtc.SoftwareVideoDecoderFactory;
import org.webrtc.VideoCodecInfo;
import org.webrtc.VideoDecoder;
import org.webrtc.VideoDecoderFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* This is a custom video decoder factory for WebRTC which behaves similarly
* to the default one in iOS. It supports the following codecs:
*
* - In hardware: H.264 (baseline)
* - In software: VP8, VP9, AV1
*/
public class WebRTCVideoDecoderFactory implements VideoDecoderFactory {
private final VideoDecoderFactory hardwareVideoDecoderFactory;
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactory();
public WebRTCVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
}
@Nullable
@Override
public VideoDecoder createDecoder(VideoCodecInfo codecInfo) {
if (codecInfo.name.equalsIgnoreCase(VideoCodecMimeType.H264.name())) {
return this.hardwareVideoDecoderFactory.createDecoder(codecInfo);
}
return this.softwareVideoDecoderFactory.createDecoder(codecInfo);
}
@Override
public VideoCodecInfo[] getSupportedCodecs() {
List<VideoCodecInfo> codecs = new ArrayList<>();
codecs.add(H264Utils.DEFAULT_H264_BASELINE_PROFILE_CODEC);
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP8.name(), new HashMap<>()));
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP9.name(), new HashMap<>()));
codecs.add(new VideoCodecInfo(VideoCodecMimeType.AV1.name(), new HashMap<>()));
return codecs.toArray(new VideoCodecInfo[codecs.size()]);
}
}

View File

@@ -1,53 +0,0 @@
package org.jitsi.meet.sdk;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.HardwareVideoEncoderFactory;
import org.webrtc.SoftwareVideoEncoderFactory;
import org.webrtc.VideoCodecInfo;
import org.webrtc.VideoEncoder;
import org.webrtc.VideoEncoderFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* This is a custom video encoder factory for WebRTC which behaves similarly
* to the default one in iOS. It supports the following codecs:
*
* - In hardware: H.264 (baseline)
* - In software: VP8, VP9, AV1
*/
public class WebRTCVideoEncoderFactory implements VideoEncoderFactory {
private final VideoEncoderFactory hardwareVideoEncoderFactory;
private final VideoEncoderFactory softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
public WebRTCVideoEncoderFactory(@Nullable EglBase.Context eglContext) {
this.hardwareVideoEncoderFactory =
new HardwareVideoEncoderFactory(eglContext, false, false);
}
@Nullable
@Override
public VideoEncoder createEncoder(VideoCodecInfo codecInfo) {
if (codecInfo.name.equalsIgnoreCase(VideoCodecMimeType.H264.name())) {
return this.hardwareVideoEncoderFactory.createEncoder(codecInfo);
}
return this.softwareVideoEncoderFactory.createEncoder(codecInfo);
}
@Override
public VideoCodecInfo[] getSupportedCodecs() {
List<VideoCodecInfo> codecs = new ArrayList<>();
codecs.add(H264Utils.DEFAULT_H264_BASELINE_PROFILE_CODEC);
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP8.name(), new HashMap<>()));
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP9.name(), new HashMap<>()));
codecs.add(new VideoCodecInfo(VideoCodecMimeType.AV1.name(), new HashMap<>()));
return codecs.toArray(new VideoCodecInfo[codecs.size()]);
}
}

View File

@@ -78,6 +78,7 @@ Component "conference.jitmeet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_hide_all";
"muc_meeting_id";
"muc_domain_mapper";
"polls";
@@ -92,6 +93,7 @@ Component "breakout.jitmeet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_hide_all";
"muc_meeting_id";
"muc_domain_mapper";
"muc_rate_limit";
@@ -105,6 +107,7 @@ Component "breakout.jitmeet.example.com" "muc"
Component "internal.auth.jitmeet.example.com" "muc"
storage = "memory"
modules_enabled = {
"muc_hide_all";
"ping";
}
admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
@@ -139,6 +142,7 @@ Component "lobby.jitmeet.example.com" "muc"
muc_room_locking = false
muc_room_default_public_jids = true
modules_enabled = {
"muc_hide_all";
"muc_rate_limit";
"polls";
}

View File

@@ -42,7 +42,7 @@ target 'JitsiMeetSDK' do
pod 'CocoaLumberjack', '3.7.2'
pod 'ObjectiveDropboxOfficial', '6.2.3'
pod 'JitsiWebRTC', '~> 106.0.0'
pod 'JitsiWebRTC', '~> 111.0.0'
end
target 'JitsiMeetSDKLite' do

View File

@@ -3,11 +3,12 @@ PODS:
- amplitude-react-native (2.7.0):
- Amplitude (= 8.7.1)
- React-Core
- AppAuth (1.4.0):
- AppAuth/Core (= 1.4.0)
- AppAuth/ExternalUserAgent (= 1.4.0)
- AppAuth/Core (1.4.0)
- AppAuth/ExternalUserAgent (1.4.0)
- AppAuth (1.6.1):
- AppAuth/Core (= 1.6.1)
- AppAuth/ExternalUserAgent (= 1.6.1)
- AppAuth/Core (1.6.1)
- AppAuth/ExternalUserAgent (1.6.1):
- AppAuth/Core
- boost (1.76.0)
- CocoaLumberjack (3.7.2):
- CocoaLumberjack/Core (= 3.7.2)
@@ -102,56 +103,56 @@ PODS:
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (~> 2.30908.0)
- GoogleDataTransport (9.1.4):
- GoogleDataTransport (9.2.2):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleSignIn (6.0.2):
- AppAuth (~> 1.4)
- GTMAppAuth (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleUtilities/AppDelegateSwizzler (7.7.0):
- GoogleSignIn (6.2.4):
- AppAuth (~> 1.5)
- GTMAppAuth (~> 1.3)
- GTMSessionFetcher/Core (< 3.0, >= 1.1)
- GoogleUtilities/AppDelegateSwizzler (7.11.1):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Environment (7.7.0):
- GoogleUtilities/Environment (7.11.1):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.7.0):
- GoogleUtilities/Logger (7.11.1):
- GoogleUtilities/Environment
- GoogleUtilities/MethodSwizzler (7.7.0):
- GoogleUtilities/MethodSwizzler (7.11.1):
- GoogleUtilities/Logger
- GoogleUtilities/Network (7.7.0):
- GoogleUtilities/Network (7.11.1):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.7.0)"
- GoogleUtilities/Reachability (7.7.0):
- "GoogleUtilities/NSData+zlib (7.11.1)"
- GoogleUtilities/Reachability (7.11.1):
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (7.7.0):
- GoogleUtilities/UserDefaults (7.11.1):
- GoogleUtilities/Logger
- GTMAppAuth (1.2.2):
- AppAuth/Core (~> 1.4)
- GTMSessionFetcher/Core (~> 1.5)
- GTMSessionFetcher/Core (1.7.0)
- JitsiWebRTC (106.0.0)
- libwebp (1.2.1):
- libwebp/demux (= 1.2.1)
- libwebp/mux (= 1.2.1)
- libwebp/webp (= 1.2.1)
- libwebp/demux (1.2.1):
- GTMAppAuth (1.3.1):
- AppAuth/Core (~> 1.6)
- GTMSessionFetcher/Core (< 3.0, >= 1.5)
- GTMSessionFetcher/Core (2.3.0)
- JitsiWebRTC (111.0.1)
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
- libwebp/webp (= 1.2.4)
- libwebp/demux (1.2.4):
- libwebp/webp
- libwebp/mux (1.2.1):
- libwebp/mux (1.2.4):
- libwebp/demux
- libwebp/webp (1.2.1)
- libwebp/webp (1.2.4)
- nanopb (2.30908.0):
- nanopb/decode (= 2.30908.0)
- nanopb/encode (= 2.30908.0)
- nanopb/decode (2.30908.0)
- nanopb/encode (2.30908.0)
- ObjectiveDropboxOfficial (6.2.3)
- PromisesObjC (2.1.1)
- PromisesSwift (2.1.1):
- PromisesObjC (= 2.1.1)
- PromisesObjC (2.2.0)
- PromisesSwift (2.2.0):
- PromisesObjC (= 2.2.0)
- RCT-Folly (2021.06.28.00-v2):
- boost
- DoubleConversion
@@ -389,8 +390,8 @@ PODS:
- react-native-video/Video (6.0.0-alpha.1):
- PromisesSwift
- React-Core
- react-native-webrtc (106.0.7):
- JitsiWebRTC (~> 106.0.0)
- react-native-webrtc (111.0.0):
- JitsiWebRTC (~> 111.0.0)
- React-Core
- react-native-webview (11.15.1):
- React-Core
@@ -471,8 +472,8 @@ PODS:
- React-Core
- RNGestureHandler (2.9.0):
- React-Core
- RNGoogleSignin (7.0.4):
- GoogleSignIn (~> 6.0.0)
- RNGoogleSignin (9.0.2):
- GoogleSignIn (~> 6.2)
- React-Core
- RNScreens (3.13.1):
- React-Core
@@ -500,7 +501,7 @@ DEPENDENCIES:
- Firebase/DynamicLinks (~> 8.0)
- "giphy-react-native-sdk (from `../node_modules/@giphy/react-native-sdk`)"
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- JitsiWebRTC (~> 106.0.0)
- JitsiWebRTC (~> 111.0.0)
- ObjectiveDropboxOfficial (= 6.2.3)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
@@ -701,7 +702,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Amplitude: 834c7332dfb9640a751e21c13efb22a07c0c12d4
amplitude-react-native: 0ed8cab759aafaa94961b82122bf56297da607ad
AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
AppAuth: e48b432bb4ba88b10cb2bcc50d7f3af21e78b9c2
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
@@ -719,17 +720,17 @@ SPEC CHECKSUMS:
giphy-react-native-sdk: 7abccf2b52123a0f30ce99da895ab6288023680c
glog: 476ee3e89abb49e07f822b48323c51c57124b572
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b
GoogleSignIn: fd381840dbe7c1137aa6dc30849a5c3e070c034a
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89
GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91
JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
GoogleDataTransport: 8378d1fa8ac49753ea6ce70d65a7cb70ce5f66e6
GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2
JitsiWebRTC: 9619c1f71cc16eeca76df68aa2d213c6d63274a8
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
PromisesSwift: 99fddfe4a0ec88a56486644c0da106694c92a604
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
RCTRequired: 92cbd71369a2de6add25fd2403ac39838f1b694f
RCTTypeSafety: 494e8af41d7410ed0b877210859ee3984f37e6b4
@@ -754,7 +755,7 @@ SPEC CHECKSUMS:
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
react-native-webrtc: 0df36747802476e758af6b6dceccdeaed8c826c2
react-native-webrtc: a9d4d8ef61adb634e006ffd956c494ad8318d95c
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
React-perflogger: 46620fc6d1c3157b60ed28434e08f7fd7f3f3353
React-RCTActionSheet: b1f7e72a0ba760ec684df335c61f730b5179f5ff
@@ -774,13 +775,13 @@ SPEC CHECKSUMS:
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c
RNGoogleSignin: 22e468a9474dbcb8618d8847205ad4f0b2575d13
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
RNWatch: dae6c858a2051dbdcfb00b9a86cf4d90400263b4
Yoga: 7929b92b1828675c1bebeb114dae8cb8fa7ef6a3
PODFILE CHECKSUM: e671cdcdb80fab67e305861c36bfae8ed5a5b0ef
PODFILE CHECKSUM: d9116cb59cd7e921956e45de7cbbd75bef3862c1
COCOAPODS: 1.11.3

View File

@@ -39,6 +39,11 @@
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
#if TARGET_IPHONE_SIMULATOR
// CallKit has started to create problems starting with the iOS 16 simulator.
// Disable it since it never worked in the simulator anyway.
[builder setFeatureFlag:@"call-integration.enabled" withBoolean:NO];
#endif
}];
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];

View File

@@ -15,7 +15,7 @@
*/
#import <Intents/Intents.h>
#import <WebRTC/RTCLogging.h>
#import "Orientation.h"
#import "JitsiMeet+Private.h"
@@ -26,6 +26,8 @@
#import "RNSplashScreen.h"
#import "ScheenshareEventEmiter.h"
#import <react-native-webrtc/WebRTCModuleOptions.h>
#if !defined(JITSI_MEET_SDK_LITE)
#import <RNGoogleSignin/RNGoogleSignin.h>
#import "Dropbox.h"
@@ -52,6 +54,12 @@
- (instancetype)init {
if (self = [super init]) {
#if 0
// Initialize WebRTC options.
WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
options.loggingSeverity = RTCLoggingSeverityInfo;
#endif
// Initialize the one and only bridge for interfacing with React Native.
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
@@ -63,11 +71,6 @@
// Register a log handler for React.
registerReactLogHandler();
#if 0
// Enable WebRTC logs
RTCSetMinDebugLogLevel(RTCLoggingSeverityInfo);
#endif
}
return self;

141
package-lock.json generated
View File

@@ -34,7 +34,7 @@
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/netinfo": "7.1.7",
"@react-native-community/slider": "4.1.12",
"@react-native-google-signin/google-signin": "7.0.4",
"@react-native-google-signin/google-signin": "9.0.2",
"@react-navigation/bottom-tabs": "6.5.3",
"@react-navigation/elements": "1.3.13",
"@react-navigation/material-top-tabs": "6.5.2",
@@ -49,7 +49,7 @@
"@types/w3c-web-hid": "1.0.3",
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.7.9",
"@xmldom/xmldom": "0.8.7",
"amplitude-js": "8.2.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -107,7 +107,7 @@
"react-native-url-polyfill": "1.3.0",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-webrtc": "106.0.7",
"react-native-webrtc": "111.0.0",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
@@ -171,7 +171,7 @@
"ts-loader": "9.4.1",
"typescript": "4.7.4",
"unorm": "1.6.0",
"webpack": "5.57.1",
"webpack": "5.76.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.7.3"
@@ -5245,12 +5245,18 @@
}
},
"node_modules/@react-native-google-signin/google-signin": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-7.0.4.tgz",
"integrity": "sha512-N5uVDlTp/mgpa5gFr6VIr8pldt68jlmHOlqcTSnSwBuZXusXMiK53DCIOWuYk4OJ1rlb8Esa9J4FJwUB0psU9Q==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-9.0.2.tgz",
"integrity": "sha512-oTD0ZT6ZSH7CZjM1i6hR/gFAXv9As7nxhN99XGliNxf6SChZcQRwGeaz1DjoYrWExWDVX00EH1Per3dMjhAWkQ==",
"peerDependencies": {
"expo": ">=47.0.0",
"react": "*",
"react-native": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"node_modules/@react-native/assets": {
@@ -6173,9 +6179,9 @@
}
},
"node_modules/@types/estree": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"version": "0.0.51",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
"node_modules/@types/express": {
@@ -7229,9 +7235,9 @@
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==",
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
"integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==",
"engines": {
"node": ">=10.0.0"
}
@@ -9688,9 +9694,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz",
"integrity": "sha512-weDYmzbBygL7HzGGS26M3hGQx68vehdEg6VUmqSOaFzXExFqlnKuSvsEJCVGQHScS8CQMbrAqftT+AzzHNt/YA==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -13382,8 +13388,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
"integrity": "sha512-OfmtbNqL4FoUHK/RA4VxkYVMT8kbPPQA2au5hmVeadB+PVhqJqlHUgbsV+H4yGJjP9AEcY7UejXtVVkPSG4aHA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
"integrity": "sha512-axTNZ1eYEq4h1ihK7YSmNXknkXIRI+xdVTAf+jqzmRU92MVdjr+dA9aw9i7GRaQzCdFaJiY8QudCliNTWNbv1Q==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -16567,10 +16573,9 @@
}
},
"node_modules/react-native-webrtc": {
"version": "106.0.7",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.7.tgz",
"integrity": "sha512-Ro/NoxN/9/b/bcZErM+oVbWC8ZCplp00sHWDOOh289KDkAkFYZdyFZAKsgJQoMj5rOJZVzIIxuJW91ox+0k92w==",
"hasInstallScript": true,
"version": "111.0.0",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-111.0.0.tgz",
"integrity": "sha512-eCvjPiU28cT85K8TBV7geAA92+uCldhmJ6F+3CMtAAG7U4AOQzxYOI5HC3h9Lt2zfTwsUzg3tyamqWf00PWndA==",
"dependencies": {
"adm-zip": "0.5.9",
"base64-js": "1.5.1",
@@ -19573,9 +19578,9 @@
}
},
"node_modules/watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -19642,35 +19647,35 @@
}
},
"node_modules/webpack": {
"version": "5.57.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
"integrity": "sha512-kHszukYjTPVfCOEyrUthA3jqJwduY/P3eO8I0gMNOZGIQWKAwZftxmp5hq6paophvwo9NoUrcZOecs9ulOyyTg==",
"version": "5.76.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.0",
"@types/estree": "^0.0.50",
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^0.0.51",
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.8.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.4",
"json-parse-better-errors": "^1.0.2",
"graceful-fs": "^4.2.9",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.2.0",
"webpack-sources": "^3.2.0"
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -24266,9 +24271,9 @@
"integrity": "sha512-CiuLZ2orueBiWHYxfaJF57jQY6HY2Q3z5pdAE4MKH8EqIImr/jgDJrJ/UxOVZHK1Ng9P+XlGIKfVIcuWZ6guuA=="
},
"@react-native-google-signin/google-signin": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-7.0.4.tgz",
"integrity": "sha512-N5uVDlTp/mgpa5gFr6VIr8pldt68jlmHOlqcTSnSwBuZXusXMiK53DCIOWuYk4OJ1rlb8Esa9J4FJwUB0psU9Q=="
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-9.0.2.tgz",
"integrity": "sha512-oTD0ZT6ZSH7CZjM1i6hR/gFAXv9As7nxhN99XGliNxf6SChZcQRwGeaz1DjoYrWExWDVX00EH1Per3dMjhAWkQ=="
},
"@react-native/assets": {
"version": "1.0.0",
@@ -24930,9 +24935,9 @@
}
},
"@types/estree": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"version": "0.0.51",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
"@types/express": {
@@ -25746,9 +25751,9 @@
"dev": true
},
"@xmldom/xmldom": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA=="
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
"integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg=="
},
"@xobotyi/scrollbar-width": {
"version": "1.9.5",
@@ -27647,9 +27652,9 @@
}
},
"enhanced-resolve": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz",
"integrity": "sha512-weDYmzbBygL7HzGGS26M3hGQx68vehdEg6VUmqSOaFzXExFqlnKuSvsEJCVGQHScS8CQMbrAqftT+AzzHNt/YA==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@@ -30458,8 +30463,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
"integrity": "sha512-OfmtbNqL4FoUHK/RA4VxkYVMT8kbPPQA2au5hmVeadB+PVhqJqlHUgbsV+H4yGJjP9AEcY7UejXtVVkPSG4aHA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
"integrity": "sha512-axTNZ1eYEq4h1ihK7YSmNXknkXIRI+xdVTAf+jqzmRU92MVdjr+dA9aw9i7GRaQzCdFaJiY8QudCliNTWNbv1Q==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -32877,9 +32882,9 @@
}
},
"react-native-webrtc": {
"version": "106.0.7",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.7.tgz",
"integrity": "sha512-Ro/NoxN/9/b/bcZErM+oVbWC8ZCplp00sHWDOOh289KDkAkFYZdyFZAKsgJQoMj5rOJZVzIIxuJW91ox+0k92w==",
"version": "111.0.0",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-111.0.0.tgz",
"integrity": "sha512-eCvjPiU28cT85K8TBV7geAA92+uCldhmJ6F+3CMtAAG7U4AOQzxYOI5HC3h9Lt2zfTwsUzg3tyamqWf00PWndA==",
"requires": {
"adm-zip": "0.5.9",
"base64-js": "1.5.1",
@@ -35130,9 +35135,9 @@
"integrity": "sha512-5otny2JrfRNKIc+zi1YSOrNxXe47trEQbpY6g/MtHrFwLumKSJyAIobGXH1tlEBezE95eIsmDokBbUZtIZTvvA=="
},
"watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
@@ -35184,35 +35189,35 @@
}
},
"webpack": {
"version": "5.57.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
"integrity": "sha512-kHszukYjTPVfCOEyrUthA3jqJwduY/P3eO8I0gMNOZGIQWKAwZftxmp5hq6paophvwo9NoUrcZOecs9ulOyyTg==",
"version": "5.76.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.0",
"@types/estree": "^0.0.50",
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^0.0.51",
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.8.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.4",
"json-parse-better-errors": "^1.0.2",
"graceful-fs": "^4.2.9",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.2.0",
"webpack-sources": "^3.2.0"
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"dependencies": {
"schema-utils": {

View File

@@ -39,7 +39,7 @@
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/netinfo": "7.1.7",
"@react-native-community/slider": "4.1.12",
"@react-native-google-signin/google-signin": "7.0.4",
"@react-native-google-signin/google-signin": "9.0.2",
"@react-navigation/bottom-tabs": "6.5.3",
"@react-navigation/elements": "1.3.13",
"@react-navigation/material-top-tabs": "6.5.2",
@@ -54,7 +54,7 @@
"@types/w3c-web-hid": "1.0.3",
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.7.9",
"@xmldom/xmldom": "0.8.7",
"amplitude-js": "8.2.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
@@ -73,7 +73,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -112,7 +112,7 @@
"react-native-url-polyfill": "1.3.0",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-webrtc": "106.0.7",
"react-native-webrtc": "111.0.0",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
@@ -176,7 +176,7 @@
"ts-loader": "9.4.1",
"typescript": "4.7.4",
"unorm": "1.6.0",
"webpack": "5.57.1",
"webpack": "5.76.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.7.3"

View File

@@ -38,7 +38,7 @@ import { navigate }
import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
import { screen } from '../../../mobile/navigation/routes';
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
import Captions from '../../../subtitles/components/Captions.native';
import Captions from '../../../subtitles/components/native/Captions';
import { setToolboxVisible } from '../../../toolbox/actions';
import Toolbox from '../../../toolbox/components/native/Toolbox';
import { isToolboxVisible } from '../../../toolbox/functions';

View File

@@ -358,7 +358,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
type => {
return {
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
id: `${type}-tab`,
id: `${type}`,
controlsId: `${type}-panel`,
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
};
@@ -369,7 +369,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
accessibilityLabel = { t('dialog.sharingTabs') }
className = 'desktop-picker-tabs-container'
onChange = { this._onTabSelected }
selected = { `${this.state.selectedTab}-tab` }
selected = { `${this.state.selectedTab}` }
tabs = { tabs } />);
}

View File

@@ -35,7 +35,7 @@ export default createStyleSheet({
*/
signOutButton: {
alignSelf: 'center',
maxWidth: 104,
maxWidth: 120,
width: 'auto'
}
});

View File

@@ -14,7 +14,7 @@ import StageParticipantNameLabel from '../../display-name/components/web/StagePa
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
import SharedVideo from '../../shared-video/components/web/SharedVideo';
import Captions from '../../subtitles/components/Captions.web';
import Captions from '../../subtitles/components/web/Captions';
import { setTileView } from '../../video-layout/actions.web';
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
import { isWhiteboardEnabled } from '../../whiteboard/functions';

View File

@@ -37,6 +37,9 @@ import SecurityDialog
import SpeakerStats
// @ts-ignore
from '../../../../../speaker-stats/components/native/SpeakerStats';
import LanguageSelectorDialog
// @ts-ignore
from '../../../../../subtitles/components/native/LanguageSelectorDialog';
// @ts-ignore
import { screen } from '../../../routes';
import {
@@ -54,7 +57,8 @@ import {
securityScreenOptions,
settingsNavigationContainerScreenOptions,
sharedDocumentScreenOptions,
speakerStatsScreenOptions
speakerStatsScreenOptions,
subtitlesScreenOptions
// @ts-ignore
} from '../../../screenOptions';
// @ts-ignore
@@ -191,6 +195,13 @@ const ConferenceNavigationContainer = () => {
...carmodeScreenOptions,
title: t('carmode.labels.title')
}} />
<ConferenceStack.Screen
component = { LanguageSelectorDialog }
name = { screen.conference.subtitles }
options = {{
...subtitlesScreenOptions,
title: t('transcribing.subtitles')
}} />
</ConferenceStack.Navigator>
</NavigationContainer>
);

View File

@@ -30,7 +30,8 @@ export const screen = {
participants: 'Participants',
gifsMenu: 'GIPHY',
invite: 'Invite',
sharedDocument: 'Shared document'
sharedDocument: 'Shared document',
subtitles: 'Subtitles'
},
lobby: {
root: 'Lobby root',

View File

@@ -134,6 +134,11 @@ export const recordingScreenOptions = presentationScreenOptions;
*/
export const liveStreamScreenOptions = presentationScreenOptions;
/**
* Screen options for subtitles modal.
*/
export const subtitlesScreenOptions = presentationScreenOptions;
/**
* Screen options for lobby modal.
*/

View File

@@ -1,7 +1,7 @@
import { IStore } from '../app/types';
import { toggleDialog } from '../base/dialog/actions';
import LanguageSelectorDialogWeb from './components/LanguageSelectorDialog.web';
import LanguageSelectorDialog from './components/web/LanguageSelectorDialog';
export * from './actions.any';
@@ -14,6 +14,6 @@ export * from './actions.any';
*/
export function toggleLanguageSelectorDialog() {
return function(dispatch: IStore['dispatch']) {
dispatch(toggleDialog(LanguageSelectorDialogWeb));
dispatch(toggleDialog(LanguageSelectorDialog));
};
}

View File

@@ -1,11 +1,12 @@
import React, { Component } from 'react';
import React, { Component, ReactElement } from 'react';
import { IReduxState } from '../../app/types';
/**
* {@code AbstractCaptions} Properties.
*/
export type AbstractCaptionsProps = {
export interface IAbstractCaptionsProps {
/**
* Whether local participant is requesting to see subtitles.
@@ -17,23 +18,22 @@ export type AbstractCaptionsProps = {
* Mapped by id just to have the keys for convenience during the rendering
* process.
*/
_transcripts?: Map<string, string>;
};
_transcripts: Map<string, string>;
}
/**
* Abstract React {@code Component} which can display speech-to-text results
* from Jigasi as subtitles.
*/
export class AbstractCaptions<P extends AbstractCaptionsProps>
extends Component<P> {
export class AbstractCaptions<P extends IAbstractCaptionsProps> extends Component<P> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Element}
* @returns {ReactElement}
*/
render() {
render(): any {
const { _requestingSubtitles, _transcripts } = this.props;
if (!_requestingSubtitles || !_transcripts || !_transcripts.size) {
@@ -42,10 +42,12 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
const paragraphs = [];
// @ts-ignore
for (const [ id, text ] of _transcripts ?? []) {
paragraphs.push(this._renderParagraph(id, text));
}
// @ts-ignore
return this._renderSubtitlesContainer(paragraphs);
}
@@ -58,7 +60,7 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
* @param {string} _text - Subtitles text formatted with the participant's
* name.
* @protected
* @returns {React$Element} - The React element which displays the text.
* @returns {ReactElement} - The React element which displays the text.
*/
_renderParagraph(_id: string, _text: string) {
return <></>;
@@ -68,12 +70,12 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
* Renders the subtitles container.
*
* @abstract
* @param {Array<React$Element>} _el - An array of elements created
* @param {Array<ReactElement>} _el - An array of elements created
* for each subtitle using the {@link _renderParagraph} method.
* @protected
* @returns {React$Element} - The subtitles container.
* @returns {ReactElement} - The subtitles container.
*/
_renderSubtitlesContainer(_el: Array<React.ReactElement>) {
_renderSubtitlesContainer(_el: Array<ReactElement>) {
return <></>;
}
}
@@ -129,7 +131,7 @@ export function _abstractMapStateToProps(state: IReduxState) {
return {
_requestingSubtitles,
// avoid rerenders by setting to props new empty Map instances.
// avoid re-renders by setting to prop new empty Map instances.
_transcripts: transcripts.size === 0 ? undefined : transcripts
};
}

View File

@@ -1,9 +1,12 @@
/* eslint-disable lines-around-comment */
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { MEET_FEATURES } from '../../base/jwt/constants';
import { isLocalParticipantModerator } from '../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
// @ts-ignore
import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
export interface IAbstractProps extends AbstractButtonProps {

View File

@@ -0,0 +1,82 @@
import React, { ComponentType, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../app/types';
import {
TRANSLATION_LANGUAGES,
TRANSLATION_LANGUAGES_HEAD
} from '../../base/i18n/i18next';
import { setRequestingSubtitles, updateTranslationLanguage } from '../actions.any';
export interface IAbstractLanguageSelectorDialogProps {
dispatch: Function;
language: string;
listItems: Array<any>;
onLanguageSelected: (e: string) => void;
subtitles: string;
t: Function;
}
/**
* Higher Order Component taking in a concrete LanguageSelector component and
* augmenting it with state/behavior common to both web and native implementations.
*
* @param {React.Component} Component - The concrete component.
* @returns {React.Component}
*/
const AbstractLanguageSelectorDialog = (Component: ComponentType<IAbstractLanguageSelectorDialogProps>) => () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const off = 'transcribing.subtitlesOff';
const [ subtitles, setSubtiles ] = useState(off);
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
const transcription = useSelector((state: IReduxState) => state['features/base/config'].transcription);
const translationLanguagesHead = transcription?.translationLanguagesHead ?? TRANSLATION_LANGUAGES_HEAD;
const languagesHead = translationLanguagesHead?.map((lang: string) => `translation-languages:${lang}`);
// The off and the head languages are always on the top of the list. But once you are selecting
// a language from the translationLanguages, that language is moved under the fixedItems list,
// until a new languages is selected. FixedItems keep their positions.
const fixedItems = [ off, ...languagesHead ];
const translationLanguages = transcription?.translationLanguages ?? TRANSLATION_LANGUAGES;
const languages = translationLanguages
.map((lang: string) => `translation-languages:${lang}`)
.filter((lang: string) => !(lang === subtitles || languagesHead?.includes(lang)));
const listItems = (fixedItems?.includes(subtitles)
? [ ...fixedItems, ...languages ]
: [ ...fixedItems, subtitles, ...languages ])
.map((lang, index) => {
return {
id: lang + index,
lang,
selected: lang === subtitles
};
});
useEffect(() => {
language ? setSubtiles(language) : setSubtiles(off);
}, []);
const onLanguageSelected = useCallback((e: string) => {
setSubtiles(e);
dispatch(updateTranslationLanguage(e));
dispatch(setRequestingSubtitles(e !== off));
}, [ language ]);
return (
<Component
dispatch = { dispatch }
language = { language }
listItems = { listItems }
onLanguageSelected = { onLanguageSelected }
subtitles = { subtitles }
t = { t } />
);
};
export default AbstractLanguageSelectorDialog;

View File

@@ -1,45 +0,0 @@
// @flow
import { connect } from 'react-redux';
import { CLOSE_CAPTIONS_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { translate } from '../../base/i18n/functions';
import { IconSubtitles } from '../../base/icons/svg';
import {
AbstractClosedCaptionButton,
_abstractMapStateToProps
} from './AbstractClosedCaptionButton';
/**
* A button which starts/stops the transcriptions.
*/
class ClosedCaptionButton
extends AbstractClosedCaptionButton {
accessibilityLabel = 'toolbar.accessibilityLabel.cc';
icon = IconSubtitles;
label = 'transcribing.start';
toggledLabel = 'transcribing.stop';
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component
* instance.
* @private
* @returns {Props}
*/
export function mapStateToProps(state: Object, ownProps: Object) {
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
const abstractProps = _abstractMapStateToProps(state, ownProps);
return {
...abstractProps,
visible: abstractProps.visible && enabled
};
}
export default translate(connect(mapStateToProps)(ClosedCaptionButton));

View File

@@ -1,141 +0,0 @@
import i18next from 'i18next';
import React, { useCallback, useEffect, useState } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect, useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../app/types';
import { translate, translateToHTML } from '../../base/i18n/functions';
import { TRANSLATION_LANGUAGES, TRANSLATION_LANGUAGES_HEAD } from '../../base/i18n/i18next';
import Dialog from '../../base/ui/components/web/Dialog';
import { openSettingsDialog } from '../../settings/actions';
import { SETTINGS_TABS } from '../../settings/constants';
import { setRequestingSubtitles, toggleLanguageSelectorDialog, updateTranslationLanguage } from '../actions';
import LanguageList from './LanguageList.web';
interface ILanguageSelectorDialogProps extends WithTranslation {
_language: string;
_translationLanguages: Array<string>;
_translationLanguagesHead: Array<string>;
}
const useStyles = makeStyles()(theme => {
return {
paragraphWrapper: {
fontSize: 14,
margin: '10px 0px',
color: theme.palette.text01
},
spanWrapper: {
fontWeight: 700,
cursor: 'pointer',
color: theme.palette.link01,
'&:hover': {
backgroundColor: theme.palette.ui04,
color: theme.palette.link01Hover
}
}
};
});
/**
* Component that renders the subtitle language selector dialog.
*
* @returns {React$Element<any>}
*/
const LanguageSelectorDialog = ({
t,
_language,
_translationLanguages,
_translationLanguagesHead
}: ILanguageSelectorDialogProps) => {
const { classes: styles } = useStyles();
const dispatch = useDispatch();
const off = 'transcribing.subtitlesOff';
const [ language, setLanguage ] = useState(off);
const languagesHead = _translationLanguagesHead.map((lang: string) => `translation-languages:${lang}`);
// The off and the head languages are always on the top of the list. But once you are selecting
// a language from the translationLanguages, that language is moved under the fixedItems list,
// until a new languages is selected. FixedItems keep their positions.
const fixedItems = [ off, ...languagesHead ];
const languages = _translationLanguages
.map((lang: string) => `translation-languages:${lang}`)
.filter((lang: string) => !(lang === language || languagesHead.includes(lang)));
const listItems = (fixedItems.includes(language)
? [ ...fixedItems, ...languages ]
: [ ...fixedItems, language, ...languages ])
.map((lang, index) => {
return {
id: lang + index,
lang,
selected: lang === language
};
});
useEffect(() => {
_language ? setLanguage(_language) : setLanguage(off);
}, []);
const onLanguageSelected = useCallback((e: string) => {
setLanguage(e);
dispatch(updateTranslationLanguage(e));
dispatch(setRequestingSubtitles(e !== off));
dispatch(toggleLanguageSelectorDialog());
}, [ _language ]);
const onSourceLanguageClick = useCallback(() => {
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
ok = {{ hidden: true }}
titleKey = 'transcribing.subtitles'>
<p className = { styles.paragraphWrapper } >
{
translateToHTML(t, 'transcribing.sourceLanguageDesc', {
'sourceLanguage': t(`languages:${i18next.language}`).toLowerCase()
})
}<span
className = { styles.spanWrapper }
onClick = { onSourceLanguageClick }>{t('transcribing.sourceLanguageHere')}.</span>
</p>
<LanguageList
items = { listItems }
onLanguageSelected = { onLanguageSelected }
selectedLanguage = { language } />
</Dialog>
);
};
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code LanguageSelectorDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
const { _language } = state['features/subtitles'];
const { transcription } = state['features/base/config'];
const languages = transcription?.translationLanguages ?? TRANSLATION_LANGUAGES;
const languagesHead = transcription?.translationLanguagesHead ?? TRANSLATION_LANGUAGES_HEAD;
return {
_conference: conference,
_language,
_translationLanguages: languages,
_translationLanguagesHead: languagesHead
};
}
export default translate(connect(mapStateToProps)(LanguageSelectorDialog));

View File

@@ -1,32 +1,35 @@
// @flow
/* eslint-disable lines-around-comment */
import React from 'react';
import React, { ReactElement } from 'react';
import { GestureResponderEvent, StyleProp } from 'react-native';
import { connect } from 'react-redux';
import Container from '../../base/react/components/native/Container';
import Text from '../../base/react/components/native/Text';
// @ts-ignore
import Container from '../../../base/react/components/native/Container';
// @ts-ignore
import Text from '../../../base/react/components/native/Text';
import {
AbstractCaptions,
type AbstractCaptionsProps,
type IAbstractCaptionsProps,
_abstractMapStateToProps
} from './AbstractCaptions';
} from '../AbstractCaptions';
// @ts-ignore
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link Captions}.
*/
type Props = AbstractCaptionsProps & {
onPress: Function
};
interface IProps extends IAbstractCaptionsProps {
onPress: (event: GestureResponderEvent) => void;
}
/**
* React {@code Component} which can display speech-to-text results from
* Jigasi as subtitles.
*/
class Captions
extends AbstractCaptions<Props> {
class Captions extends AbstractCaptions<IProps> {
/**
* Renders the transcription text.
*
@@ -35,14 +38,14 @@ class Captions
* @param {string} text - Subtitles text formatted with the participant's
* name.
* @protected
* @returns {React$Element} - The React element which displays the text.
* @returns {ReactElement} - The React element which displays the text.
*/
_renderParagraph(id: string, text: string): React$Element<*> {
_renderParagraph(id: string, text: string): ReactElement {
return (
<Text
key = { id }
onPress = { this.props.onPress }
style = { styles.subtitle } >
style = { styles.captionsSubtitles as StyleProp<Object> } >
{ text }
</Text>
);
@@ -51,19 +54,19 @@ class Captions
/**
* Renders the subtitles container.
*
* @param {Array<React$Element>} paragraphs - An array of elements created
* @param {Array<ReactElement>} paragraphs - An array of elements created
* for each subtitle using the {@link _renderParagraph} method.
* @protected
* @returns {React$Element} - The subtitles container.
* @returns {ReactElement} - The subtitles container.
*/
_renderSubtitlesContainer(
paragraphs: Array<React$Element<*>>): React$Element<*> {
_renderSubtitlesContainer(paragraphs: Array<ReactElement>): ReactElement {
return (
<Container style = { styles.subtitlesContainer } >
<Container style = { styles.captionsSubtitlesContainer } >
{ paragraphs }
</Container>
);
}
}
// @ts-ignore
export default connect(_abstractMapStateToProps)(Captions);

View File

@@ -0,0 +1,65 @@
/* eslint-disable lines-around-comment */
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { CLOSE_CAPTIONS_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconSubtitles } from '../../../base/icons/svg';
import { navigate }
// @ts-ignore
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../../../mobile/navigation/routes';
import {
AbstractClosedCaptionButton,
IAbstractProps,
_abstractMapStateToProps
} from '../AbstractClosedCaptionButton';
/**
* A button which starts/stops the transcriptions.
*/
class ClosedCaptionButton
extends AbstractClosedCaptionButton {
accessibilityLabel = 'toolbar.accessibilityLabel.cc';
icon = IconSubtitles;
label = 'toolbar.startSubtitles';
labelProps = {
language: this.props.t(this.props._language),
languages: this.props.t(this.props.languages ?? ''),
languagesHead: this.props.t(this.props.languagesHead ?? '')
};
/**
* Toggle language selection dialog.
*
* @returns {void}
*/
_handleClickOpenLanguageSelector() {
navigate(screen.conference.subtitles);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component
* instance.
* @private
* @returns {Props}
*/
export function mapStateToProps(state: IReduxState, ownProps: IAbstractProps) {
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
const abstractProps = _abstractMapStateToProps(state, ownProps);
return {
...abstractProps,
visible: abstractProps.visible && enabled
};
}
export default translate(connect(mapStateToProps)(ClosedCaptionButton));

View File

@@ -0,0 +1,48 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { ScrollView } from 'react-native';
import LanguageListItem from './LanguageListItem';
// @ts-ignore
import styles from './styles';
interface ILanguageListProps {
items: Array<ILanguageItem>;
onLanguageSelected: (lang: string) => void;
selectedLanguage: string;
}
interface ILanguageItem {
id: string;
lang: string;
selected: boolean;
}
/**
* Component that renders the security options dialog.
*
* @returns {React$Element<any>}
*/
const LanguageList = ({ items, onLanguageSelected }: ILanguageListProps) => {
const listItems = items?.map(item => (
<LanguageListItem
key = { item.id }
// @ts-ignore
lang = { item.lang }
onLanguageSelected = { onLanguageSelected }
selected = { item.selected } />
));
return (
<ScrollView
bounces = { false }
style = { styles.itemsContainer }>
{ listItems }
</ScrollView>
);
};
export default LanguageList;

View File

@@ -0,0 +1,70 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { StyleProp, TouchableHighlight, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck } from '../../../base/icons/svg';
// @ts-ignore
import styles from './styles';
interface ILanguageListItemProps extends WithTranslation {
/**
* Language string.
*/
lang: string;
/**
* Callback for language selection.
*/
onLanguageSelected: (lang: string) => void;
/**
* If language item is selected or not.
*/
selected?: boolean;
}
/**
* Component that renders the language list item.
*
* @returns {React$Element<any>}
*/
const LanguageListItem = ({ t, lang, selected, onLanguageSelected
}: ILanguageListItemProps) => {
const onLanguageSelectedWrapper
= useCallback(() => onLanguageSelected(lang), [ lang ]);
return (
<View style = { styles.languageItemWrapper as StyleProp<ViewStyle> }>
<View style = { styles.iconWrapper }>
{
selected
&& <Icon
size = { 20 }
src = { IconCheck } />
}
</View>
<TouchableHighlight
onPress = { onLanguageSelectedWrapper }
underlayColor = { 'transparent' } >
<Text
style = { [
styles.languageItemText,
selected && styles.activeLanguageItemText ] }>
{ t(lang) }
</Text>
</TouchableHighlight>
</View>
);
};
export default translate(LanguageListItem);

View File

@@ -0,0 +1,45 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
// @ts-ignore
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { goBack }
// @ts-ignore
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import AbstractLanguageSelectorDialog, {
IAbstractLanguageSelectorDialogProps
} from '../AbstractLanguageSelectorDialog';
import LanguageList from './LanguageList';
// @ts-ignore
import styles from './styles';
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
const { language, listItems, onLanguageSelected, subtitles } = props;
const onSelected = useCallback((e: string) => {
onLanguageSelected(e);
goBack();
}, [ language ]);
return (
// @ts-ignore
<JitsiScreen
disableForcedKeyboardDismiss = { true }
style = { styles.subtitlesContainer }>
<LanguageList
items = { listItems }
onLanguageSelected = { onSelected }
selectedLanguage = { subtitles } />
</JitsiScreen>
);
};
/*
* We apply AbstractLanguageSelector to fill in the AbstractProps common
* to both the web and native implementations.
*/
// eslint-disable-next-line new-cap
export default AbstractLanguageSelectorDialog(LanguageSelectorDialog);

View File

@@ -0,0 +1,63 @@
import { BoxModel } from '../../../base/styles/components/styles/BoxModel';
import {
ColorPalette
} from '../../../base/styles/components/styles/ColorPalette';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The styles of the React {@code Component}s of the feature subtitles.
*/
export default {
languageItemWrapper: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row'
},
iconWrapper: {
width: 32
},
activeLanguageItemText: {
...BaseTheme.typography.bodyShortBoldLarge
},
languageItemText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginLeft: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
subtitlesContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1
},
/**
* Style for subtitle paragraph.
*/
captionsSubtitles: {
backgroundColor: ColorPalette.black,
borderRadius: BoxModel.margin / 4,
color: ColorPalette.white,
marginBottom: BoxModel.margin,
padding: BoxModel.padding / 2
},
/**
* Style for the subtitles container.
*/
captionsSubtitlesContainer: {
alignItems: 'center',
flexDirection: 'column',
flexGrow: 0,
justifyContent: 'flex-end',
margin: BoxModel.margin
},
itemsContainer: {
marginHorizontal: BaseTheme.spacing[4],
marginVertical: BaseTheme.spacing[4]
}
};

View File

@@ -1,33 +0,0 @@
// @flow
import { BoxModel } from '../../base/styles/components/styles/BoxModel';
import { ColorPalette } from '../../base/styles/components/styles/ColorPalette';
import { createStyleSheet } from '../../base/styles/functions.any';
/**
* The styles of the React {@code Component}s of the feature subtitles.
*/
export default createStyleSheet({
/**
* Style for subtitle paragraph.
*/
subtitle: {
backgroundColor: ColorPalette.black,
borderRadius: BoxModel.margin / 4,
color: ColorPalette.white,
marginBottom: BoxModel.margin,
padding: BoxModel.padding / 2
},
/**
* Style for the subtitles container.
*/
subtitlesContainer: {
alignItems: 'center',
flexDirection: 'column',
flexGrow: 0,
justifyContent: 'flex-end',
margin: BoxModel.margin
}
});

View File

@@ -1,23 +1,23 @@
import React from 'react';
import React, { ReactElement } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../../base/participants/functions';
import { getLargeVideoParticipant } from '../../large-video/functions';
import { isLayoutTileView } from '../../video-layout/functions.web';
import { IReduxState } from '../../../app/types';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getLargeVideoParticipant } from '../../../large-video/functions';
import { isLayoutTileView } from '../../../video-layout/functions.web';
import {
AbstractCaptions,
type AbstractCaptionsProps,
type IAbstractCaptionsProps,
_abstractMapStateToProps
} from './AbstractCaptions';
} from '../AbstractCaptions';
interface IProps extends AbstractCaptionsProps {
interface IProps extends IAbstractCaptionsProps {
/**
* Whether the subtitles container is lifted above the invite box.
*/
_isLifted: boolean;
_isLifted: boolean | undefined;
}
/**
@@ -34,9 +34,9 @@ class Captions extends AbstractCaptions<IProps> {
* @param {string} text - Subtitles text formatted with the participant's
* name.
* @protected
* @returns {React$Element} - The React element which displays the text.
* @returns {ReactElement} - The React element which displays the text.
*/
_renderParagraph(id: string, text: string) {
_renderParagraph(id: string, text: string): ReactElement {
return (
<p key = { id }>
<span>{ text }</span>
@@ -47,14 +47,15 @@ class Captions extends AbstractCaptions<IProps> {
/**
* Renders the subtitles container.
*
* @param {Array<React$Element>} paragraphs - An array of elements created
* @param {Array<ReactElement>} paragraphs - An array of elements created
* for each subtitle using the {@link _renderParagraph} method.
* @protected
* @returns {React$Element} - The subtitles container.
* @returns {ReactElement} - The subtitles container.
*/
_renderSubtitlesContainer(paragraphs: Array<React.ReactElement>) {
const className = this.props._isLifted ? 'transcription-subtitles lifted' : 'transcription-subtitles';
_renderSubtitlesContainer(paragraphs: Array<ReactElement>): ReactElement {
const className = this.props._isLifted
? 'transcription-subtitles lifted'
: 'transcription-subtitles';
return (
<div className = { className } >
@@ -83,4 +84,5 @@ function mapStateToProps(state: IReduxState) {
};
}
// @ts-ignore
export default connect(mapStateToProps)(Captions);

View File

@@ -1,13 +1,12 @@
import { connect } from 'react-redux';
import { translate } from '../../base/i18n/functions';
import { IconSubtitles } from '../../base/icons/svg';
import { toggleLanguageSelectorDialog } from '../actions.web';
import { translate } from '../../../base/i18n/functions';
import { IconSubtitles } from '../../../base/icons/svg';
import { toggleLanguageSelectorDialog } from '../../actions.web';
import {
AbstractClosedCaptionButton,
_abstractMapStateToProps
} from './AbstractClosedCaptionButton';
} from '../AbstractClosedCaptionButton';
/**
* A button which starts/stops the transcriptions.

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { makeStyles } from 'tss-react/mui';
import LanguageListItem from './LanguageListItem.web';
import LanguageListItem from './LanguageListItem';
interface ILanguageListProps {
items: Array<ILanguageItem>;
@@ -36,11 +36,13 @@ const LanguageList = ({
onLanguageSelected
}: ILanguageListProps) => {
const { classes: styles } = useStyles();
const listItems = items.map(item => (<LanguageListItem
key = { item.id }
lang = { item.lang }
onLanguageSelected = { onLanguageSelected }
selected = { item.selected } />));
const listItems = items.map(item => (
<LanguageListItem
key = { item.id }
lang = { item.lang }
onLanguageSelected = { onLanguageSelected }
selected = { item.selected } />
));
return (
<div className = { styles.itemsContainer }>{listItems}</div>

View File

@@ -2,24 +2,24 @@ import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { translate } from '../../base/i18n/functions';
import Icon from '../../base/icons/components/Icon';
import { IconCheck } from '../../base/icons/svg';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck } from '../../../base/icons/svg';
interface ILanguageListItemProps extends WithTranslation {
/**
* Whether or not the button should be full width.
* Language string.
*/
lang: string;
/**
* Click callback.
* Callback for language selection.
*/
onLanguageSelected: (lang: string) => void;
/**
* The id of the button.
* If language item is selected or not.
*/
selected?: boolean;
}

View File

@@ -0,0 +1,78 @@
import i18next from 'i18next';
import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui';
import { translate, translateToHTML } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import { openSettingsDialog } from '../../../settings/actions.web';
import { SETTINGS_TABS } from '../../../settings/constants';
import { toggleLanguageSelectorDialog } from '../../actions.web';
import AbstractLanguageSelectorDialog, {
IAbstractLanguageSelectorDialogProps
} from '../AbstractLanguageSelectorDialog';
import LanguageList from './LanguageList';
const useStyles = makeStyles()(theme => {
return {
paragraphWrapper: {
fontSize: 14,
margin: '10px 0px',
color: theme.palette.text01
},
spanWrapper: {
fontWeight: 700,
cursor: 'pointer',
color: theme.palette.link01,
'&:hover': {
backgroundColor: theme.palette.ui04,
color: theme.palette.link01Hover
}
}
};
});
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
const { dispatch, language, listItems, onLanguageSelected, subtitles, t } = props;
const { classes: styles } = useStyles();
const onSelected = useCallback((e: string) => {
onLanguageSelected(e);
dispatch(toggleLanguageSelectorDialog());
}, [ language ]);
const onSourceLanguageClick = useCallback(() => {
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
ok = {{ hidden: true }}
titleKey = 'transcribing.subtitles'>
<p className = { styles.paragraphWrapper } >
{
translateToHTML(t, 'transcribing.sourceLanguageDesc', {
'sourceLanguage': t(`languages:${i18next.language}`).toLowerCase()
})
}<span
className = { styles.spanWrapper }
onClick = { onSourceLanguageClick }>{t('transcribing.sourceLanguageHere')}.</span>
</p>
<LanguageList
items = { listItems }
onLanguageSelected = { onSelected }
selectedLanguage = { subtitles } />
</Dialog>
);
};
/*
* We apply AbstractLanguageSelector to fill in the AbstractProps common
* to both the web and native implementations.
*/
// eslint-disable-next-line new-cap
export default translate(AbstractLanguageSelectorDialog(LanguageSelectorDialog));

View File

@@ -11,7 +11,7 @@ import {
import {
removeTranscriptMessage,
updateTranscriptMessage
} from './actions';
} from './actions.any';
import logger from './logger';
/**

View File

@@ -24,7 +24,7 @@ interface ITranscriptMessage {
export interface ISubtitlesState {
_language: string;
_requestingSubtitles: boolean;
_transcriptMessages: Map<string, ITranscriptMessage>;
_transcriptMessages: Map<string, ITranscriptMessage> | any;
}
/**

View File

@@ -18,7 +18,7 @@ import SecurityDialogButton
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/ClosedCaptionButton.native';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import TileViewButton from '../../../video-layout/components/TileViewButton';
import styles from '../../../video-menu/components/native/styles';
import { getMovableButtons } from '../../functions.native';

View File

@@ -29,17 +29,17 @@ import {
hasRaisedHand,
isLocalParticipantModerator
} from '../../../base/participants/functions';
import { getLocalVideoTrack } from '../../../base/tracks/functions';
import { getLocalVideoTrack } from '../../../base/tracks/functions.web';
import { ITrack } from '../../../base/tracks/types';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { toggleChat } from '../../../chat/actions.web';
import ChatButton from '../../../chat/components/web/ChatButton';
import EmbedMeetingButton from '../../../embed-meeting/components/EmbedMeetingButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton';
import FeedbackButton from '../../../feedback/components/FeedbackButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.web';
import FeedbackButton from '../../../feedback/components/FeedbackButton.web';
import { setGifMenuVisibility } from '../../../gifs/actions';
import { isGifEnabled } from '../../../gifs/functions';
import { isGifEnabled } from '../../../gifs/functions.web';
import InviteButton from '../../../invite/components/add-people-dialog/web/InviteButton';
import { isVpaasMeeting } from '../../../jaas/functions';
import KeyboardShortcutsButton from '../../../keyboard-shortcuts/components/web/KeyboardShortcutsButton';
@@ -50,11 +50,13 @@ import {
} from '../../../participants-pane/actions.web';
import ParticipantsPaneButton from '../../../participants-pane/components/web/ParticipantsPaneButton';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { addReactionToBuffer } from '../../../reactions/actions.any';
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
import {
addReactionToBuffer,
toggleReactionsMenuVisibility
} from '../../../reactions/actions.web';
import ReactionsMenuButton from '../../../reactions/components/web/ReactionsMenuButton';
import { REACTIONS } from '../../../reactions/constants';
import { isReactionsEnabled } from '../../../reactions/functions.any';
import { isReactionsEnabled } from '../../../reactions/functions.web';
import LiveStreamButton from '../../../recording/components/LiveStream/web/LiveStreamButton';
import RecordButton from '../../../recording/components/Recording/web/RecordButton';
import { isSalesforceEnabled } from '../../../salesforce/functions';
@@ -71,12 +73,12 @@ import SettingsButton from '../../../settings/components/web/SettingsButton';
import SharedVideoButton from '../../../shared-video/components/web/SharedVideoButton';
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
import SpeakerStatsButton from '../../../speaker-stats/components/web/SpeakerStatsButton';
import ClosedCaptionButton from '../../../subtitles/components/ClosedCaptionButton';
import ClosedCaptionButton from '../../../subtitles/components/web/ClosedCaptionButton';
import { toggleTileView } from '../../../video-layout/actions.web';
import TileViewButton from '../../../video-layout/components/TileViewButton';
import { shouldDisplayTileView } from '../../../video-layout/functions.web';
import VideoQualityButton from '../../../video-quality/components/VideoQualityButton';
import VideoQualityDialog from '../../../video-quality/components/VideoQualityDialog';
import VideoQualityButton from '../../../video-quality/components/VideoQualityButton.web';
import VideoQualityDialog from '../../../video-quality/components/VideoQualityDialog.web';
import VideoBackgroundButton from '../../../virtual-background/components/VideoBackgroundButton';
import { iAmVisitor } from '../../../visitors/functions';
import WhiteboardButton from '../../../whiteboard/components/web/WhiteboardButton';
@@ -87,10 +89,13 @@ import {
setOverflowMenuVisible,
setToolbarHovered,
showToolbox
} from '../../actions';
} from '../../actions.web';
import { NOTIFY_CLICK_MODE, NOT_APPLICABLE, THRESHOLDS } from '../../constants';
import { isDesktopShareButtonDisabled, isToolboxVisible } from '../../functions';
import { getJwtDisabledButtons } from '../../functions.any';
import {
getJwtDisabledButtons,
isDesktopShareButtonDisabled,
isToolboxVisible
} from '../../functions.web';
import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';

View File

@@ -0,0 +1,85 @@
-- Can be used to ban users based on external http service
-- Copyright (C) 2023-present 8x8, Inc.
local ACCESS_MANAGER_URL = module:get_option_string("muc_prosody_jitsi_access_manager_url");
if not ACCESS_MANAGER_URL then
module:log("warn", "No 'muc_prosody_jitsi_access_manager_url' option set, disabling module");
return
end
local json = require "cjson.safe";
local http = require "net.http";
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
-- we will cache banned tokens to avoid extra requests
-- on destroying session, websocket retries 2 more times before giving up
local cache = require "util.cache".new(100);
local CACHE_DURATION = 5*60; -- 5 mins
local cache_timer = module:add_timer(CACHE_DURATION, function()
for k, v in cache:items() do
if socket.gettime() > v + CACHE_DURATION then
cache:set(k, nil);
end
end
if cache:count() > 0 then
-- rescheduling the timer
return CACHE_DURATION;
end
-- skipping return value stops the timer
end);
local function shouldAllow(session)
local token = session.auth_token;
if token ~= nil then
module:log("debug", "Checking whether user should be banned ")
-- cached tokens are banned
if cache:get(token) then
return false;
end
-- TODO: do this only for enabled customers
ban_check_count();
local function cb(content, code, response, request)
if code == 200 then
local r = json.decode(content)
if r['access'] ~= nil and r['access'] == false then
module:log("debug", "user is banned")
ban_check_users_banned_count();
session:close();
-- if the cache is empty and the timer is not running reschedule it
if cache:count() == 0 then
cache_timer:reschedule(CACHE_DURATION);
end
cache:set(token, socket.gettime());
end
end
end
local request_headers = {}
request_headers['Authorization'] = 'Bearer ' .. token;
http.request(ACCESS_MANAGER_URL, {
headers = request_headers,
method = "GET",
}, cb);
return true;
end
end
prosody.events.add_handler("jitsi-access-ban-check", function(session)
return shouldAllow(session)
end)

View File

@@ -0,0 +1,113 @@
-- A global module which can be used as http endpoint to end meetings. The provided token
--- in the request is verified whether it has the right to do so.
-- Copyright (C) 2023-present 8x8, Inc.
module:set_global();
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local get_room_from_jid = util.get_room_from_jid;
local starts_with = util.starts_with;
local neturl = require "net.url";
local parse = neturl.parseQuery;
-- will be initialized once the main virtual host module is initialized
local token_util;
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
local event_count = module:measure("muc_end_meeting_rate", "rate")
local event_count_success = module:measure("muc_end_meeting_success", "rate")
function verify_token(token)
if token == nil then
module:log("warn", "no token provided");
return false;
end
local session = {};
session.auth_token = token;
local verified, reason, msg = token_util:process_and_verify_token(session);
if not verified then
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
return false;
end
return true;
end
function handle_terminate_meeting (event)
module:log("info", "Request for terminate meeting received: reqid %s", event.request.headers["request_id"])
event_count()
if not event.request.url.query then
return { status_code = 400 };
end
local params = parse(event.request.url.query);
local conference = params["conference"];
local room_jid;
if conference then
room_jid = room_jid_match_rewrite(conference)
else
module:log('warn', "conference param was not provided")
return { status_code = 400 };
end
-- verify access
local token = event.request.headers["authorization"]
if not token then
module:log("error", "Authorization header was not provided for conference %s", conference)
return { status_code = 401 };
end
if starts_with(token, 'Bearer ') then
token = token:sub(8, #token)
else
module:log("error", "Authorization header is invalid")
return { status_code = 401 };
end
if not verify_token(token, room_jid) then
return { status_code = 401 };
end
local room = get_room_from_jid(room_jid);
if not room then
module:log("warn", "Room not found")
return { status_code = 404 };
else
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
end
event_count_success()
return { status_code = 200 };
end
-- module API called on virtual host added, passing the host module
function module.add_host(host_module)
if host_module.host == muc_domain_base then
-- the main virtual host
module:log("info", "Initialize token_util using %s", host_module.host)
token_util = module:require "token/util".new(host_module);
if asapKeyServer then
-- init token util with our asap keyserver
token_util:set_asap_key_server(asapKeyServer)
end
module:log("info", "Adding http handler for /end-meeting on %s", host_module.host);
host_module:depends("http");
host_module:provides("http", {
default_path = "/";
route = {
["POST end-meeting"] = function(event)
return async_handler_wrapper(event, handle_terminate_meeting)
end;
};
});
end
end

View File

@@ -0,0 +1,27 @@
-- Restricts access to a muc component to certain domains
-- Copyright (C) 2023-present 8x8, Inc.
-- a list of (authenticated)domains that can access rooms(send presence)
local whitelist = module:get_option_set("muc_filter_whitelist");
if not whitelist then
module:log("warn", "No 'muc_filter_whitelist' option set, disabling muc_filter_access, plugin inactive");
return
end
local jid_split = require "util.jid".split;
local function incoming_presence_filter(event)
local stanza = event.stanza;
local _, domain, _ = jid_split(stanza.attr.from);
if not stanza.attr.from or not whitelist:contains(domain) then
-- Filter presence
module:log("error", "Filtering unauthorized presence: %s", stanza:top_tag());
return true;
end
end
for _, jid_type in ipairs({ "host", "bare", "full" }) do
module:hook("presence/"..jid_type, incoming_presence_filter, 2000);
end

View File

@@ -0,0 +1,218 @@
-- Allows flipping device. When a presence contains flip_device tag
-- and the used jwt matches the id(session.jitsi_meet_context_user.id) of another user this is indication that the user
-- is moving from one device to another. The flip feature should be present and enabled in the token features.
-- Copyright (C) 2023-present 8x8, Inc.
local oss_util = module:require "util";
local is_healthcheck_room = oss_util.is_healthcheck_room;
local um_is_admin = require "core.usermanager".is_admin;
local inspect = require('inspect');
local jid_bare = require "util.jid".bare;
local jid = require "util.jid";
local MUC_NS = "http://jabber.org/protocol/muc";
local lobby_host;
local lobby_muc_service;
local lobby_muc_component_config = 'lobby.' .. module:get_option_string("muc_mapper_domain_base");
if lobby_muc_component_config == nil then
module:log('error', 'lobby not enabled missing lobby_muc config');
return ;
end
local function is_admin(occupant_jid)
return um_is_admin(occupant_jid, module.host);
end
local function remove_flip_tag(stanza)
stanza:maptags(function(tag)
if tag and tag.name == "flip_device" then
module:log("debug", "Removing %s tag from presence stanza!", tag.name);
return nil;
else
return tag;
end
end)
end
-- Make user that switch devices bypass lobby or password.
-- A user is considered to join from another device if the
-- id from jwt is the same as another occupant and the presence
-- stanza has flip_device tag
module:hook("muc-occupant-pre-join", function(event)
local room, occupant = event.room, event.occupant;
local session = event.origin;
local stanza = event.stanza;
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
return ;
end
local flip_device_tag = stanza:get_child("flip_device");
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
local participants = room._data.participants_details or {};
local id = session.jitsi_meet_context_user.id;
local first_device_occ_nick = participants[id];
if flip_device_tag then
if first_device_occ_nick and session.jitsi_meet_context_features.flip and (session.jitsi_meet_context_features.flip == true or session.jitsi_meet_context_features.flip == "true") then
room._data.kicked_participant_nick = first_device_occ_nick;
room._data.flip_participant_nick = occupant.nick;
-- allow participant from flip device to bypass Lobby
local occupant_jid = stanza.attr.from;
local affiliation = room:get_affiliation(occupant_jid);
if not affiliation or affiliation == 0 then
module:log("debug", "Bypass lobby invitee %s", occupant_jid)
occupant.role = "participant";
room:set_affiliation(true, jid_bare(occupant_jid), "member")
room:save();
end
-- bypass password on the flip device
local join = stanza:get_child("x", MUC_NS);
if not join then
join = stanza:tag("x", { xmlns = MUC_NS });
end
local password = join:get_child("password", MUC_NS);
if password then
join:maptags(
function(tag)
for k, v in pairs(tag) do
if k == "name" and v == "password" then
return nil
end
end
return tag
end);
end
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
elseif not session.jitsi_meet_context_features.flip or session.jitsi_meet_context_features.flip == false or session.jitsi_meet_context_features.flip == "false" then
module:log("warn", "Flip device tag present without jwt permission")
--remove flip_device tag if somebody wants to abuse this feature
remove_flip_tag(stanza)
else
module:log("warn", "Flip device tag present without user from different device")
--remove flip_device tag if somebody wants to abuse this feature
remove_flip_tag(stanza)
end
end
-- update authenticated participant list
participants[id] = occupant.nick;
room._data.participants_details = participants
module:log("debug", "current details list %s", inspect(participants))
else
if flip_device_tag then
module:log("warn", "Flip device tag present for a guest user")
-- remove flip_device tag because a guest want to do a sneaky join
remove_flip_tag(stanza)
end
end
end)
-- Kick participant from the the first device from the main room and lobby if applies
-- and transfer role from the previous participant, this will take care of the grant
-- moderation case
module:hook("muc-occupant-joined", function(event)
local room, occupant = event.room, event.occupant;
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
return ;
end
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
-- make joining participant from flip device have the same role and affiliation as for the previous device
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
if initial_affiliation == "owner" then
event.occupant.role = "moderator";
elseif initial_affiliation == "member" then
event.occupant.role = "participant";
end
-- Kick participant from the first device from the main room
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
room:set_role(true, room._data.kicked_participant_nick, 'none')
room:save()
-- Kick participant from the first device from the lobby room
if room._data.lobbyroom then
local lobby_room_jid = room._data.lobbyroom;
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid)
for _, occupant in lobby_room:each_occupant() do
local node = jid.split(occupant.jid);
if kicked_participant_node_jid == node then
module:log("info", "Kick participant from lobby %s", occupant.jid)
lobby_room:set_role(true, occupant.nick, 'none')
end
end
end
event.room._data.flip_participant_nick = nil
event.room._data.kicked_participant_nick = nil;
end
end,-2)
-- Update the local table after a participant leaves
module:hook("muc-occupant-left", function(event)
local occupant = event.occupant;
local session = event.origin;
if is_healthcheck_room(event.room.jid) or is_admin(occupant.bare_jid) then
return ;
end
if session and session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
local id = session.jitsi_meet_context_user.id
local participants = event.room._data.participants_details or {};
local occupant_left_nick = participants[id]
if occupant_left_nick == occupant.nick then
participants[id] = nil
event.room._data.participants_details = participants
end
end
end)
-- Add a flip_device tag on the unavailable presence from the kicked participant in order to silent the notifications
module:hook('muc-broadcast-presence', function(event)
local kicked_participant_nick = event.room._data.kicked_participant_nick
local stanza = event.stanza;
if kicked_participant_nick and stanza.attr.from == kicked_participant_nick and stanza.attr.type == 'unavailable' then
module:log("debug", "Add flip_device tag for presence unavailable from occupant nick %s", kicked_participant_nick)
stanza:tag("flip_device"):up();
end
end)
function process_lobby_muc_loaded(lobby_muc, host_module)
module:log('info', 'Lobby muc loaded');
lobby_muc_service = lobby_muc;
lobby_host = module:context(host_module);
end
-- process a host module directly if loaded or hooks to wait for its load
function process_host_module(name, callback)
local function process_host(host)
if host == name then
callback(module:context(host), host);
end
end
if prosody.hosts[name] == nil then
module:log('debug', 'No host/component found, will wait for it: %s', name)
-- when a host or component is added
prosody.events.add_handler('host-activated', process_host);
else
process_host(name);
end
end
-- process or waits to process the lobby muc component
process_host_module(lobby_muc_component_config, function(host_module, host)
-- lobby muc component created
module:log('info', 'Lobby component loaded %s', host);
local muc_module = prosody.hosts[host].modules.muc;
if muc_module then
process_lobby_muc_loaded(muc_module, host_module);
else
module:log('debug', 'Will wait for muc to be available');
prosody.hosts[host].events.add_handler('module-loaded', function(event)
if (event.module == 'muc') then
process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
end
end);
end
end);

View File

@@ -0,0 +1,6 @@
-- This module makes all MUCs in Prosody unavailable on disco#items query
-- Copyright (C) 2023-present 8x8, Inc.
module:hook("muc-room-pre-create", function(event)
event.room:set_hidden(true);
end, -1);

View File

@@ -0,0 +1,203 @@
-- A http endpoint to invite jigasi to a meeting via http endpoint
-- jwt is used to validate access
-- Copyright (C) 2023-present 8x8, Inc.
local jid_split = require "util.jid".split;
local hashes = require "util.hashes";
local random = require "util.random";
local st = require("util.stanza");
local json = require "util.json";
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
-- This module chooses jigasi from the brewery room, so it needs information for the configured brewery
local muc_domain = module:get_option_string("muc_internal_domain_base", 'internal.auth.' .. muc_domain_base);
local jigasi_brewery_room_jid = module:get_option_string("muc_jigasi_brewery_jid", 'jigasibrewery@' .. muc_domain);
local jigasi_bare_jid = module:get_option_string("muc_jigasi_jid", "jigasi@auth." .. muc_domain_base);
local focus_jid = module:get_option_string("muc_jicofo_brewery_jid", jigasi_brewery_room_jid .. "/focus");
local main_muc_service;
local JSON_CONTENT_TYPE = "application/json";
local event_count = module:measure("muc_invite_jigasi_rate", "rate")
local event_count_success = module:measure("muc_invite_jigasi_success", "rate")
local ASAP_KEY_SERVER = module:get_option_string("prosody_password_public_key_repo_url", "");
local token_util = module:require "token/util".new(module);
if ASAP_KEY_SERVER then
-- init token util with our asap keyserver
token_util:set_asap_key_server(ASAP_KEY_SERVER)
end
local function invite_jigasi(conference, phone_no)
local jigasi_brewery_room = main_muc_service.get_room_from_jid(jigasi_brewery_room_jid);
if not jigasi_brewery_room then
module:log("error", "Jigasi brewery room not found")
return 404, 'Brewery room was not found'
end
module:log("info", "Invite jigasi from %s to join conference %s and outbound phone_no %s", jigasi_brewery_room.jid, conference, phone_no)
--select least stressed Jigasi
local least_stressed_value = math.huge;
local least_stressed_jigasi_jid;
for occupant_jid, occupant in jigasi_brewery_room:each_occupant() do
local _, _, resource = jid_split(occupant_jid);
if resource ~= 'focus' then
local occ = occupant:get_presence();
local stats_child = occ:get_child("stats", "http://jitsi.org/protocol/colibri")
local is_sip_jigasi = true;
for stats_tag in stats_child:children() do
if stats_tag.attr.name == 'supports_sip' and stats_tag.attr.value == 'false' then
is_sip_jigasi = false;
end
end
if is_sip_jigasi then
for stats_tag in stats_child:children() do
if stats_tag.attr.name == 'stress_level' then
local stress_level = tonumber(stats_tag.attr.value);
module:log("debug", "Stressed level %s %s ", stress_level, occupant_jid)
if stress_level < least_stressed_value then
least_stressed_jigasi_jid = occupant_jid
least_stressed_value = stress_level
end
end
end
end
end
end
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_jid, least_stressed_value)
if not least_stressed_jigasi_jid then
module:log("error", "Cannot invite jigasi from room %s", jigasi_brewery_room.jid)
return 404, 'Jigasi not found'
end
-- invite Jigasi to join the conference
local _, _, jigasi_res = jid_split(least_stressed_jigasi_jid)
local jigasi_full_jid = jigasi_bare_jid .. "/" .. jigasi_res;
local stanza_id = hashes.sha256(random.bytes(8), true);
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = jigasi_full_jid, from = focus_jid, id = stanza_id })
:tag("dial", { xmlns = "urn:xmpp:rayo:1", from = "fromnumber", to = phone_no })
:tag("header", { xmlns = "urn:xmpp:rayo:1", name = "JvbRoomName", value = conference })
module:log("debug", "Invite jigasi stanza %s", invite_jigasi_stanza)
jigasi_brewery_room:route_stanza(invite_jigasi_stanza);
return 200
end
local function is_token_valid(token)
if token == nil then
module:log("warn", "no token provided");
return false;
end
local session = {};
session.auth_token = token;
local verified, reason, msg = token_util:process_and_verify_token(session);
if not verified then
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
return false;
end
return true;
end
local function handle_jigasi_invite(event)
module:log("debug", "Request for invite jigasi received: reqId %s", event.request.headers["request_id"])
event_count()
local request = event.request;
-- verify access
local token = event.request.headers["authorization"]
if not token then
module:log("error", "Authorization header was not provided for conference %s", conference)
return { status_code = 401 };
end
if util.starts_with(token, 'Bearer ') then
token = token:sub(8, #token)
else
module:log("error", "Authorization header is invalid")
return { status_code = 401 };
end
if not is_token_valid(token) then
return { status_code = 401 };
end
-- verify payload
if request.headers.content_type ~= JSON_CONTENT_TYPE
or (not request.body or #request.body == 0) then
module:log("warn", "Wrong content type: %s or missing payload", request.headers.content_type);
return { status_code = 400; }
end
local payload = json.decode(request.body);
local conference = payload["conference"];
local phone_no = payload["phoneNo"];
if not conference then
module:log("warn", "Missing conference param")
return { status_code = 400; }
end
if not phone_no then
module:log("warn", "Missing phone no param")
return { status_code = 400; }
end
--invite jigasi
local status_code, error_msg = invite_jigasi(conference, phone_no)
if not error_msg then
event_count_success()
return { status_code = 200 }
else
return { status_code = status_code, body = json.encode({ error = error_msg }) }
end
end
module:log("info", "Adding http handler for /invite-jigasi on %s", module.host);
module:depends("http");
module:provides("http", {
default_path = "/";
route = {
["POST invite-jigasi"] = function(event)
return async_handler_wrapper(event, handle_jigasi_invite)
end;
};
});
-- process a host module directly if loaded or hooks to wait for its load
function process_host_module(name, callback)
local function process_host(host)
if host == name then
callback(module:context(host), host);
end
end
if prosody.hosts[name] == nil then
module:log('info', 'No host/component found, will wait for it: %s', name)
-- when a host or component is added
prosody.events.add_handler('host-activated', process_host);
else
process_host(name);
end
end
process_host_module(muc_domain, function(_, host)
local muc_module = prosody.hosts[host].modules.muc;
if muc_module then
main_muc_service = muc_module;
module:log('info', 'Found main_muc_service: %s', main_muc_service);
else
module:log('info', 'Will wait for muc to be available');
prosody.hosts[host].events.add_handler('module-loaded', function(event)
if (event.module == 'muc') then
main_muc_service = prosody.hosts[host].modules.muc;
module:log('info', 'Found(on loaded) main_muc_service: %s', main_muc_service);
end
end);
end
end);

View File

@@ -0,0 +1,158 @@
-- http endpoint to kick participants, access is based on provided jwt token
-- the correct jigasi we fined based on the display name and the number provided
-- Copyright (C) 2023-present 8x8, Inc.
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local starts_with = util.starts_with;
local formdecode = require "util.http".formdecode;
local urlencode = require "util.http".urlencode;
local jid = require "util.jid";
local json = require "util.json";
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
if not muc_domain_base then
module:log("warn", "No 'muc_domain_base' option set, disabling kick check endpoint.");
return ;
end
local json_content_type = "application/json";
local token_util = module:require "token/util".new(module);
local asapKeyServer = module:get_option_string('prosody_password_public_key_repo_url', '');
if asapKeyServer == '' then
module:log('warn', 'No "prosody_password_public_key_repo_url" option set, disabling kick endpoint.');
return ;
end
token_util:set_asap_key_server(asapKeyServer);
--- Verifies the token
-- @param token the token we received
-- @param room_address the full room address jid
-- @return true if values are ok or false otherwise
function verify_token(token, room_address)
if token == nil then
module:log("warn", "no token provided for %s", room_address);
return false;
end
local session = {};
session.auth_token = token;
local verified, reason, msg = token_util:process_and_verify_token(session);
if not verified then
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
return false;
end
return true;
end
-- Validates the request by checking for required url param room and
-- validates the token provided with the request
-- @param request - The request to validate.
-- @return [error_code, room]
local function validate_and_get_room(request)
if not request.url.query then
module:log("warn", "No query");
return 400, nil;
end
local params = formdecode(request.url.query);
local room_name = urlencode(params.room) or "";
local subdomain = urlencode(params.prefix) or "";
if not room_name then
module:log("warn", "Missing room param for %s", room_name);
return 400, nil;
end
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
if subdomain and subdomain ~= "" then
room_address = "["..subdomain.."]"..room_address;
end
-- verify access
local token = request.headers["authorization"]
if token and starts_with(token,'Bearer ') then
token = token:sub(8,#token)
end
if not verify_token(token, room_address) then
return 403, nil;
end
local room = get_room_from_jid(room_address);
if not room then
module:log("warn", "No room found for %s", room_address);
return 404, nil;
else
return 200, room;
end
end
function handle_kick_participant (event)
local request = event.request;
if request.headers.content_type ~= json_content_type
or (not request.body or #request.body == 0) then
module:log("warn", "Wrong content type: %s", request.headers.content_type);
return { status_code = 400; }
end
local params = json.decode(request.body);
if not params then
module:log("warn", "Missing params");
return { status_code = 400; }
end
local number = params["number"];
if not number then
module:log("warn", "Missing number param");
return { status_code = 400; };
end
local error_code, room = validate_and_get_room(request);
if error_code and error_code ~= 200 then
module:log("error", "Error validating %s", error_code);
return { error_code = 400; }
end
if not room then
return { status_code = 404; }
end
for _, occupant in room:each_occupant() do
local pr = occupant:get_presence();
local displayName = pr:get_child_text(
'nick', 'http://jabber.org/protocol/nick');
local initiator = pr:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
if initiator and displayName and starts_with(displayName, number) then
room:set_role(true, occupant.nick, nil);
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
return { status_code = 200; }
end
end
-- not found participant to kick
return { status_code = 404; };
end
module:log("info","Adding http handler for /kick-participant on %s", module.host);
module:depends("http");
module:provides("http", {
default_path = "/";
route = {
["PUT kick-participant"] = function (event) return async_handler_wrapper(event, handle_kick_participant) end;
};
});

View File

@@ -0,0 +1,214 @@
-- Rate limits connection based on their ip address.
-- Rate limits creating sessions (new connections),
-- rate limits sent stanzas from same ip address (presence, iq, messages)
-- Copyright (C) 2023-present 8x8, Inc.
local cache = require"util.cache";
local ceil = math.ceil;
local http_server = require "net.http.server";
local gettime = require "util.time".now
local filters = require "util.filters";
local new_throttle = require "util.throttle".create;
local timer = require "util.timer";
local ip_util = require "util.ip";
local new_ip = ip_util.new_ip;
local match_ip = ip_util.match;
local parse_cidr = ip_util.parse_cidr;
local config = {};
local limits_resolution = 1;
local function load_config()
-- Max allowed login rate in events per second.
config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
config.session_rate = module:get_option_number("rate_limit_session_rate", 2000);
-- The time in seconds, after which the limit for an IP address is lifted.
config.timeout = module:get_option_number("rate_limit_timeout", 60);
-- List of regular expressions for IP addresses that are not limited by this module.
config.whitelist = module:get_option_set("rate_limit_whitelist", { "127.0.0.1", "::1" })._items;
-- The size of the cache that saves state for IP addresses
config.cache_size = module:get_option_number("rate_limit_cache_size", 10000);
-- Max allowed presence rate in events per second.
config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
-- Max allowed iq rate in events per second.
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 10);
-- Max allowed message rate in events per second.
config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
-- A list of jids for which sessions we ignore rate limiting
config.whitelist_jids = module:get_option_set("rate_limit_whitelist_jids", {});
local wl = "";
for ip in config.whitelist do wl = wl .. ip .. "," end
local wl_jids = "";
for j in config.whitelist_jids do wl_jids = wl_jids .. j .. "," end
module:log("info", "Loaded configuration: ");
module:log("info", "- session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_jids=%s",
config.session_rate, config.timeout, config.cache_size, wl, wl_jids);
module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
end
load_config();
-- Maps an IP address to a util.throttle which keeps the rate of login/join events from that IP.
local login_rates = cache.new(config.cache_size);
-- Keeps the IP addresses that have exceeded the allowed login/join rate (i.e. the IP addresses whose sessions need
-- to be limited). Mapped to the last instant at which the rate was exceeded.
local limited_ips = cache.new(config.cache_size);
local function is_whitelisted(ip)
local parsed_ip = new_ip(ip)
for entry in config.whitelist do
if match_ip(parsed_ip, parse_cidr(entry)) then
return true;
end
end
return false;
end
local function is_whitelisted_jid(jid)
return config.whitelist_jids:contains(jid);
end
-- Discover real remote IP of a session
-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3,
-- this code provides backwards compatibility with older versions
local get_request_from_conn = http_server.get_request_from_conn or function (conn)
local response = conn and conn._http_open_response;
return response and response.request or nil;
end;
-- Add an IP to the set of limied IPs
local function limit_ip(ip)
module:log("info", "Limiting %s due to login/join rate exceeded.", ip);
limited_ips:set(ip, gettime());
end
-- Installable as a session filter to limit the reading rate for a session. Based on mod_limits.
local function limit_bytes_in(bytes, session)
local sess_throttle = session.jitsi_throttle;
if sess_throttle then
-- if the limit timeout has elapsed let's stop the throttle
if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
module:log("info", "Stop throttling session=%s, ip=%s.", session, session.ip);
session.jitsi_throttle = nil;
return bytes;
end
local ok, _, outstanding = sess_throttle:poll(#bytes, true);
if not ok then
session.log("debug",
"Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding);
outstanding = ceil(outstanding);
session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
local outstanding_data = bytes:sub(-outstanding);
bytes = bytes:sub(1, #bytes-outstanding);
timer.add_task(limits_resolution, function ()
if not session.conn then return; end
if sess_throttle:peek(#outstanding_data) then
session.log("debug", "Resuming paused session");
session.conn:resume();
end
-- Handle what we can of the outstanding data
session.data(outstanding_data);
end);
end
end
return bytes;
end
-- Throttles reading from the connection of a specific session.
local function throttle_session(session)
if not session.jitsi_throttle then
if (session.conn and session.conn.setlimit) then
-- TODO: we don't have a mechanism to unthrottle a session in this case.
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s.", config.session_rate, session, session.ip);
session.conn:setlimit(config.session_rate);
else
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", config.session_rate, session, session.ip);
session.jitsi_throttle = new_throttle(config.session_rate, 2);
filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
-- throttle.start used for stop throttling after the timeout
session.jitsi_throttle.start = gettime();
end
else
-- update the throttling start
session.jitsi_throttle.start = gettime();
end
end
-- checks different stanzas for rate limiting (per session)
function filter_stanza(stanza, session)
local rate = session[stanza.name.."_rate"];
if rate then
local ok, _, _ = rate:poll(1, true);
if not ok then
module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
throttle_session(session);
end
end
return stanza;
end
local function on_login(session, ip)
local login_rate = login_rates:get(ip);
if not login_rate then
module:log("debug", "Create new join rate for %s", ip);
login_rate = new_throttle(config.login_rate, 2);
login_rates:set(ip, login_rate);
end
local ok, _, _ = login_rate:poll(1, true);
if not ok then
module:log("info", "Join rate exceeded for %s, limiting.", ip);
limit_ip(ip);
throttle_session(session);
end
end
local function filter_hook(session)
local request = get_request_from_conn(session.conn);
local ip = request and request.ip or session.ip;
module:log("debug", "New session from %s", ip);
if is_whitelisted(ip) or (session.username and is_whitelisted_jid(session.username..'@'..session.host)) then
return;
end
on_login(session, ip);
-- creates the stanzas rates
session.presence_rate = new_throttle(config.presence_rate, 2);
session.iq_rate = new_throttle(config.iq_rate, 2);
session.message_rate = new_throttle(config.message_rate, 2);
filters.add_filter(session, "stanzas/in", filter_stanza);
local oldt = limited_ips:get(ip);
if oldt then
local newt = gettime();
local elapsed = newt - oldt;
if elapsed < 5 then
module:log("info", "IP address %s was limitted %s seconds ago, refreshing.", ip, elapsed);
limited_ips:set(ip, newt);
throttle_session(session);
elseif elapsed < config.timeout then
throttle_session(session);
else
module:log("info", "Removing the limit for %s", ip);
limited_ips:set(ip, nil);
end
end
end
function module.load()
filters.add_filter_hook(filter_hook);
end
function module.unload()
filters.remove_filter_hook(filter_hook);
end
module:hook_global("config-reloaded", load_config);

View File

@@ -0,0 +1,31 @@
-- http endpoint to expose turn credentials for other services
-- Copyright (C) 2023-present 8x8, Inc.
local ext_services = module:depends("external_services");
local get_services = ext_services.get_services;
local async_handler_wrapper = module:require "util".async_handler_wrapper;
local json = require "util.json";
--- Handles request for retrieving turn credentials
-- @param event the http event, holds the request query
-- @return GET response, containing a json with participants details
function handle_get_turn_credentials (event)
local GET_response = {
headers = {
content_type = "application/json";
};
body = json.encode(get_services());
};
return GET_response;
end;
function module.load()
module:depends("http");
module:provides("http", {
default_path = "/";
route = {
["GET turn-credentials"] = function (event) return async_handler_wrapper(event,handle_get_turn_credentials) end;
};
});
end