diff --git a/README.md b/README.md index 9f62c45..7e9939e 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,44 @@ pluginManifests=[{"url":"/path/to/manifest.json"}] Or additionally, you can add this same configuration in the `.properties` file from `bbb-web` in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` +## URL Placeholders + +When sharing a link, you can use placeholders in the URL that will be dynamically replaced with user or session information. This allows you to personalize the shared content for each participant. + +### Supported Placeholders + +- `{name}`: The user's display name +- `{extId}`: The user's external ID +- `{role}`: The user's role +- `{presenter}`: `true` if the user is a presenter, otherwise `false` +- `{genericLinkShare_KEY}`: Any additional user metadata field, where `KEY` is the metadata key name (e.g., `{genericLinkShare_info}` will be replaced with the user's *info* if available) + +**Note:** +To use user metadata as a placeholder, the metadata key must start with the prefix `genericLinkShare_`. +For example, if the metadata is `{genericLinkShare_mykey}`, in the join call, it is necessary to send: + +``` + userdata-genericLinkShare_mykey=abc_example +``` + + +If additional user metadata is available, you can also use placeholders matching those metadata keys. + + +### Example + +If you enter the following link to share: + +``` +https://bigbluebutton.org/?name={name}&id={extId}&role={role}&presenter={presenter}&pass={genericLinkShare_mykey} +``` + +Each participant will see a personalized link with their own name, role, and the value of `genericLinkShare_mykey` (if set) substituted in the URL. + +### Security + +Sensitive placeholders are replaced securely to prevent injection or misuse. Only the placeholders listed above and those present in user metadata with the `genericLinkShare_` prefix are supported. + ## Development mode As for development mode (running this plugin from source), please, refer back to https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk section `Running the Plugin from Source` diff --git a/src/components/generic-link-share/component.tsx b/src/components/generic-link-share/component.tsx index 5a911c8..8cec561 100644 --- a/src/components/generic-link-share/component.tsx +++ b/src/components/generic-link-share/component.tsx @@ -18,19 +18,28 @@ import { parseTags } from '../utils'; import GenericComponentLinkShare from '../generic-component/component'; -import { DataToGenericLink, DecreaseVolumeOnSpeakProps } from './types'; +import { + CurrentUserData, + DataToGenericLink, + DecreaseVolumeOnSpeakProps, + UserMetadataGraphqlResponse, +} from './types'; import { ModalToShareLink } from '../modal-to-share-link/component'; import { LinkTag } from '../modal-to-share-link/types'; import { REGEX } from './constants'; -import { replaceUrlPlaceholders } from './utils'; +import { mergePlaceholdersList, replaceUrlPlaceholders } from './utils'; +import { USER_METADATA } from './subscription'; function GenericLinkShare( { pluginUuid: uuid }: DecreaseVolumeOnSpeakProps, ): React.ReactElement { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); + const userMetaDataSubscription = pluginApi + .useCustomSubscription(USER_METADATA); const { data: currentUser } = pluginApi.useCurrentUser(); const { data: urlToGenericLink, pushEntry: pushEntryUrlToGenericLink, deleteEntry: deleteEntryUrlToGenericLink } = pluginApi.useDataChannel('urlToGenericLink'); + const { data: userData } = userMetaDataSubscription; const currentPresentationResponse = pluginApi.useCurrentPresentation(); const currentLayout = pluginApi.useUiData(LayoutPresentatioAreaUiDataNames.CURRENT_ELEMENT, [{ isOpen: true, @@ -46,6 +55,13 @@ function GenericLinkShare( url: null, }); + const currentUserPlaceholders: CurrentUserData = { + name: currentUser?.name ?? '', + extId: currentUser?.extId ?? '', + role: currentUser?.role ?? '', + presenter: !!currentUser?.presenter, + }; + useEffect(() => { const isGenericComponentInPile = currentLayout.some((gc) => ( gc.currentElement === UiLayouts.GENERIC_CONTENT @@ -239,6 +255,10 @@ function GenericLinkShare( useEffect(() => { if (link && link !== '') { + const placeholdersList = mergePlaceholdersList( + currentUserPlaceholders, + userData.user_metadata, + ); pluginApi.setGenericContentItems([]); setGenericContentd(pluginApi.setGenericContentItems([ new GenericContentMainArea({ @@ -246,12 +266,7 @@ function GenericLinkShare( const root = ReactDOM.createRoot(element); root.render( , ); return root; diff --git a/src/components/generic-link-share/subscription.ts b/src/components/generic-link-share/subscription.ts index 8c23a5f..e60e749 100644 --- a/src/components/generic-link-share/subscription.ts +++ b/src/components/generic-link-share/subscription.ts @@ -9,4 +9,13 @@ subscription MeetingSubscription { } `; -export default MEETING_SUBSCRIPTION; +const USER_METADATA = ` + subscription { + user_metadata { + parameter + value + } +} +`; + +export { MEETING_SUBSCRIPTION, USER_METADATA }; diff --git a/src/components/generic-link-share/types.ts b/src/components/generic-link-share/types.ts index 57e0b96..dda6949 100644 --- a/src/components/generic-link-share/types.ts +++ b/src/components/generic-link-share/types.ts @@ -18,4 +18,25 @@ interface DataToGenericLink { viewerUrl?: string } +export interface CurrentUserData { + name: string; + extId: string; + role: string; + presenter: boolean; +} + +export interface UserMetadata { + parameter: string; + value: string; +} + +export interface UserMetadataGraphqlResponse { + user_metadata: UserMetadata[]; +} + +export interface PlaceholderEntry { + placeholder: string; + value: string | boolean; +} + export { DecreaseVolumeOnSpeakProps, ExternalVideoMeetingSubscription, DataToGenericLink }; diff --git a/src/components/generic-link-share/utils.ts b/src/components/generic-link-share/utils.ts index 8953da3..df775b2 100644 --- a/src/components/generic-link-share/utils.ts +++ b/src/components/generic-link-share/utils.ts @@ -1,14 +1,47 @@ -type PlaceholderValues = { - name: string; - extId: string; - role: string; - presenter: boolean; -}; - -export function replaceUrlPlaceholders(url: string, values: PlaceholderValues): string { - return url - .replace(/{name}/g, encodeURIComponent(values.name)) - .replace(/{extId}/g, encodeURIComponent(values.extId)) - .replace(/{role}/g, encodeURIComponent(values.role)) - .replace(/{presenter}/g, encodeURIComponent(String(values.presenter))); +import { CurrentUserData, PlaceholderEntry, UserMetadata } from './types'; + +const DEFAULT_PREFIX = 'genericLinkShare'; + +export function mergePlaceholdersList( + currentUserParams?: CurrentUserData, + userdataParams?: UserMetadata[], +): PlaceholderEntry[] { + const placeholdersList: PlaceholderEntry[] = []; + + if (currentUserParams) { + Object.entries(currentUserParams).forEach(([key, value]) => { + const placeholder = `{${key}}`; + placeholdersList.push({ placeholder, value }); + }); + } + + if (userdataParams) { + const allowedUserdataParams = userdataParams?.filter( + (entry) => entry.parameter.startsWith(DEFAULT_PREFIX), + ); + + allowedUserdataParams.forEach((userdata) => { + const userdataUrlParameterPattern = `{${userdata.parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}}`; + const userdataUrlValuePattern = userdata.value.replace(/[.*+?^${}()|[\]\\]/g, ''); + + placeholdersList + .push({ placeholder: userdataUrlParameterPattern, value: userdataUrlValuePattern }); + }); + } + + return placeholdersList; +} + +export function replaceUrlPlaceholders( + url: string, + placeholdersList: PlaceholderEntry[], +): string { + let result = url; + + placeholdersList.forEach(({ placeholder, value }) => { + const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); + result = result.replace(regex, encodeURIComponent(String(value))); + }); + + return result; }