summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/action.php134
-rw-r--r--lib/channel.php237
-rw-r--r--lib/command.php419
-rw-r--r--lib/commandinterpreter.php197
-rw-r--r--lib/common.php3
-rw-r--r--lib/dberroraction.php73
-rw-r--r--lib/facebookutil.php109
-rw-r--r--lib/featureduserssection.php5
-rw-r--r--lib/feed.php110
-rw-r--r--lib/feedlist.php94
-rw-r--r--lib/grouplist.php2
-rw-r--r--lib/htmloutputter.php19
-rw-r--r--lib/jabber.php7
-rw-r--r--lib/mail.php50
-rw-r--r--lib/popularnoticesection.php5
-rw-r--r--lib/profilelist.php2
-rw-r--r--lib/router.php363
-rw-r--r--lib/section.php2
-rw-r--r--lib/twitter.php97
-rw-r--r--lib/util.php454
-rw-r--r--lib/xmlstringer.php68
21 files changed, 1937 insertions, 513 deletions
diff --git a/lib/action.php b/lib/action.php
index 3e236d714..8ff9e646b 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -151,31 +151,45 @@ class Action extends HTMLOutputter // lawsuit
*/
function showStylesheets()
{
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
- foreach (array(6,7) as $ver) {
- if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
- // Yes, IE people should be put in jail.
- $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ if (Event::handle('StartShowStyles', array($this))) {
+ if (Event::handle('StartShowLaconicaStyles', array($this))) {
+
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ if (common_config('site', 'mobile')) {
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
+ // TODO: "handheld" CSS for other mobile devices
+ 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
+ }
+ Event::handle('EndShowLaconicaStyles', array($this));
}
- }
- $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
- if (common_config('site', 'mobile')) {
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
- // TODO: "handheld" CSS for other mobile devices
- 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
+ if (Event::handle('StartShowUAStyles', array($this))) {
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ foreach (array(6,7) as $ver) {
+ if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
+ // Yes, IE people should be put in jail.
+ $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ }
+ }
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
+ Event::handle('EndShowUAStyles', array($this));
+ }
+ Event::handle('EndShowStyles', array($this));
}
}
@@ -194,6 +208,13 @@ class Action extends HTMLOutputter // lawsuit
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.form.js')),
' ');
+
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')),
+ ' ');
+
+
Event::handle('EndShowJQueryScripts', array($this));
}
if (Event::handle('StartShowLaconicaScripts', array($this))) {
@@ -203,6 +224,18 @@ class Action extends HTMLOutputter // lawsuit
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
' ');
+
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/flowplayer-3.0.5.min.js')),
+ ' ');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/video.js')),
+ ' ');
+
+
+
Event::handle('EndShowLaconicaScripts', array($this));
}
Event::handle('EndShowScripts', array($this));
@@ -232,9 +265,19 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showFeeds()
{
- // does nothing by default
+ $feeds = $this->getFeeds();
+
+ if ($feeds) {
+ foreach ($feeds as $feed) {
+ $this->element('link', array('rel' => $feed->rel(),
+ 'href' => $feed->url,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title));
+ }
+ }
}
/**
@@ -272,9 +315,15 @@ class Action extends HTMLOutputter // lawsuit
{
$this->elementStart('body', array('id' => $this->trimmed('action')));
$this->elementStart('div', array('id' => 'wrap'));
- $this->showHeader();
+ if (Event::handle('StartShowHeader', array($this))) {
+ $this->showHeader();
+ Event::handle('EndShowHeader', array($this));
+ }
$this->showCore();
- $this->showFooter();
+ if (Event::handle('StartShowFooter', array($this))) {
+ $this->showFooter();
+ Event::handle('EndShowFooter', array($this));
+ }
$this->elementEnd('div');
$this->elementEnd('body');
}
@@ -429,7 +478,10 @@ class Action extends HTMLOutputter // lawsuit
{
$this->elementStart('div', array('id' => 'core'));
$this->showLocalNavBlock();
- $this->showContentBlock();
+ if (Event::handle('StartShowContentBlock', array($this))) {
+ $this->showContentBlock();
+ Event::handle('EndShowContentBlock', array($this));
+ }
$this->showAside();
$this->elementEnd('div');
}
@@ -547,15 +599,16 @@ class Action extends HTMLOutputter // lawsuit
/**
* Show export data feeds.
*
- * MAY overload if there are feeds
- *
- * @return nothing
+ * @return void
*/
+
function showExportData()
{
- // is there structure to this?
- // list of (visible!) feed links
- // can we reuse list of feeds from showFeeds() ?
+ $feeds = $this->getFeeds();
+ if ($feeds) {
+ $fl = new FeedList($this);
+ $fl->show($feeds);
+ }
}
/**
@@ -931,4 +984,17 @@ class Action extends HTMLOutputter // lawsuit
$this->elementEnd('div');
}
}
+
+ /**
+ * An array of feeds for this action.
+ *
+ * Returns an array of potential feeds for this action.
+ *
+ * @return array Feed object to show in head and links
+ */
+
+ function getFeeds()
+ {
+ return null;
+ }
}
diff --git a/lib/channel.php b/lib/channel.php
new file mode 100644
index 000000000..f1e205546
--- /dev/null
+++ b/lib/channel.php
@@ -0,0 +1,237 @@
+<?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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+class Channel
+{
+ function on($user)
+ {
+ return false;
+ }
+
+ function off($user)
+ {
+ return false;
+ }
+
+ function output($user, $text)
+ {
+ return false;
+ }
+
+ function error($user, $text)
+ {
+ return false;
+ }
+
+ function source()
+ {
+ return null;
+ }
+}
+
+class XMPPChannel extends Channel
+{
+
+ var $conn = null;
+
+ function source()
+ {
+ return 'xmpp';
+ }
+
+ function __construct($conn)
+ {
+ $this->conn = $conn;
+ }
+
+ function on($user)
+ {
+ return $this->set_notify($user, 1);
+ }
+
+ function off($user)
+ {
+ return $this->set_notify($user, 0);
+ }
+
+ function output($user, $text)
+ {
+ $text = '['.common_config('site', 'name') . '] ' . $text;
+ jabber_send_message($user->jabber, $text);
+ }
+
+ function error($user, $text)
+ {
+ $text = '['.common_config('site', 'name') . '] ' . $text;
+ jabber_send_message($user->jabber, $text);
+ }
+
+ function set_notify(&$user, $notify)
+ {
+ $orig = clone($user);
+ $user->jabbernotify = $notify;
+ $result = $user->update($orig);
+ if (!$result) {
+ $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
+ common_log(LOG_ERR,
+ 'Could not set notify flag to ' . $notify .
+ ' for user ' . common_log_objstring($user) .
+ ': ' . $last_error->message);
+ return false;
+ } else {
+ common_log(LOG_INFO,
+ 'User ' . $user->nickname . ' set notify flag to ' . $notify);
+ return true;
+ }
+ }
+}
+
+class WebChannel extends Channel
+{
+ var $out = null;
+
+ function __construct($out=null)
+ {
+ $this->out = $out;
+ }
+
+ function source()
+ {
+ return 'web';
+ }
+
+ function on($user)
+ {
+ return false;
+ }
+
+ function off($user)
+ {
+ return false;
+ }
+
+ function output($user, $text)
+ {
+ # XXX: buffer all output and send it at the end
+ # XXX: even better, redirect to appropriate page
+ # depending on what command was run
+ $this->out->startHTML();
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Command results'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'command_result'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+
+ function error($user, $text)
+ {
+ common_user_error($text);
+ }
+}
+
+class AjaxWebChannel extends WebChannel
+{
+ function output($user, $text)
+ {
+ $this->out->startHTML('text/xml;charset=utf-8');
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Command results'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'command_result'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+
+ function error($user, $text)
+ {
+ $this->out->startHTML('text/xml;charset=utf-8');
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Ajax Error'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'error'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+}
+
+class MailChannel extends Channel
+{
+
+ var $addr = null;
+
+ function source()
+ {
+ return 'mail';
+ }
+
+ function __construct($addr=null)
+ {
+ $this->addr = $addr;
+ }
+
+ function on($user)
+ {
+ return $this->set_notify($user, 1);
+ }
+
+ function off($user)
+ {
+ return $this->set_notify($user, 0);
+ }
+
+ function output($user, $text)
+ {
+
+ $headers['From'] = $user->incomingemail;
+ $headers['To'] = $this->addr;
+
+ $headers['Subject'] = _('Command complete');
+
+ return mail_send(array($this->addr), $headers, $text);
+ }
+
+ function error($user, $text)
+ {
+
+ $headers['From'] = $user->incomingemail;
+ $headers['To'] = $this->addr;
+
+ $headers['Subject'] = _('Command failed');
+
+ return mail_send(array($this->addr), $headers, $text);
+ }
+
+ function set_notify($user, $value)
+ {
+ $orig = clone($user);
+ $user->smsnotify = $value;
+ $result = $user->update($orig);
+ if (!$result) {
+ common_log_db_error($user, 'UPDATE', __FILE__);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/lib/command.php b/lib/command.php
new file mode 100644
index 000000000..507990a0b
--- /dev/null
+++ b/lib/command.php
@@ -0,0 +1,419 @@
+<?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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once(INSTALLDIR.'/lib/channel.php');
+
+class Command
+{
+
+ var $user = null;
+
+ function __construct($user=null)
+ {
+ $this->user = $user;
+ }
+
+ function execute($channel)
+ {
+ return false;
+ }
+}
+
+class UnimplementedCommand extends Command
+{
+ function execute($channel)
+ {
+ $channel->error($this->user, _("Sorry, this command is not yet implemented."));
+ }
+}
+
+class TrackingCommand extends UnimplementedCommand
+{
+}
+
+class TrackOffCommand extends UnimplementedCommand
+{
+}
+
+class TrackCommand extends UnimplementedCommand
+{
+ var $word = null;
+ function __construct($user, $word)
+ {
+ parent::__construct($user);
+ $this->word = $word;
+ }
+}
+
+class UntrackCommand extends UnimplementedCommand
+{
+ var $word = null;
+ function __construct($user, $word)
+ {
+ parent::__construct($user);
+ $this->word = $word;
+ }
+}
+
+class NudgeCommand extends UnimplementedCommand
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+}
+
+class InviteCommand extends UnimplementedCommand
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+}
+
+class StatsCommand extends Command
+{
+ function execute($channel)
+ {
+
+ $subs = new Subscription();
+ $subs->subscriber = $this->user->id;
+ $subs_count = (int) $subs->count() - 1;
+
+ $subbed = new Subscription();
+ $subbed->subscribed = $this->user->id;
+ $subbed_count = (int) $subbed->count() - 1;
+
+ $notices = new Notice();
+ $notices->profile_id = $this->user->id;
+ $notice_count = (int) $notices->count();
+
+ $channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n".
+ "Subscribers: %2\$s\n".
+ "Notices: %3\$s"),
+ $subs_count,
+ $subbed_count,
+ $notice_count));
+ }
+}
+
+class FavCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+
+ $recipient =
+ common_relative_profile($this->user, common_canonical_nickname($this->other));
+
+ if (!$recipient) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+ $notice = $recipient->getCurrentNotice();
+ if (!$notice) {
+ $channel->error($this->user, _('User has no last notice'));
+ return;
+ }
+
+ $fave = Fave::addNew($this->user, $notice);
+
+ if (!$fave) {
+ $channel->error($this->user, _('Could not create favorite.'));
+ return;
+ }
+
+ $other = User::staticGet('id', $recipient->id);
+
+ if ($other && $other->id != $user->id) {
+ if ($other->email && $other->emailnotifyfav) {
+ mail_notify_fave($other, $this->user, $notice);
+ }
+ }
+
+ $this->user->blowFavesCache();
+
+ $channel->output($this->user, _('Notice marked as fave.'));
+ }
+}
+
+class WhoisCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ $recipient =
+ common_relative_profile($this->user, common_canonical_nickname($this->other));
+
+ if (!$recipient) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+
+ $whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
+ $recipient->profileurl);
+ if ($recipient->fullname) {
+ $whois .= "\n" . sprintf(_('Fullname: %s'), $recipient->fullname);
+ }
+ if ($recipient->location) {
+ $whois .= "\n" . sprintf(_('Location: %s'), $recipient->location);
+ }
+ if ($recipient->homepage) {
+ $whois .= "\n" . sprintf(_('Homepage: %s'), $recipient->homepage);
+ }
+ if ($recipient->bio) {
+ $whois .= "\n" . sprintf(_('About: %s'), $recipient->bio);
+ }
+ $channel->output($this->user, $whois);
+ }
+}
+
+class MessageCommand extends Command
+{
+ var $other = null;
+ var $text = null;
+ function __construct($user, $other, $text)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ $this->text = $text;
+ }
+
+ function execute($channel)
+ {
+ $other = User::staticGet('nickname', common_canonical_nickname($this->other));
+ $len = mb_strlen($this->text);
+ if ($len == 0) {
+ $channel->error($this->user, _('No content!'));
+ return;
+ } else if ($len > 140) {
+ $content = common_shorten_links($content);
+ if (mb_strlen($content) > 140) {
+ $channel->error($this->user, sprintf(_('Message too long - maximum is 140 characters, you sent %d'), $len));
+ return;
+ }
+ }
+
+ if (!$other) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ } else if (!$this->user->mutuallySubscribed($other)) {
+ $channel->error($this->user, _('You can\'t send a message to this user.'));
+ return;
+ } else if ($this->user->id == $other->id) {
+ $channel->error($this->user, _('Don\'t send a message to yourself; just say it to yourself quietly instead.'));
+ return;
+ }
+ $message = Message::saveNew($this->user->id, $other->id, $this->text, $channel->source());
+ if ($message) {
+ $channel->output($this->user, sprintf(_('Direct message to %s sent'), $this->other));
+ } else {
+ $channel->error($this->user, _('Error sending direct message.'));
+ }
+ }
+}
+
+class GetCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ $target_nickname = common_canonical_nickname($this->other);
+
+ $target =
+ common_relative_profile($this->user, $target_nickname);
+
+ if (!$target) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+ $notice = $target->getCurrentNotice();
+ if (!$notice) {
+ $channel->error($this->user, _('User has no last notice'));
+ return;
+ }
+ $notice_content = $notice->content;
+
+ $channel->output($this->user, $target_nickname . ": " . $notice_content);
+ }
+}
+
+class SubCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+
+ if (!$this->other) {
+ $channel->error($this->user, _('Specify the name of the user to subscribe to'));
+ return;
+ }
+
+ $result = subs_subscribe_user($this->user, $this->other);
+
+ if ($result == 'true') {
+ $channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
+ } else {
+ $channel->error($this->user, $result);
+ }
+ }
+}
+
+class UnsubCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ if(!$this->other) {
+ $channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
+ return;
+ }
+
+ $result=subs_unsubscribe_user($this->user, $this->other);
+
+ if ($result) {
+ $channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
+ } else {
+ $channel->error($this->user, $result);
+ }
+ }
+}
+
+class OffCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other=null)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+ function execute($channel)
+ {
+ if ($other) {
+ $channel->error($this->user, _("Command not yet implemented."));
+ } else {
+ if ($channel->off($this->user)) {
+ $channel->output($this->user, _('Notification off.'));
+ } else {
+ $channel->error($this->user, _('Can\'t turn off notification.'));
+ }
+ }
+ }
+}
+
+class OnCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other=null)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ if ($other) {
+ $channel->error($this->user, _("Command not yet implemented."));
+ } else {
+ if ($channel->on($this->user)) {
+ $channel->output($this->user, _('Notification on.'));
+ } else {
+ $channel->error($this->user, _('Can\'t turn on notification.'));
+ }
+ }
+ }
+}
+
+class HelpCommand extends Command
+{
+ function execute($channel)
+ {
+ $channel->output($this->user,
+ _("Commands:\n".
+ "on - turn on notifications\n".
+ "off - turn off notifications\n".
+ "help - show this help\n".
+ "follow <nickname> - subscribe to user\n".
+ "leave <nickname> - unsubscribe from user\n".
+ "d <nickname> <text> - direct message to user\n".
+ "get <nickname> - get last notice from user\n".
+ "whois <nickname> - get profile info on user\n".
+ "fav <nickname> - add user's last notice as a 'fave'\n".
+ "stats - get your stats\n".
+ "stop - same as 'off'\n".
+ "quit - same as 'off'\n".
+ "sub <nickname> - same as 'follow'\n".
+ "unsub <nickname> - same as 'leave'\n".
+ "last <nickname> - same as 'get'\n".
+ "on <nickname> - not yet implemented.\n".
+ "off <nickname> - not yet implemented.\n".
+ "nudge <nickname> - not yet implemented.\n".
+ "invite <phone number> - not yet implemented.\n".
+ "track <word> - not yet implemented.\n".
+ "untrack <word> - not yet implemented.\n".
+ "track off - not yet implemented.\n".
+ "untrack all - not yet implemented.\n".
+ "tracks - not yet implemented.\n".
+ "tracking - not yet implemented.\n"));
+ }
+}
diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php
new file mode 100644
index 000000000..49c733c03
--- /dev/null
+++ b/lib/commandinterpreter.php
@@ -0,0 +1,197 @@
+<?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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/lib/command.php';
+
+class CommandInterpreter
+{
+ function handle_command($user, $text)
+ {
+ # XXX: localise
+
+ $text = preg_replace('/\s+/', ' ', trim($text));
+ list($cmd, $arg) = explode(' ', $text, 2);
+
+ # We try to support all the same commands as Twitter, see
+ # http://getsatisfaction.com/twitter/topics/what_are_the_twitter_commands
+ # There are a few compatibility commands from earlier versions of
+ # Laconica
+
+ switch(strtolower($cmd)) {
+ case 'help':
+ if ($arg) {
+ return null;
+ }
+ return new HelpCommand($user);
+ case 'on':
+ if ($arg) {
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new OnCommand($user, $other);
+ }
+ } else {
+ return new OnCommand($user);
+ }
+ case 'off':
+ if ($arg) {
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new OffCommand($user, $other);
+ }
+ } else {
+ return new OffCommand($user);
+ }
+ case 'stop':
+ case 'quit':
+ if ($arg) {
+ return null;
+ } else {
+ return new OffCommand($user);
+ }
+ case 'follow':
+ case 'sub':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new SubCommand($user, $other);
+ }
+ case 'leave':
+ case 'unsub':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new UnsubCommand($user, $other);
+ }
+ case 'get':
+ case 'last':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new GetCommand($user, $other);
+ }
+ case 'd':
+ case 'dm':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if (!$extra) {
+ return null;
+ } else {
+ return new MessageCommand($user, $other, $extra);
+ }
+ case 'whois':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new WhoisCommand($user, $other);
+ }
+ case 'fav':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new FavCommand($user, $other);
+ }
+ case 'nudge':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new NudgeCommand($user, $other);
+ }
+ case 'stats':
+ if ($arg) {
+ return null;
+ }
+ return new StatsCommand($user);
+ case 'invite':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new InviteCommand($user, $other);
+ }
+ case 'track':
+ if (!$arg) {
+ return null;
+ }
+ list($word, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else if ($word == 'off') {
+ return new TrackOffCommand($user);
+ } else {
+ return new TrackCommand($user, $word);
+ }
+ case 'untrack':
+ if (!$arg) {
+ return null;
+ }
+ list($word, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else if ($word == 'all') {
+ return new TrackOffCommand($user);
+ } else {
+ return new UntrackCommand($user, $word);
+ }
+ case 'tracks':
+ case 'tracking':
+ if ($arg) {
+ return null;
+ }
+ return new TrackingCommand($user);
+ default:
+ return false;
+ }
+ }
+}
+
diff --git a/lib/common.php b/lib/common.php
index 7bfd14c42..4fc749ca0 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -106,7 +106,8 @@ $config =
array('server' => null),
'public' =>
array('localonly' => true,
- 'blacklist' => array()),
+ 'blacklist' => array(),
+ 'autosource' => array()),
'theme' =>
array('server' => null),
'throttle' =>
diff --git a/lib/dberroraction.php b/lib/dberroraction.php
new file mode 100644
index 000000000..0dc92490c
--- /dev/null
+++ b/lib/dberroraction.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * DB error action.
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * 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/>.
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/servererroraction.php';
+
+/**
+ * Class for displaying DB Errors
+ *
+ * This only occurs if there's been a DB_DataObject_Error that's
+ * reported through PEAR, so we try to avoid doing anything that connects
+ * to the DB, so we don't trigger it again.
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ */
+
+class DBErrorAction extends ServerErrorAction
+{
+ function __construct($message='Error', $code=500)
+ {
+ parent::__construct($message, $code);
+ }
+
+ function title()
+ {
+ return _('Database error');
+ }
+
+ function getLanguage()
+ {
+ // Don't try to figure out user's language; just show the page
+ return common_config('site', 'language');
+ }
+
+ function showPrimaryNav()
+ {
+ // don't show primary nav
+ }
+}
diff --git a/lib/facebookutil.php b/lib/facebookutil.php
index beab51366..ec3987273 100644
--- a/lib/facebookutil.php
+++ b/lib/facebookutil.php
@@ -25,21 +25,6 @@ define("FACEBOOK_SERVICE", 2); // Facebook is foreign_service ID 2
define("FACEBOOK_NOTICE_PREFIX", 1);
define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
-// Gets all the notices from users with a Facebook link since a given ID
-function getFacebookNotices($since)
-{
- $qry = 'SELECT notice.* ' .
- 'FROM notice ' .
- 'JOIN foreign_link ' .
- 'WHERE notice.profile_id = foreign_link.user_id ' .
- 'AND foreign_link.service = 2';
-
- // XXX: What should the limit be?
- //static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since) {
-
- return Notice::getStreamDirect($qry, 0, 1000, 0, 0, null, $since);
-}
-
function getFacebook()
{
$apikey = common_config('facebook', 'apikey');
@@ -52,3 +37,97 @@ function updateProfileBox($facebook, $flink, $notice) {
$fbaction->updateProfileBox($notice);
}
+function isFacebookBound($notice, $flink) {
+
+ // If the user does not want to broadcast to Facebook, move along
+ if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
+ common_log(LOG_INFO, "Skipping notice $notice->id " .
+ 'because user has FOREIGN_NOTICE_SEND bit off.');
+ return false;
+ }
+
+ $success = false;
+
+ // If it's not a reply, or if the user WANTS to send @-replies...
+ if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+
+ $success = true;
+
+ // The two condition below are deal breakers:
+
+ // Avoid a loop
+ if ($notice->source == 'Facebook') {
+ common_log(LOG_INFO, "Skipping notice $notice->id because its " .
+ 'source is Facebook.');
+ $success = false;
+ }
+
+ $facebook = getFacebook();
+ $fbuid = $flink->foreign_id;
+
+ try {
+
+ // Check to see if the user has given the FB app status update perms
+ $result = $facebook->api_client->
+ users_hasAppPermission('status_update', $fbuid);
+
+ if ($result != 1) {
+ $user = $flink->getUser();
+ $msg = "Can't send notice $notice->id to Facebook " .
+ "because user $user->nickname hasn't given the " .
+ 'Facebook app \'status_update\' permission.';
+ common_log(LOG_INFO, $msg);
+ $success = false;
+ }
+
+ } catch(FacebookRestClientException $e){
+ common_log(LOG_ERR, $e->getMessage());
+ $success = false;
+ }
+
+ }
+
+ return $success;
+
+}
+
+
+function facebookBroadcastNotice($notice)
+{
+ $facebook = getFacebook();
+ $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
+ $fbuid = $flink->foreign_id;
+
+ if (isFacebookBound($notice, $flink)) {
+
+ $status = null;
+
+ // Get the status 'verb' (prefix) the user has set
+ try {
+ $prefix = $facebook->api_client->
+ data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
+
+ $status = "$prefix $notice->content";
+
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+ }
+
+ // Okay, we're good to go!
+
+ try {
+ $facebook->api_client->users_setStatus($status, $fbuid, false, true);
+ updateProfileBox($facebook, $flink, $notice);
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+
+ // Should we remove flink if this fails?
+ }
+
+ }
+
+ return true;
+}
diff --git a/lib/featureduserssection.php b/lib/featureduserssection.php
index 2935d8363..aed94b1a5 100644
--- a/lib/featureduserssection.php
+++ b/lib/featureduserssection.php
@@ -86,4 +86,9 @@ class FeaturedUsersSection extends ProfileSection
{
return 'featured_users';
}
+
+ function moreUrl()
+ {
+ return common_local_url('featured');
+ }
}
diff --git a/lib/feed.php b/lib/feed.php
new file mode 100644
index 000000000..466926844
--- /dev/null
+++ b/lib/feed.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Data structure for info about syndication feeds (RSS 1.0, RSS 2.0, Atom)
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category Feed
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Data structure for feeds
+ *
+ * This structure is a helpful container for shipping around information about syndication feeds.
+ *
+ * @category Feed
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class Feed
+{
+ const RSS1 = 1;
+ const RSS2 = 2;
+ const ATOM = 3;
+ const FOAF = 4;
+
+ var $type = null;
+ var $url = null;
+ var $title = null;
+
+ function __construct($type, $url, $title)
+ {
+ $this->type = $type;
+ $this->url = $url;
+ $this->title = $title;
+ }
+
+ function mimeType()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return 'application/rdf+xml';
+ case Feed::RSS2:
+ return 'application/rss+xml';
+ case Feed::ATOM:
+ return 'application/atom+xml';
+ case Feed::FOAF:
+ return 'application/rdf+xml';
+ default:
+ return null;
+ }
+ }
+
+ function typeName()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return _('RSS 1.0');
+ case Feed::RSS2:
+ return _('RSS 2.0');
+ case Feed::ATOM:
+ return _('Atom');
+ case Feed::FOAF:
+ return _('FOAF');
+ default:
+ return null;
+ }
+ }
+
+ function rel()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ case Feed::ATOM:
+ return 'alternate';
+ case Feed::FOAF:
+ return 'meta';
+ default:
+ return null;
+ }
+ }
+}
diff --git a/lib/feedlist.php b/lib/feedlist.php
index 47d909e96..927e43c33 100644
--- a/lib/feedlist.php
+++ b/lib/feedlist.php
@@ -50,7 +50,7 @@ if (!defined('LACONICA')) {
class FeedList extends Widget
{
var $action = null;
-
+
function __construct($action=null)
{
parent::__construct($action);
@@ -64,8 +64,8 @@ class FeedList extends Widget
$this->out->element('h2', null, _('Export data'));
$this->out->elementStart('ul', array('class' => 'xoxo'));
- foreach ($feeds as $key => $value) {
- $this->feedItem($feeds[$key]);
+ foreach ($feeds as $feed) {
+ $this->feedItem($feed);
}
$this->out->elementEnd('ul');
@@ -74,85 +74,27 @@ class FeedList extends Widget
function feedItem($feed)
{
- $nickname = $this->action->trimmed('nickname');
-
- switch($feed['item']) {
- case 'notices': default:
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'allrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for $nickname and friends";
- $feed['textContent'] = "RSS";
- break;
-
- case 'repliesrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for replies to $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'publicrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
+ $classname = null;
- case 'publicatom':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "Atom";
+ switch ($feed->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ $classname = 'rss';
break;
-
- case 'tagrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for this tag";
- $feed['textContent'] = "RSS";
+ case Feed::ATOM:
+ $classname = 'atom';
break;
-
- case 'favoritedrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Favorited ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'foaf':
- $feed_classname = "foaf";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's FOAF file";
- $feed['textContent'] = "FOAF";
- break;
-
- case 'favoritesrss':
- $feed_classname = "favorites";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Feed for favorites of $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'usertimeline':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "Atom";
+ case Feed::FOAF:
+ $classname = 'foaf';
break;
}
+
$this->out->elementStart('li');
- $this->out->element('a', array('href' => $feed['href'],
- 'class' => $feed_classname,
- 'type' => $feed_mimetype,
- 'title' => $feed_title),
- $feed['textContent']);
+ $this->out->element('a', array('href' => $feed->url,
+ 'class' => $classname,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title),
+ $feed->typeName());
$this->out->elementEnd('li');
}
}
diff --git a/lib/grouplist.php b/lib/grouplist.php
index 4c448e250..6801ab426 100644
--- a/lib/grouplist.php
+++ b/lib/grouplist.php
@@ -124,7 +124,7 @@ class GroupList extends Widget
if ($this->group->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index e2319b1fd..06603ac05 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -101,29 +101,32 @@ class HTMLOutputter extends XMLOutputter
$type = common_negotiate_type($cp, $sp);
if (!$type) {
- common_user_error(_('This page is not available in a '.
- 'media type you accept'), 406);
- exit(0);
+ throw new ClientException(_('This page is not available in a '.
+ 'media type you accept'), 406);
}
}
header('Content-Type: '.$type);
-
+
$this->extraHeaders();
$this->startXML('html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
- // FIXME: correct language for interface
-
- $language = common_language();
+ $language = $this->getLanguage();
$this->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
'xml:lang' => $language,
'lang' => $language));
}
+ function getLanguage()
+ {
+ // FIXME: correct language for interface
+ return common_language();
+ }
+
/**
* Ends an HTML document
*
@@ -134,7 +137,7 @@ class HTMLOutputter extends XMLOutputter
$this->elementEnd('html');
$this->endXML();
}
-
+
/**
* To specify additional HTTP headers for the action
*
diff --git a/lib/jabber.php b/lib/jabber.php
index f41d984d6..3fbb3e1ab 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -114,7 +114,7 @@ function jabber_connect($resource=null)
try {
$conn->connect(true); // true = persistent connection
} catch (XMPPHP_Exception $e) {
- common_log(LOG_ERROR, $e->getMessage());
+ common_log(LOG_ERR, $e->getMessage());
return false;
}
@@ -186,6 +186,11 @@ function jabber_format_entry($profile, $notice)
$entry .= "<id>". $notice->uri . "</id>\n";
$entry .= "<published>".common_date_w3dtf($notice->created)."</published>\n";
$entry .= "<updated>".common_date_w3dtf($notice->modified)."</updated>\n";
+ if ($notice->reply_to) {
+ $replyurl = common_local_url('shownotice',
+ array('notice' => $notice->reply_to));
+ $entry .= "<link rel='related' href='" . $replyurl . "'/>\n";
+ }
$entry .= "</entry>\n";
$html = "\n<html xmlns='http://jabber.org/protocol/xhtml-im'>\n";
diff --git a/lib/mail.php b/lib/mail.php
index a1faefc80..9fa86de5c 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -573,3 +573,53 @@ function mail_notify_fave($other, $user, $notice)
common_init_locale();
mail_to_user($other, $subject, $body);
}
+
+/**
+ * notify a user that they have received an "attn:" message AKA "@-reply"
+ *
+ * @param User $user The user who recevied the notice
+ * @param Notice $notice The notice that was sent
+ *
+ * @return void
+ */
+
+function mail_notify_attn($user, $notice)
+{
+ if (!$user->email || !$user->emailnotifyattn) {
+ return;
+ }
+
+ $sender = $notice->getProfile();
+
+ $bestname = $sender->getBestName();
+
+ common_init_locale($user->language);
+
+ $subject = sprintf(_('%s sent a notice to your attention'), $bestname);
+
+ $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+ "The notice is here:\n\n".
+ "\t%3\$s\n\n" .
+ "It reads:\n\n".
+ "\t%4\$s\n\n" .
+ "You can reply back here:\n\n".
+ "\t%5\$s\n\n" .
+ "The list of all @-replies for you here:\n\n" .
+ "%6\$s\n\n" .
+ "Faithfully yours,\n" .
+ "%2\$s\n\n" .
+ "P.S. You can turn off these email notifications here: %7\$s\n"),
+ $bestname,
+ common_config('site', 'name'),
+ common_local_url('shownotice',
+ array('notice' => $notice->id)),
+ $notice->content,
+ common_local_url('newnotice',
+ array('replyto' => $sender->nickname)),
+ common_local_url('replies',
+ array('nickname' => $user->nickname)),
+ common_local_url('emailsettings'));
+
+ common_init_locale();
+ mail_to_user($user, $subject, $body);
+}
diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php
index 5734d8001..5380563b9 100644
--- a/lib/popularnoticesection.php
+++ b/lib/popularnoticesection.php
@@ -80,4 +80,9 @@ class PopularNoticeSection extends NoticeSection
{
return 'popular_notices';
}
+
+ function moreUrl()
+ {
+ return common_local_url('favorited');
+ }
}
diff --git a/lib/profilelist.php b/lib/profilelist.php
index 4d924b039..8bef49dce 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -123,7 +123,7 @@ class ProfileList extends Widget
if ($this->profile->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
diff --git a/lib/router.php b/lib/router.php
new file mode 100644
index 000000000..d47ad7118
--- /dev/null
+++ b/lib/router.php
@@ -0,0 +1,363 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * URL routing utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category URL
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once 'Net/URL/Mapper.php';
+
+/**
+ * URL Router
+ *
+ * Cheap wrapper around Net_URL_Mapper
+ *
+ * @category URL
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class Router
+{
+ static $m = null;
+ static $inst = null;
+
+ static function get()
+ {
+ if (!Router::$inst) {
+ Router::$inst = new Router();
+ }
+ return Router::$inst;
+ }
+
+ function __construct()
+ {
+ if (!$this->m) {
+ $this->m = $this->initialize();
+ }
+ }
+
+ function initialize() {
+
+ $m = Net_URL_Mapper::getInstance();
+
+ // In the "root"
+
+ $m->connect('', array('action' => 'public'));
+ $m->connect('rss', array('action' => 'publicrss'));
+ $m->connect('xrds', array('action' => 'publicxrds'));
+ $m->connect('featuredrss', array('action' => 'featuredrss'));
+ $m->connect('favoritedrss', array('action' => 'favoritedrss'));
+ $m->connect('opensearch/people', array('action' => 'opensearch',
+ 'type' => 'people'));
+ $m->connect('opensearch/notice', array('action' => 'opensearch',
+ 'type' => 'notice'));
+
+ // docs
+
+ $m->connect('doc/:title', array('action' => 'doc'));
+
+ // facebook
+
+ $m->connect('facebook', array('action' => 'facebookhome'));
+ $m->connect('facebook/index.php', array('action' => 'facebookhome'));
+ $m->connect('facebook/settings.php', array('action' => 'facebooksettings'));
+ $m->connect('facebook/invite.php', array('action' => 'facebookinvite'));
+ $m->connect('facebook/remove', array('action' => 'facebookremove'));
+
+ // main stuff is repetitive
+
+ $main = array('login', 'logout', 'register', 'subscribe',
+ 'unsubscribe', 'confirmaddress', 'recoverpassword',
+ 'invite', 'favor', 'disfavor', 'sup',
+ 'tagother', 'block');
+
+ foreach ($main as $a) {
+ $m->connect('main/'.$a, array('action' => $a));
+ }
+
+ // these take a code
+
+ foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
+ $m->connect('main/'.$c.'/:code', array('action' => $c));
+ }
+
+ // exceptional
+
+ $m->connect('main/openid', array('action' => 'openidlogin'));
+ $m->connect('main/remote', array('action' => 'remotesubscribe'));
+
+ // settings
+
+ foreach (array('profile', 'avatar', 'password', 'openid', 'im',
+ 'email', 'sms', 'twitter', 'other') as $s) {
+ $m->connect('settings/'.$s, array('action' => $s.'settings'));
+ }
+
+ // search
+
+ foreach (array('group', 'people', 'notice') as $s) {
+ $m->connect('search/'.$s, array('action' => $s.'search'));
+ }
+
+ $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
+
+ // notice
+
+ $m->connect('notice/new', array('action' => 'newnotice'));
+ $m->connect('notice/:notice',
+ array('action' => 'shownotice'),
+ array('notice' => '[0-9]+'));
+ $m->connect('notice/delete', array('action' => 'deletenotice'));
+ $m->connect('notice/delete/:notice',
+ array('action' => 'deletenotice'),
+ array('notice' => '[0-9]+'));
+
+ $m->connect('message/new', array('action' => 'newmessage'));
+ $m->connect('message/:message',
+ array('action' => 'showmessage'),
+ array('message' => '[0-9]+'));
+
+ $m->connect('user/:id',
+ array('action' => 'userbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('tags/', array('action' => 'publictagcloud'));
+ $m->connect('tag/', array('action' => 'publictagcloud'));
+ $m->connect('tags', array('action' => 'publictagcloud'));
+ $m->connect('tag', array('action' => 'publictagcloud'));
+ $m->connect('tag/:tag/rss',
+ array('action' => 'tagrss'),
+ array('tag' => '[a-zA-Z0-9]+'));
+ $m->connect('tag/:tag',
+ array('action' => 'tag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('peopletag/:tag',
+ array('action' => 'peopletag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('featured/', array('action' => 'featured'));
+ $m->connect('featured', array('action' => 'featured'));
+ $m->connect('favorited/', array('action' => 'favorited'));
+ $m->connect('favorited', array('action' => 'favorited'));
+
+ // groups
+
+ $m->connect('group/new', array('action' => 'newgroup'));
+
+ foreach (array('edit', 'join', 'leave') as $v) {
+ $m->connect('group/:nickname/'.$v,
+ array('action' => $v.'group'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ foreach (array('members', 'logo', 'rss') as $n) {
+ $m->connect('group/:nickname/'.$n,
+ array('action' => 'group'.$n),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ $m->connect('group/:id/id',
+ array('action' => 'groupbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('group/:nickname',
+ array('action' => 'showgroup'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+
+ $m->connect('group/', array('action' => 'groups'));
+ $m->connect('group', array('action' => 'groups'));
+ $m->connect('groups/', array('action' => 'groups'));
+ $m->connect('groups', array('action' => 'groups'));
+
+ // Twitter-compatible API
+
+ // statuses API
+
+ $m->connect('api/statuses/:method',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+
+ $m->connect('api/statuses/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(user_timeline|show|destroy|friends|followers)'));
+
+ // users
+
+ $m->connect('api/users/show/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'users'));
+
+ $m->connect('api/users/:method',
+ array('action' => 'api',
+ 'apiaction' => 'users'),
+ array('method' => 'show(\.(xml|json|atom|rss))?'));
+
+ // direct messages
+
+ $m->connect('api/direct_messages/:method',
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages'),
+ array('method' => '(sent|new)(\.(xml|json|atom|rss))?'));
+
+ $m->connect('api/direct_messages/destroy/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages'));
+
+ $m->connect('api/:method',
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages'),
+ array('method' => 'direct_messages(\.(xml|json|atom|rss))?'));
+
+ // friendships
+
+ $m->connect('api/friendships/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => '(create|destroy)'));
+
+ $m->connect('api/friendships/:method',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => 'exists(\.(xml|json|rss|atom))'));
+
+ // account
+
+ $m->connect('api/account/:method',
+ array('action' => 'api',
+ 'apiaction' => 'account'));
+
+ // favorites
+
+ $m->connect('api/favorites/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ $m->connect('api/favorites/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites',
+ 'method' => 'favorites'));
+
+ $m->connect('api/:method',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'),
+ array('method' => 'favorites(\.(xml|json|rss|atom))?'));
+
+ // notifications
+
+ $m->connect('api/notifications/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ // blocks
+
+ $m->connect('api/blocks/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'blocks'));
+
+ // help
+
+ $m->connect('api/help/:method',
+ array('action' => 'api',
+ 'apiaction' => 'help'));
+
+ // laconica
+
+ $m->connect('api/laconica/:method',
+ array('action' => 'api',
+ 'apiaction' => 'laconica'));
+
+ // user stuff
+
+ foreach (array('subscriptions', 'subscribers',
+ 'nudge', 'xrds', 'all', 'foaf',
+ 'replies', 'inbox', 'outbox', 'microsummary') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => $a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('subscriptions', 'subscribers') as $a) {
+ $m->connect(':nickname/'.$a.'/:tag',
+ array('action' => $a),
+ array('tag' => '[a-zA-Z0-9]+',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('rss', 'groups') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => 'user'.$a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('all', 'replies', 'favorites') as $a) {
+ $m->connect(':nickname/'.$a.'/rss',
+ array('action' => $a.'rss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ $m->connect(':nickname/favorites',
+ array('action' => 'showfavorites'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname/avatar/:size',
+ array('action' => 'avatarbynickname'),
+ array('size' => '(original|96|48|24)',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname',
+ array('action' => 'showstream'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ return $m;
+ }
+
+ function map($path)
+ {
+ return $this->m->match($path);
+ }
+
+ function build($action, $args=null, $fragment=null)
+ {
+ $action_arg = array('action' => $action);
+
+ if ($args) {
+ $args = array_merge($action_arg, $args);
+ } else {
+ $args = $action_arg;
+ }
+
+ return $this->m->generate($args, null, $fragment);
+ }
+} \ No newline at end of file
diff --git a/lib/section.php b/lib/section.php
index 0c32ddcf8..d14575086 100644
--- a/lib/section.php
+++ b/lib/section.php
@@ -103,6 +103,6 @@ class Section extends Widget
function moreTitle()
{
- return null;
+ return _('More...');
}
}
diff --git a/lib/twitter.php b/lib/twitter.php
index 197298549..deb6fd276 100644
--- a/lib/twitter.php
+++ b/lib/twitter.php
@@ -19,6 +19,8 @@
if (!defined('LACONICA')) { exit(1); }
+define("TWITTER_SERVICE", 1); // Twitter is foreign_service ID 1
+
function get_twitter_data($uri, $screen_name, $password)
{
@@ -28,14 +30,13 @@ function get_twitter_data($uri, $screen_name, $password)
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
- # CURLOPT_USERAGENT => "identi.ca",
+ CURLOPT_USERAGENT => "Laconica",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
# Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
-
$ch = curl_init($uri);
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
@@ -95,7 +96,7 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser->nickname = $screen_name;
$fuser->uri = 'http://twitter.com/' . $screen_name;
$fuser->id = $twitter_id;
- $fuser->service = 1; // Twitter
+ $fuser->service = TWITTER_SERVICE; // Twitter
$fuser->created = common_sql_now();
$result = $fuser->insert();
@@ -206,3 +207,93 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password)
return true;
}
+function is_twitter_bound($notice, $flink) {
+
+ // Check to see if notice should go to Twitter
+ if (($flink->noticesync & FOREIGN_NOTICE_SEND)) {
+
+ // If it's not a Twitter-style reply, or if the user WANTS to send replies.
+ if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function broadcast_twitter($notice)
+{
+ global $config;
+ $success = true;
+
+ $flink = Foreign_link::getByUserID($notice->profile_id,
+ TWITTER_SERVICE);
+
+ // XXX: Not sure WHERE to check whether a notice should go to
+ // Twitter. Should we even put in the queue if it shouldn't? --Zach
+ if (is_twitter_bound($notice, $flink)) {
+
+ $fuser = $flink->getForeignUser();
+ $twitter_user = $fuser->nickname;
+ $twitter_password = $flink->credentials;
+ $uri = 'http://www.twitter.com/statuses/update.json';
+
+ // XXX: Hack to get around PHP cURL's use of @ being a a meta character
+ $statustxt = preg_replace('/^@/', ' @', $notice->content);
+
+ $options = array(
+ CURLOPT_USERPWD => "$twitter_user:$twitter_password",
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS =>
+ array(
+ 'status' => $statustxt,
+ 'source' => $config['integration']['source']
+ ),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FAILONERROR => true,
+ CURLOPT_HEADER => false,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_USERAGENT => "Laconica",
+ CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be?
+ CURLOPT_TIMEOUT => 120,
+
+ # Twitter is strict about accepting invalid "Expect" headers
+ CURLOPT_HTTPHEADER => array('Expect:')
+ );
+
+ $ch = curl_init($uri);
+ curl_setopt_array($ch, $options);
+ $data = curl_exec($ch);
+ $errmsg = curl_error($ch);
+
+ if ($errmsg) {
+ common_debug("cURL error: $errmsg - " .
+ "trying to send notice for $twitter_user.",
+ __FILE__);
+ $success = false;
+ }
+
+ curl_close($ch);
+
+ if (!$data) {
+ common_debug("No data returned by Twitter's " .
+ "API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+
+ // Twitter should return a status
+ $status = json_decode($data);
+
+ if (!$status->id) {
+ common_debug("Unexpected data returned by Twitter " .
+ " API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+ }
+
+ return $success;
+}
+
diff --git a/lib/util.php b/lib/util.php
index c5a092f63..46aa7b9df 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -394,32 +394,32 @@ function common_render_text($text)
function common_replace_urls_callback($text, $callback) {
// Start off with a regex
- $regex = '#
- (?:
- (?:
- (?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://
- |
- (?:mailto|aim|tel):
- )
- [^.\s]+\.[^\s]+
- |
- (?:[^.\s/:]+\.)+
- (?:museum|travel|[a-z]{2,4})
- (?:[:/][^\s]*)?
- )
- #ix';
+ $regex = '#'.
+ '(?:'.
+ '(?:'.
+ '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://'.
+ '|'.
+ '(?:mailto|aim|tel):'.
+ ')'.
+ '[^.\s]+\.[^\s]+'.
+ '|'.
+ '(?:[^.\s/:]+\.)+'.
+ '(?:museum|travel|[a-z]{2,4})'.
+ '(?:[:/][^\s]*)?'.
+ ')'.
+ '#ix';
preg_match_all($regex, $text, $matches);
// Then clean up what the regex left behind
$offset = 0;
- foreach($matches[0] as $url) {
- $url = htmlspecialchars_decode($url);
+ foreach($matches[0] as $orig_url) {
+ $url = htmlspecialchars_decode($orig_url);
// Make sure we didn't pick up an email address
if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
- // Remove trailing punctuation
- $url = rtrim($url, '.?!,;:\'"`');
+ // Remove surrounding punctuation
+ $url = trim($url, '.?!,;:\'"`([<');
// Remove surrounding parens and the like
preg_match('/[)\]>]+$/', $url, $trailing);
@@ -446,7 +446,7 @@ function common_replace_urls_callback($text, $callback) {
// If the first part wasn't cap'd but the last part was, we captured too much
if ((!$prev_part && $last_part)) {
- $url = substr_replace($url, '', mb_strpos($url, '.'.$url_parts[2], 0));
+ $url = mb_substr($url, 0 , mb_strpos($url, '.'.$url_parts['2'], 0));
}
// Capture the new TLD
@@ -456,6 +456,9 @@ function common_replace_urls_callback($text, $callback) {
if (!in_array($url_parts[2], $tlds)) continue;
+ // Put the url back the way we found it.
+ $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
+
// Call user specified func
$modified_url = $callback($url);
@@ -469,16 +472,25 @@ function common_replace_urls_callback($text, $callback) {
}
function common_linkify($url) {
+ // It comes in special'd, so we unspecial it before passing to the stringifying
+ // functions
+ $ext = pathinfo($url, PATHINFO_EXTENSION);
+ $url = htmlspecialchars_decode($url);
+ $video_ext = array('mp4', 'flv', 'avi', 'mpg', 'mp3', 'ogg');
$display = $url;
- $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url:$url;
+ $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url;
+
+ $attrs = array('href' => $url, 'rel' => 'external');
+
+ if (in_array($ext, $video_ext)) {
+ $attrs['class'] = 'media';
+ }
if ($longurl = common_longurl($url)) {
- $longurl = htmlentities($longurl, ENT_QUOTES, 'UTF-8');
- $title = "title=\"$longurl\"";
+ $attrs['title'] = $longurl;
}
- else $title = '';
- return "<a href=\"$url\" $title rel=\"external\">$display</a>";
+ return XMLStringer::estring('a', $attrs, $display);
}
function common_longurl($short_url)
@@ -579,7 +591,13 @@ function common_tag_link($tag)
{
$canonical = common_canonical_tag($tag);
$url = common_local_url('tag', array('tag' => $canonical));
- return '<span class="tag"><a href="' . htmlspecialchars($url) . '" rel="tag">' . htmlspecialchars($tag) . '</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => 'tag'),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
}
function common_canonical_tag($tag)
@@ -597,7 +615,14 @@ function common_at_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
if ($recipient) {
- return '<span class="vcard"><a href="'.htmlspecialchars($recipient->profileurl).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $xs = new XMLStringer(false);
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $recipient->profileurl,
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -608,7 +633,14 @@ function common_group_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$group = User_group::staticGet('nickname', common_canonical_nickname($nickname));
if ($group && $sender->isMember($group)) {
- return '<span class="vcard"><a href="'.htmlspecialchars($group->permalink()).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $group->permalink(),
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -625,7 +657,13 @@ function common_at_hash_link($sender_id, $tag)
$url = common_local_url('subscriptions',
array('nickname' => $user->nickname,
'tag' => $tag));
- return '<span class="tag"><a href="'.htmlspecialchars($url).'" rel="tag">'.$tag.'</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => $tag),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $tag;
}
@@ -669,275 +707,18 @@ function common_relative_profile($sender, $nickname, $dt=null)
function common_local_url($action, $args=null, $fragment=null)
{
- $url = null;
+ $r = Router::get();
+ $path = $r->build($action, $args, $fragment);
+ if ($path) {
+ }
if (common_config('site','fancy')) {
- $url = common_fancy_url($action, $args);
+ $url = common_path(mb_substr($path, 1));
} else {
- $url = common_simple_url($action, $args);
- }
- if (!is_null($fragment)) {
- $url .= '#'.$fragment;
+ $url = common_path('index.php'.$path);
}
return $url;
}
-function common_fancy_url($action, $args=null)
-{
- switch (strtolower($action)) {
- case 'public':
- if ($args && isset($args['page'])) {
- return common_path('?page=' . $args['page']);
- } else {
- return common_path('');
- }
- case 'featured':
- if ($args && isset($args['page'])) {
- return common_path('featured?page=' . $args['page']);
- } else {
- return common_path('featured');
- }
- case 'favorited':
- if ($args && isset($args['page'])) {
- return common_path('favorited?page=' . $args['page']);
- } else {
- return common_path('favorited');
- }
- case 'publicrss':
- return common_path('rss');
- case 'publicatom':
- return common_path("api/statuses/public_timeline.atom");
- case 'publicxrds':
- return common_path('xrds');
- case 'tagrss':
- return common_path('tag/' . $args['tag'] . '/rss');
- case 'featuredrss':
- return common_path('featuredrss');
- case 'favoritedrss':
- return common_path('favoritedrss');
- case 'opensearch':
- if ($args && $args['type']) {
- return common_path('opensearch/'.$args['type']);
- } else {
- return common_path('opensearch/people');
- }
- case 'doc':
- return common_path('doc/'.$args['title']);
- case 'block':
- case 'login':
- case 'logout':
- case 'subscribe':
- case 'unsubscribe':
- case 'invite':
- return common_path('main/'.$action);
- case 'tagother':
- return common_path('main/tagother?id='.$args['id']);
- case 'register':
- if ($args && $args['code']) {
- return common_path('main/register/'.$args['code']);
- } else {
- return common_path('main/register');
- }
- case 'remotesubscribe':
- if ($args && $args['nickname']) {
- return common_path('main/remote?nickname=' . $args['nickname']);
- } else {
- return common_path('main/remote');
- }
- case 'nudge':
- return common_path($args['nickname'].'/nudge');
- case 'openidlogin':
- return common_path('main/openid');
- case 'profilesettings':
- return common_path('settings/profile');
- case 'passwordsettings':
- return common_path('settings/password');
- case 'emailsettings':
- return common_path('settings/email');
- case 'openidsettings':
- return common_path('settings/openid');
- case 'smssettings':
- return common_path('settings/sms');
- case 'twittersettings':
- return common_path('settings/twitter');
- case 'othersettings':
- return common_path('settings/other');
- case 'deleteprofile':
- return common_path('settings/delete');
- case 'newnotice':
- if ($args && $args['replyto']) {
- return common_path('notice/new?replyto='.$args['replyto']);
- } else {
- return common_path('notice/new');
- }
- case 'shownotice':
- return common_path('notice/'.$args['notice']);
- case 'deletenotice':
- if ($args && $args['notice']) {
- return common_path('notice/delete/'.$args['notice']);
- } else {
- return common_path('notice/delete');
- }
- case 'microsummary':
- case 'xrds':
- case 'foaf':
- return common_path($args['nickname'].'/'.$action);
- case 'all':
- case 'replies':
- case 'inbox':
- case 'outbox':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/'.$action.'?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/'.$action);
- }
- case 'subscriptions':
- case 'subscribers':
- $nickname = $args['nickname'];
- unset($args['nickname']);
- if (isset($args['tag'])) {
- $tag = $args['tag'];
- unset($args['tag']);
- }
- $params = http_build_query($args);
- if ($params) {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : '') . '?' . $params);
- } else {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : ''));
- }
- case 'allrss':
- return common_path($args['nickname'].'/all/rss');
- case 'repliesrss':
- return common_path($args['nickname'].'/replies/rss');
- case 'userrss':
- if (isset($args['limit']))
- return common_path($args['nickname'].'/rss?limit=' . $args['limit']);
- return common_path($args['nickname'].'/rss');
- case 'showstream':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'?page=' . $args['page']);
- } else {
- return common_path($args['nickname']);
- }
-
- case 'usertimeline':
- return common_path("api/statuses/user_timeline/".$args['nickname'].".atom");
- case 'confirmaddress':
- return common_path('main/confirmaddress/'.$args['code']);
- case 'userbyid':
- return common_path('user/'.$args['id']);
- case 'recoverpassword':
- $path = 'main/recoverpassword';
- if ($args['code']) {
- $path .= '/' . $args['code'];
- }
- return common_path($path);
- case 'imsettings':
- return common_path('settings/im');
- case 'avatarsettings':
- return common_path('settings/avatar');
- case 'groupsearch':
- return common_path('search/group' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'peoplesearch':
- return common_path('search/people' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearch':
- return common_path('search/notice' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearchrss':
- return common_path('search/notice/rss' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'avatarbynickname':
- return common_path($args['nickname'].'/avatar/'.$args['size']);
- case 'tag':
- $path = 'tag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'publictagcloud':
- return common_path('tags');
- case 'peopletag':
- $path = 'peopletag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'tags':
- return common_path('tags' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'favor':
- return common_path('main/favor');
- case 'disfavor':
- return common_path('main/disfavor');
- case 'showfavorites':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/favorites?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/favorites');
- }
- case 'favoritesrss':
- return common_path($args['nickname'].'/favorites/rss');
- case 'showmessage':
- return common_path('message/' . $args['message']);
- case 'newmessage':
- return common_path('message/new' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'api':
- // XXX: do fancy URLs for all the API methods
- switch (strtolower($args['apiaction'])) {
- case 'statuses':
- switch (strtolower($args['method'])) {
- case 'user_timeline.rss':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.rss');
- case 'user_timeline.atom':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.atom');
- case 'user_timeline.json':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.json');
- case 'user_timeline.xml':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.xml');
- default: return common_simple_url($action, $args);
- }
- default: return common_simple_url($action, $args);
- }
- case 'sup':
- if ($args && isset($args['seconds'])) {
- return common_path('main/sup?seconds='.$args['seconds']);
- } else {
- return common_path('main/sup');
- }
- case 'newgroup':
- return common_path('group/new');
- case 'showgroup':
- return common_path('group/'.$args['nickname'] . (($args['page']) ? ('?page=' . $args['page']) : ''));
- case 'editgroup':
- return common_path('group/'.$args['nickname'].'/edit');
- case 'joingroup':
- return common_path('group/'.$args['nickname'].'/join');
- case 'leavegroup':
- return common_path('group/'.$args['nickname'].'/leave');
- case 'groupbyid':
- return common_path('group/'.$args['id'].'/id');
- case 'grouprss':
- return common_path('group/'.$args['nickname'].'/rss');
- case 'groupmembers':
- return common_path('group/'.$args['nickname'].'/members' . (($args['page']) ? ('?page=' . $args['page']) : ''));
- case 'grouplogo':
- return common_path('group/'.$args['nickname'].'/logo');
- case 'usergroups':
- $nickname = $args['nickname'];
- unset($args['nickname']);
- return common_path($nickname.'/groups' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'groups':
- return common_path('group' . (($args) ? ('?' . http_build_query($args)) : ''));
- default:
- return common_simple_url($action, $args);
- }
-}
-
-function common_simple_url($action, $args=null)
-{
- global $config;
- /* XXX: pretty URLs */
- $extra = '';
- if ($args) {
- foreach ($args as $key => $value) {
- $extra .= "&${key}=${value}";
- }
- }
- return common_path("index.php?action=${action}${extra}");
-}
-
function common_path($relative)
{
global $config;
@@ -1046,24 +827,6 @@ function common_redirect($url, $code=307)
function common_broadcast_notice($notice, $remote=false)
{
-
- // Check to see if notice should go to Twitter
- $flink = Foreign_link::getByUserID($notice->profile_id, 1); // 1 == Twitter
- if (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND) {
-
- // If it's not a Twitter-style reply, or if the user WANTS to send replies...
-
- if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
- (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY)) {
-
- $result = common_twitter_broadcast($notice, $flink);
-
- if (!$result) {
- common_debug('Unable to send notice: ' . $notice->id . ' to Twitter.', __FILE__);
- }
- }
- }
-
if (common_config('queue', 'enabled')) {
// Do it later!
return common_enqueue_notice($notice);
@@ -1072,73 +835,11 @@ function common_broadcast_notice($notice, $remote=false)
}
}
-function common_twitter_broadcast($notice, $flink)
-{
- global $config;
- $success = true;
- $fuser = $flink->getForeignUser();
- $twitter_user = $fuser->nickname;
- $twitter_password = $flink->credentials;
- $uri = 'http://www.twitter.com/statuses/update.json';
-
- // XXX: Hack to get around PHP cURL's use of @ being a a meta character
- $statustxt = preg_replace('/^@/', ' @', $notice->content);
-
- $options = array(
- CURLOPT_USERPWD => "$twitter_user:$twitter_password",
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => array(
- 'status' => $statustxt,
- 'source' => $config['integration']['source']
- ),
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FAILONERROR => true,
- CURLOPT_HEADER => false,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_USERAGENT => "Laconica",
- CURLOPT_CONNECTTIMEOUT => 120, // XXX: Scary!!!! How long should this be?
- CURLOPT_TIMEOUT => 120,
-
- # Twitter is strict about accepting invalid "Expect" headers
- CURLOPT_HTTPHEADER => array('Expect:')
- );
-
- $ch = curl_init($uri);
- curl_setopt_array($ch, $options);
- $data = curl_exec($ch);
- $errmsg = curl_error($ch);
-
- if ($errmsg) {
- common_debug("cURL error: $errmsg - trying to send notice for $twitter_user.",
- __FILE__);
- $success = false;
- }
-
- curl_close($ch);
-
- if (!$data) {
- common_debug("No data returned by Twitter's API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- // Twitter should return a status
- $status = json_decode($data);
-
- if (!$status->id) {
- common_debug("Unexpected data returned by Twitter API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- return $success;
-}
-
// Stick the notice on the queue
function common_enqueue_notice($notice)
{
- foreach (array('jabber', 'omb', 'sms', 'public') as $transport) {
+ foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook') as $transport) {
$qi = new Queue_item();
$qi->notice_id = $notice->id;
$qi->transport = $transport;
@@ -1185,6 +886,15 @@ function common_real_broadcast($notice, $remote=false)
common_log(LOG_ERR, 'Error in public broadcast for notice ' . $notice->id);
}
}
+ if ($success) {
+ $success = broadcast_twitter($notice);
+ if (!$success) {
+ common_log(LOG_ERR, 'Error in Twitter broadcast for notice ' . $notice->id);
+ }
+ }
+
+ // XXX: Do a real-time FB broadcast here?
+
// XXX: broadcast notices to other IM
return $success;
}
diff --git a/lib/xmlstringer.php b/lib/xmlstringer.php
new file mode 100644
index 000000000..951b13b67
--- /dev/null
+++ b/lib/xmlstringer.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Generator for in-memory XML
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Create in-memory XML
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see Action
+ * @see HTMLOutputter
+ */
+
+class XMLStringer extends XMLOutputter
+{
+ function __construct($indent=false)
+ {
+ $this->xw = new XMLWriter();
+ $this->xw->openMemory();
+ $this->xw->setIndent($indent);
+ }
+
+ function getString()
+ {
+ return $this->xw->outputMemory();
+ }
+
+ // utility for quickly creating XML-strings
+
+ static function estring($tag, $attrs=null, $content=null)
+ {
+ $xs = new XMLStringer();
+ $xs->element($tag, $attrs, $content);
+ return $xs->getString();
+ }
+} \ No newline at end of file