Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,44 @@ pluginManifests=[{"url":"<your-domain>/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`
31 changes: 23 additions & 8 deletions src/components/generic-link-share/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserMetadataGraphqlResponse>(USER_METADATA);
const { data: currentUser } = pluginApi.useCurrentUser();
const { data: urlToGenericLink, pushEntry: pushEntryUrlToGenericLink, deleteEntry: deleteEntryUrlToGenericLink } = pluginApi.useDataChannel<DataToGenericLink>('urlToGenericLink');
const { data: userData } = userMetaDataSubscription;
const currentPresentationResponse = pluginApi.useCurrentPresentation();
const currentLayout = pluginApi.useUiData(LayoutPresentatioAreaUiDataNames.CURRENT_ELEMENT, [{
isOpen: true,
Expand All @@ -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
Expand Down Expand Up @@ -239,19 +255,18 @@ function GenericLinkShare(

useEffect(() => {
if (link && link !== '') {
const placeholdersList = mergePlaceholdersList(
currentUserPlaceholders,
userData.user_metadata,
);
pluginApi.setGenericContentItems([]);
setGenericContentd(pluginApi.setGenericContentItems([
new GenericContentMainArea({
contentFunction: (element: HTMLElement) => {
const root = ReactDOM.createRoot(element);
root.render(
<GenericComponentLinkShare
link={link && currentUser ? replaceUrlPlaceholders(link, {
name: currentUser?.name ?? '',
extId: currentUser?.extId ?? '',
role: currentUser?.role ?? '',
presenter: !!currentUser?.presenter,
}) : link}
link={replaceUrlPlaceholders(link, placeholdersList)}
/>,
);
return root;
Expand Down
11 changes: 10 additions & 1 deletion src/components/generic-link-share/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,13 @@ subscription MeetingSubscription {
}
`;

export default MEETING_SUBSCRIPTION;
const USER_METADATA = `
subscription {
user_metadata {
parameter
value
}
}
`;

export { MEETING_SUBSCRIPTION, USER_METADATA };
21 changes: 21 additions & 0 deletions src/components/generic-link-share/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
59 changes: 46 additions & 13 deletions src/components/generic-link-share/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}