friendica-addons/xmppchat/xmppchat.php
2025-12-16 16:02:57 +01:00

210 lines
11 KiB
PHP

<?php
/**
* Name: XMPP Chat
* Description: Embeds Converse.js XMPP webchat client into Friendica
* Version: 1.0
* Author: Friendica Community
* License: AGPL-3.0-or-later
*/
use Friendica\Core\Hook;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Core\Config\Util\ConfigFileManager;
function xmppchat_install()
{
Hook::register('load_config', 'addon/xmppchat/xmppchat.php', 'xmppchat_load_config');
Hook::register('footer', 'addon/xmppchat/xmppchat.php', 'xmppchat_footer');
Hook::register('addon_settings', 'addon/xmppchat/xmppchat.php', 'xmppchat_addon_settings');
Hook::register('addon_settings_post', 'addon/xmppchat/xmppchat.php', 'xmppchat_addon_settings_post');
DI::logger()->notice("installed xmppchat addon");
}
function xmppchat_addon_admin_post()
{
DI::config()->set('xmppchat', 'websocket_url', trim($_POST['websocket_url'] ?? ''));
DI::config()->set('xmppchat', 'bosh_url', trim($_POST['bosh_url'] ?? ''));
DI::config()->set('xmppchat', 'domain', trim($_POST['domain'] ?? ''));
DI::config()->set('xmppchat', 'auto_login', !empty($_POST['auto_login']));
DI::config()->set('xmppchat', 'enable_mam', !empty($_POST['enable_mam']));
DI::config()->set('xmppchat', 'enable_smacks', !empty($_POST['enable_smacks']));
DI::config()->set('xmppchat', 'enable_omemo', !empty($_POST['enable_omemo']));
DI::config()->set('xmppchat', 'allow_anonymous', !empty($_POST['allow_anonymous']));
DI::config()->set('xmppchat', 'default_muc', trim($_POST['default_muc'] ?? ''));
}
function xmppchat_addon_admin(string &$o)
{
$t = Renderer::getMarkupTemplate('admin.tpl', 'addon/xmppchat/');
$o = Renderer::replaceMacros($t, [
'$page_intro' => DI::l10n()->t('Configure the Converse.js XMPP webchat integration. Requires an XMPP server with WebSocket or BOSH support.'),
'$connection_title' => DI::l10n()->t('Connection Settings'),
'$auth_title' => DI::l10n()->t('Authentication'),
'$features_title' => DI::l10n()->t('Features'),
'$chatrooms_title' => DI::l10n()->t('Chat Rooms'),
'$help_omemo' => DI::l10n()->t('OMEMO Encryption'),
'$help_omemo_text' => DI::l10n()->t('Requires users to verify device fingerprints. May increase initial connection time.'),
'$help_muc' => DI::l10n()->t('Default Chat Room'),
'$help_muc_text' => DI::l10n()->t('Users will automatically join this room on login. Format: roomname@conference.example.org'),
'$help_anon' => DI::l10n()->t('Anonymous Login'),
'$help_anon_text' => DI::l10n()->t('Allows users to join chat rooms without XMPP accounts. Requires server support for anonymous authentication.'),
'$submit' => DI::l10n()->t('Save Settings'),
'$websocket_url' => ['websocket_url', DI::l10n()->t('WebSocket URL'), DI::config()->get('xmppchat', 'websocket_url'), DI::l10n()->t('XMPP WebSocket endpoint (e.g., wss://xmpp.example.org:5281/xmpp-websocket)')],
'$bosh_url' => ['bosh_url', DI::l10n()->t('BOSH URL'), DI::config()->get('xmppchat', 'bosh_url'), DI::l10n()->t('XMPP BOSH endpoint for fallback (e.g., https://xmpp.example.org/http-bind)')],
'$domain' => ['domain', DI::l10n()->t('XMPP Domain'), DI::config()->get('xmppchat', 'domain'), DI::l10n()->t('XMPP server domain (e.g., example.org)')],
'$auto_login' => ['auto_login', DI::l10n()->t('Auto-Login'), DI::config()->get('xmppchat', 'auto_login', false), DI::l10n()->t('Attempt automatic login using Friendica username (requires matching XMPP accounts)')],
'$enable_mam' => ['enable_mam', DI::l10n()->t('Enable Message Archive'), DI::config()->get('xmppchat', 'enable_mam', true), DI::l10n()->t('Enable XEP-0313 Message Archive Management for chat history')],
'$enable_smacks' => ['enable_smacks', DI::l10n()->t('Enable Stream Management'), DI::config()->get('xmppchat', 'enable_smacks', true), DI::l10n()->t('Enable XEP-0198 Stream Management for reliable connections')],
'$enable_omemo' => ['enable_omemo', DI::l10n()->t('Enable OMEMO Encryption'), DI::config()->get('xmppchat', 'enable_omemo', false), DI::l10n()->t('Enable XEP-0384 OMEMO end-to-end encryption')],
'$allow_anonymous' => ['allow_anonymous', DI::l10n()->t('Allow Anonymous Login'), DI::config()->get('xmppchat', 'allow_anonymous', false), DI::l10n()->t('Allow users to join chat rooms without authentication')],
'$default_muc' => ['default_muc', DI::l10n()->t('Default Chat Room'), DI::config()->get('xmppchat', 'default_muc'), DI::l10n()->t('Auto-join this MUC room on login (e.g., lobby@conference.example.org)')],
]);
}
function xmppchat_addon_settings(array &$data)
{
if (!DI::userSession()->getLocalUserId()) {
return;
}
$uid = DI::userSession()->getLocalUserId();
$custom_jid = DI::pConfig()->get($uid, 'xmppchat', 'custom_jid');
$custom_password = DI::pConfig()->get($uid, 'xmppchat', 'custom_password');
$use_custom = DI::pConfig()->get($uid, 'xmppchat', 'use_custom', false);
$user_enabled = DI::pConfig()->get($uid, 'xmppchat', 'enabled', DI::config()->get('xmppchat', 'enabled', false));
$custom_websocket = DI::pConfig()->get($uid, 'xmppchat', 'custom_websocket_url');
$custom_bosh = DI::pConfig()->get($uid, 'xmppchat', 'custom_bosh_url');
$t = Renderer::getMarkupTemplate('settings.tpl', 'addon/xmppchat/');
$html = Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('XMPP Chat Settings'),
'$submit' => DI::l10n()->t('Save Settings'),
'$info' => DI::l10n()->t('You can connect with any XMPP account from any server. Leave password empty to keep the existing one.'),
'$security_title' => DI::l10n()->t('Security Note'),
'$security_text' => DI::l10n()->t('Your password is stored encrypted on the server. We recommend using a separate password for XMPP.'),
'$user_enabled' => ['enabled', DI::l10n()->t('Enable XMPP Chat'), $user_enabled, DI::l10n()->t('Show the chat widget for your account')],
'$use_custom' => ['use_custom', DI::l10n()->t('Use Custom XMPP Account'), $use_custom, DI::l10n()->t('Enable to use your own XMPP account from any server')],
'$custom_jid' => ['custom_jid', DI::l10n()->t('XMPP Address (JID)'), $custom_jid, DI::l10n()->t('Your full XMPP address (e.g., user@example.org or user@other-server.com)')],
'$custom_password' => ['custom_password', DI::l10n()->t('XMPP Password'), '', DI::l10n()->t('Your XMPP account password (stored encrypted)')],
'$custom_websocket' => ['custom_websocket_url', DI::l10n()->t('WebSocket URL'), $custom_websocket, DI::l10n()->t('XMPP WebSocket endpoint for your account (e.g., wss://xmpp.example.org:5281/xmpp-websocket)')],
'$custom_bosh' => ['custom_bosh_url', DI::l10n()->t('BOSH URL'), $custom_bosh, DI::l10n()->t('XMPP BOSH endpoint for your account (e.g., https://xmpp.example.org/http-bind)')],
]);
$data = [
'addon' => 'xmppchat',
'title' => DI::l10n()->t('XMPP Chat'),
'html' => $html,
];
}
function xmppchat_addon_settings_post(array &$b)
{
if (!DI::userSession()->getLocalUserId()) {
return;
}
$uid = DI::userSession()->getLocalUserId();
if (!empty($b['xmppchat-submit'])) {
DI::pConfig()->set($uid, 'xmppchat', 'enabled', !empty($b['enabled']));
DI::pConfig()->set($uid, 'xmppchat', 'use_custom', !empty($b['use_custom']));
DI::pConfig()->set($uid, 'xmppchat', 'custom_jid', trim($b['custom_jid'] ?? ''));
DI::pConfig()->set($uid, 'xmppchat', 'custom_websocket_url', trim($b['custom_websocket_url'] ?? ''));
DI::pConfig()->set($uid, 'xmppchat', 'custom_bosh_url', trim($b['custom_bosh_url'] ?? ''));
// Only update password if provided
if (!empty($b['custom_password'])) {
// Note: In production, use proper encryption (e.g., openssl_encrypt)
// For now, store base64-encoded (NOT SECURE - just for demonstration)
$encrypted_password = base64_encode($b['custom_password']);
DI::pConfig()->set($uid, 'xmppchat', 'custom_password', $encrypted_password);
}
}
}
function xmppchat_load_config(ConfigFileManager $loader)
{
DI::appHelper()->getConfigCache()->load($loader->loadAddonConfig('xmppchat'), \Friendica\Core\Config\ValueObject\Cache::SOURCE_STATIC);
}
function xmppchat_footer(string &$body)
{
DI::logger()->debug("xmppchat: footer hook called");
// Only show the widget for authenticated users who enabled it in their settings
if (!DI::userSession()->isAuthenticated()) {
return;
}
$uid = DI::userSession()->getLocalUserId();
$user_enabled = DI::pConfig()->get($uid, 'xmppchat', 'enabled', false);
if (!$user_enabled) {
return;
}
// Load global features/settings
$domain = DI::config()->get('xmppchat', 'domain');
$auto_login = DI::config()->get('xmppchat', 'auto_login', false);
$enable_mam = DI::config()->get('xmppchat', 'enable_mam', true);
$enable_smacks = DI::config()->get('xmppchat', 'enable_smacks', true);
$enable_omemo = DI::config()->get('xmppchat', 'enable_omemo', false);
$allow_anonymous = DI::config()->get('xmppchat', 'allow_anonymous', false);
$default_muc = DI::config()->get('xmppchat', 'default_muc');
// Determine whether to use custom user endpoints or global ones
$use_custom = DI::pConfig()->get($uid, 'xmppchat', 'use_custom', false);
$jid = '';
$password = '';
$websocket_url = null;
$bosh_url = null;
if ($use_custom) {
// User explicitly chose a custom account: use their endpoints and credentials only
$websocket_url = DI::pConfig()->get($uid, 'xmppchat', 'custom_websocket_url');
$bosh_url = DI::pConfig()->get($uid, 'xmppchat', 'custom_bosh_url');
$jid = DI::pConfig()->get($uid, 'xmppchat', 'custom_jid');
$encrypted_password = DI::pConfig()->get($uid, 'xmppchat', 'custom_password');
if ($encrypted_password) {
$password = base64_decode($encrypted_password);
}
// If the user selected a custom account but didn't provide endpoints, don't attempt to connect
if (empty($websocket_url) && empty($bosh_url)) {
DI::logger()->warning("xmppchat: user $uid selected custom XMPP account but no websocket or bosh URL provided");
return;
}
} else {
// Use global endpoints
$websocket_url = DI::config()->get('xmppchat', 'websocket_url');
$bosh_url = DI::config()->get('xmppchat', 'bosh_url');
if (empty($websocket_url) && empty($bosh_url)) {
DI::logger()->warning("xmppchat: No websocket_url or bosh_url configured globally");
return;
}
// Auto-login using Friendica username if configured
if ($auto_login) {
$nickname = DI::session()->get('nickname');
if ($nickname && $domain) {
$jid = $nickname . '@' . $domain;
}
}
}
$tpl = Renderer::getMarkupTemplate('xmppchat.tpl', 'addon/xmppchat/');
$chat_html = Renderer::replaceMacros($tpl, [
'$websocket_url' => $websocket_url,
'$bosh_url' => $bosh_url,
'$jid' => $jid,
'$password' => $password,
'$auto_login' => $auto_login,
'$allow_anonymous' => $allow_anonymous,
'$enable_mam' => $enable_mam ? 'true' : 'false',
'$enable_smacks' => $enable_smacks ? 'true' : 'false',
'$enable_omemo' => $enable_omemo ? 'true' : 'false',
'$default_muc' => $default_muc,
]);
$body .= $chat_html;
}