2008-06-26 16:03:36 +01:00
#!/usr/bin/env php
2008-06-23 03:27:10 +01:00
< ? php
/*
* Laconica - a distributed open - source microblogging tool
* Copyright ( C ) 2008 , Controlez - Vous , 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 />.
*/
2008-07-16 06:49:32 +01:00
function xmppdaemon_error_handler ( $errno , $errstr , $errfile , $errline , $errcontext ) {
2008-07-16 07:47:36 +01:00
switch ( $errno ) {
case E_USER_ERROR :
2008-08-27 14:16:03 +01:00
echo " ERROR: [ $errno ] $errstr ( $errfile : $errline ) \n " ;
echo " Fatal error on line $errline in file $errfile " ;
echo " , PHP " . PHP_VERSION . " ( " . PHP_OS . " ) \n " ;
echo " Aborting... \n " ;
exit ( 1 );
break ;
2008-07-20 06:57:02 +01:00
2008-08-27 14:16:03 +01:00
case E_USER_WARNING :
echo " WARNING [ $errno ] $errstr ( $errfile : $errline ) \n " ;
break ;
2008-07-20 06:57:02 +01:00
2008-07-16 07:47:36 +01:00
case E_USER_NOTICE :
2008-08-27 14:16:03 +01:00
echo " NOTICE [ $errno ] $errstr ( $errfile : $errline ) \n " ;
break ;
2008-07-16 07:47:36 +01:00
}
2008-07-20 06:57:02 +01:00
2008-07-16 07:47:36 +01:00
/* Don't execute PHP internal error handler */
return true ;
2008-07-16 06:49:32 +01:00
}
set_error_handler ( 'xmppdaemon_error_handler' );
2008-06-23 04:05:23 +01:00
# Abort if called from a web server
if ( isset ( $_SERVER ) && array_key_exists ( 'REQUEST_METHOD' , $_SERVER )) {
print " This script must be run from the command line \n " ;
exit ();
}
2008-08-13 16:46:03 +01:00
define ( 'INSTALLDIR' , realpath ( dirname ( __FILE__ ) . '/..' ));
2008-06-23 03:27:10 +01:00
define ( 'LACONICA' , true );
2008-08-22 19:59:55 +01:00
define ( 'CLAIM_TIMEOUT' , 100000 );
2008-06-23 03:27:10 +01:00
require_once ( INSTALLDIR . '/lib/common.php' );
2008-06-26 08:07:03 +01:00
require_once ( INSTALLDIR . '/lib/jabber.php' );
2008-06-23 03:27:10 +01:00
2008-06-26 16:12:02 +01:00
# This is kind of clunky; we create a class to call the global functions
# in jabber.php, which create a new XMPP class. A more elegant (?) solution
# might be to use make this a subclass of XMPP.
2008-06-23 03:27:10 +01:00
class XMPPDaemon {
2008-06-23 04:02:59 +01:00
2008-06-26 08:59:20 +01:00
function XMPPDaemon ( $resource = NULL ) {
2008-06-26 16:03:36 +01:00
static $attrs = array ( 'server' , 'port' , 'user' , 'password' , 'host' );
2008-06-23 04:09:30 +01:00
foreach ( $attrs as $attr )
{
2008-06-23 03:27:10 +01:00
$this -> $attr = common_config ( 'xmpp' , $attr );
}
2008-06-26 08:59:20 +01:00
if ( $resource ) {
$this -> resource = $resource ;
2008-06-26 16:03:36 +01:00
} else {
$this -> resource = common_config ( 'xmpp' , 'resource' ) . 'daemon' ;
2008-06-26 08:59:20 +01:00
}
2008-06-26 16:03:36 +01:00
$this -> log ( LOG_INFO , " { $this -> user } @ { $this -> server } / { $this -> resource } " );
2008-06-23 03:27:10 +01:00
}
function connect () {
2008-07-05 21:24:12 +01:00
2008-06-26 16:03:36 +01:00
$connect_to = ( $this -> host ) ? $this -> host : $this -> server ;
$this -> log ( LOG_INFO , " Connecting to $connect_to on port $this->port " );
$this -> conn = jabber_connect ( $this -> resource );
2008-06-23 03:27:10 +01:00
if ( ! $this -> conn ) {
return false ;
}
2008-07-20 06:57:02 +01:00
2008-07-05 19:47:29 +01:00
return ! $this -> conn -> isDisconnected ();
2008-06-23 03:27:10 +01:00
}
2008-06-23 04:02:59 +01:00
2008-06-23 03:27:10 +01:00
function handle () {
2008-07-05 18:28:37 +01:00
static $parts = array ( 'message' , 'presence' ,
'end_stream' , 'session_start' );
2008-07-05 21:24:12 +01:00
while ( ! $this -> conn -> isDisconnected ()) {
2008-07-05 18:28:37 +01:00
$payloads = $this -> conn -> processUntil ( $parts , 10 );
if ( $payloads ) {
foreach ( $payloads as $event ) {
$pl = $event [ 1 ];
switch ( $event [ 0 ]) {
case 'message' :
$this -> handle_message ( $pl );
break ;
case 'presence' :
$this -> handle_presence ( $pl );
break ;
case 'session_start' :
$this -> handle_session ( $pl );
break ;
}
2008-06-23 03:27:10 +01:00
}
}
2008-07-05 18:28:37 +01:00
$this -> broadcast_queue ();
2008-07-06 08:13:19 +01:00
$this -> confirmation_queue ();
2008-06-23 03:27:10 +01:00
}
}
2008-07-20 06:57:02 +01:00
2008-07-14 13:47:33 +01:00
function handle_session ( $pl ) {
# XXX what to do here?
return true ;
}
2008-07-20 06:57:02 +01:00
2008-06-26 19:11:44 +01:00
function get_user ( $from ) {
$user = User :: staticGet ( 'jabber' , jabber_normalize_jid ( $from ));
return $user ;
}
function get_confirmation ( $from ) {
$confirm = new Confirm_address ();
$confirm -> address = $from ;
$confirm -> address_type = 'jabber' ;
if ( $confirm -> find ( TRUE )) {
return $confirm ;
} else {
return NULL ;
}
}
2008-06-23 03:27:10 +01:00
function handle_message ( & $pl ) {
2008-06-23 23:36:41 +01:00
if ( $pl [ 'type' ] != 'chat' ) {
return ;
}
2008-08-25 18:31:05 +01:00
if ( mb_strlen ( $pl [ 'body' ]) == 0 ) {
2008-06-23 23:36:41 +01:00
return ;
}
2008-06-26 21:39:35 +01:00
$from = jabber_normalize_jid ( $pl [ 'from' ]);
$user = $this -> get_user ( $from );
2008-06-23 03:27:10 +01:00
if ( ! $user ) {
2008-07-05 18:28:37 +01:00
$this -> from_site ( $from , 'Unknown user; go to ' .
2008-07-04 21:17:16 +01:00
common_local_url ( 'imsettings' ) .
' to add your address to your account' );
2008-06-23 05:18:16 +01:00
$this -> log ( LOG_WARNING , 'Message from unknown user ' . $from );
2008-06-23 03:27:10 +01:00
return ;
}
if ( $this -> handle_command ( $user , $pl [ 'body' ])) {
return ;
2008-07-08 01:30:55 +01:00
} else if ( $this -> is_autoreply ( $pl [ 'body' ])) {
$this -> log ( LOG_INFO , 'Ignoring auto reply from ' . $from );
return ;
2008-08-06 01:21:34 +01:00
} else if ( $this -> is_otr ( $pl [ 'body' ])) {
$this -> log ( LOG_INFO , 'Ignoring OTR from ' . $from );
return ;
2008-06-23 03:27:10 +01:00
} else {
2008-08-25 18:31:05 +01:00
$len = mb_strlen ( $pl [ 'body' ]);
if ( $len > 140 ) {
$this -> from_site ( $from , 'Message too long - maximum is 140 characters, you sent ' . $len );
2008-08-18 22:30:31 +01:00
return ;
}
2008-06-23 03:27:10 +01:00
$this -> add_notice ( $user , $pl );
}
}
2008-07-08 01:30:55 +01:00
function is_autoreply ( $txt ) {
if ( preg_match ( '/[\[\(]?[Aa]uto-?[Rr]eply[\]\)]/' , $txt )) {
return true ;
} else {
return false ;
}
}
2008-07-20 06:57:02 +01:00
2008-08-06 01:21:34 +01:00
function is_otr ( $txt ) {
if ( preg_match ( '/^\?OTR/' , $txt )) {
return true ;
} else {
return false ;
}
}
2008-08-27 14:16:03 +01:00
2008-07-04 21:17:16 +01:00
function from_site ( $address , $msg ) {
$text = '[' . common_config ( 'site' , 'name' ) . '] ' . $msg ;
jabber_send_message ( $address , $text );
}
2008-07-05 18:28:37 +01:00
2008-06-23 03:27:10 +01:00
function handle_command ( $user , $body ) {
# XXX: localise
2008-08-22 20:10:32 +01:00
$p = explode ( ' ' , $body );
if ( count ( $p ) > 2 )
return false ;
switch ( $p [ 0 ]) {
case 'help' :
if ( count ( $p ) != 1 )
return false ;
$this -> from_site ( $user -> jabber , " Commands: \n on - turn on notifications \n off - turn off notifications \n help - show this help \n sub - subscribe to user \n unsub - unsubscribe from user " );
return true ;
2008-06-23 03:27:10 +01:00
case 'on' :
2008-08-22 20:10:32 +01:00
if ( count ( $p ) != 1 )
return false ;
2008-06-23 03:27:10 +01:00
$this -> set_notify ( $user , true );
2008-07-04 21:17:16 +01:00
$this -> from_site ( $user -> jabber , 'notifications on' );
2008-06-23 03:27:10 +01:00
return true ;
case 'off' :
2008-08-22 20:10:32 +01:00
if ( count ( $p ) != 1 )
return false ;
2008-06-23 03:27:10 +01:00
$this -> set_notify ( $user , false );
2008-07-04 21:17:16 +01:00
$this -> from_site ( $user -> jabber , 'notifications off' );
2008-06-23 03:27:10 +01:00
return true ;
2008-08-22 20:10:32 +01:00
case 'sub' :
if ( count ( $p ) == 1 ) {
$this -> from_site ( $user -> jabber , 'Specify the name of the user to subscribe to' );
return true ;
}
$result = subs_subscribe_user ( $user , $p [ 1 ]);
if ( $result == 'true' )
$this -> from_site ( $user -> jabber , 'Subscribed to ' . $p [ 1 ]);
else
$this -> from_site ( $user -> jabber , $result );
return true ;
case 'unsub' :
if ( count ( $p ) == 1 ) {
$this -> from_site ( $user -> jabber , 'Specify the name of the user to unsubscribe from' );
return true ;
}
$result = subs_unsubscribe_user ( $user , $p [ 1 ]);
if ( $result == 'true' )
$this -> from_site ( $user -> jabber , 'Unsubscribed from ' . $p [ 1 ]);
else
$this -> from_site ( $user -> jabber , $result );
return true ;
2008-06-23 03:27:10 +01:00
default :
return false ;
}
}
2008-06-23 03:38:59 +01:00
function set_notify ( & $user , $notify ) {
$orig = clone ( $user );
$user -> jabbernotify = $notify ;
$result = $user -> update ( $orig );
2008-08-19 08:13:21 +01:00
if ( ! $result ) {
2008-06-23 03:38:59 +01:00
$last_error = & PEAR :: getStaticProperty ( 'DB_DataObject' , 'lastError' );
2008-07-18 20:08:35 +01:00
$this -> log ( LOG_ERR ,
2008-06-23 03:38:59 +01:00
'Could not set notify flag to ' . $notify .
2008-06-23 04:02:59 +01:00
' for user ' . common_log_objstring ( $user ) .
2008-06-23 03:38:59 +01:00
': ' . $last_error -> message );
} else {
$this -> log ( LOG_INFO ,
'User ' . $user -> nickname . ' set notify flag to ' . $notify );
}
}
2008-06-23 04:02:59 +01:00
2008-06-23 03:27:10 +01:00
function add_notice ( & $user , & $pl ) {
2008-08-18 03:55:49 +01:00
$notice = Notice :: saveNew ( $user -> id , trim ( mb_substr ( $pl [ 'body' ], 0 , 140 )), 'xmpp' );
2008-07-30 03:28:56 +01:00
if ( is_string ( $notice )) {
$this -> log ( LOG_ERR , $notice );
2008-06-23 03:27:10 +01:00
return ;
}
2008-07-04 08:29:09 +01:00
common_real_broadcast ( $notice );
2008-06-23 03:38:59 +01:00
$this -> log ( LOG_INFO ,
'Added notice ' . $notice -> id . ' from user ' . $user -> nickname );
2008-06-23 03:27:10 +01:00
}
2008-06-23 04:02:59 +01:00
2008-06-23 03:27:10 +01:00
function handle_presence ( & $pl ) {
2008-06-24 01:15:23 +01:00
$from = jabber_normalize_jid ( $pl [ 'from' ]);
2008-06-26 07:46:46 +01:00
switch ( $pl [ 'type' ]) {
2008-07-05 18:28:37 +01:00
case 'subscribe' :
# We let anyone subscribe
$this -> subscribed ( $from );
$this -> log ( LOG_INFO ,
'Accepted subscription from ' . $from );
break ;
case 'subscribed' :
case 'unsubscribed' :
case 'unsubscribe' :
$this -> log ( LOG_INFO ,
'Ignoring "' . $pl [ 'type' ] . '" from ' . $from );
break ;
default :
if ( ! $pl [ 'type' ]) {
$user = User :: staticGet ( 'jabber' , $from );
if ( ! $user ) {
2008-07-17 14:24:52 +01:00
$this -> log ( LOG_WARNING , 'Presence from unknown user ' . $from );
2008-07-05 18:28:37 +01:00
return ;
2008-06-26 07:46:46 +01:00
}
2008-07-05 18:28:37 +01:00
if ( $user -> updatefrompresence ) {
$this -> log ( LOG_INFO , 'Updating ' . $user -> nickname .
' status from presence.' );
$this -> add_notice ( $user , $pl );
}
}
break ;
2008-06-23 03:27:10 +01:00
}
}
2008-06-23 04:02:59 +01:00
2008-06-23 03:27:10 +01:00
function log ( $level , $msg ) {
common_log ( $level , 'XMPPDaemon(' . $this -> resource . '): ' . $msg );
}
2008-06-26 07:46:46 +01:00
function subscribed ( $to ) {
2008-06-26 16:03:36 +01:00
jabber_special_presence ( 'subscribed' , $to );
2008-06-26 07:46:46 +01:00
}
2008-06-26 16:03:36 +01:00
function set_status ( $status ) {
2008-06-26 16:12:02 +01:00
$this -> log ( LOG_INFO , 'Setting status to "' . $status . '"' );
2008-06-26 16:03:36 +01:00
jabber_send_presence ( $status );
2008-06-26 07:46:46 +01:00
}
2008-07-05 18:28:37 +01:00
function top_queue_item () {
$qi = new Queue_item ();
$qi -> orderBy ( 'created' );
$qi -> whereAdd ( 'claimed is NULL' );
$qi -> limit ( 1 );
$cnt = $qi -> find ( TRUE );
if ( $cnt ) {
# XXX: potential race condition
# can we force it to only update if claimed is still NULL
# (or old)?
$this -> log ( LOG_INFO , 'claiming queue item = ' . $qi -> notice_id );
$orig = clone ( $qi );
2008-08-25 19:23:38 +01:00
$qi -> claimed = common_sql_now ();
2008-07-05 18:28:37 +01:00
$result = $qi -> update ( $orig );
if ( $result ) {
$this -> log ( LOG_INFO , 'claim succeeded.' );
return $qi ;
} else {
$this -> log ( LOG_INFO , 'claim failed.' );
}
}
$qi = NULL ;
return NULL ;
}
function broadcast_queue () {
$this -> clear_old_claims ();
$this -> log ( LOG_INFO , 'checking for queued notices' );
do {
$qi = $this -> top_queue_item ();
if ( $qi ) {
2008-07-05 19:39:55 +01:00
$this -> log ( LOG_INFO , 'Got item enqueued ' . common_exact_date ( $qi -> created ));
2008-07-05 18:28:37 +01:00
$notice = Notice :: staticGet ( $qi -> notice_id );
if ( $notice ) {
$this -> log ( LOG_INFO , 'broadcasting notice ID = ' . $notice -> id );
# XXX: what to do if broadcast fails?
$result = common_real_broadcast ( $notice , $this -> is_remote ( $notice ));
if ( ! $result ) {
$this -> log ( LOG_WARNING , 'Failed broadcast for notice ID = ' . $notice -> id );
$orig = $qi ;
$qi -> claimed = NULL ;
$qi -> update ( $orig );
$this -> log ( LOG_WARNING , 'Abandoned claim for notice ID = ' . $notice -> id );
continue ;
}
$this -> log ( LOG_INFO , 'finished broadcasting notice ID = ' . $notice -> id );
$notice = NULL ;
} else {
$this -> log ( LOG_WARNING , 'queue item for notice that does not exist' );
}
$qi -> delete ();
}
} while ( $qi );
}
function clear_old_claims () {
$qi = new Queue_item ();
2008-07-06 10:03:09 +01:00
$qi -> claimed = NULL ;
2008-07-05 18:28:37 +01:00
$qi -> whereAdd ( 'now() - claimed > ' . CLAIM_TIMEOUT );
$qi -> update ( DB_DATAOBJECT_WHEREADD_ONLY );
}
function is_remote ( $notice ) {
$user = User :: staticGet ( $notice -> profile_id );
return ! $user ;
}
2008-07-20 06:57:02 +01:00
2008-07-06 04:57:07 +01:00
function confirmation_queue () {
2008-07-06 07:14:37 +01:00
# $this->clear_old_confirm_claims();
2008-07-06 04:57:07 +01:00
$this -> log ( LOG_INFO , 'checking for queued confirmations' );
do {
$confirm = $this -> next_confirm ();
if ( $confirm ) {
$this -> log ( LOG_INFO , 'Sending confirmation for ' . $confirm -> address );
$user = User :: staticGet ( $confirm -> user_id );
if ( ! $user ) {
$this -> log ( LOG_WARNING , 'Confirmation for unknown user ' . $confirm -> user_id );
continue ;
}
$success = jabber_confirm_address ( $confirm -> code ,
2008-07-06 07:14:37 +01:00
$user -> nickname ,
$confirm -> address );
2008-07-06 04:57:07 +01:00
if ( ! $success ) {
2008-07-18 20:08:35 +01:00
$this -> log ( LOG_ERR , 'Confirmation failed for ' . $confirm -> address );
2008-07-06 04:57:07 +01:00
# Just let the claim age out; hopefully things work then
continue ;
} else {
$this -> log ( LOG_INFO , 'Confirmation sent for ' . $confirm -> address );
# Mark confirmation sent
$original = clone ( $confirm );
2008-07-06 10:03:09 +01:00
$confirm -> sent = $confirm -> claimed ;
2008-07-06 04:57:07 +01:00
$result = $confirm -> update ( $original );
if ( ! $result ) {
2008-07-18 20:08:35 +01:00
$this -> log ( LOG_ERR , 'Cannot mark sent for ' . $confirm -> address );
2008-07-06 04:57:07 +01:00
# Just let the claim age out; hopefully things work then
continue ;
}
}
}
} while ( $confirm );
}
2008-07-20 06:57:02 +01:00
2008-07-06 04:57:07 +01:00
function next_confirm () {
$confirm = new Confirm_address ();
2008-07-06 07:14:37 +01:00
$confirm -> whereAdd ( 'claimed IS NULL' );
2008-07-06 10:03:09 +01:00
$confirm -> whereAdd ( 'sent IS NULL' );
2008-07-06 04:58:38 +01:00
# XXX: eventually we could do other confirmations in the queue, too
$confirm -> address_type = 'jabber' ;
2008-07-06 04:57:07 +01:00
$confirm -> orderBy ( 'modified DESC' );
$confirm -> limit ( 1 );
if ( $confirm -> find ( TRUE )) {
$this -> log ( LOG_INFO , 'Claiming confirmation for ' . $confirm -> address );
2008-07-06 10:03:09 +01:00
# working around some weird DB_DataObject behaviour
$confirm -> whereAdd ( '' ); # clears where stuff
$original = clone ( $confirm );
2008-08-25 19:23:38 +01:00
$confirm -> claimed = common_sql_now ();
2008-07-06 04:57:07 +01:00
$result = $confirm -> update ( $original );
if ( $result ) {
2008-07-06 10:03:09 +01:00
$this -> log ( LOG_INFO , 'Succeeded in claim! ' . $result );
2008-07-06 04:57:07 +01:00
return $confirm ;
} else {
$this -> log ( LOG_INFO , 'Failed in claim!' );
return false ;
}
}
return NULL ;
}
2008-07-20 06:57:02 +01:00
2008-07-06 04:57:07 +01:00
function clear_old_confirm_claims () {
$confirm = new Confirm ();
2008-07-06 10:03:09 +01:00
$confirm -> claimed = NULL ;
2008-07-06 04:57:07 +01:00
$confirm -> whereAdd ( 'now() - claimed > ' . CLAIM_TIMEOUT );
$confirm -> update ( DB_DATAOBJECT_WHEREADD_ONLY );
}
2008-07-20 06:57:02 +01:00
2008-06-23 03:27:10 +01:00
}
2008-08-18 04:30:49 +01:00
mb_internal_encoding ( 'UTF-8' );
2008-06-26 16:03:36 +01:00
$resource = ( $argc > 1 ) ? $argv [ 1 ] : NULL ;
$daemon = new XMPPDaemon ( $resource );
2008-06-23 03:27:10 +01:00
2008-06-23 04:01:50 +01:00
if ( $daemon -> connect ()) {
2008-06-26 16:03:36 +01:00
$daemon -> set_status ( " Send me a message to post a notice " );
2008-06-23 04:01:50 +01:00
$daemon -> handle ();
}
2008-06-26 16:12:02 +01:00
2008-06-23 03:27:10 +01:00
?>