3 * StatusNet, the distributed open-source microblogging tool
5 * Superclass for plugins that do instant messaging
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Craig Andrews <candrews@integralblue.com>
25 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
26 * @link http://status.net/
29 if (!defined('STATUSNET') && !defined('LACONICA')) {
34 * Superclass for plugins that do authentication
36 * Implementations will likely want to override onStartIoManagerClasses() so that their
41 * @author Craig Andrews <candrews@integralblue.com>
42 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
43 * @link http://status.net/
46 abstract class ImPlugin extends Plugin
48 //name of this IM transport
49 public $transport = null;
50 //list of screennames that should get all public notices
51 public $public = array();
54 * normalize a screenname for comparison
56 * @param string $screenname screenname to normalize
58 * @return string an equivalent screenname in normalized form
60 abstract function normalize($screenname);
63 * validate (ensure the validity of) a screenname
65 * @param string $screenname screenname to validate
69 abstract function validate($screenname);
72 * get the internationalized/translated display name of this IM service
76 abstract function getDisplayName();
79 * send a single notice to a given screenname
80 * The implementation should put raw data, ready to send, into the outgoing
81 * queue using enqueueOutgoingRaw()
83 * @param string $screenname screenname to send to
84 * @param Notice $notice notice to send
86 * @return boolean success value
88 function sendNotice($screenname, $notice)
90 return $this->sendMessage($screenname, $this->formatNotice($notice));
94 * send a message (text) to a given screenname
95 * The implementation should put raw data, ready to send, into the outgoing
96 * queue using enqueueOutgoingRaw()
98 * @param string $screenname screenname to send to
99 * @param Notice $body text to send
101 * @return boolean success value
103 abstract function sendMessage($screenname, $body);
106 * receive a raw message
107 * Raw IM data is taken from the incoming queue, and passed to this function.
108 * It should parse the raw message and call handleIncoming()
110 * Returning false may CAUSE REPROCESSING OF THE QUEUE ITEM, and should
111 * be used for temporary failures only. For permanent failures such as
112 * unrecognized addresses, return true to indicate your processing has
115 * @param object $data raw IM data
117 * @return boolean true if processing completed, false for temporary failures
119 abstract function receiveRawMessage($data);
122 * get the screenname of the daemon that sends and receives message for this service
124 * @return string screenname of this plugin
126 abstract function daemonScreenname();
129 * get the microid uri of a given screenname
131 * @param string $screenname screenname
133 * @return string microid uri
135 function microiduri($screenname)
137 return $this->transport . ':' . $screenname;
139 //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - MISC ========================\
142 * Put raw message data (ready to send) into the outgoing queue
144 * @param object $data
146 function enqueueOutgoingRaw($data)
148 $qm = QueueManager::get();
149 $qm->enqueue($data, $this->transport . '-out');
153 * Put raw message data (received, ready to be processed) into the incoming queue
155 * @param object $data
157 function enqueueIncomingRaw($data)
159 $qm = QueueManager::get();
160 $qm->enqueue($data, $this->transport . '-in');
164 * given a screenname, get the corresponding user
166 * @param string $screenname
170 function getUser($screenname)
172 $user_im_prefs = $this->getUserImPrefsFromScreenname($screenname);
174 $user = User::staticGet('id', $user_im_prefs->user_id);
175 $user_im_prefs->free();
183 * given a screenname, get the User_im_prefs object for this transport
185 * @param string $screenname
187 * @return User_im_prefs user_im_prefs
189 function getUserImPrefsFromScreenname($screenname)
191 $user_im_prefs = User_im_prefs::pkeyGet(
192 array('transport' => $this->transport,
193 'screenname' => $this->normalize($screenname)));
194 if ($user_im_prefs) {
195 return $user_im_prefs;
202 * given a User, get their screenname
206 * @return string screenname of that user
208 function getScreenname($user)
210 $user_im_prefs = $this->getUserImPrefsFromUser($user);
211 if ($user_im_prefs) {
212 return $user_im_prefs->screenname;
219 * given a User, get their User_im_prefs
223 * @return User_im_prefs user_im_prefs of that user
225 function getUserImPrefsFromUser($user)
227 $user_im_prefs = User_im_prefs::pkeyGet(
228 array('transport' => $this->transport,
229 'user_id' => $user->id));
231 return $user_im_prefs;
236 //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - SENDING ========================\
238 * Send a message to a given screenname from the site
240 * @param string $screenname screenname to send the message to
241 * @param string $msg message contents to send
243 * @param boolean success
245 protected function sendFromSite($screenname, $msg)
247 $text = '['.common_config('site', 'name') . '] ' . $msg;
248 $this->sendMessage($screenname, $text);
252 * send a confirmation code to a user
254 * @param string $screenname screenname sending to
255 * @param string $code the confirmation code
256 * @param User $user user sending to
258 * @return boolean success value
260 function sendConfirmationCode($screenname, $code, $user)
262 $body = sprintf(_('User "%s" on %s has said that your %s screenname belongs to them. ' .
263 'If that\'s true, you can confirm by clicking on this URL: ' .
265 ' . (If you cannot click it, copy-and-paste it into the ' .
266 'address bar of your browser). If that user isn\'t you, ' .
267 'or if you didn\'t request this confirmation, just ignore this message.'),
268 $user->nickname, common_config('site', 'name'), $this->getDisplayName(), common_local_url('confirmaddress', array('code' => $code)));
270 return $this->sendMessage($screenname, $body);
274 * send a notice to all public listeners
276 * For notices that are generated on the local system (by users), we can optionally
277 * forward them to remote listeners by XMPP.
279 * @param Notice $notice notice to broadcast
281 * @return boolean success flag
284 function publicNotice($notice)
286 // Now, users who want everything
288 // FIXME PRIV don't send out private messages here
289 // XXX: should we send out non-local messages if public,localonly
290 // = false? I think not
292 foreach ($this->public as $screenname) {
294 'Sending notice ' . $notice->id .
295 ' to public listener ' . $screenname,
297 $this->sendNotice($screenname, $notice);
304 * broadcast a notice to all subscribers and reply recipients
306 * This function will send a notice to all subscribers on the local server
307 * who have IM addresses, and have IM notification enabled, and
308 * have this subscription enabled for IM. It also sends the notice to
309 * all recipients of @-replies who have IM addresses and IM notification
310 * enabled. This is really the heart of IM distribution in StatusNet.
312 * @param Notice $notice The notice to broadcast
314 * @return boolean success flag
317 function broadcastNotice($notice)
320 $ni = $notice->whoGets();
322 foreach ($ni as $user_id => $reason) {
323 $user = User::staticGet($user_id);
325 // either not a local user, or just not found
328 $user_im_prefs = $this->getUserImPrefsFromUser($user);
329 if(!$user_im_prefs || !$user_im_prefs->notify){
334 case NOTICE_INBOX_SOURCE_REPLY:
335 if (!$user_im_prefs->replies) {
339 case NOTICE_INBOX_SOURCE_SUB:
340 $sub = Subscription::pkeyGet(array('subscriber' => $user->id,
341 'subscribed' => $notice->profile_id));
342 if (empty($sub) || !$sub->jabber) {
346 case NOTICE_INBOX_SOURCE_GROUP:
349 throw new Exception(sprintf(_("Unknown inbox source %d."), $reason));
353 'Sending notice ' . $notice->id . ' to ' . $user_im_prefs->screenname,
355 $this->sendNotice($user_im_prefs->screenname, $notice);
356 $user_im_prefs->free();
363 * makes a plain-text formatted version of a notice, suitable for IM distribution
365 * @param Notice $notice notice being sent
367 * @return string plain-text version of the notice, with user nickname prefixed
370 function formatNotice($notice)
372 $profile = $notice->getProfile();
373 return $profile->nickname . ': ' . $notice->content . ' [' . $notice->id . ']';
375 //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - RECEIVING ========================\
378 * Attempt to handle a message as a command
379 * @param User $user user the message is from
380 * @param string $body message text
381 * @return boolean true if the message was a command and was executed, false if it was not a command
383 protected function handleCommand($user, $body)
385 $inter = new CommandInterpreter();
386 $cmd = $inter->handle_command($user, $body);
388 $chan = new IMChannel($this);
389 $cmd->execute($chan);
397 * Is some text an autoreply message?
398 * @param string $txt message text
399 * @return boolean true if autoreply
401 protected function isAutoreply($txt)
403 if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) {
405 } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) {
413 * Is some text an OTR message?
414 * @param string $txt message text
415 * @return boolean true if OTR
417 protected function isOtr($txt)
419 if (preg_match('/^\?OTR/', $txt)) {
427 * Helper for handling incoming messages
428 * Your incoming message handler will probably want to call this function
430 * @param string $from screenname the message was sent from
431 * @param string $message message contents
433 * @param boolean success
435 protected function handleIncoming($from, $notice_text)
437 $user = $this->getUser($from);
438 // For common_current_user to work
443 $this->sendFromSite($from, 'Unknown user; go to ' .
444 common_local_url('imsettings') .
445 ' to add your address to your account');
446 common_log(LOG_WARNING, 'Message from unknown user ' . $from);
449 if ($this->handleCommand($user, $notice_text)) {
450 common_log(LOG_INFO, "Command message by $from handled.");
452 } else if ($this->isAutoreply($notice_text)) {
453 common_log(LOG_INFO, 'Ignoring auto reply from ' . $from);
455 } else if ($this->isOtr($notice_text)) {
456 common_log(LOG_INFO, 'Ignoring OTR from ' . $from);
460 common_log(LOG_INFO, 'Posting a notice from ' . $user->nickname);
462 $this->addNotice($from, $user, $notice_text);
472 * Helper for handling incoming messages
473 * Your incoming message handler will probably want to call this function
475 * @param string $from screenname the message was sent from
476 * @param string $message message contents
478 * @param boolean success
480 protected function addNotice($screenname, $user, $body)
482 $body = trim(strip_tags($body));
483 $content_shortened = common_shorten_links($body);
484 if (Notice::contentTooLong($content_shortened)) {
485 $this->sendFromSite($screenname, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'),
486 Notice::maxContent(),
487 mb_strlen($content_shortened)));
492 $notice = Notice::saveNew($user->id, $content_shortened, $this->transport);
493 } catch (Exception $e) {
494 common_log(LOG_ERR, $e->getMessage());
495 $this->sendFromSite($from, $e->getMessage());
500 'Added notice ' . $notice->id . ' from user ' . $user->nickname);
505 //========================EVENT HANDLERS========================\
508 * Register notice queue handler
510 * @param QueueManager $manager
512 * @return boolean hook return
514 function onEndInitializeQueueManager($manager)
516 $manager->connect($this->transport . '-in', new ImReceiverQueueHandler($this), 'im');
517 $manager->connect($this->transport, new ImQueueHandler($this));
518 $manager->connect($this->transport . '-out', new ImSenderQueueHandler($this), 'im');
522 function onStartImDaemonIoManagers(&$classes)
524 //$classes[] = new ImManager($this); // handles sending/receiving/pings/reconnects
528 function onStartEnqueueNotice($notice, &$transports)
530 $profile = Profile::staticGet($notice->profile_id);
533 common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
534 'unknown profile ' . common_log_objstring($notice),
537 $transports[] = $this->transport;
543 function onEndShowHeadElements($action)
545 $aname = $action->trimmed('action');
547 if ($aname == 'shownotice') {
549 $user_im_prefs = new User_im_prefs();
550 $user_im_prefs->user_id = $action->profile->id;
551 $user_im_prefs->transport = $this->transport;
553 if ($user_im_prefs->find() && $user_im_prefs->fetch() && $user_im_prefs->microid && $action->notice->uri) {
554 $id = new Microid($this->microiduri($user_im_prefs->screenname),
555 $action->notice->uri);
556 $action->element('meta', array('name' => 'microid',
557 'content' => $id->toString()));
560 } else if ($aname == 'showstream') {
562 $user_im_prefs = new User_im_prefs();
563 $user_im_prefs->user_id = $action->user->id;
564 $user_im_prefs->transport = $this->transport;
566 if ($user_im_prefs->find() && $user_im_prefs->fetch() && $user_im_prefs->microid && $action->profile->profileurl) {
567 $id = new Microid($this->microiduri($user_im_prefs->screenname),
569 $action->element('meta', array('name' => 'microid',
570 'content' => $id->toString()));
575 function onNormalizeImScreenname($transport, &$screenname)
577 if($transport == $this->transport)
579 $screenname = $this->normalize($screenname);
584 function onValidateImScreenname($transport, $screenname, &$valid)
586 if($transport == $this->transport)
588 $valid = $this->validate($screenname);
593 function onGetImTransports(&$transports)
595 $transports[$this->transport] = array(
596 'display' => $this->getDisplayName(),
597 'daemonScreenname' => $this->daemonScreenname());
600 function onSendImConfirmationCode($transport, $screenname, $code, $user)
602 if($transport == $this->transport)
604 $this->sendConfirmationCode($screenname, $code, $user);
609 function onUserDeleteRelated($user, &$tables)
611 $tables[] = 'User_im_prefs';
615 function initialize()
617 if( ! common_config('queue', 'enabled'))
619 throw new ServerException("Queueing must be enabled to use IM plugins");
622 if(is_null($this->transport)){
623 throw new ServerException('transport cannot be null');