Верите ли вы в настоящую любовь? И я сейчас не про то мимолетное увлечение, когда прям с первого взгляда «искра, буря, безумие», а про выстроенную годами усилий, литрами пота и крови любовь. Лично я — верю! И хоть на новой работе я больше не использую XWiki, но время от времени так и хочется провести с ней удивительные мгновения и поделиться новым опытом с дорогими читателями.
На этом лирическое отступление завершается, и мы переходим к практической части.
В этой статье мы расширим функции популярной opensource-замены Confluence (со слов разработчиков) и добавим в XWiki возможность совершить телефонный звонок пользователю прямо из браузера, по нажатию всего одной кнопки на боковой панели.
Поможет нам в этом простой и доступный инструмент для работы с IP-телефонией — Exolve WebSDK. Именно с помощью данной JavaScript (TypeScript) библиотеки мы будем совершать звонки. Кстати, новые пользователи могут протестировать МТС Evolve бесплатно, в рамках тестового баланса, в том числе и Mobile SDK, пусть и с некоторыми ограничениями (подробнее см. в документации).
Приёмы, которые мы используем при внедрении Exolve WebSDK в XWiki, также будут полезны при подключении других сторонних JS библиотек. Поэтому надеюсь, что статья будет интересна не только тем, кто интересуется IP-телефонией, но и фанатам XWiki.
Описание задачи
Если совсем кратко, то мы создаём внутри XWiki виджет, который с помощью WebSDK позволит нам прямо из браузера позвонить другому пользователю XWiki, у которого в профиле есть мобильный телефон. При этом второму пользователю поступит звонок с обычного мобильного номера, и он даже не узнает, что мы разговариваем с ним через компьютер.
Для большей наглядности я подготовил иллюстрацию сценария.

Подготовительные работы
Прежде чем приступить к реализации, убедимся, что всё подготовлено.
Если у вас уже настроено приложение и получен SIP-логин в МТС Exolve, то раздел можно пропустить.
Для начала нам необходимо зарегистрироваться, подтвердить свой номер телефона, создать приложение и привязать к нему тестовый номер телефона МТС Exolve.
Все эти шаги отражены в официальной документации, поэтому не будем останавливаться на них подробно.
Как только все шаги выполнены, перейдите в созданное приложение, откройте раздел «SIP-соединения» и нажмите кнопку «Создать SIP ID». Заполнение полей страницы интуитивно понятно. После создания SIP ID скопируйте себе sipLogin (Юзернейм) и sipPassword (пароль), чтобы указать их далее в скрипте.

