Skip to content

Commit

Permalink
Support for Scalelite Tagged Servers (#5790)
Browse files Browse the repository at this point in the history
* init server tags prototype with config variable and corresponding dropdown in room settings

* moved ServerTag row to separate file + rubocop

* eslint happiness

* add db migration for room option meta_server-tag

* split the meta_server-tag parameter into two 'virtual' room options in GL: serverTag and serverTagRequired

* dynamically render the server tag Dropdown from the config string + hide it entirely when the string is empty

* now actually reflect and mutate the serverTag setting in DB via the Dropdown selection + tweaks&fixes

* now use server tag room option as actual parameter on create call

* rename config SERVER_TAGS_MAP->SERVER_TAG_NAMES and now handle it in Rails config

* add config variable for tags<->roles and document the full server tag config in sample.env

* fully implemented the tag<->role restriction as per config variable

* add configurable default tag name for untagged servers

* added checkbox to set if server tag is required

* add logic to handle serverTagRequired in meeting_starter service

* rubocop + change tagRequired toggle to react-bootstrap Radio buttons inside Cols

* eslint + add description above server tag row

* make tags Dropdown drop up + cleanpu

* finalized DB migration for the server tags options

* final polish (including translated strings)

* add rake task server_tags_sync to clear invalid/disallowed server tags from the database (after changing the config)

* adds translated string for default tag name /  DEFAULT_TAG_NAME env now serves as override

* show a specific error message when a required server type is unavailable (based on blindsidenetworks/scalelite#1091)

* move tag fetching logic from CurrentUserSerializer to new ServerTagsController

* refactor ServerTagRow and remove tags-related env variables completely from react

* remove DEFAULT_TAG_NAME from sample.env

* now silently fall back to untagged if an invalid/forbidden tag is used in meeting_starter.rb (which matches what the user would see in terms of UI, namely the default tag name)

* check room owner's role_id instead of current user's for role restrictions of tags / list of allowed tags

* improve guarding for the cases of disabled tags or invalid room_ids in ServerTagsController

* avoid checking serverTags.isLoading

* streamline server tags migration

* massively increase performance of server tags migration if database is in the expected state (i.e. no tags related options present)

---------

Co-authored-by: Ahmad Farhat <[email protected]>
  • Loading branch information
Ithanil and farhatahmad authored Jul 24, 2024
1 parent 77873eb commit f123946
Show file tree
Hide file tree
Showing 15 changed files with 357 additions and 5 deletions.
5 changes: 5 additions & 0 deletions app/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@
"wrong_access_code": "Wrong Access Code",
"generate_viewers_access_code": "Generate access code for viewers",
"generate_mods_access_code": "Generate access code for moderators",
"server_tag": "Select a server type for this room",
"default_tag_name": "Default",
"server_tag_desired": "Desired",
"server_tag_required": "Required",
"are_you_sure_delete_room": "Are you sure you want to delete this room?"
}
},
Expand Down Expand Up @@ -437,6 +441,7 @@
},
"error": {
"problem_completing_action": "The action can't be completed. \n Please try again.",
"server_type_unavailable": "The required server type is unavailable. Please select a different type in the room settings.",
"file_type_not_supported": "The file type is not supported.",
"file_size_too_large": "The file size is too large.",
"file_upload_error": "The file can't be uploaded.",
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def start
begin
MeetingStarter.new(room: @room, base_url: request.base_url, current_user:, provider: current_provider).call
rescue BigBlueButton::BigBlueButtonException => e
return render_error status: :bad_request unless e.key == 'idNotUnique'
return render_error status: :bad_request, errors: e.key unless e.key == 'idNotUnique'
end

render_data data: BigBlueButtonApi.new(provider: current_provider).join_meeting(
Expand Down
37 changes: 37 additions & 0 deletions app/controllers/api/v1/server_tags_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
#
# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Greenlight; if not, see <http://www.gnu.org/licenses/>.

# frozen_string_literal: true

module Api
module V1
class ServerTagsController < ApiController
# GET /api/v1/server_tags/:friendly_id
# Returns a list of all allowed tags&names for the room's owner
def show
tag_names = Rails.configuration.server_tag_names
tag_roles = Rails.configuration.server_tag_roles
return render_data data: {}, status: :ok if tag_names.blank?

room = Room.find_by(friendly_id: params[:friendly_id])
return render_data data: {}, status: :ok if room.nil?

allowed_tag_names = tag_names.reject { |tag, _| tag_roles.key?(tag) && tag_roles[tag].exclude?(room.user.role_id) }
render_data data: allowed_tag_names, status: :ok
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { useAuth } from '../../../../contexts/auth/AuthProvider';
import UpdateRoomNameForm from './forms/UpdateRoomNameForm';
import useRoom from '../../../../hooks/queries/rooms/useRoom';
import UnshareRoom from './UnshareRoom';
import useServerTags from '../../../../hooks/queries/rooms/useServerTags';
import ServerTagRow from './ServerTagRow';

export default function RoomSettings() {
const { t } = useTranslation();
Expand All @@ -41,6 +43,7 @@ export default function RoomSettings() {
const roomSetting = useRoomSettings(friendlyId);
const { data: roomConfigs } = useRoomConfigs();
const { data: room } = useRoom(friendlyId);
const { data: serverTags } = useServerTags(friendlyId);

const updateMutationWrapper = () => useUpdateRoomSetting(friendlyId);
const deleteMutationWrapper = (args) => useDeleteRoom({ friendlyId, ...args });
Expand All @@ -66,6 +69,15 @@ export default function RoomSettings() {
config={roomConfigs?.glModeratorAccessCode}
description={t('room.settings.generate_mods_access_code')}
/>
{serverTags && Object.keys(serverTags).length !== 0 && (
<ServerTagRow
updateMutation={updateMutationWrapper}
currentTag={roomSetting?.data?.serverTag}
tagRequired={roomSetting?.data?.serverTagRequired === 'true'}
serverTags={serverTags}
description={t('room.settings.server_tag')}
/>
)}
</Col>
<Col className="ps-4">
<Row> <h6 className="text-brand">{ t('room.settings.user_settings') }</h6> </Row>
Expand Down
118 changes: 118 additions & 0 deletions app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
//
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
//
// This program is free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free Software
// Foundation; either version 3.0 of the License, or (at your option) any later
// version.
//
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.

import React from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import {
Row, Col, Dropdown, ButtonGroup, ToggleButton,
} from 'react-bootstrap';
import SimpleSelect from '../../../shared_components/utilities/SimpleSelect';

export default function ServerTagRow({
updateMutation: useUpdateAPI, currentTag, tagRequired, serverTags, description,
}) {
const updateAPI = useUpdateAPI();
const { t } = useTranslation();

function getDefaultTagName() {
return t('room.settings.default_tag_name');
}

function getTagName(tag) {
if (tag in serverTags) {
return serverTags[tag];
}
return getDefaultTagName();
}

const dropdownTags = Object.entries(serverTags).map(([tagString, tagName]) => (
(
<Dropdown.Item
key={tagString}
value={tagName}
onClick={() => updateAPI.mutate({ settingName: 'serverTag', settingValue: tagString })}
>
{tagName}
</Dropdown.Item>
)
));

return (
<Row>
<h6 className="text-brand">{description}</h6>
<Col>
<SimpleSelect defaultValue={getTagName(currentTag)} dropUp>
{[
<Dropdown.Item
key=""
value={getDefaultTagName()}
disabled={updateAPI.isLoading}
onClick={() => updateAPI.mutate({ settingName: 'serverTag', settingValue: '' })}
>
{getDefaultTagName()}
</Dropdown.Item>,
].concat(dropdownTags)}
</SimpleSelect>
</Col>
<Col>
<ButtonGroup>
<ToggleButton
key="desired"
id="desired"
type="radio"
variant="outline-success"
name="radio"
checked={tagRequired === false}
disabled={updateAPI.isLoading}
onChange={() => {
updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: false });
}}
>
{t('room.settings.server_tag_desired')}
</ToggleButton>
<ToggleButton
key="required"
id="required"
type="radio"
variant="outline-danger"
name="radio"
checked={tagRequired === true}
disabled={updateAPI.isLoading}
onChange={() => {
updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: true });
}}
>
{t('room.settings.server_tag_required')}
</ToggleButton>
</ButtonGroup>
</Col>
</Row>
);
}

ServerTagRow.defaultProps = {
currentTag: '',
tagRequired: false,
};

ServerTagRow.propTypes = {
updateMutation: PropTypes.func.isRequired,
currentTag: PropTypes.string,
tagRequired: PropTypes.bool,
serverTags: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
description: PropTypes.string.isRequired,
};
8 changes: 6 additions & 2 deletions app/javascript/hooks/mutations/rooms/useStartMeeting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export default function useStartMeeting(friendlyId) {
onSuccess: (joinUrl) => {
window.location.href = joinUrl;
},
onError: () => {
toast.error(t('toast.error.problem_completing_action'));
onError: (error) => {
if (error.response.data.errors !== 'serverTagUnavailable') {
toast.error(t('toast.error.problem_completing_action'));
} else {
toast.error(t('toast.error.server_type_unavailable'));
}
},
},
);
Expand Down
25 changes: 25 additions & 0 deletions app/javascript/hooks/queries/rooms/useServerTags.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
//
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
//
// This program is free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free Software
// Foundation; either version 3.0 of the License, or (at your option) any later
// version.
//
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.

import { useQuery } from 'react-query';
import axios from '../../../helpers/Axios';

export default function useServerTags(friendlyId) {
return useQuery(
['getServerTags', friendlyId],
() => axios.get(`/server_tags/${friendlyId}.json`).then((resp) => resp.data.data),
);
}
19 changes: 19 additions & 0 deletions app/services/meeting_starter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def call
settings: 'glViewerAccessCode'
).call

