diff --git a/app/assets/locales/de.json b/app/assets/locales/de.json index aa4342ea37..a4c7ab5f70 100644 --- a/app/assets/locales/de.json +++ b/app/assets/locales/de.json @@ -17,6 +17,7 @@ "close": "Schließen", "delete": "Löschen", "copy": "Einladungslink kopieren", + "copy_voice_bridge": "Telefoneinwahl kopieren", "or": "oder", "online": "Online", "help_center": "Hilfe", @@ -382,7 +383,8 @@ "access_code_copied": "Zugangscode kopiert.", "access_code_generated": "Zugangscode generiert.", "access_code_deleted": "Zugriffcode wurde gelöscht.", - "copied_meeting_url": "Die URL der Konferenz wurde kopiert. Der Link kann verwendet werden, um an der Konferenz teilzunehmen." + "copied_meeting_url": "Die URL der Konferenz wurde kopiert. Der Link kann verwendet werden, um an der Konferenz teilzunehmen.", + "copied_voice_bridge": "Die Telefonnummer und der Pin wurden kopiert. Diese können genutzt werden um an der Konferenz teilzunehmen." }, "site_settings": { "site_setting_updated": "Grundeinstellungen aktualisiert.", diff --git a/app/assets/locales/en.json b/app/assets/locales/en.json index e7df24ed3c..a15ff35d06 100644 --- a/app/assets/locales/en.json +++ b/app/assets/locales/en.json @@ -17,6 +17,7 @@ "close": "Close", "delete": "Delete", "copy": "Copy Join Link", + "copy_voice_bridge": "Copy phone dialup", "or": "Or", "online": "Online", "help_center": "Help Center", @@ -382,7 +383,8 @@ "access_code_copied": "The access code has been copied.", "access_code_generated": "A new access code has been generated.", "access_code_deleted": "The access code has been deleted.", - "copied_meeting_url": "The meeting URL has been copied. The link can be used to join the meeting." + "copied_meeting_url": "The meeting URL has been copied. The link can be used to join the meeting.", + "copied_voice_bridge": "The phone number and pin have been copied. They can be used to join the conference." }, "site_settings": { "site_setting_updated": "The site setting has been updated.", diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb index 0cfd7962d6..0d8682ee8c 100644 --- a/app/controllers/api/v1/rooms_controller.rb +++ b/app/controllers/api/v1/rooms_controller.rb @@ -93,7 +93,7 @@ def create room = Room.new(name: room_params[:name], user_id: room_params[:user_id]) if room.save - logger.info "room(friendly_id):#{room.friendly_id} created for user(id):#{room.user_id}" + logger.info "room(friendly_id):#{room.friendly_id} created for user(id):#{room.user_id} with voice brige: #{room.voice_bridge}" render_data status: :created else render_error errors: room.errors.to_a, status: :bad_request @@ -160,7 +160,7 @@ def find_room end def room_params - params.require(:room).permit(:name, :user_id, :presentation) + params.require(:room).permit(:name, :user_id, :voice_bridge, :presentation) end end end diff --git a/app/javascript/components/rooms/RoomCard.jsx b/app/javascript/components/rooms/RoomCard.jsx index 63790698b8..5993612a75 100644 --- a/app/javascript/components/rooms/RoomCard.jsx +++ b/app/javascript/components/rooms/RoomCard.jsx @@ -18,7 +18,7 @@ import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Card, Stack } from 'react-bootstrap'; import PropTypes from 'prop-types'; -import { DocumentDuplicateIcon, LinkIcon } from '@heroicons/react/24/outline'; +import { DocumentDuplicateIcon, LinkIcon, PhoneIcon } from '@heroicons/react/24/outline'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; import { useAuth } from '../../contexts/auth/AuthProvider'; @@ -41,6 +41,11 @@ export default function RoomCard({ room }) { toast.success(t('toast.success.room.copied_meeting_url')); } + function copyVoiceBridge(voice_bridge, voice_bridge_phone_number) { + navigator.clipboard.writeText(`Tel.: ${voice_bridge_phone_number} Pin: ${voice_bridge}`); + toast.success(t('toast.success.room.copied_voice_bridge')); + } + return ( @@ -73,6 +78,12 @@ export default function RoomCard({ room }) { > + {typeof room.voice_bridge_phone_number !== 'undefined' && } + { typeof room.voice_bridge_phone_number !== 'undefined' && }) +} diff --git a/app/models/room.rb b/app/models/room.rb index 649c5c445c..cad804f6a2 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -30,6 +30,7 @@ class Room < ApplicationRecord validates :name, presence: true validates :friendly_id, presence: true, uniqueness: true validates :meeting_id, presence: true, uniqueness: true + validates :voice_bridge, uniqueness: true validates :presentation, content_type: Rails.configuration.uploads[:presentations][:formats], size: { less_than: Rails.configuration.uploads[:presentations][:max_size] } @@ -37,7 +38,7 @@ class Room < ApplicationRecord validates :name, length: { minimum: 2, maximum: 255 } validates :recordings_processing, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - before_validation :set_friendly_id, :set_meeting_id, on: :create + before_validation :set_friendly_id, :set_meeting_id, :set_voice_brige, on: :create after_create :create_meeting_options attr_accessor :shared, :active, :participants @@ -99,4 +100,16 @@ def set_meeting_id rescue StandardError retry end + + # Create unique pin for voice brige max 10^5 - 10000 unique ids + def set_voice_brige + if Rails.application.config.voice_bridge_phone_number != nil + id = SecureRandom.random_number((10.pow(5)) - 1) + raise if Room.exists?(voice_bridge: id) || id < 10000 + + self.voice_bridge = id + end + rescue StandardError + retry + end end diff --git a/app/serializers/current_room_serializer.rb b/app/serializers/current_room_serializer.rb index d4e8e98785..515747c704 100644 --- a/app/serializers/current_room_serializer.rb +++ b/app/serializers/current_room_serializer.rb @@ -23,6 +23,9 @@ class CurrentRoomSerializer < ApplicationSerializer attribute :last_session, if: -> { object.last_session } + attribute :voice_bridge, if: -> { Rails.application.config.voice_bridge_phone_number } + attribute :voice_bridge_phone_number, if: -> { Rails.application.config.voice_bridge_phone_number } + def presentation_name presentation_file_name(object) end @@ -34,4 +37,8 @@ def thumbnail def owner_name object.user.name end + + def voice_bridge_phone_number + Rails.application.config.voice_bridge_phone_number + end end diff --git a/app/serializers/room_serializer.rb b/app/serializers/room_serializer.rb index c236a94ed8..6f5adddb1b 100644 --- a/app/serializers/room_serializer.rb +++ b/app/serializers/room_serializer.rb @@ -20,8 +20,14 @@ class RoomSerializer < ApplicationSerializer attributes :id, :name, :friendly_id, :online, :participants, :last_session attribute :shared_owner, if: -> { object.shared } + attribute :voice_bridge, if: -> { Rails.application.config.voice_bridge_phone_number } + attribute :voice_bridge_phone_number, if: -> { Rails.application.config.voice_bridge_phone_number } def shared_owner object.user.name end + + def voice_bridge_phone_number + Rails.application.config.voice_bridge_phone_number + end end diff --git a/app/services/meeting_starter.rb b/app/services/meeting_starter.rb index 4575aef490..b3bb782c6d 100644 --- a/app/services/meeting_starter.rb +++ b/app/services/meeting_starter.rb @@ -28,7 +28,7 @@ def initialize(room:, base_url:, current_user:, provider:) def call # TODO: amir - Check the legitimately of the action. - options = RoomSettingsGetter.new(room_id: @room.id, provider: @room.user.provider, current_user: @current_user, only_bbb_options: true).call + options = RoomSettingsGetter.new(room_id: @room.id, provider: @room.user.provider, current_user: @current_user, only_bbb_options: true, voice_bridge: @room.voice_bridge).call viewer_code = RoomSettingsGetter.new( room_id: @room.id, provider: @room.user.provider, diff --git a/app/services/room_settings_getter.rb b/app/services/room_settings_getter.rb index 1d033d41aa..52b85666d0 100644 --- a/app/services/room_settings_getter.rb +++ b/app/services/room_settings_getter.rb @@ -23,13 +23,14 @@ class RoomSettingsGetter # Hash(` => {'true' => , 'false' => })` SPECIAL_OPTIONS = { 'guestPolicy' => { 'true' => 'ASK_MODERATOR', 'false' => 'ALWAYS_ACCEPT' } }.freeze - def initialize(room_id:, provider:, current_user:, settings: [], show_codes: false, only_enabled: false, only_bbb_options: false) + def initialize(room_id:, provider:, current_user:, settings: [], show_codes: false, only_enabled: false, only_bbb_options: false, voice_bridge: nil) @current_user = current_user @room_id = room_id @only_bbb_options = only_bbb_options # When used only BBB options (not prefixed with 'gl') will be returned. @only_enabled = only_enabled # When used only optional and force enabled options will be returned. @show_codes = show_codes # When used access code values will be returned. @settings = settings # When given only the settings contained in the Array will be returned. + @voice_bridge = voice_bridge # Fetching only rooms configs that are not optional to overwrite the settings values. @rooms_configs = MeetingOption.joins(:rooms_configurations) @@ -55,6 +56,8 @@ def call infer_codes(room_settings:, access_codes:) # Access codes should map their forced values as intended. infer_can_record(room_settings:) if room_settings['record'] && @rooms_configs['record'].nil? + set_voice_brige(room_settings:) + room_settings end @@ -98,4 +101,10 @@ def infer_can_record(room_settings:) room_settings['record'] = 'false' end + + def set_voice_brige(room_settings:) + if @voice_bridge != nil + room_settings['voiceBridge'] = "#{@voice_bridge}" + end + end end diff --git a/config/application.rb b/config/application.rb index d37e2120b3..9f876b403c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,6 +79,9 @@ class Application < Rails::Application config.bigbluebutton_secret = ENV.fetch('BIGBLUEBUTTON_SECRET', '8cd8ef52e8e101574e400365b55e11a6') + config.voice_bridge_phone_number = ENV.fetch('VOICE_BRIDGE_PHONE_NUMBER', nil) + + config.relative_url_root = ENV.fetch('RELATIVE_URL_ROOT', '/') # Fetch 'RELATIVE_URL_ROOT' ENV variable value while removing any trailing slashes. config.relative_url_root = ENV.fetch('RELATIVE_URL_ROOT', nil)&.sub(%r{/*\z}, '') config.relative_url_root = '/' if config.relative_url_root.blank? diff --git a/db/data/20230328124724_populate_voice_brige_for_existing_rooms.rb b/db/data/20230328124724_populate_voice_brige_for_existing_rooms.rb new file mode 100644 index 0000000000..acdb99b8a8 --- /dev/null +++ b/db/data/20230328124724_populate_voice_brige_for_existing_rooms.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class PopulateVoiceBrigeForExistingRooms < ActiveRecord::Migration[7.0] + def up + if Rails.application.config.voice_bridge_phone_number == nil + return + end + + if Room.all.length > 89999 + raise "The db contains to many rooms to assign each one a unique voice_brige" + end + + Room.where(voice_bridge: nil).each do |room| + id = SecureRandom.random_number((10.pow(5)) - 1) + + if id < 10000 + id = id + 10000 + end + + while Room.exists?(voice_bridge: id) + id = id + 1 + if id >= 99999 + id = 10000 + end + end + + room.update(voice_bridge: id) + end + end + + def down + Room.update_all(voice_bridge: nil) + end +end diff --git a/db/data_schema.rb b/db/data_schema.rb index 4013193075..157472de8b 100644 --- a/db/data_schema.rb +++ b/db/data_schema.rb @@ -1 +1 @@ -DataMigrate::Data.define(version: 20230228193705) +DataMigrate::Data.define(version: 20230328124724) diff --git a/db/migrate/20230321125010_add_voice_brige_to_romms.rb b/db/migrate/20230321125010_add_voice_brige_to_romms.rb new file mode 100644 index 0000000000..42fac35f2a --- /dev/null +++ b/db/migrate/20230321125010_add_voice_brige_to_romms.rb @@ -0,0 +1,6 @@ +class AddVoiceBrigeToRomms < ActiveRecord::Migration[7.0] + def change + add_column :rooms, :voice_bridge, :integer, null: true, default: nil + add_index :rooms, :voice_bridge + end +end diff --git a/db/schema.rb b/db/schema.rb index f0760fed4a..28b87151d9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -132,9 +132,11 @@ t.datetime "updated_at", null: false t.integer "recordings_processing", default: 0 t.boolean "online", default: false + t.integer "voice_bridge" t.index ["friendly_id"], name: "index_rooms_on_friendly_id", unique: true t.index ["meeting_id"], name: "index_rooms_on_meeting_id", unique: true t.index ["user_id"], name: "index_rooms_on_user_id" + t.index ["voice_bridge"], name: "index_rooms_on_voice_bridge" end create_table "rooms_configurations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/sample.env b/sample.env index 405ae57f2f..4be5e5c388 100644 --- a/sample.env +++ b/sample.env @@ -72,6 +72,11 @@ REDIS_URL= # [en, ar, fr, es] #DEFAULT_LOCALE=en +# Define the phone number for the voice bridge. +# This number is not sent to bbb and is only displayed in the greenlight UI, but it should match the number in the bbb instance to avoid user confusion. +# If this number is defined, each newly created room will be assigned a static voiceBridge pin. +#VOICE_BRIDGE_PHONE_NUMBER= + # Set this if you like to deploy Greenlight on a relative root path other than / #RELATIVE_URL_ROOT=/gl