]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/xmppdaemon.php
XMPP daemon that doesn't eat messages - note change to XMPPHP as well (Ticket #551)
[quix0rs-gnu-social.git] / scripts / xmppdaemon.php
1 #!/usr/bin/env php
2 <?php
3 /*
4  * Laconica - a distributed open-source microblogging tool
5  * Copyright (C) 2008, Controlez-Vous, Inc.
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 function xmppdaemon_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
22     switch ($errno) {
23      case E_USER_ERROR:
24                 echo "ERROR: [$errno] $errstr ($errfile:$errline)\n";
25                 echo "  Fatal error on line $errline in file $errfile";
26                 echo ", PHP " . PHP_VERSION . " (" . PHP_OS . ")\n";
27                 echo "Aborting...\n";
28                 exit(1);
29                 break;
30
31          case E_USER_WARNING:
32                 echo "WARNING [$errno] $errstr ($errfile:$errline)\n";
33                 break;
34
35      case E_USER_NOTICE:
36                 echo "NOTICE [$errno] $errstr ($errfile:$errline)\n";
37                 break;
38     }
39
40     /* Don't execute PHP internal error handler */
41     return true;
42 }
43
44 set_error_handler('xmppdaemon_error_handler');
45
46 # Abort if called from a web server
47 if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
48         print "This script must be run from the command line\n";
49         exit();
50 }
51
52 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
53 define('LACONICA', true);
54 define('CLAIM_TIMEOUT', 100000);
55
56 require_once(INSTALLDIR . '/lib/common.php');
57 require_once(INSTALLDIR . '/lib/jabber.php');
58
59 # This is kind of clunky; we create a class to call the global functions
60 # in jabber.php, which create a new XMPP class. A more elegant (?) solution
61 # might be to use make this a subclass of XMPP.
62
63 class XMPPDaemon {
64
65         function XMPPDaemon($resource=NULL) {
66                 static $attrs = array('server', 'port', 'user', 'password', 'host');
67
68                 foreach ($attrs as $attr)
69                 {
70                         $this->$attr = common_config('xmpp', $attr);
71                 }
72
73                 if ($resource) {
74                         $this->resource = $resource;
75                 } else {
76                         $this->resource = common_config('xmpp', 'resource') . 'daemon';
77                 }
78
79                 $this->log(LOG_INFO, "{$this->user}@{$this->server}/{$this->resource}");
80         }
81
82         function connect() {
83
84                 $connect_to = ($this->host) ? $this->host : $this->server;
85
86                 $this->log(LOG_INFO, "Connecting to $connect_to on port $this->port");
87
88                 $this->conn = jabber_connect($this->resource);
89
90                 if (!$this->conn) {
91                         return false;
92                 }
93
94                 return !$this->conn->isDisconnected();
95         }
96
97         function handle() {
98
99                 $this->conn->addEventHandler('message','handle_message',$this);
100                 $this->conn->addEventHandler('presence','handle_presence',$this);
101                 while(!$this->conn->isDisconnected()) {
102
103                         $this->conn->processTime(10);
104                         $this->broadcast_queue();
105                         $this->confirmation_queue();
106                 }
107         }
108
109
110         function get_user($from) {
111                 $user = User::staticGet('jabber', jabber_normalize_jid($from));
112                 return $user;
113         }
114
115         function get_confirmation($from) {
116                 $confirm = new Confirm_address();
117                 $confirm->address = $from;
118                 $confirm->address_type = 'jabber';
119                 if ($confirm->find(TRUE)) {
120                         return $confirm;
121                 } else {
122                         return NULL;
123                 }
124         }
125
126         function handle_message(&$pl) {
127                 if ($pl['type'] != 'chat') {
128                         return;
129                 }
130                 if (mb_strlen($pl['body']) == 0) {
131                         return;
132                 }
133
134                 $from = jabber_normalize_jid($pl['from']);
135                 $user = $this->get_user($from);
136
137                 if (!$user) {
138                         $this->from_site($from, 'Unknown user; go to ' .
139                                                          common_local_url('imsettings') .
140                                                          ' to add your address to your account');
141                         $this->log(LOG_WARNING, 'Message from unknown user ' . $from);
142                         return;
143                 }
144                 if ($this->handle_command($user, $pl['body'])) {
145                         return;
146                 } else if ($this->is_autoreply($pl['body'])) {
147                         $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from);
148                         return;
149                 } else if ($this->is_otr($pl['body'])) {
150                         $this->log(LOG_INFO, 'Ignoring OTR from ' . $from);
151                         return;
152                 } else {
153                         $len = mb_strlen($pl['body']);
154                         if($len > 140) {
155                                 $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len);
156                                 return;
157                         }
158                         $this->add_notice($user, $pl);
159                 }
160         }
161
162         function is_autoreply($txt) {
163                 if (preg_match('/[\[\(]?[Aa]uto-?[Rr]eply[\]\)]/', $txt)) {
164                         return true;
165                 } else {
166                         return false;
167                 }
168         }
169
170         function is_otr($txt) {
171                 if (preg_match('/^\?OTR/', $txt)) {
172                         return true;
173                 } else {
174                         return false;
175                 }
176         }
177
178         function from_site($address, $msg) {
179                 $text = '['.common_config('site', 'name') . '] ' . $msg;
180                 jabber_send_message($address, $text);
181         }
182
183         function handle_command($user, $body) {
184                 # XXX: localise
185                 $p=explode(' ',$body);
186                 if(count($p)>2)
187                         return false;
188                 switch($p[0]) {
189                  case 'help':
190                         if(count($p)!=1)
191                                 return false;
192                         $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");
193                         return true;
194                  case 'on':
195                         if(count($p)!=1)
196                                 return false;
197                         $this->set_notify($user, true);
198                         $this->from_site($user->jabber, 'notifications on');
199                         return true;
200                  case 'off':
201                         if(count($p)!=1)
202                                 return false;
203                         $this->set_notify($user, false);
204                         $this->from_site($user->jabber, 'notifications off');
205                         return true;
206                  case 'sub':
207                         if(count($p)==1) {
208                                 $this->from_site($user->jabber, 'Specify the name of the user to subscribe to');
209                                 return true;
210                         }
211                         $result=subs_subscribe_user($user, $p[1]);
212                         if($result=='true')
213                                 $this->from_site($user->jabber, 'Subscribed to ' . $p[1]);
214                         else
215                                 $this->from_site($user->jabber, $result);
216                         return true;
217                  case 'unsub':
218                         if(count($p)==1) {
219                                 $this->from_site($user->jabber, 'Specify the name of the user to unsubscribe from');
220                                 return true;
221                         }
222                         $result=subs_unsubscribe_user($user, $p[1]);
223                         if($result=='true')
224                                 $this->from_site($user->jabber, 'Unsubscribed from ' . $p[1]);
225                         else
226                                 $this->from_site($user->jabber, $result);
227                         return true;
228                  default:
229                         return false;
230                 }
231         }
232
233         function set_notify(&$user, $notify) {
234                 $orig = clone($user);
235                 $user->jabbernotify = $notify;
236                 $result = $user->update($orig);
237                 if (!$result) {
238                         $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
239                         $this->log(LOG_ERR,
240                                            'Could not set notify flag to ' . $notify .
241                                            ' for user ' . common_log_objstring($user) .
242                                            ': ' . $last_error->message);
243                 } else {
244                         $this->log(LOG_INFO,
245                                            'User ' . $user->nickname . ' set notify flag to ' . $notify);
246                 }
247         }
248
249         function add_notice(&$user, &$pl) {
250                 $notice = Notice::saveNew($user->id, trim(mb_substr($pl['body'], 0, 140)), 'xmpp');
251                 if (is_string($notice)) {
252                         $this->log(LOG_ERR, $notice);
253                         return;
254                 }
255                 common_real_broadcast($notice);
256                 $this->log(LOG_INFO,
257                                    'Added notice ' . $notice->id . ' from user ' . $user->nickname);
258         }
259
260         function handle_presence(&$pl) {
261                 $from = jabber_normalize_jid($pl['from']);
262                 switch ($pl['type']) {
263                  case 'subscribe':
264                         # We let anyone subscribe
265                         $this->subscribed($from);
266                         $this->log(LOG_INFO,
267                                            'Accepted subscription from ' . $from);
268                         break;
269                  case 'subscribed':
270                  case 'unsubscribed':
271                  case 'unsubscribe':
272                         $this->log(LOG_INFO,
273                                            'Ignoring  "' . $pl['type'] . '" from ' . $from);
274                         break;
275                  default:
276                         if (!$pl['type']) {
277                                 $user = User::staticGet('jabber', $from);
278                                 if (!$user) {
279                                         $this->log(LOG_WARNING, 'Presence from unknown user ' . $from);
280                                         return;
281                                 }
282                                 if ($user->updatefrompresence) {
283                                         $this->log(LOG_INFO, 'Updating ' . $user->nickname .
284                                                            ' status from presence.');
285                                         $this->add_notice($user, $pl);
286                                 }
287                         }
288                         break;
289                 }
290         }
291
292         function log($level, $msg) {
293                 common_log($level, 'XMPPDaemon('.$this->resource.'): '.$msg);
294         }
295
296         function subscribed($to) {
297                 jabber_special_presence('subscribed', $to);
298         }
299
300         function set_status($status) {
301                 $this->log(LOG_INFO, 'Setting status to "' . $status . '"');
302                 jabber_send_presence($status);
303         }
304
305         function top_queue_item() {
306
307                 $qi = new Queue_item();
308                 $qi->orderBy('created');
309                 $qi->whereAdd('claimed is NULL');
310
311                 $qi->limit(1);
312
313                 $cnt = $qi->find(TRUE);
314
315                 if ($cnt) {
316                         # XXX: potential race condition
317                         # can we force it to only update if claimed is still NULL
318                         # (or old)?
319                         $this->log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id);
320                         $orig = clone($qi);
321                         $qi->claimed = common_sql_now();
322                         $result = $qi->update($orig);
323                         if ($result) {
324                                 $this->log(LOG_INFO, 'claim succeeded.');
325                                 return $qi;
326                         } else {
327                                 $this->log(LOG_INFO, 'claim failed.');
328                         }
329                 }
330                 $qi = NULL;
331                 return NULL;
332         }
333
334         function broadcast_queue() {
335                 $this->clear_old_claims();
336                 $this->log(LOG_INFO, 'checking for queued notices');
337                 do {
338                         $qi = $this->top_queue_item();
339                         if ($qi) {
340                                 $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created));
341                                 $notice = Notice::staticGet($qi->notice_id);
342                                 if ($notice) {
343                                         $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id);
344                                         # XXX: what to do if broadcast fails?
345                                         $result = common_real_broadcast($notice, $this->is_remote($notice));
346                                         if (!$result) {
347                                                 $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id);
348                                                 $orig = $qi;
349                                                 $qi->claimed = NULL;
350                                                 $qi->update($orig);
351                                                 $this->log(LOG_WARNING, 'Abandoned claim for notice ID = ' . $notice->id);
352                                                 continue;
353                                         }
354                                         $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id);
355                                         $notice = NULL;
356                                 } else {
357                                         $this->log(LOG_WARNING, 'queue item for notice that does not exist');
358                                 }
359                                 $qi->delete();
360                         }
361                 } while ($qi);
362         }
363
364         function clear_old_claims() {
365                 $qi = new Queue_item();
366                 $qi->claimed = NULL;
367                 $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
368                 $qi->update(DB_DATAOBJECT_WHEREADD_ONLY);
369         }
370
371         function is_remote($notice) {
372                 $user = User::staticGet($notice->profile_id);
373                 return !$user;
374         }
375
376         function confirmation_queue() {
377             # $this->clear_old_confirm_claims();
378                 $this->log(LOG_INFO, 'checking for queued confirmations');
379                 do {
380                         $confirm = $this->next_confirm();
381                         if ($confirm) {
382                                 $this->log(LOG_INFO, 'Sending confirmation for ' . $confirm->address);
383                                 $user = User::staticGet($confirm->user_id);
384                                 if (!$user) {
385                                         $this->log(LOG_WARNING, 'Confirmation for unknown user ' . $confirm->user_id);
386                                         continue;
387                                 }
388                                 $success = jabber_confirm_address($confirm->code,
389                                                                   $user->nickname,
390                                                                   $confirm->address);
391                                 if (!$success) {
392                                         $this->log(LOG_ERR, 'Confirmation failed for ' . $confirm->address);
393                                         # Just let the claim age out; hopefully things work then
394                                         continue;
395                                 } else {
396                                         $this->log(LOG_INFO, 'Confirmation sent for ' . $confirm->address);
397                                         # Mark confirmation sent
398                                         $original = clone($confirm);
399                                         $confirm->sent = $confirm->claimed;
400                                         $result = $confirm->update($original);
401                                         if (!$result) {
402                                                 $this->log(LOG_ERR, 'Cannot mark sent for ' . $confirm->address);
403                                                 # Just let the claim age out; hopefully things work then
404                                                 continue;
405                                         }
406                                 }
407                         }
408                 } while ($confirm);
409         }
410
411         function next_confirm() {
412                 $confirm = new Confirm_address();
413                 $confirm->whereAdd('claimed IS NULL');
414                 $confirm->whereAdd('sent IS NULL');
415                 # XXX: eventually we could do other confirmations in the queue, too
416                 $confirm->address_type = 'jabber';
417                 $confirm->orderBy('modified DESC');
418                 $confirm->limit(1);
419                 if ($confirm->find(TRUE)) {
420                         $this->log(LOG_INFO, 'Claiming confirmation for ' . $confirm->address);
421                         # working around some weird DB_DataObject behaviour
422                         $confirm->whereAdd(''); # clears where stuff
423                         $original = clone($confirm);
424                         $confirm->claimed = common_sql_now();
425                         $result = $confirm->update($original);
426                         if ($result) {
427                                 $this->log(LOG_INFO, 'Succeeded in claim! '. $result);
428                                 return $confirm;
429                         } else {
430                                 $this->log(LOG_INFO, 'Failed in claim!');
431                                 return false;
432                         }
433                 }
434                 return NULL;
435         }
436
437         function clear_old_confirm_claims() {
438                 $confirm = new Confirm();
439                 $confirm->claimed = NULL;
440                 $confirm->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
441                 $confirm->update(DB_DATAOBJECT_WHEREADD_ONLY);
442         }
443
444 }
445
446 mb_internal_encoding('UTF-8');
447
448 $resource = ($argc > 1) ? $argv[1] : NULL;
449
450 $daemon = new XMPPDaemon($resource);
451
452 if ($daemon->connect()) {
453         $daemon->set_status("Send me a message to post a notice");
454         $daemon->handle();
455 }
456
457 ?>