handle_server_tag(meeting_options: options)

options.merge!(computed_options(access_code: viewer_code['glViewerAccessCode']))

retries = 0
Expand Down Expand Up @@ -73,6 +75,23 @@ def computed_options(access_code:)
}
end

def handle_server_tag(meeting_options:)
if meeting_options['serverTag'].present?
tag_names = Rails.configuration.server_tag_names
tag_roles = Rails.configuration.server_tag_roles
tag = meeting_options.delete('serverTag')
tag_required = meeting_options.delete('serverTagRequired')

if tag_names.key?(tag) && !(tag_roles.key?(tag) && tag_roles[tag].exclude?(@room.user.role_id))
tag_param = tag_required == 'true' ? "#{tag} !" : tag
meeting_options.store('meta_server-tag', tag_param)
end
else
meeting_options.delete('serverTag')
meeting_options.delete('serverTagRequired')
end
end

def presentation_url
return unless @room.presentation.attached?

Expand Down
5 changes: 5 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,10 @@ class Application < Rails::Application
I18n.load_path += Dir[Rails.root.join('config/locales/*.{rb,yml}').to_s]
config.i18n.fallbacks = %i[en]
config.i18n.enforce_available_locales = false

# Handle server tag config
config.server_tag_names = ENV.fetch('SERVER_TAG_NAMES', '').split(',').to_h { |pair| pair.split(':') }
config.server_tag_roles = ENV.fetch('SERVER_TAG_ROLES', '').split(',').to_h { |pair| pair.split(':') }
config.server_tag_roles = config.server_tag_roles.transform_values! { |v| v.split('/') }
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
resources :site_settings, only: :index
resources :rooms_configurations, only: %i[index show], param: :name
resources :locales, only: %i[index show], param: :name
resources :server_tags, only: :show, param: :friendly_id