Сборка JavaScript скрипта
Как всегда пришла пора дисклеймера.
Я не разработчик, поэтому предложенные подходы не являются эталонными. Некоторые вещи я и вовсе не до конца понимаю, но всё же они работают. Вы всегда можете предложить лучшие варианты в комментариях.
Всё, формальности улажены, можно приступать к делу.
Если вы хотите сверится или сразу попробовать функции в деле, то готовый проект и виджет XWiki можно скачать в GitHub.
Уместно рассказать пару слов о WebSDK. Фактически это JavaScript (TypeScript) библиотека, которую мы можем использовать для создания соединения и совершения вызова.
Более подробно можно ознакомиться в документации:
Должен признаться, что мои познания в области JS остались в тех далёких и прекрасных годах, когда умы разработчиков только захватывал AJAX, а JS использовали для простых скриптов, например онлайн-калькуляторов.
Поэтому я сделал обёртку для SDK, которая будет импортироваться в браузер в качестве глобальной переменной JS. Если у вас есть лучший способ реализации, обязательно напишите в комментариях, мне будет интересно с ним ознакомиться.
Прежде чем начать работу убедитесь, что ваша версия Node.js не ниже 18. Это необходимо для корректной работы SDK.
Создайте пустую папку для проекта и установите SDK командой:
npm install @mts-exolve/web-voice-sdk --save
Для того, чтобы собрать всё в один JS файл исполняемый в браузере, установите компилятор TypeScript и Webpack командой:
npm install typescript webpack webpack-cli ts-loader --save-dev
Затем создайте папку src, а в ней файл index.ts
Листинг src/index.ts
import * as WebVoiceSdk from '@mts-exolve/web-voice-sdk';
/**
* Функция для создания экземпляра SIP
* @param {sipParams} SipLogin - логин SIP в ЛК MTS Exolve sipPassword - пароль для данного SIP логина
*/
export function createSipInstance(sipParams: { sipLogin: string, sipPassword: string }) {
return WebVoiceSdk.createSipInstance(sipParams);
}
Как видите, код очень простой, мы импортируем WebSDK и по сути делаем обёртку для функции, которая создаёт экземпляр для работы с SIP. Затем мы экспортируем обёртку, чтобы потом обращаться к ней как к глобальной переменной в скрипте.
Исходная функция SDK — WebVoiceSdk.createSipInstance принимает строковые параметры sipLoginb и sipPassword, которые мы получили в предыдущем разделе. Подробнее о работе функции см. в документации.
Обратите внимание: в демонстрационном примере логины sipLogin и sipPassword легко обнаружить в исходном коде страницы. Для продуктовых решений необходимо самостоятельно решить вопрос безопасности, например, ограничив доступ к XWiki внутренней сетью или зашифровав логин и пароль в коде bundle.js.
На первый взгляд решение сделать свою обёртку выглядит немного бессмысленным, но зато вы можете самостоятельно расширить функции, добавить ещё каких-нибудь библиотек или вшить дополнительную бизнес-логику. Например, в изначальной версии проекта, я хотел экспортировать функцию, которая сразу бы совершала вызов, но в итоге отказался от данного решения.
Далее нам необходимы файлы конфигурации для компилятора TypeScript и Webpack.
Их я сгенерировал с помощью нейросети ибо не до конца понимаю все настройки, поэтому не буду подробно на них останавливаться.
Листинг tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"outDir": "dist",
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src"
]
Листинг webpack.config.js
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src', 'index.ts'),
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'WebVoiceSdk',
libraryTarget: 'var',
},
module: {
rules: [
{
test: /.ts$/,
exclude: /node_modules/,
use: 'ts-loader',
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
};
Единственное на что обращу внимание – параметры:
library: 'WebVoiceSdk' и libraryTarget: 'var'
позволят нам использовать обертку как глобальную переменную, а не как модуль.
Несмотря на то, что package.json должен создаться автоматически, дополним его вызовом скрипта build:
"scripts": {
"build": "webpack"
},
Теперь можно вызывать сборку командой npm run build
В качестве альтернативы можно запускать сборку командой npx webpack
Полный листинг package.json
{
"scripts": {
"build": "webpack"
},
"dependencies": {
"@mts-exolve/web-voice-sdk": "^1.9.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.6.2",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4"
}
}
У нас все готово для сборки командой npm run build.
После её запуска и сборки без ошибок, в папке /dist появится файл bundle.js
Осталось проверить работоспособность скрипта перед импортом его в XWiki.
Создайте в корне проекта файл index.html
Полный листинг index.html
<!DOCTYPE html>
<html>
<head>
<title>Web Voice SDK Example</title>
<script src="https://requirejs.org/docs/release/2.3.7/minified/require.js"></script>
</head>
<body>
<script>
// Используем WebVoiceSdk.createSipInstance
requirejs(["dist/bundle"], function () {
//This function is called when dist/bundle is loaded.
window.sipInstance = WebVoiceSdk.createSipInstance({
sipLogin: "Ваш SIP логин",
sipPassword: "Ваш SIP пароль",
});
window.sipInstance.register();
});
</script>
<input
id="PhoneToCall"
placeholder="Введите номер"
/>
<input
value="Позвонить"
type="button"
id="startCallButton"
onclick="window.sipSession = window.sipInstance.call(document.getElementById('PhoneToCall').value)"
/>
<input
value="Отбой вызова"
type="button"
id="stopCallButton"
onclick="window.sipSession.terminate()"
/>
</body>
</html>
Файл достаточно простой, но у вас может возникнуть вопрос: почему мы импортируем WebVoiceSdk с помощью require.js, а не напрямую.
На самом деле, в данном случае скрипт bundle.js можно загрузить напрямую, поскольку с очень высокой вероятностью скрипт успеет загрузится до попытки создать объект с помощью WebVoiceSdk.createSipInstance
. Но в XWiki данный скрипт точно не успеет загрузится. Поэтому для унификации я решил реализовать его загрузку через require.js. Это гарантирует создание объекта только после завершения загрузки скрипта и убережет от ошибок.
Обратите внимание, помимо создания объекта мы еще регистрируем соединение функцией window.sipInstance.register()
. Несмотря на то, что в документации написано, что метод необходим для получения входящих звонков, исходящий звонок у меня без вызова этого метода не сработал.
Для удобства тестирования у нас есть поле ввода для указания номера телефона (в формате 71112223344) и кнопки старта и отбоя вызова.
Для простоты объект SIP соединения хранится в глобальной переменной, поэтому при нажатии на кнопку «Позвонить» мы просто обращаемся к методу совершения вызова call (“номер телефона”)
из WebSDK (см. документацию)
Для того, чтобы звонок можно было сбросить, мы сохраняем сессию звонка в новую глобальную переменную window.sipSession.
И при нажатии на кнопку «Отбой вызова» вызываем метод terminate(), который по сути завершает сессию и прекращает вызов.
На всякий случай сверим структуру файлов.
В итоге должно получиться следующее дерево проекта:
xwiki_web_sdk
├── node_modules ─ установленные модули не показаны
├── dist
│ └── bundle.js ─ готовый скрипт для работы в браузере
├── index.html ─ страница для проверки сборки
├── src
│ └── index.ts ─ Исходник
├── tsconfig.json ─ Файлы конфигурации
├── package.json
└── webpack.config.js
Осталось всё протестировать.
Откройте index.html в браузере, введите номер и нажмите позвонить.
Обязательно предоставьте разрешение на доступ к микрофону.

Если всё успешно, то на ваш номер поступит вызов.
Теперь мы готовы внедрить скрипт в XWiki.
Интеграция скрипта в XWiki
Я надеюсь, что у вас уже установлена XWiki. Но если нет, то для целей тестирования всегда можно скачать предустановленную версию. Для её запуска необходимо только распаковать архив и запустить соответствующий .bat /.sh файл. Напомню, что логин и пароль администратора по умолчанию будет Admin и admin соответственно.
Для этой статьи я использовал версию 15.10.12 с предустановленным Standard Flavor (ссылка на страницу загрузки) и включил в настройках русский язык.
Далее я считаю, что вы успешно запустили XWiki и авторизовались как администратор.
Прежде чем приступить к работе убедитесь что в настройках профиля у вас выбран тип пользователя «Продвинутый». Это необходимо для работы с объектами.
Перейдем к созданию панели. Кстати, изначально я хотел внедрить кнопки управления вызовом непосредственно в шаблон отображения профиля пользователя, но в таком случае при обновлении XWiki есть вероятность затереть правки, поэтому я решил вынести все в отдельную панель.
Перейдите в панель администрирования XWiki в раздел «Настройки интерфейса → Мастер панелей» и нажмите на ссылку «Перейти к панелям» (можно просто перейти по адресу {host}/xwiki/bin/view/Panels/).
Создайте новую панель. Я для своей выбрал название PhoneCall.

На странице редактирования оставьте настройки без изменений. Можете указать свое описание панели и изменить категорию на tools, но это все не критично. Нажмите «Сохранить и посмотреть».
Сразу добавим объект JS расширения, в который копируем код из budle.js.
В меню редактирования выберете «Объекты». Добавьте новый объект JavaScriptExtension и скопируйте в него полностью код из bundle.js. В поле «политика кэширования» установите пока «без кеширования»
Остальные настройки можно не изменять.

Сохраните и закройте страницу.
Кстати существуют и другие способы добавить скрипт, например с помощью WebJar.
Но как гласит один популярный мем:

Если вам любопытно, то с другими вариантами интеграции JS библиотек можно ознакомится в документации XWiki.
После возвращения на страницу панели, перейдите в режим редактирования страницы и в поле содержимое вставьте следующий код:
Скрипт для панели
{{velocity}}
$xwiki.jsx.use('Panels.PhoneCall')
## credentials to Web SDK
#set ($sipLogin = 'Ваш SIP логин' )
#set ($sipPassword = 'Ваш пароль к SIP логину' )
## Draw header
#panelheader('PhoneCall')
## Get some user's properties if it's user profil page
#set ($userObj = $doc.getObject('XWiki.XWikiUsers'))
#if ($userObj)
#set ($firstName = $userObj.getValue('first_name'))
#set ($lastName = $userObj.getValue('last_name'))
#set ($phone = $userObj.getValue('phone'))
#if ($firstName != "")
**Имя**: $firstName
#end
#if ($lastName != "")
**Фамилия**: $lastName
#end
#if ($phone != "")
**Телефон**: $phone
{{html}}
<script>
require.config({
paths: {
'web-voice': '$xwiki.getURL("Panels.PhoneCall", "jsx", "language=$xcontext.language")'
}
});
requirejs(['web-voice'], function () {
window.sipInstance = WebVoiceSdk.createSipInstance({
sipLogin: "$sipLogin",
sipPassword: "$sipPassword",
});
window.sipInstance.register();
});
</script>
<input
value="Позвонить"
type="button"
id="startCallButton"
onclick="window.sipSession = window.sipInstance.call('$phone')"
/>
<input
value="Отбой вызова"
type="button"
id="stopCallButton"
onclick="window.sipSession.terminate()"
/>
{{/html}}
#else
В профиле не заполнен
номер телефона
#end
#else
Перейдите на страницу
профиля пользователя
для совершения звонка.
#end
#panelfooter()
{{/velocity}}
Давайте разберем код подробнее.
Обратите внимание, мы используем в скрипте Velocity, JS, и HTML.
Весь скрипт у нас обернут в тег {{velocity}}, что позволяет удобно использовать функции и переменные XWiki.
Разберем некоторые фрагменты:
$xwiki.jsx.use('Panels.PhoneCall') — импортирует JavaScriptExtension который мы создали чуть ранее, Panels.PhoneCall — это адрес страницы с панелью, который можно посмотреть во вкладке «Информация».
## credentials to Web SDK
#set ($sipLogin = 'Ваш SIP логин' )
#set ($sipPassword = 'Ваш пароль к SIP логину' )
Чтобы не лазить по всему листингу, в шапку мы вынесем логи и пароль к SIP ID.
Далее по коду эти Velocity переменные мы вставим в JS обработчики.
Не забудьте перенести сюда ваши логин и пароль.
#panelheader('PhoneCall')
— выводит заголовок панели.
#set ($userObj = $doc.getObject('XWiki.XWikiUsers'))
— получает объект с профилем пользователя, если он есть.
#if ($userObj)
#set ($firstName = $userObj.getValue('first_name'))
#set ($lastName = $userObj.getValue('last_name'))
#set ($phone = $userObj.getValue('phone'))
#if ($firstName != "")
**Имя**: $firstName
#end
#if ($lastName != "")
**Фамилия**: $lastName
#end
Проверяем, перешли ли мы на страницу профиля. Если перешли, то также проверяем заполнение имени и фамилии в профиле, чтобы не показывать пустые поля.
Дальше проверяем заполнение номера телефона.
#if ($phone != "")
**Телефон**: $phone
{{html}}
<script>
require.config({
paths: {
'web-voice': '$xwiki.getURL("Panels.PhoneCall", "jsx", "language=$xcontext.language")'
}
});
requirejs(['web-voice'], function () {
window.sipInstance = WebVoiceSdk.createSipInstance({
sipLogin: "$sipLogin",
sipPassword: "$sipPassword",
});
window.sipInstance.register();
});
</script>
<input
value="Позвонить"
type="button"
id="startCallButton"
onclick="window.sipSession = window.sipInstance.call('$phone')"
/>
<input
value="Отбой вызова"
type="button"
id="stopCallButton"
onclick="window.sipSession.terminate()"
/>
{{/html}}
#else
В профиле не заполнен
номер телефона
#end
Если телефон заполнен, то с помощью тега {{html}} вставляем код очень похожий на index.html в предыдущем разделе.
На что стоит обратить внимание.
Во-первых, поскольку {{html}} лежит внутри обработчика {{velocity}} то мы можем вставлять Velocity код в любом месте.
Например, в
sipLogin: "$sipLogin",
sipPassword: "$sipPassword"
По сути в JS переменные подставляем значения из Velocity переменных.
Второе отличие: это немного более сложная конфигурация библиотеки require.js
require.config({
paths: {
'web-voice': '$xwiki.getURL("Panels.PhoneCall", "jsx", "language=$xcontext.language")'
}
});
Она встроена в XWiki по умолчанию и нам остается только сконфигурировать путь до JSX объекта, получив URL до JS кода с помощью функции $xwiki.getURL("Panels.PhoneCall", "jsx", "language=$xcontext.language")'
.
Теперь мы будем уверены, что рано или поздно скрипт для создания WebVoiceSdk.createSipInstance загрузится.
#else
В профиле не заполнен
номер телефона
#end
#else
Перейдите на страницу
профиля пользователя
для совершения звонка.
#end
#panelfooter()
{{/velocity}}
Оставшийся код. Отображает сообщение в случаях, если телефон в профиле не заполнен и если мы открыли страницу, на которой нет данных о профиле пользователя.
И в самом конце мы выводим футер панели и закрываем тег {{/velocity}}.
Сохраняем и закрываем режим редактирования.
Если все прошло успешно, то на странице с предпросмотром панели мы увидим текст о том, что необходимо перейти на страницу профиля.

Добавим панель в интерфейс XWiki.
Вернитесь обратно в раздел «Настройки интерфейса → Мастер панелей». Откройте вкладку «Список панелей». И просто перетащите панель PhoneCall на левый сайдбар.
Сохраните изменения.
Теперь перейдём на страницу профиля, например профиля Admin.
Заполним поле с номером телефона и почти всё готово.
Однако, если вы используете для тестирования локальную версию XWiki, возможно у вас по умолчанию будет запрещён доступ к микрофону. Я смог его разблокировать, перейдя в chrome://flags/ и добавив адрес XWiki в исключения флага Insecure origins treated as secure.
С подготовкой теперь точно всё, осталось проверить работоспособность.

Вс1 прошло успешно, на мой телефон поступил вызов.

Спасибо всем, кто прочитал до конца. Надеюсь статья вам понравилась. Мне будет приятно прочитать конструктивные комментарии.
Автор: BosonBeard