kolab_chat: Add direct-message notifications, use one token per session
This commit is contained in:
parent
4fc313aaed
commit
71fed5584b
5 changed files with 511 additions and 53 deletions
|
@ -11,16 +11,14 @@
|
|||
|
||||
ProxyPreserveHost Off
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} /api/v[0-9]+/(users/)?websocket [NC,OR]
|
||||
RewriteCond %{REQUEST_URI} (/mattermost)?/api/v[0-9]+/(users/)?websocket [NC,OR]
|
||||
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
|
||||
RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
|
||||
RewriteRule .* ws://mattermost.example.com:8065%{REQUEST_URI} [P,QSA,L]
|
||||
ProxyPass /mattermost http://mattermost.example.com:8065
|
||||
ProxyPass /static http://mattermost.example.com:8065/static
|
||||
ProxyPass /help http://mattermost.example.com:8065/help
|
||||
ProxyPass /api http://mattermost.example.com:8065/api
|
||||
ProxyPass /api/v4/websocket ws://mattermost.example.com:8065/api/v4/websocket
|
||||
ProxyPass /api/v4/users/websocket ws://mattermost.example.com:8065/api/v4/users/websocket
|
||||
RewriteRule (/mattermost)?(/api/v[0-9]+/(users/)?websocket) ws://mattermost.example.com:8065$2 [P,QSA,L]
|
||||
ProxyPass /mattermost http://mattermost.example.com:8065
|
||||
ProxyPass /static http://mattermost.example.com:8065/static
|
||||
ProxyPass /help http://mattermost.example.com:8065/help
|
||||
ProxyPass /api http://mattermost.example.com:8065/api
|
||||
|
||||
2. Enabling CORS connections in Mattermost config: AllowCorsFrom:"*"
|
||||
*/
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
class kolab_chat_mattermost
|
||||
{
|
||||
public $rc;
|
||||
public $plugin;
|
||||
private $rc;
|
||||
private $plugin;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -41,62 +41,193 @@ class kolab_chat_mattermost
|
|||
/**
|
||||
* Returns location of the chat app
|
||||
*
|
||||
* @param bool $websocket Return websocket URL
|
||||
*
|
||||
* @return string The chat app location
|
||||
*/
|
||||
public function url()
|
||||
public function url($websocket = false)
|
||||
{
|
||||
return rtrim($this->rc->config->get('kolab_chat_url'), '/');
|
||||
$url = rtrim($this->rc->config->get('kolab_chat_url'), '/');
|
||||
|
||||
if ($websocket) {
|
||||
$url = str_replace(array('http://', 'https://'), array('ws://', 'wss://'), $url);
|
||||
$url .= '/api/v4/websocket';
|
||||
}
|
||||
else if ($this->rc->action == 'index' && $this->rc->task == 'kolab-chat') {
|
||||
if (($channel = rcube_utils::get_input_value('_channel', rcube_utils::INPUT_GET))
|
||||
&& ($channel = $this->get_channel($channel))
|
||||
) {
|
||||
// FIXME: This does not work yet because team_id is empty for direct-message channels
|
||||
$url .= '/' . urlencode($channel['team_name']) . '/channels/' . urlencode($channel['id']);
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the user and sets cookies to auto-login the user
|
||||
* Add/register UI elements
|
||||
*/
|
||||
public function ui()
|
||||
{
|
||||
if ($this->rc->task != 'kolab-chat') {
|
||||
$this->plugin->include_script("js/mattermost.js");
|
||||
$this->plugin->add_label('openchat', 'directmessage');
|
||||
}
|
||||
else if ($this->get_token()) {
|
||||
rcube_utils::setcookie('MMUSERID', $_SESSION['mattermost'][0], 0, false);
|
||||
rcube_utils::setcookie('MMAUTHTOKEN', $_SESSION['mattermost'][1], 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver specific actions handler
|
||||
*/
|
||||
public function action()
|
||||
{
|
||||
$result = array(
|
||||
'url' => $this->url(true),
|
||||
'token' => $this->get_token(),
|
||||
);
|
||||
|
||||
echo rcube_output::json_serialize($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Mattermost session token
|
||||
* Note: This works only if the user/pass is the same in Kolab and Mattermost
|
||||
*
|
||||
* @param string $user Username
|
||||
* @param string $pass Password
|
||||
* @return string Session token
|
||||
*/
|
||||
public function authenticate($user, $pass)
|
||||
protected function get_token()
|
||||
{
|
||||
$url = $this->url() . '/api/v4/users/login';
|
||||
$user = $_SESSION['username'];
|
||||
$pass = $this->rc->decrypt($_SESSION['password']);
|
||||
|
||||
$config = array(
|
||||
// Use existing token if still valid
|
||||
if (!empty($_SESSION['mattermost'])) {
|
||||
$user_id = $_SESSION['mattermost'][0];
|
||||
$token = $_SESSION['mattermost'][1];
|
||||
|
||||
try {
|
||||
$request = $this->get_api_request('GET', '/api/v4/users/me');
|
||||
$request->setHeader('Authorization', "Bearer $token");
|
||||
|
||||
$response = $request->send();
|
||||
$status = $response->getStatus();
|
||||
|
||||
if ($status != 200) {
|
||||
$token = null;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
$token = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Request a new session token
|
||||
if (empty($token)) {
|
||||
$request = $this->get_api_request('POST', '/api/v4/users/login');
|
||||
$request->setBody(json_encode(array(
|
||||
'login_id' => $user,
|
||||
'password' => $pass,
|
||||
)));
|
||||
|
||||
// send request to the API, get token and user ID
|
||||
try {
|
||||
$response = $request->send();
|
||||
$status = $response->getStatus();
|
||||
$token = $response->getHeader('Token');
|
||||
$body = json_decode($response->getBody(), true);
|
||||
|
||||
if ($status == 200) {
|
||||
$user_id = $body['id'];
|
||||
}
|
||||
else if (is_array($body) && $body['message']) {
|
||||
throw new Exception($body['message']);
|
||||
}
|
||||
else {
|
||||
throw new Exception("Failed to authenticate the chat user ($user). Status: $status");
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($user_id && $token) {
|
||||
$_SESSION['mattermost'] = array($user_id, $token);
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Mattermost channel info
|
||||
*
|
||||
* @param string $channel_id Channel ID
|
||||
*
|
||||
* @return array Channel information
|
||||
*/
|
||||
protected function get_channel($channel_id)
|
||||
{
|
||||
$token = $this->get_token();
|
||||
|
||||
if ($token) {
|
||||
$channel = $this->api_get('/api/v4/channels/' . urlencode($channel_id), $token);
|
||||
}
|
||||
|
||||
if (is_array($channel) && !empty($channel['team_id'])) {
|
||||
if ($team = $this->api_get('/api/v4/teams/' . urlencode($channel['team_id']), $token)) {
|
||||
$channel['team_name'] = $team['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HTTP/Request2 instance for Mattermost API connection
|
||||
*/
|
||||
protected function get_api_request($type, $path)
|
||||
{
|
||||
$url = rtrim($this->rc->config->get('kolab_chat_url'), '/');
|
||||
$defaults = array(
|
||||
'store_body' => true,
|
||||
'follow_redirects' => true,
|
||||
);
|
||||
|
||||
$config = array_merge($config, (array) $this->rc->config->get('kolab_chat_http_request'));
|
||||
$request = libkolab::http_request($url, 'POST', $config);
|
||||
$config = array_merge($defaults, (array) $this->rc->config->get('kolab_chat_http_request'));
|
||||
|
||||
$request->setBody(json_encode(array(
|
||||
'login_id' => $user,
|
||||
'password' => $pass,
|
||||
)));
|
||||
return libkolab::http_request($url . $path, $type, $config);
|
||||
}
|
||||
|
||||
// send request to the API, get token and user ID
|
||||
try {
|
||||
$response = $request->send();
|
||||
$status = $response->getStatus();
|
||||
$token = $response->getHeader('Token');
|
||||
$body = json_decode($response->getBody(), true);
|
||||
|
||||
if ($status == 200) {
|
||||
$user_id = $body['id'];
|
||||
}
|
||||
else if (is_array($body) && $body['message']) {
|
||||
throw new Exception($body['message']);
|
||||
}
|
||||
else {
|
||||
throw new Exception("Failed to authenticate the chat user ($user). Status: $status");
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
/**
|
||||
* Call API GET command
|
||||
*/
|
||||
protected function api_get($path, $token = null)
|
||||
{
|
||||
if (!$token) {
|
||||
$token = $this->get_token();
|
||||
}
|
||||
|
||||
// Set cookies
|
||||
if ($user_id && $token) {
|
||||
rcube_utils::setcookie('MMUSERID', $user_id, 0, false);
|
||||
rcube_utils::setcookie('MMAUTHTOKEN', $token, 0, false);
|
||||
if ($token) {
|
||||
try {
|
||||
$request = $this->get_api_request('GET', $path);
|
||||
$request->setHeader('Authorization', "Bearer $token");
|
||||
|
||||
$response = $request->send();
|
||||
$status = $response->getStatus();
|
||||
$body = $response->getBody();
|
||||
|
||||
if ($status == 200) {
|
||||
return json_decode($body, true);
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
315
plugins/kolab_chat/js/mattermost.js
Normal file
315
plugins/kolab_chat/js/mattermost.js
Normal file
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* Mattermost driver
|
||||
* Websocket code based on https://github.com/mattermost/mattermost-redux/master/client/websocket_client.js
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@kolabsys.com>
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this file.
|
||||
*
|
||||
* Copyright (C) 2015-2018, Kolab Systems AG <contact@kolabsys.com>
|
||||
* Copyright (C) 2015-2018, Mattermost, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice
|
||||
* for the JavaScript code in this file.
|
||||
*/
|
||||
|
||||
var MAX_WEBSOCKET_FAILS = 7;
|
||||
var MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
var MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
function MMWebSocketClient()
|
||||
{
|
||||
var Socket;
|
||||
|
||||
this.conn = null;
|
||||
this.connectionUrl = null;
|
||||
this.token = null;
|
||||
this.sequence = 1;
|
||||
this.connectFailCount = 0;
|
||||
this.eventCallback = null;
|
||||
this.firstConnectCallback = null;
|
||||
this.reconnectCallback = null;
|
||||
this.errorCallback = null;
|
||||
this.closeCallback = null;
|
||||
this.connectingCallback = null;
|
||||
this.dispatch = null;
|
||||
this.getState = null;
|
||||
this.stop = false;
|
||||
this.platform = '';
|
||||
|
||||
this.initialize = function(token, dispatch, getState, opts)
|
||||
{
|
||||
var forceConnection = opts.forceConnection || true,
|
||||
webSocketConnector = opts.webSocketConnector || WebSocket,
|
||||
connectionUrl = opts.connectionUrl,
|
||||
platform = opts.platform,
|
||||
self = this;
|
||||
|
||||
if (platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
if (forceConnection) {
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (self.conn) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionUrl == null) {
|
||||
console.log('websocket must have connection url');
|
||||
reject('websocket must have connection url');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dispatch) {
|
||||
console.log('websocket must have a dispatch');
|
||||
reject('websocket must have a dispatch');
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + connectionUrl);
|
||||
}
|
||||
|
||||
Socket = webSocketConnector;
|
||||
if (self.connectingCallback) {
|
||||
self.connectingCallback(dispatch, getState);
|
||||
}
|
||||
|
||||
var regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/;
|
||||
var captured = (regex).exec(connectionUrl);
|
||||
var origin;
|
||||
|
||||
if (captured) {
|
||||
origin = captured[0];
|
||||
|
||||
if (platform === 'android') {
|
||||
// this is done cause for android having the port 80 or 443 will fail the connection
|
||||
// the websocket will append them
|
||||
var split = origin.split(':');
|
||||
var port = split[2];
|
||||
if (port === '80' || port === '443') {
|
||||
origin = split[0] + ':' + split[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway
|
||||
console.warn('websocket failed to parse origin from ' + connectionUrl);
|
||||
}
|
||||
|
||||
self.conn = new Socket(connectionUrl, [], {headers: {origin}});
|
||||
self.connectionUrl = connectionUrl;
|
||||
self.token = token;
|
||||
self.dispatch = dispatch;
|
||||
self.getState = getState;
|
||||
|
||||
self.conn.onopen = function() {
|
||||
if (token && platform !== 'android') {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
self.sendMessage('authentication_challenge', {token});
|
||||
}
|
||||
|
||||
if (self.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection');
|
||||
if (self.reconnectCallback) {
|
||||
self.reconnectCallback(self.dispatch, self.getState);
|
||||
}
|
||||
} else if (self.firstConnectCallback) {
|
||||
self.firstConnectCallback(self.dispatch, self.getState);
|
||||
}
|
||||
|
||||
self.connectFailCount = 0;
|
||||
resolve();
|
||||
};
|
||||
|
||||
self.conn.onclose = function() {
|
||||
self.conn = null;
|
||||
self.sequence = 1;
|
||||
|
||||
if (self.connectFailCount === 0) {
|
||||
console.log('websocket closed');
|
||||
}
|
||||
|
||||
self.connectFailCount++;
|
||||
|
||||
if (self.closeCallback) {
|
||||
self.closeCallback(self.connectFailCount, self.dispatch, self.getState);
|
||||
}
|
||||
|
||||
var retryTime = MIN_WEBSOCKET_RETRY_TIME;
|
||||
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (self.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * self.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if (self.stop) {
|
||||
return;
|
||||
}
|
||||
self.initialize(token, dispatch, getState, Object.assign({}, opts, {forceConnection: true}));
|
||||
},
|
||||
retryTime
|
||||
);
|
||||
};
|
||||
|
||||
self.conn.onerror = function(evt) {
|
||||
if (self.connectFailCount <= 1) {
|
||||
console.log('websocket error');
|
||||
console.log(evt);
|
||||
}
|
||||
|
||||
if (self.errorCallback) {
|
||||
self.errorCallback(evt, self.dispatch, self.getState);
|
||||
}
|
||||
};
|
||||
|
||||
self.conn.onmessage = function(evt) {
|
||||
var msg = JSON.parse(evt.data);
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg);
|
||||
}
|
||||
} else if (self.eventCallback) {
|
||||
self.eventCallback(msg, self.dispatch, self.getState);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.setConnectingCallback = function(callback)
|
||||
{
|
||||
this.connectingCallback = callback;
|
||||
}
|
||||
|
||||
this.setEventCallback = function(callback)
|
||||
{
|
||||
this.eventCallback = callback;
|
||||
}
|
||||
|
||||
this.setFirstConnectCallback = function(callback)
|
||||
{
|
||||
this.firstConnectCallback = callback;
|
||||
}
|
||||
|
||||
this.setReconnectCallback = function(callback)
|
||||
{
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
|
||||
this.setErrorCallback = function(callback)
|
||||
{
|
||||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
this.setCloseCallback = function(callback) {
|
||||
this.closeCallback = callback;
|
||||
}
|
||||
|
||||
this.close = function(stop)
|
||||
{
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.sequence = 1;
|
||||
|
||||
if (this.conn && this.conn.readyState === Socket.OPEN) {
|
||||
this.conn.onclose = function(){};
|
||||
this.conn.close();
|
||||
this.conn = null;
|
||||
console.log('websocket closed');
|
||||
}
|
||||
}
|
||||
|
||||
this.sendMessage = function(action, data)
|
||||
{
|
||||
var msg = {
|
||||
action,
|
||||
seq: this.sequence++,
|
||||
data
|
||||
};
|
||||
|
||||
if (this.conn && this.conn.readyState === Socket.OPEN) {
|
||||
this.conn.send(JSON.stringify(msg));
|
||||
} else if (!this.conn || this.conn.readyState === Socket.CLOSED) {
|
||||
this.conn = null;
|
||||
this.initialize(this.token, this.dispatch, this.getState, {forceConnection: true, platform: this.platform});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts websocket connection with Mattermost
|
||||
*/
|
||||
function mattermost_websocket_init(url, token)
|
||||
{
|
||||
var api = new MMWebSocketClient();
|
||||
|
||||
api.setEventCallback(function(e) {
|
||||
mattermost_event_handler(e);
|
||||
});
|
||||
|
||||
api.initialize(token, {}, {}, {connectionUrl: url});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles websocket events
|
||||
*/
|
||||
function mattermost_event_handler(event)
|
||||
{
|
||||
// We're interested only in direct messages for now
|
||||
if (event.event == 'posted' && event.data.channel_type == 'D') {
|
||||
var user = event.data.sender_name,
|
||||
channel_id = event.broadcast.channel_id,
|
||||
// channel_name = event.data.channel_display_name,
|
||||
msg = rcmail.gettext('kolab_chat.directmessage').replace('$u', user),
|
||||
link = $('<a>').text(rcmail.gettext('kolab_chat.openchat'))
|
||||
.attr('href', '?_task=kolab-chat&_channel=' + urlencode(channel_id));
|
||||
|
||||
if (rcmail.env.kolab_chat_extwin) {
|
||||
link.attr('target', '_blank').attr('href', link.attr('href') + '&redirect=1');
|
||||
}
|
||||
|
||||
msg = $('<p>').text(msg + ' ').append(link).html();
|
||||
|
||||
// FIXME: Should we display it indefinitely?
|
||||
rcmail.display_message(msg, 'notice chat', 10 * 60 * 1000, 'chat-user-' + user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.WebSocket && window.rcmail && rcmail.addEventListener('init', function() {
|
||||
// Use ajax to get the token for websocket connection
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '?_task=kolab-chat&_action=action&_get=token',
|
||||
success: function(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data && data.token) {
|
||||
// rcmail.set_env({mattermost_url: data.url, mattermost_token: data.token});
|
||||
mattermost_websocket_init(data.url, data.token);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -23,10 +23,9 @@
|
|||
|
||||
class kolab_chat extends rcube_plugin
|
||||
{
|
||||
public $task = '^(?!login|logout).*$';
|
||||
public $noajax = true;
|
||||
public $rc;
|
||||
public $task = '^(?!login|logout).*$';
|
||||
|
||||
private $rc;
|
||||
private $driver;
|
||||
|
||||
|
||||
|
@ -66,8 +65,9 @@ class kolab_chat extends rcube_plugin
|
|||
// Register UI end-points
|
||||
$this->register_task('kolab-chat');
|
||||
$this->register_action('index', array($this, 'ui'));
|
||||
$this->register_action('action', array($this, 'action'));
|
||||
|
||||
if (!$this->rc->output->framed) {
|
||||
if ($this->rc->output->type == 'html' && !$this->rc->output->get_env('framed')) {
|
||||
$this->rc->output->set_env('kolab_chat_extwin', (bool) $extwin);
|
||||
$this->rc->output->add_script(
|
||||
"rcmail.addEventListener('beforeswitch-task', function(p) {
|
||||
|
@ -87,9 +87,11 @@ class kolab_chat extends rcube_plugin
|
|||
'label' => 'kolab_chat.chat',
|
||||
'type' => 'link',
|
||||
), 'taskbar');
|
||||
|
||||
$this->driver->ui();
|
||||
}
|
||||
|
||||
if ($args['task'] == 'settings') {
|
||||
if ($this->rc->output->type == 'html' && $args['task'] == 'settings') {
|
||||
// add hooks for Chat settings
|
||||
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
|
||||
$this->add_hook('preferences_list', array($this, 'preferences_list'));
|
||||
|
@ -104,7 +106,7 @@ class kolab_chat extends rcube_plugin
|
|||
function ui()
|
||||
{
|
||||
if ($this->driver) {
|
||||
$this->driver->authenticate($_SESSION['username'], $this->rc->decrypt($_SESSION['password']));
|
||||
$this->driver->ui();
|
||||
|
||||
$url = rcube::JQ($this->driver->url());
|
||||
|
||||
|
@ -127,6 +129,16 @@ class kolab_chat extends rcube_plugin
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for driver specific actions
|
||||
*/
|
||||
function action()
|
||||
{
|
||||
if ($this->driver) {
|
||||
$this->driver->action();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for preferences_sections_list hook.
|
||||
* Adds Chat settings section into preferences sections list.
|
||||
|
|
|
@ -10,3 +10,5 @@
|
|||
|
||||
$labels['chat'] = 'Chat';
|
||||
$labels['showinextwin'] = 'Open chat application in a new window';
|
||||
$labels['directmessage'] = 'A direct message from $u.';
|
||||
$labels['openchat'] = 'Open chat';
|
||||
|
|
Loading…
Add table
Reference in a new issue