namespace :admin do
resources :users, only: %i[update] do
Expand Down
73 changes: 73 additions & 0 deletions db/data/20240423162700_create_server_tags_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
#
# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Greenlight; if not, see <http://www.gnu.org/licenses/>.

# frozen_string_literal: true

class CreateServerTagsOption < ActiveRecord::Migration[7.0]
def up
MeetingOption.create!(name: 'serverTag', default_value: '') unless MeetingOption.exists?(name: 'serverTag')
tag_option = MeetingOption.find_by!(name: 'serverTag')
MeetingOption.create!(name: 'serverTagRequired', default_value: 'false') unless MeetingOption.exists?(name: 'serverTagRequired')
tag_required_option = MeetingOption.find_by!(name: 'serverTagRequired')

unless RoomsConfiguration.exists?(meeting_option: tag_option, provider: 'greenlight')
RoomsConfiguration.create!(meeting_option: tag_option, value: 'optional', provider: 'greenlight')
end
unless RoomsConfiguration.exists?(meeting_option: tag_required_option, provider: 'greenlight')
RoomsConfiguration.create!(meeting_option: tag_required_option, value: 'optional', provider: 'greenlight')
end
Tenant.all.each do |tenant|
unless RoomsConfiguration.exists?(meeting_option: tag_option, provider: tenant.name)
RoomsConfiguration.create!(meeting_option: tag_option, value: 'optional', provider: tenant.name)
end
unless RoomsConfiguration.exists?(meeting_option: tag_required_option, provider: tenant.name)
RoomsConfiguration.create!(meeting_option: tag_required_option, value: 'optional', provider: tenant.name)
end
end

if RoomMeetingOption.exists?(meeting_option: tag_option) || RoomMeetingOption.exists?(meeting_option: tag_required_option)
# slow variant that works with existing tag options
Room.find_each do |room|
RoomMeetingOption.find_or_create_by!(room:, meeting_option: tag_option)
unless RoomMeetingOption.exists?(room:, meeting_option: tag_required_option)
RoomMeetingOption.create!(room:, meeting_option: tag_required_option, value: 'false')
end
end
else
# much faster variant without checks/validation
Room.find_in_batches do |batch|
tag_options_batch = batch.map { |room| { room_id: room.id, meeting_option_id: tag_option.id } }
tag_required_options_batch = batch.map { |room| { room_id: room.id, meeting_option_id: tag_required_option.id, value: 'false' } }
# rubocop:disable Rails/SkipsModelValidations
RoomMeetingOption.insert_all!(tag_options_batch)
RoomMeetingOption.insert_all!(tag_required_options_batch)
# rubocop:enable Rails/SkipsModelValidations
end
end
end

def down
tag_option = MeetingOption.find_by!(name: 'serverTag')
RoomMeetingOption.destroy_by(meeting_option: tag_option)
RoomsConfiguration.destroy_by(meeting_option: tag_option)
tag_option.destroy

tag_required_option = MeetingOption.find_by!(name: 'serverTagRequired')
RoomMeetingOption.destroy_by(meeting_option: tag_required_option)
RoomsConfiguration.destroy_by(meeting_option: tag_required_option)
tag_required_option.destroy
end
end
2 changes: 1 addition & 1 deletion db/data_schema.rb
Original file line number Diff line number Diff line change
@@ -1 +1 @@
DataMigrate::Data.define(version: 20240209155229)
DataMigrate::Data.define(version: 20240423162700)
2 changes: 1 addition & 1 deletion esbuild.dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ esbuild.context({
}).catch((e) => {
console.error('build failed:', e);
process.exit(1)
})
})
Loading

0 comments on commit f123946

Please sign in to comment.