]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/xmppdaemon.php
8b5c727b6f6676468a6887dc69e0fdcf08b73b2a
[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                 $this->conn->addEventHandler('session_start', 'handle_session_start', $this);
102                 
103                 while(!$this->conn->isDisconnected()) {
104                         $this->conn->processTime(10);
105                         $this->broadcast_queue();
106                         $this->confirmation_queue();
107                 }
108         }
109
110         function handle_session_start(&$pl) {
111                 $this->conn->getRoster();
112                 $this->set_status("Send me a message to post a notice");
113         }
114         
115         function get_user($from) {
116                 $user = User::staticGet('jabber', jabber_normalize_jid($from));
117                 return $user;
118         }
119
120         function get_confirmation($from) {
121                 $confirm = new Confirm_address();
122                 $confirm->address = $from;
123                 $confirm->address_type = 'jabber';
124                 if ($confirm->find(TRUE)) {
125                         return $confirm;
126                 } else {
127                         return NULL;
128                 }
129         }
130
131         function handle_message(&$pl) {
132                 if ($pl['type'] != 'chat') {
133                         return;
134                 }
135                 if (mb_strlen($pl['body']) == 0) {
136                         return;
137                 }
138
139                 $from = jabber_normalize_jid($pl['from']);
140                 $user = $this->get_user($from);
141
142                 if (!$user) {
143                         $this->from_site($from, 'Unknown user; go to ' .
144                                                          common_local_url('imsettings') .
145                                                          ' to add your address to your account');
146                         $this->log(LOG_WARNING, 'Message from unknown user ' . $from);
147                         return;
148                 }
149                 if ($this->handle_command($user, $pl['body'])) {
150                         return;
151                 } else if ($this->is_autoreply($pl['body'])) {
152                         $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from);
153                         return;
154                 } else if ($this->is_otr($pl['body'])) {
155                         $this->log(LOG_INFO, 'Ignoring OTR from ' . $from);
156                         return;
157                 } else {
158                         $len = mb_strlen($pl['body']);
159                         if($len > 140) {
160                                 $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len);
161                                 return;
162                         }
163                         $this->add_notice($user, $pl);
164                 }
165         }
166
167         function is_autoreply($txt) {
168                 if (preg_match('/[\[\(]?[Aa]uto-?[Rr]eply[\]\)]/', $txt)) {
169                         return true;
170                 } else {
171                         return false;
172                 }
173         }
174
175         function is_otr($txt) {
176                 if (preg_match('/^\?OTR/', $txt)) {
177                         return true;
178                 } else {
179                         return false;
180                 }
181         }
182
183         function from_site($address, $msg) {
184                 $text = '['.common_config('site', 'name') . '] ' . $msg;
185                 jabber_send_message($address, $text);
186         }
187
188         function handle_command($user, $body) {
189                 # XXX: localise
190                 $p=explode(' ',$body);
191                 if(count($p)>2)
192                         return false;
193                 switch($p[0]) {
194                  case 'help':
195                         if(count($p)!=1)
196                                 return false;
197                         $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");
198                         return true;
199                  case 'on':
200                         if(count($p)!=1)
201                                 return false;
202                         $this->set_notify($user, true);
203                         $this->from_site($user->jabber, 'notifications on');
204                         return true;
205                  case 'off':
206                         if(count($p)!=1)
207                                 return false;
208                         $this->set_notify($user, false);
209                         $this->from_site($user->jabber, 'notifications off');
210                         return true;
211                  case 'sub':
212                         if(count($p)==1) {
213                                 $this->from_site($user->jabber, 'Specify the name of the user to subscribe to');
214                                 return true;
215                         }
216                         $result=subs_subscribe_user($user, $p[1]);
217                         if($result=='true')
218                                 $this->from_site($user->jabber, 'Subscribed to ' . $p[1]);
219                         else
220                                 $this->from_site($user->jabber, $result);
221                         return true;
222                  case 'unsub':
223                         if(count($p)==1) {
224                                 $this->from_site($user->jabber, 'Specify the name of the user to unsubscribe from');
225                                 return true;
226                         }
227                         $result=subs_unsubscribe_user($user, $p[1]);
228                         if($result=='true')
229                                 $this->from_site($user->jabber, 'Unsubscribed from ' . $p[1]);
230                         else
231                                 $this->from_site($user->jabber, $result);
232                         return true;
233                  default:
234                         return false;
235                 }
236         }
237
238         function set_notify(&$user, $notify) {
239                 $orig = clone($user);
240                 $user->jabbernotify = $notify;
241                 $result = $user->update($orig);
242                 if (!$result) {
243                         $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
244                         $this->log(LOG_ERR,
245                                            'Could not set notify flag to ' . $notify .
246                                            ' for user ' . common_log_objstring($user) .
247                                            ': ' . $last_error->message);
248                 } else {
249                         $this->log(LOG_INFO,
250                                            'User ' . $user->nickname . ' set notify flag to ' . $notify);
251                 }
252         }
253
254         function add_notice(&$user, &$pl) {
255                 $notice = Notice::saveNew($user->id, trim(mb_substr($pl['body'], 0, 140)), 'xmpp');
256                 if (is_string($notice)) {
257                         $this->log(LOG_ERR, $notice);
258                         return;
259                 }
260                 common_real_broadcast($notice);
261                 $this->log(LOG_INFO,
262                                    'Added notice ' . $notice->id . ' from user ' . $user->nickname);
263         }
264
265         function handle_presence(&$pl) {
266                 $from = jabber_normalize_jid($pl['from']);
267                 switch ($pl['type']) {
268                  case 'subscribe':
269                         # We let anyone subscribe
270                         $this->subscribed($from);
271                         $this->log(LOG_INFO,
272                                            'Accepted subscription from ' . $from);
273                         break;
274                  case 'subscribed':
275                  case 'unsubscribed':
276                  case 'unsubscribe':
277                         $this->log(LOG_INFO,
278                                            'Ignoring  "' . $pl['type'] . '" from ' . $from);
279                         break;
280                  default:
281                         if (!$pl['type']) {
282                                 $user = User::staticGet('jabber', $from);
283                                 if (!$user) {
284                                         $this->log(LOG_WARNING, 'Presence from unknown user ' . $from);
285                                         return;
286                                 }
287                                 if ($user->updatefrompresence) {
288                                         $this->log(LOG_INFO, 'Updating ' . $user->nickname .
289                                                            ' status from presence.');
290                                         $this->add_notice($user, $pl);
291                                 }
292                         }
293                         break;
294                 }
295         }
296
297         function log($level, $msg) {
298                 common_log($level, 'XMPPDaemon('.$this->resource.'): '.$msg);
299         }
300
301         function subscribed($to) {
302                 jabber_special_presence('subscribed', $to);
303         }
304
305         function set_status($status) {
306                 $this->log(LOG_INFO, 'Setting status to "' . $status . '"');
307                 jabber_send_presence($status);
308         }
309
310         function top_queue_item() {
311
312                 $qi = new Queue_item();
313                 $qi->orderBy('created');
314                 $qi->whereAdd('claimed is NULL');
315
316                 $qi->limit(1);
317
318                 $cnt = $qi->find(TRUE);
319
320                 if ($cnt) {
321                         # XXX: potential race condition
322                         # can we force it to only update if claimed is still NULL
323                         # (or old)?
324                         $this->log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id);
325                         $orig = clone($qi);
326                         $qi->claimed = common_sql_now();
327                         $result = $qi->update($orig);
328                         if ($result) {
329                                 $this->log(LOG_INFO, 'claim succeeded.');
330                                 return $qi;
331                         } else {
332                                 $this->log(LOG_INFO, 'claim failed.');
333                         }
334                 }
335                 $qi = NULL;
336                 return NULL;
337         }
338
339         function broadcast_queue() {
340                 $this->clear_old_claims();
341                 $this->log(LOG_INFO, 'checking for queued notices');
342                 do {
343                         $qi = $this->top_queue_item();
344                         if ($qi) {
345                                 $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created));
346                                 $notice = Notice::staticGet($qi->notice_id);
347                                 if ($notice) {
348                                         $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id);
349                                         # XXX: what to do if broadcast fails?
350                                         $result = common_real_broadcast($notice, $this->is_remote($notice));
351                                         if (!$result) {
352                                                 $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id);
353                                                 $orig = $qi;
354                                                 $qi->claimed = NULL;
355                                                 $qi->update($orig);
356                                                 $this->log(LOG_WARNING, 'Abandoned claim for notice ID = ' . $notice->id);
357                                                 continue;
358                                         }
359                                         $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id);
360                                         $notice = NULL;
361                                 } else {
362                                         $this->log(LOG_WARNING, 'queue item for notice that does not exist');
363                                 }
364                                 $qi->delete();
365                         }
366                 } while ($qi);
367         }
368
369         function clear_old_claims() {
370                 $qi = new Queue_item();
371                 $qi->claimed = NULL;
372                 $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
373                 $qi->update(DB_DATAOBJECT_WHEREADD_ONLY);
374         }
375
376         function is_remote($notice) {
377                 $user = User::staticGet($notice->profile_id);
378                 return !$user;
379         }
380
381         function confirmation_queue() {
382             # $this->clear_old_confirm_claims();
383                 $this->log(LOG_INFO, 'checking for queued confirmations');
384                 do {
385                         $confirm = $this->next_confirm();
386                         if ($confirm) {
387                                 $this->log(LOG_INFO, 'Sending confirmation for ' . $confirm->address);
388                                 $user = User::staticGet($confirm->user_id);
389                                 if (!$user) {
390                                         $this->log(LOG_WARNING, 'Confirmation for unknown user ' . $confirm->user_id);
391                                         continue;
392                                 }
393                                 $success = jabber_confirm_address($confirm->code,
394                                                                   $user->nickname,
395                                                                   $confirm->address);
396                                 if (!$success) {
397                                         $this->log(LOG_ERR, 'Confirmation failed for ' . $confirm->address);
398                                         # Just let the claim age out; hopefully things work then
399                                         continue;
400                                 } else {
401                                         $this->log(LOG_INFO, 'Confirmation sent for ' . $confirm->address);
402                                         # Mark confirmation sent
403                                         $original = clone($confirm);
404                                         $confirm->sent = $confirm->claimed;
405                                         $result = $confirm->update($original);
406                                         if (!$result) {
407                                                 $this->log(LOG_ERR, 'Cannot mark sent for ' . $confirm->address);
408                                                 # Just let the claim age out; hopefully things work then
409                                                 continue;
410                                         }
411                                 }
412                         }
413                 } while ($confirm);
414         }
415
416         function next_confirm() {
417                 $confirm = new Confirm_address();
418                 $confirm->whereAdd('claimed IS NULL');
419                 $confirm->whereAdd('sent IS NULL');
420                 # XXX: eventually we could do other confirmations in the queue, too
421                 $confirm->address_type = 'jabber';
422                 $confirm->orderBy('modified DESC');
423                 $confirm->limit(1);
424                 if ($confirm->find(TRUE)) {
425                         $this->log(LOG_INFO, 'Claiming confirmation for ' . $confirm->address);
426                         # working around some weird DB_DataObject behaviour
427                         $confirm->whereAdd(''); # clears where stuff
428                         $original = clone($confirm);
429                         $confirm->claimed = common_sql_now();
430                         $result = $confirm->update($original);
431                         if ($result) {
432                                 $this->log(LOG_INFO, 'Succeeded in claim! '. $result);
433                                 return $confirm;
434                         } else {
435                                 $this->log(LOG_INFO, 'Failed in claim!');
436                                 return false;
437                         }
438                 }
439                 return NULL;
440         }
441
442         function clear_old_confirm_claims() {
443                 $confirm = new Confirm();
444                 $confirm->claimed = NULL;
445                 $confirm->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
446                 $confirm->update(DB_DATAOBJECT_WHEREADD_ONLY);
447         }
448
449 }
450
451 mb_internal_encoding('UTF-8');
452
453 $resource = ($argc > 1) ? $argv[1] : NULL;
454
455 $daemon = new XMPPDaemon($resource);
456
457 if ($daemon->connect()) {
458         $daemon->set_status("Send me a message to post a notice");
459         $daemon->handle();
460 }
461
462 ?>