code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social 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.
//
// GNU social 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 GNU social.  If not, see <http://www.gnu.org/licenses/>.

defined('GNUSOCIAL') || die();

/**
 * Superclass for plugins which add Activity types and such
 *
 * @category  Activity
 * @package   GNUsocial
 * @author    Mikael Nordfeldth <mmn@hethane.se>
 * @copyright 2014 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
abstract class ActivityHandlerPlugin extends Plugin
{
    /**
     * Returns a key string which represents this activity in HTML classes,
     * ids etc, as when offering selection of what type of post to make.
     * In MicroAppPlugin, this is paired with the user-visible localizable appTitle().
     *
     * @return string (compatible with HTML classes)
     */
    abstract public function tag();

    /**
     * Return a list of ActivityStreams object type IRIs
     * which this micro-app handles. Default implementations
     * of the base class will use this list to check if a
     * given ActivityStreams object belongs to us, via
     * $this->isMyNotice() or $this->isMyActivity.
     *
     * An empty list means any type is ok. (Favorite verb etc.)
     *
     * @return array of strings
     */
    abstract public function types();

    /**
     * Return a list of ActivityStreams verb IRIs which
     * this micro-app handles. Default implementations
     * of the base class will use this list to check if a
     * given ActivityStreams verb belongs to us, via
     * $this->isMyNotice() or $this->isMyActivity.
     *
     * All micro-app classes must override this method.
     *
     * @return array of strings
     */
    public function verbs()
    {
        return array(ActivityVerb::POST);
    }

    /**
     * Check if a given ActivityStreams activity should be handled by this
     * micro-app plugin.
     *
     * The default implementation checks against the activity type list
     * returned by $this->types(), and requires that exactly one matching
     * object be present. You can override this method to expand
     * your checks or to compare the activity's verb, etc.
     *
     * @param Activity $activity
     * @return boolean
     */
    public function isMyActivity(Activity $act)
    {
        return (count($act->objects) == 1
            && ($act->objects[0] instanceof ActivityObject)
            && $this->isMyVerb($act->verb)
            && $this->isMyType($act->objects[0]->type));
    }

    /**
     * Check if a given notice object should be handled by this micro-app
     * plugin.
     *
     * The default implementation checks against the activity type list
     * returned by $this->types(). You can override this method to expand
     * your checks, but follow the execution chain to get it right.
     *
     * @param Notice $notice
     * @return boolean
     */
    public function isMyNotice(Notice $notice)
    {
        return $this->isMyVerb($notice->verb) && $this->isMyType($notice->object_type);
    }

    public function isMyVerb($verb)
    {
        $verb = $verb ?: ActivityVerb::POST;    // post is the default verb
        return ActivityUtils::compareVerbs($verb, $this->verbs());
    }

    public function isMyType($type)
    {
        // Third argument to compareTypes is true, to allow for notices with empty object_type for example (verb-only)
        return count($this->types()) === 0 || ActivityUtils::compareTypes($type, $this->types());
    }

    /**
     * Given a parsed ActivityStreams activity, your plugin
     * gets to figure out how to actually save it into a notice
     * and any additional data structures you require.
     *
     * This function is deprecated and in the future, Notice::saveActivity
     * should be called from onStartHandleFeedEntryWithProfile in this class
     * (which instead turns to saveObjectFromActivity).
     *
     * @param Activity $activity
     * @param Profile $actor
     * @param array $options =array()
     *
     * @return Notice the resulting notice
     */
    public function saveNoticeFromActivity(Activity $activity, Profile $actor, array $options = array())
    {
        // Any plugin which has not implemented saveObjectFromActivity _must_
        // override this function until they are migrated (this function will
        // be deleted when all plugins are migrated to saveObjectFromActivity).

        if (isset($this->oldSaveNew)) {
            throw new ServerException('A function has been called for new saveActivity functionality, but is still set with an oldSaveNew configuration');
        }

        return Notice::saveActivity($activity, $actor, $options);
    }

    /**
     * Given a parsed ActivityStreams activity, your plugin gets
     * to figure out itself how to store the additional data into
     * the database, besides the base data stored by the core.
     *
     * This will handle just about all events where an activity
     * object gets saved, whether it is via AtomPub, OStatus
     * (WebSub and Salmon transports), or ActivityStreams-based
     * backup/restore of account data.
     *
     * You should be able to accept as input the output from an
     * asActivity() call on the stored object. Where applicable,
     * try to use existing ActivityStreams structures and object
     * types, and be liberal in accepting input from what might
     * be other compatible apps.
     *
     * All micro-app classes must override this method.
     *
     * @fixme are there any standard options?
     *
     * @param Activity $activity
     * @param Notice $stored The notice in our database for this certain object
     * @param array $options =array()
     *
     * @return object    If the verb handling plugin creates an object, it can be returned here (otherwise true)
     * @throws exception On any error.
     */
    protected function saveObjectFromActivity(Activity $activity, Notice $stored, array $options = array())
    {
        throw new ServerException('This function should be abstract when all plugins have migrated to saveObjectFromActivity');
    }

    /*
     * This usually gets called from Notice::saveActivity after a Notice object has been created,
     * so it contains a proper id and a uri for the object to be saved.
     */
    public function onStoreActivityObject(Activity $act, Notice $stored, array $options, &$object)
    {
        // $this->oldSaveNew is there during a migration period of plugins, to start using
        // Notice::saveActivity instead of Notice::saveNew
        if (!$this->isMyActivity($act) || isset($this->oldSaveNew)) {
            return true;
        }
        $object = $this->saveObjectFromActivity($act, $stored, $options);
        return false;
    }

    /**
     * Given an existing Notice object, your plugin gets to
     * figure out how to arrange it into an ActivityStreams
     * object.
     *
     * This will be how your specialized notice gets output in
     * Atom feeds and JSON-based ActivityStreams output, including
     * account backup/restore and OStatus (WebSub and Salmon transports).
     *
     * You should be able to round-trip data from this format back
     * through $this->saveNoticeFromActivity(). Where applicable, try
     * to use existing ActivityStreams structures and object types,
     * and consider interop with other compatible apps.
     *
     * All micro-app classes must override this method.
     *
     * @fixme this outputs an ActivityObject, not an Activity. Any compat issues?
     *
     * @param Notice $notice
     *
     * @return ActivityObject
     */
    abstract public function activityObjectFromNotice(Notice $notice);

    /**
     * When a notice is deleted, you'll be called here for a chance
     * to clean up any related resources.
     *
     * All micro-app classes must override this method.
     *
     * @param Notice $notice
     */
    abstract public function deleteRelated(Notice $notice);

    protected function notifyMentioned(Notice $stored, array &$mentioned_ids)
    {
        // pass through silently by default

        // If we want to stop any other plugin from notifying based on this activity, return false instead.
        return true;
    }

    /**
     * Called when generating Atom XML ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Note that you can only add output of additional XML elements,
     * not change existing stuff here.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param XMLOutputter $out to add elements at end of object
     */
    public function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
    {
        // default is a no-op
    }

    /**
     * Called when generating JSON ActivityStreams output from an
     * ActivityObject belonging to this plugin. Gives the plugin
     * a chance to add custom output.
     *
     * Modify the array contents to your heart's content, and it'll
     * all get serialized out as JSON.
     *
     * If output is already handled by the base Activity classes,
     * you can leave this base implementation as a no-op.
     *
     * @param ActivityObject $obj
     * @param array &$out JSON-targeted array which can be modified
     */
    public function activityObjectOutputJson(ActivityObject $obj, array &$out)
    {
        // default is a no-op
    }

    /**
     * When a notice is deleted, delete the related objects
     * by calling the overridable $this->deleteRelated().
     *
     * @param Notice $notice Notice being deleted
     *
     * @return boolean hook value
     */
    public function onNoticeDeleteRelated(Notice $notice)
    {
        if ($this->isMyNotice($notice)) {
            try {
                $this->deleteRelated($notice);
            } catch (NoProfileException $e) {
                // we failed because of database lookup failure, Notice has no recognized profile as creator
                // so we skip this. If we want to remove missing notices we should do a SQL constraints check
                // in the affected plugin.
            } catch (AlreadyFulfilledException $e) {
                // Nothing to see here, it's obviously already gone...
            }
        }

        // Always continue this event in our activity handling plugins.
        return true;
    }

    /**
     * @param Notice $stored The notice being distributed
     * @param array  &$mentioned_ids List of profiles (from $stored->getReplies())
     */
    public function onStartNotifyMentioned(Notice $stored, array &$mentioned_ids)
    {
        if (!$this->isMyNotice($stored)) {
            return true;
        }

        return $this->notifyMentioned($stored, $mentioned_ids);
    }

    /**
     * Render a notice as one of our objects
     *
     * @param Notice $notice Notice to render
     * @param ActivityObject &$object Empty object to fill
     *
     * @return boolean hook value
     */
    public function onStartActivityObjectFromNotice(Notice $notice, &$object)
    {
        if (!$this->isMyNotice($notice)) {
            return true;
        }

        $object = $this->activityObjectFromNotice($notice);
        return false;
    }

    /**
     * Handle a posted object from WebSub
     *
     * @param Activity $activity activity to handle
     * @param Profile $actor Profile for the feed
     *
     * @return boolean hook value
     */
    public function onStartHandleFeedEntryWithProfile(Activity $activity, Profile $profile, &$notice)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

        // We are guaranteed to get a Profile back from checkAuthorship (or it throws an exception)
        $profile = ActivityUtils::checkAuthorship($activity, $profile);

        $object = $activity->objects[0];

        $options = [
            'uri'      => $object->id,
            'url'      => $object->link,
            'self'     => $object->selfLink,
            'is_local' => Notice::REMOTE,
            'source'   => 'ostatus',
        ];

        if (!isset($this->oldSaveNew)) {
            $notice = Notice::saveActivity($activity, $profile, $options);
        } else {
            $notice = $this->saveNoticeFromActivity($activity, $profile, $options);
        }

        return false;
    }

    /**
     * Handle a posted object from Salmon
     *
     * @param Activity $activity activity to handle
     * @param mixed $target user or group targeted
     *
     * @return boolean hook value
     */

    public function onStartHandleSalmonTarget(Activity $activity, $target)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }
        if (!isset($this->oldSaveNew)) {
            // Handle saveActivity in OStatus class for incoming salmon, remove this event
            // handler when all plugins have gotten rid of "oldSaveNew".
            return true;
        }

        $this->log(LOG_INFO, get_called_class() . " checking {$activity->id} as a valid Salmon slap.");

        if ($target instanceof User_group || $target->isGroup()) {
            $uri = $target->getUri();
            if (!array_key_exists($uri, $activity->context->attention)) {
                // @todo FIXME: please document (i18n).
                // TRANS: Client exception thrown when ...
                throw new ClientException(_('Object not posted to this group.'));
            }
        } elseif ($target instanceof Profile && $target->isLocal()) {
            $original = null;
            // FIXME: Shouldn't favorites show up with a 'target' activityobject?
            if (!ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) {
                // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note)
                if (!empty($activity->objects[0]->id)) {
                    $activity->context->replyToID = $activity->objects[0]->id;
                }
            }
            if (!empty($activity->context->replyToID)) {
                $original = Notice::getKV('uri', $activity->context->replyToID);
            }
            if ((!$original instanceof Notice || $original->profile_id != $target->id)
                && !array_key_exists($target->getUri(), $activity->context->attention)) {
                // @todo FIXME: Please document (i18n).
                // TRANS: Client exception when ...
                throw new ClientException(_('Object not posted to this user.'));
            }
        } else {
            // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled.
            throw new ServerException(_('Do not know how to handle this kind of target.'));
        }

        $oactor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
        $actor = $oactor->localProfile();

        // FIXME: will this work in all cases? I made it work for Favorite...
        if (ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST))) {
            $object = $activity->objects[0];
        } else {
            $object = $activity;
        }

        $options = [
            'uri'      => $object->id,
            'url'      => $object->link,
            'self'     => $object->selfLink,
            'is_local' => Notice::REMOTE,
            'source'   => 'ostatus',
        ];

        $notice = $this->saveNoticeFromActivity($activity, $actor, $options);

        return false;
    }

    /**
     * Handle object posted via AtomPub
     *
     * @param Activity $activity Activity that was posted
     * @param Profile $scoped Profile of user posting
     * @param Notice   &$notice Resulting notice
     *
     * @return boolean hook value
     */
    public function onStartAtomPubNewActivity(Activity $activity, Profile $scoped, Notice &$notice = null)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

        $options = array('source' => 'atompub');

        $notice = $this->saveNoticeFromActivity($activity, $scoped, $options);

        return false;
    }

    /**
     * Handle object imported from a backup file
     *
     * @param User $user User to import for
     * @param ActivityObject $author Original author per import file
     * @param Activity $activity Activity to import
     * @param boolean $trusted Is this a trusted user?
     * @param boolean        &$done Is this done (success or unrecoverable error)
     *
     * @return boolean hook value
     */
    public function onStartImportActivity($user, $author, Activity $activity, $trusted, &$done)
    {
        if (!$this->isMyActivity($activity)) {
            return true;
        }

        $obj = $activity->objects[0];

        $options = [
            'uri'    => $object->id,
            'url'    => $object->link,
            'self'   => $object->selfLink,
            'source' => 'restore',
        ];

        // $user->getProfile() is a Profile
        $saved = $this->saveNoticeFromActivity(
            $activity,
            $user->getProfile(),
            $options
        );

        if (!empty($saved)) {
            $done = true;
        }

        return false;
    }

    /**
     * Event handler gives the plugin a chance to add custom
     * Atom XML ActivityStreams output from a previously filled-out
     * ActivityObject.
     *
     * The atomOutput method is called if it's one of
     * our matching types.
     *
     * @param ActivityObject $obj
     * @param XMLOutputter $out to add elements at end of object
     * @return boolean hook return value
     */
    public function onEndActivityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
    {
        if (in_array($obj->type, $this->types())) {
            $this->activityObjectOutputAtom($obj, $out);
        }
        return true;
    }

    /**
     * Event handler gives the plugin a chance to add custom
     * JSON ActivityStreams output from a previously filled-out
     * ActivityObject.
     *
     * The activityObjectOutputJson method is called if it's one of
     * our matching types.
     *
     * @param ActivityObject $obj
     * @param array &$out JSON-targeted array which can be modified
     * @return boolean hook return value
     */
    public function onEndActivityObjectOutputJson(ActivityObject $obj, array &$out)
    {
        if (in_array($obj->type, $this->types())) {
            $this->activityObjectOutputJson($obj, $out);
        }
        return true;
    }

    public function onStartOpenNoticeListItemElement(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->openNoticeListItemElement($nli);

        Event::handle('EndOpenNoticeListItemElement', [$nli]);
        return false;
    }

    public function onStartCloseNoticeListItemElement(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->closeNoticeListItemElement($nli);

        Event::handle('EndCloseNoticeListItemElement', [$nli]);
        return false;
    }

    protected function openNoticeListItemElement(NoticeListItem $nli)
    {
        // Build up the attributes
        $attrs = [];

        // -> The id
        $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
        $id_decl = "notice-{$id}";
        $attrs['id'] = $id_decl;

        // -> The class
        $class = 'h-entry notice ' . $this->tag();
        if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
            $class .= ' limited-scope';
        }
        try {
            $class .= ' notice-source-' . common_to_alphanumeric($nli->notice->source);
        } catch (Exception $e) {
            // either source or what we filtered out was a zero-length string
        }
        $attrs['class'] = $class;

        // -> Robots
        if (!$nli->notice->isLocal()) {
            $attrs['data-nosnippet'] = 'true';
        }

        $nli->out->elementStart('li', $attrs);
    }

    protected function closeNoticeListItemElement(NoticeListItem $nli)
    {
        $nli->out->elementEnd('li');
    }

    // FIXME: This is overriden in MicroAppPlugin but shouldn't have to be
    public function onStartShowNoticeItem(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        try {
            $this->showNoticeListItem($nli);
        } catch (Exception $e) {
            common_log(LOG_ERR, 'Error showing notice ' . $nli->getNotice()->getID() . ': ' . $e->getMessage());
            $nli->out->element('p', 'error', sprintf(_('Error showing notice: %s'), $e->getMessage()));
        }

        Event::handle('EndShowNoticeItem', array($nli));
        return false;
    }

    protected function showNoticeListItem(NoticeListItem $nli)
    {
        $nli->showNoticeHeaders();
        $nli->showContent();
        $nli->showNoticeFooter();
    }

    public function onStartShowNoticeItemNotice(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        $this->showNoticeItemNotice($nli);

        Event::handle('EndShowNoticeItemNotice', array($nli));
        return false;
    }

    protected function showNoticeItemNotice(NoticeListItem $nli)
    {
        $nli->showNoticeTitle();
        $nli->showAuthor();
        $nli->showAddressees();
        $nli->showContent();
    }

    public function onStartShowNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped = null)
    {
        if (!$this->isMyNotice($stored)) {
            return true;
        }

        try {
            $this->showNoticeContent($stored, $out, $scoped);
        } catch (Exception $e) {
            $out->element('div', 'error', $e->getMessage());
        }
        return false;
    }

    protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped = null)
    {
        $out->text($stored->getContent());
    }
}


code:
<?php
/*
 * GNU Social - a federating social network
 * Copyright (C) 2014, Free Software Foundation, 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('GNUSOCIAL')) { exit(1); }

/**
 * @package     Activity
 * @maintainer  Mikael Nordfeldth <mmn@hethane.se>
 */
abstract class ActivityVerbHandlerModule extends ActivityHandlerModule
{
    public function onActivityVerbTitle(ManagedAction $action, $verb, Notice $target, Profile $scoped, &$title)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        $title = $this->getActionTitle($action, $verb, $target, $scoped);
        return false;
    }
    abstract protected function getActionTitle(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    public function onActivityVerbShowContent(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->showActionContent($action, $verb, $target, $scoped);
    }
    protected function showActionContent(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!GNUsocial::isAjax()) {
            $nl = new NoticeListItem($target, $action, array('options'=>false, 'attachments'=>false,
                                                             'item_tag'=>'div', 'id_prefix'=>'fave'));
            $nl->show();
        }

        $form = $this->getActivityForm($action, $verb, $target, $scoped);
        $form->show();

        return false;
    }

    public function onActivityVerbDoPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->doActionPreparation($action, $verb, $target, $scoped);
    }
    abstract protected function doActionPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    public function onActivityVerbDoPost(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->doActionPost($action, $verb, $target, $scoped);
    }
    abstract protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    abstract protected function getActivityForm(ManagedAction $action, $verb, Notice $target, Profile $scoped);
}

repo_name: suraj/sloth

license: mit


code:
<?php
/*
 * GNU Social - a federating social network
 * Copyright (C) 2014, Free Software Foundation, 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('GNUSOCIAL')) { exit(1); }

/**
 * @package     Activity
 * @maintainer  Mikael Nordfeldth <mmn@hethane.se>
 */
abstract class ActivityVerbHandlerPlugin extends ActivityHandlerPlugin
{
    public function onActivityVerbTitle(ManagedAction $action, $verb, Notice $target, Profile $scoped, &$title)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        $title = $this->getActionTitle($action, $verb, $target, $scoped);
        return false;
    }
    abstract protected function getActionTitle(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    public function onActivityVerbShowContent(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->showActionContent($action, $verb, $target, $scoped);
    }
    protected function showActionContent(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!GNUsocial::isAjax()) {
            $nl = new NoticeListItem($target, $action, array('options'=>false, 'attachments'=>false,
                                                             'item_tag'=>'div', 'id_prefix'=>'fave'));
            $nl->show();
        }

        $form = $this->getActivityForm($action, $verb, $target, $scoped);
        $form->show();

        return false;
    }

    public function onActivityVerbDoPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->doActionPreparation($action, $verb, $target, $scoped);
    }
    abstract protected function doActionPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    public function onActivityVerbDoPost(ManagedAction $action, $verb, Notice $target, Profile $scoped)
    {
        if (!$this->isMyVerb($verb)) {
            return true;
        }

        return $this->doActionPost($action, $verb, $target, $scoped);
    }
    abstract protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped);

    abstract protected function getActivityForm(ManagedAction $action, $verb, Notice $target, Profile $scoped);
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Superclass for plugins that do authentication and/or authorization
 *
 * 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  Plugin
 * @package   StatusNet
 * @author    Craig Andrews <candrews@integralblue.com>
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Superclass for plugins that do authentication
 *
 * @category Module
 * @package  GNUsocial
 * @author   Craig Andrews <candrews@integralblue.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
abstract class AuthenticationModule extends Module
{
    //is this plugin authoritative for authentication?
    public $authoritative = false;

    //should accounts be automatically created after a successful login attempt?
    public $autoregistration = false;

    //can the user change their email address
    public $password_changeable=true;

    //unique name for this authentication provider
    public $provider_name;

    //------------Auth plugin should implement some (or all) of these methods------------\\
    /**
    * Check if a nickname/password combination is valid
    * @param username
    * @param password
    * @return boolean true if the credentials are valid, false if they are invalid.
    */
    function checkPassword($username, $password)
    {
        return false;
    }

    /**
    * Automatically register a user when they attempt to login with valid credentials.
    * User::register($data) is a very useful method for this implementation
    * @param username username (that is used to login and find the user in the authentication provider) of the user to be registered
    * @param nickname nickname of the user in the SN system. If nickname is null, then set nickname = username
    * @return mixed instance of User, or false (if user couldn't be created)
    */
    function autoRegister($username, $nickname = null)
    {
        if(is_null($nickname)){
            $nickname = $username;
        }
        $registration_data = array();
        $registration_data['nickname'] = $nickname;
        return User::register($registration_data);
    }

    /**
    * Change a user's password
    * The old password has been verified to be valid by this plugin before this call is made
    * @param username
    * @param oldpassword
    * @param newpassword
    * @return boolean true if the password was changed, false if password changing failed for some reason
    */
    function changePassword($username,$oldpassword,$newpassword)
    {
        return false;
    }

    /**
    * Given a username, suggest what the nickname should be
    * Used during autoregistration
    * Useful if your usernames are ugly, and you want to suggest
    * nice looking nicknames when users initially sign on
    * All nicknames returned by this function should be valid
    *  implementations may want to use common_nicknamize() to ensure validity
    * @param username
    * @return string nickname
    */
    function suggestNicknameForUsername($username)
    {
        return common_nicknamize($username);
    }

    //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
    function onInitializePlugin(){
        if(!isset($this->provider_name)){
            throw new Exception("must specify a provider_name for this authentication provider");
        }
    }

    /**
    * Internal AutoRegister event handler
    * @param nickname
    * @param provider_name
    * @param user - the newly registered user
    */
    function onAutoRegister($nickname, $provider_name, &$user)
    {
        if($provider_name == $this->provider_name && $this->autoregistration){
            $suggested_nickname = $this->suggestNicknameForUsername($nickname);
            $test_user = User::getKV('nickname', $suggested_nickname);
            if($test_user) {
                //someone already exists with the suggested nickname, so used the passed nickname
                $suggested_nickname = common_nicknamize($nickname);
            }
            $test_user = User::getKV('nickname', $suggested_nickname);
            if($test_user) {
                //someone already exists with the suggested nickname
                //not much else we can do
            }else{
                $user = $this->autoRegister($nickname, $suggested_nickname);
                if ($user instanceof User) {
                    User_username::register($user,$nickname,$this->provider_name);
                    return false;
                }
            }
        }
    }

    function onStartCheckPassword($nickname, $password, &$authenticatedUser){
        //map the nickname to a username
        $user_username = new User_username();
        $user_username->username=$nickname;
        $user_username->provider_name=$this->provider_name;
        if($user_username->find() && $user_username->fetch()){
            $authenticated = $this->checkPassword($user_username->username, $password);
            if($authenticated){
                $authenticatedUser = User::getKV('id', $user_username->user_id);
                return false;
            }
        }else{
            //$nickname is the username used to login
            //$suggested_nickname is the nickname the auth provider suggests for that username
            $suggested_nickname = $this->suggestNicknameForUsername($nickname);
            $user = User::getKV('nickname', $suggested_nickname);
            if($user){
                //make sure this user isn't claimed
                $user_username = new User_username();
                $user_username->user_id=$user->id;
                $we_can_handle = false;
                if($user_username->find()){
                    //either this provider, or another one, has already claimed this user
                    //so we cannot. Let another plugin try.
                    return;
                }else{
                    //no other provider claims this user, so it's safe for us to handle it
                    $authenticated = $this->checkPassword($nickname, $password);
                    if($authenticated){
                        $authenticatedUser = $user;
                        User_username::register($authenticatedUser,$nickname,$this->provider_name);
                        return false;
                    }
                }
            }else{
                $authenticated = $this->checkPassword($nickname, $password);
                if($authenticated){
                    if(! Event::handle('AutoRegister', array($nickname, $this->provider_name, &$authenticatedUser))){
                        //unlike most Event::handle lines of code, this one has a ! (not)
                        //we want to do this if the event *was* handled - this isn't a "default" implementation
                        //like most code of this form.
                        if($authenticatedUser){
                            return false;
                        }
                    }
                }
            }
        }
        if($this->authoritative){
            return false;
        }else{
            //we're not authoritative, so let other handlers try
            return;
        }
    }

    function onStartChangePassword(Profile $target ,$oldpassword, $newpassword)
    {
        if($this->password_changeable){
            $user_username = new User_username();
            $user_username->user_id = $target->getID();
            $user_username->provider_name=$this->provider_name;
            if ($user_username->find(true)) {
                $authenticated = $this->checkPassword($user_username->username, $oldpassword);
                if($authenticated){
                    $result = $this->changePassword($user_username->username,$oldpassword,$newpassword);
                    if($result){
                        //stop handling of other handlers, because what was requested was done
                        return false;
                    }else{
                        // TRANS: Exception thrown when a password change fails.
                        throw new Exception(_('Password changing failed.'));
                    }
                }else{
                    if($this->authoritative){
                        //since we're authoritative, no other plugin could do this
                        // TRANS: Exception thrown when a password change fails.
                        throw new Exception(_('Password changing failed.'));
                    }else{
                        //let another handler try
                        return null;
                    }
                }
            }
        }else{
            if($this->authoritative){
                //since we're authoritative, no other plugin could do this
                // TRANS: Exception thrown when a password change attempt fails because it is not allowed.
                throw new Exception(_('Password changing is not allowed.'));
            }
        }
    }

    function onStartAccountSettingsPasswordMenuItem($widget)
    {
        if($this->authoritative && !$this->password_changeable){
            //since we're authoritative, no other plugin could change passwords, so do not render the menu item
            return false;
        }
    }

    function onCheckSchema() {
        $schema = Schema::get();
        $schema->ensureTable('user_username', User_username::schemaDef());
        return true;
    }

    function onUserDeleteRelated($user, &$tables)
    {
        $tables[] = 'User_username';
        return true;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Superclass for plugins that do authentication and/or authorization
 *
 * 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  Plugin
 * @package   StatusNet
 * @author    Craig Andrews <candrews@integralblue.com>
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Superclass for plugins that do authentication
 *
 * @category Module
 * @package  GNUsocial
 * @author   Craig Andrews <candrews@integralblue.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
abstract class AuthenticationPlugin extends Plugin
{
    //is this plugin authoritative for authentication?
    public $authoritative = false;

    //should accounts be automatically created after a successful login attempt?
    public $autoregistration = false;

    //can the user change their email address
    public $password_changeable=true;

    //unique name for this authentication provider
    public $provider_name;

    //------------Auth plugin should implement some (or all) of these methods------------\\
    /**
    * Check if a nickname/password combination is valid
    * @param username
    * @param password
    * @return boolean true if the credentials are valid, false if they are invalid.
    */
    function checkPassword($username, $password)
    {
        return false;
    }

    /**
    * Automatically register a user when they attempt to login with valid credentials.
    * User::register($data) is a very useful method for this implementation
    * @param username username (that is used to login and find the user in the authentication provider) of the user to be registered
    * @param nickname nickname of the user in the SN system. If nickname is null, then set nickname = username
    * @return mixed instance of User, or false (if user couldn't be created)
    */
    function autoRegister($username, $nickname = null)
    {
        if(is_null($nickname)){
            $nickname = $username;
        }
        $registration_data = array();
        $registration_data['nickname'] = $nickname;
        return User::register($registration_data);
    }

    /**
    * Change a user's password
    * The old password has been verified to be valid by this plugin before this call is made
    * @param username
    * @param oldpassword
    * @param newpassword
    * @return boolean true if the password was changed, false if password changing failed for some reason
    */
    function changePassword($username,$oldpassword,$newpassword)
    {
        return false;
    }

    /**
    * Given a username, suggest what the nickname should be
    * Used during autoregistration
    * Useful if your usernames are ugly, and you want to suggest
    * nice looking nicknames when users initially sign on
    * All nicknames returned by this function should be valid
    *  implementations may want to use common_nicknamize() to ensure validity
    * @param username
    * @return string nickname
    */
    function suggestNicknameForUsername($username)
    {
        return common_nicknamize($username);
    }

    //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
    function onInitializePlugin(){
        if(!isset($this->provider_name)){
            throw new Exception("must specify a provider_name for this authentication provider");
        }
    }

    /**
    * Internal AutoRegister event handler
    * @param nickname
    * @param provider_name
    * @param user - the newly registered user
    */
    function onAutoRegister($nickname, $provider_name, &$user)
    {
        if($provider_name == $this->provider_name && $this->autoregistration){
            $suggested_nickname = $this->suggestNicknameForUsername($nickname);
            $test_user = User::getKV('nickname', $suggested_nickname);
            if($test_user) {
                //someone already exists with the suggested nickname, so used the passed nickname
                $suggested_nickname = common_nicknamize($nickname);
            }
            $test_user = User::getKV('nickname', $suggested_nickname);
            if($test_user) {
                //someone already exists with the suggested nickname
                //not much else we can do
            }else{
                $user = $this->autoRegister($nickname, $suggested_nickname);
                if ($user instanceof User) {
                    User_username::register($user,$nickname,$this->provider_name);
                    return false;
                }
            }
        }
    }

    function onStartCheckPassword($nickname, $password, &$authenticatedUser){
        //map the nickname to a username
        $user_username = new User_username();
        $user_username->username=$nickname;
        $user_username->provider_name=$this->provider_name;
        if($user_username->find() && $user_username->fetch()){
            $authenticated = $this->checkPassword($user_username->username, $password);
            if($authenticated){
                $authenticatedUser = User::getKV('id', $user_username->user_id);
                return false;
            }
        }else{
            //$nickname is the username used to login
            //$suggested_nickname is the nickname the auth provider suggests for that username
            $suggested_nickname = $this->suggestNicknameForUsername($nickname);
            $user = User::getKV('nickname', $suggested_nickname);
            if($user){
                //make sure this user isn't claimed
                $user_username = new User_username();
                $user_username->user_id=$user->id;
                $we_can_handle = false;
                if($user_username->find()){
                    //either this provider, or another one, has already claimed this user
                    //so we cannot. Let another plugin try.
                    return;
                }else{
                    //no other provider claims this user, so it's safe for us to handle it
                    $authenticated = $this->checkPassword($nickname, $password);
                    if($authenticated){
                        $authenticatedUser = $user;
                        User_username::register($authenticatedUser,$nickname,$this->provider_name);
                        return false;
                    }
                }
            }else{
                $authenticated = $this->checkPassword($nickname, $password);
                if($authenticated){
                    if(! Event::handle('AutoRegister', array($nickname, $this->provider_name, &$authenticatedUser))){
                        //unlike most Event::handle lines of code, this one has a ! (not)
                        //we want to do this if the event *was* handled - this isn't a "default" implementation
                        //like most code of this form.
                        if($authenticatedUser){
                            return false;
                        }
                    }
                }
            }
        }
        if($this->authoritative){
            return false;
        }else{
            //we're not authoritative, so let other handlers try
            return;
        }
    }

    function onStartChangePassword(Profile $target ,$oldpassword, $newpassword)
    {
        if($this->password_changeable){
            $user_username = new User_username();
            $user_username->user_id = $target->getID();
            $user_username->provider_name=$this->provider_name;
            if ($user_username->find(true)) {
                $authenticated = $this->checkPassword($user_username->username, $oldpassword);
                if($authenticated){
                    $result = $this->changePassword($user_username->username,$oldpassword,$newpassword);
                    if($result){
                        //stop handling of other handlers, because what was requested was done
                        return false;
                    }else{
                        // TRANS: Exception thrown when a password change fails.
                        throw new Exception(_('Password changing failed.'));
                    }
                }else{
                    if($this->authoritative){
                        //since we're authoritative, no other plugin could do this
                        // TRANS: Exception thrown when a password change fails.
                        throw new Exception(_('Password changing failed.'));
                    }else{
                        //let another handler try
                        return null;
                    }
                }
            }
        }else{
            if($this->authoritative){
                //since we're authoritative, no other plugin could do this
                // TRANS: Exception thrown when a password change attempt fails because it is not allowed.
                throw new Exception(_('Password changing is not allowed.'));
            }
        }
    }

    function onStartAccountSettingsPasswordMenuItem($widget)
    {
        if($this->authoritative && !$this->password_changeable){
            //since we're authoritative, no other plugin could change passwords, so do not render the menu item
            return false;
        }
    }

    function onCheckSchema() {
        $schema = Schema::get();
        $schema->ensureTable('user_username', User_username::schemaDef());
        return true;
    }

    function onUserDeleteRelated($user, &$tables)
    {
        $tables[] = 'User_username';
        return true;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Superclass for plugins that do authorization
 *
 * 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  Plugin
 * @package   StatusNet
 * @author    Craig Andrews <candrews@integralblue.com>
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
}

/**
 * Superclass for plugins that do authorization
 *
 * @category Module
 * @package  GNUsocial
 * @author   Craig Andrews <candrews@integralblue.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
abstract class AuthorizationPlugin extends Plugin
{
    //is this plugin authoritative for authorization?
    public $authoritative = false;

    //------------Auth plugin should implement some (or all) of these methods------------\\

    /**
    * Is a user allowed to log in?
    * @param user
    * @return boolean true if the user is allowed to login, false if explicitly not allowed to login, null if we don't explicitly allow or deny login
    */
    function loginAllowed($user) {
        return null;
    }

    /**
    * Does a profile grant the user a named role?
    * @param profile
    * @return boolean true if the profile has the role, false if not
    */
    function hasRole($profile, $name) {
        return false;
    }

    //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\

    function onStartSetUser($user) {
        $loginAllowed = $this->loginAllowed($user);
        if($loginAllowed === true){
            return;
        }else if($loginAllowed === false){
            $user = null;
            return false;
        }else{
            if($this->authoritative) {
                $user = null;
                return false;
            }else{
                return;
            }
        }
    }

    function onStartSetApiUser($user) {
        return $this->onStartSetUser($user);
    }

    function onStartHasRole($profile, $name, &$has_role) {
        if($this->hasRole($profile, $name)){
            $has_role = true;
            return false;
        }else{
            if($this->authoritative) {
                $has_role = false;
                return false;
            }else{
                return;
            }
        }
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Superclass for plugins that do instant messaging
 *
 * 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  Plugin
 * @package   StatusNet
 * @author    Craig Andrews <candrews@integralblue.com>
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
}

/**
 * Superclass for plugins that do IM transport
 *
 * Implementations will likely want to override onStartIoManagerClasses() so that their
 *   IO manager is used
 *
 * @category Module
 * @package  GNUsocial
 * @author   Craig Andrews <candrews@integralblue.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
abstract class ImPlugin extends Plugin
{
    //name of this IM transport
    public $transport = null;
    //list of screennames that should get all public notices
    public $public = array();

    protected $requires_cli = true;

    /**
     * normalize a screenname for comparison
     *
     * @param string $screenname screenname to normalize
     *
     * @return string an equivalent screenname in normalized form
     */
    abstract function normalize($screenname);

    /**
     * validate (ensure the validity of) a screenname
     *
     * @param string $screenname screenname to validate
     *
     * @return boolean
     */
    abstract function validate($screenname);

    /**
     * get the internationalized/translated display name of this IM service
     *
     * @return string
     */
    abstract function getDisplayName();

    /**
     * send a single notice to a given screenname
     * The implementation should put raw data, ready to send, into the outgoing
     *   queue using enqueueOutgoingRaw()
     *
     * @param string $screenname screenname to send to
     * @param Notice $notice notice to send
     *
     * @return boolean success value
     */
    function sendNotice($screenname, Notice $notice)
    {
        return $this->sendMessage($screenname, $this->formatNotice($notice));
    }

    /**
     * send a message (text) to a given screenname
     * The implementation should put raw data, ready to send, into the outgoing
     *   queue using enqueueOutgoingRaw()
     *
     * @param string $screenname screenname to send to
     * @param Notice $body text to send
     *
     * @return boolean success value
     */
    abstract function sendMessage($screenname, $body);

    /**
     * receive a raw message
     * Raw IM data is taken from the incoming queue, and passed to this function.
     * It should parse the raw message and call handleIncoming()
     *
     * Returning false may CAUSE REPROCESSING OF THE QUEUE ITEM, and should
     * be used for temporary failures only. For permanent failures such as
     * unrecognized addresses, return true to indicate your processing has
     * completed.
     *
     * @param object $data raw IM data
     *
     * @return boolean true if processing completed, false for temporary failures
     */
    abstract function receiveRawMessage($data);

    /**
     * get the screenname of the daemon that sends and receives message for this service
     *
     * @return string screenname of this plugin
     */
    abstract function daemonScreenname();

    //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - MISC ========================\

    /**
     * Put raw message data (ready to send) into the outgoing queue
     *
     * @param object $data
     */
    function enqueueOutgoingRaw($data)
    {
        $qm = QueueManager::get();
        $qm->enqueue($data, $this->transport . '-out');
    }

    /**
     * Put raw message data (received, ready to be processed) into the incoming queue
     *
     * @param object $data
     */
    function enqueueIncomingRaw($data)
    {
        $qm = QueueManager::get();
        $qm->enqueue($data, $this->transport . '-in');
    }

    /**
     * given a screenname, get the corresponding user
     *
     * @param string $screenname
     *
     * @return User user
     */
    function getUser($screenname)
    {
        $user_im_prefs = $this->getUserImPrefsFromScreenname($screenname);
        if($user_im_prefs){
            $user = User::getKV('id', $user_im_prefs->user_id);
            $user_im_prefs->free();
            return $user;
        }else{
            return false;
        }
    }

    /**
     * given a screenname, get the User_im_prefs object for this transport
     *
     * @param string $screenname
     *
     * @return User_im_prefs user_im_prefs
     */
    function getUserImPrefsFromScreenname($screenname)
    {
        $user_im_prefs = User_im_prefs::pkeyGet(
            array('transport' => $this->transport,
                  'screenname' => $this->normalize($screenname)));
        if ($user_im_prefs) {
            return $user_im_prefs;
        } else {
            return false;
        }
    }

    /**
     * given a User, get their screenname
     *
     * @param User $user
     *
     * @return string screenname of that user
     */
    function getScreenname($user)
    {
        $user_im_prefs = $this->getUserImPrefsFromUser($user);
        if ($user_im_prefs) {
            return $user_im_prefs->screenname;
        } else {
            return false;
        }
    }

    /**
     * given a User, get their User_im_prefs
     *
     * @param User $user
     *
     * @return User_im_prefs user_im_prefs of that user
     */
    function getUserImPrefsFromUser($user)
    {
        $user_im_prefs = User_im_prefs::pkeyGet(
            array('transport' => $this->transport,
                  'user_id' => $user->id));
        if ($user_im_prefs){
            return $user_im_prefs;
        } else {
            return false;
        }
    }
    //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - SENDING ========================\
    /**
     * Send a message to a given screenname from the site
     *
     * @param string $screenname screenname to send the message to
     * @param string $msg message contents to send
     *
     * @param boolean success
     */
    protected function sendFromSite($screenname, $msg)
    {
        $text = '['.common_config('site', 'name') . '] ' . $msg;
        $this->sendMessage($screenname, $text);
    }

    /**
     * Send a confirmation code to a user
     *
     * @param string $screenname screenname sending to
     * @param string $code the confirmation code
     * @param Profile $target For whom the code is valid for
     *
     * @return boolean success value
     */
    function sendConfirmationCode($screenname, $code, Profile $target)
    {
        // TRANS: Body text for confirmation code e-mail.
        // TRANS: %1$s is a user nickname, %2$s is the StatusNet sitename,
        // TRANS: %3$s is the display name of an IM plugin.
        $body = sprintf(_('User "%1$s" on %2$s has said that your %3$s screenname belongs to them. ' .
          'If that is true, you can confirm by clicking on this URL: ' .
          '%4$s' .
          ' . (If you cannot click it, copy-and-paste it into the ' .
          'address bar of your browser). If that user is not you, ' .
          'or if you did not request this confirmation, just ignore this message.'),
          $target->getNickname(), common_config('site', 'name'), $this->getDisplayName(), common_local_url('confirmaddress', null, array('code' => $code)));

        return $this->sendMessage($screenname, $body);
    }

    /**
     * send a notice to all public listeners
     *
     * For notices that are generated on the local system (by users), we can optionally
     * forward them to remote listeners by XMPP.
     *
     * @param Notice $notice notice to broadcast
     *
     * @return boolean success flag
     */

    function publicNotice($notice)
    {
        // Now, users who want everything

        // FIXME PRIV don't send out private messages here
        // XXX: should we send out non-local messages if public,localonly
        // = false? I think not

        foreach ($this->public as $screenname) {
            common_log(LOG_INFO,
                       'Sending notice ' . $notice->id .
                       ' to public listener ' . $screenname,
                       __FILE__);
            $this->sendNotice($screenname, $notice);
        }

        return true;
    }

    /**
     * broadcast a notice to all subscribers and reply recipients
     *
     * This function will send a notice to all subscribers on the local server
     * who have IM addresses, and have IM notification enabled, and
     * have this subscription enabled for IM. It also sends the notice to
     * all recipients of @-replies who have IM addresses and IM notification
     * enabled. This is really the heart of IM distribution in StatusNet.
     *
     * @param Notice $notice The notice to broadcast
     *
     * @return boolean success flag
     */

    function broadcastNotice($notice)
    {
        $ni = $notice->whoGets();

        foreach ($ni as $user_id => $reason) {
            $user = User::getKV($user_id);
            if (empty($user)) {
                // either not a local user, or just not found
                continue;
            }
            $user_im_prefs = $this->getUserImPrefsFromUser($user);
            if(!$user_im_prefs || !$user_im_prefs->notify){
                continue;
            }

            switch ($reason) {
            case NOTICE_INBOX_SOURCE_REPLY:
                if (!$user_im_prefs->replies) {
                    continue 2;
                }
                break;
            case NOTICE_INBOX_SOURCE_SUB:
                $sub = Subscription::pkeyGet(array('subscriber' => $user->id,
                                                   'subscribed' => $notice->profile_id));
                if (empty($sub) || !$sub->jabber) {
                    continue 2;
                }
                break;
            case NOTICE_INBOX_SOURCE_GROUP:
                break;
            default:
                // TRANS: Exception thrown when trying to deliver a notice to an unknown inbox.
                // TRANS: %d is the unknown inbox ID (number).
                throw new Exception(sprintf(_('Unknown inbox source %d.'), $reason));
            }

            common_log(LOG_INFO,
                       'Sending notice ' . $notice->id . ' to ' . $user_im_prefs->screenname,
                       __FILE__);
            $this->sendNotice($user_im_prefs->screenname, $notice);
            $user_im_prefs->free();
        }

        return true;
    }

    /**
     * makes a plain-text formatted version of a notice, suitable for IM distribution
     *
     * @param Notice  $notice  notice being sent
     *
     * @return string plain-text version of the notice, with user nickname prefixed
     */

    protected function formatNotice(Notice $notice)
    {
        $profile = $notice->getProfile();
        $nicknames = $profile->getNickname();

        try {
            $parent = $notice->getParent();
            $orig_profile = $parent->getProfile();
            $nicknames = sprintf('%1$s => %2$s', $profile->getNickname(), $orig_profile->getNickname());
        } catch (NoParentNoticeException $e) {
            // Not a reply, no parent notice stored
        } catch (NoResultException $e) {
            // Parent notice was probably deleted
        }

        return sprintf('%1$s: %2$s [%3$u]', $nicknames, $notice->content, $notice->id);
    }
    //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - RECEIVING ========================\

    /**
     * Attempt to handle a message as a command
     * @param User $user user the message is from
     * @param string $body message text
     * @return boolean true if the message was a command and was executed, false if it was not a command
     */
    protected function handleCommand($user, $body)
    {
        $inter = new CommandInterpreter();
        $cmd = $inter->handle_command($user, $body);
        if ($cmd) {
            $chan = new IMChannel($this);
            $cmd->execute($chan);
            return true;
        }
        return false;
    }

    /**
     * Is some text an autoreply message?
     * @param string $txt message text
     * @return boolean true if autoreply
     */
    protected function isAutoreply($txt)
    {
        if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) {
            return true;
        } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Is some text an OTR message?
     * @param string $txt message text
     * @return boolean true if OTR
     */
    protected function isOtr($txt)
    {
        if (preg_match('/^\?OTR/', $txt)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Helper for handling incoming messages
     * Your incoming message handler will probably want to call this function
     *
     * @param string $from screenname the message was sent from
     * @param string $message message contents
     *
     * @param boolean success
     */
    protected function handleIncoming($from, $notice_text)
    {
        $user = $this->getUser($from);
        // For common_current_user to work
        global $_cur;
        $_cur = $user;

        if (!$user) {
            $this->sendFromSite($from, 'Unknown user; go to ' .
                             common_local_url('imsettings') .
                             ' to add your address to your account');
            common_log(LOG_WARNING, 'Message from unknown user ' . $from);
            return;
        }
        if ($this->handleCommand($user, $notice_text)) {
            common_log(LOG_INFO, "Command message by $from handled.");
            return;
        } else if ($this->isAutoreply($notice_text)) {
            common_log(LOG_INFO, 'Ignoring auto reply from ' . $from);
            return;
        } else if ($this->isOtr($notice_text)) {
            common_log(LOG_INFO, 'Ignoring OTR from ' . $from);
            return;
        } else {

            common_log(LOG_INFO, 'Posting a notice from ' . $user->nickname);

            $this->addNotice($from, $user, $notice_text);
        }

        $user->free();
        unset($user);
        unset($_cur);
        unset($message);
    }

    /**
     * Helper for handling incoming messages
     * Your incoming message handler will probably want to call this function
     *
     * @param string $from screenname the message was sent from
     * @param string $message message contents
     *
     * @param boolean success
     */
    protected function addNotice($screenname, $user, $body)
    {
        $body = trim(strip_tags($body));
        $content_shortened = common_shorten_links($body);
        if (Notice::contentTooLong($content_shortened)) {
          $this->sendFromSite($screenname,
                              // TRANS: Message given when a status is too long. %1$s is the maximum number of characters,
                              // TRANS: %2$s is the number of characters sent (used for plural).
                              sprintf(_m('Message too long - maximum is %1$d character, you sent %2$d.',
                                         'Message too long - maximum is %1$d characters, you sent %2$d.',
                                         Notice::maxContent()),
                                      Notice::maxContent(),
                                      mb_strlen($content_shortened)));
          return;
        }

        try {
            $notice = Notice::saveNew($user->id, $content_shortened, $this->transport);
        } catch (Exception $e) {
            common_log(LOG_ERR, $e->getMessage());
            $this->sendFromSite($from, $e->getMessage());
            return;
        }

        common_log(LOG_INFO,
                   'Added notice ' . $notice->id . ' from user ' . $user->nickname);
        $notice->free();
        unset($notice);
    }

    //========================EVENT HANDLERS========================\

    /**
     * Register notice queue handler
     *
     * @param QueueManager $manager
     *
     * @return boolean hook return
     */
    function onEndInitializeQueueManager($manager)
    {
        // If we don't require CLI mode, or if we do and GNUSOCIAL_CLI _is_ set, then connect the transports
        // This check is made mostly because some IM plugins can't deliver to transports unless they
        // have continuously running daemons (such as XMPP) and we can't have that over HTTP requests.
        if (!$this->requires_cli || defined('GNUSOCIAL_CLI')) {
            $manager->connect($this->transport . '-in', new ImReceiverQueueHandler($this), 'im');
            $manager->connect($this->transport, new ImQueueHandler($this));
            $manager->connect($this->transport . '-out', new ImSenderQueueHandler($this), 'im');
        }
        return true;
    }

    function onStartImDaemonIoManagers(&$classes)
    {
        //$classes[] = new ImManager($this); // handles sending/receiving/pings/reconnects
        return true;
    }

    function onStartEnqueueNotice($notice, &$transports)
    {
        $profile = Profile::getKV($notice->profile_id);

        if (!$profile) {
            common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
                       'unknown profile ' . common_log_objstring($notice),
                       __FILE__);
        }else{
            $transports[] = $this->transport;
        }

        return true;
    }

    function onEndShowHeadElements(Action $action)
    {
        if ($action instanceof ShownoticeAction) {

            $user_im_prefs = new User_im_prefs();
            $user_im_prefs->user_id = $action->notice->getProfile()->getID();
            $user_im_prefs->transport = $this->transport;

        } elseif ($action instanceof ShowstreamAction) {

            $user_im_prefs = new User_im_prefs();
            $user_im_prefs->user_id = $action->getTarget()->getID();
            $user_im_prefs->transport = $this->transport;

        }
    }

    function onNormalizeImScreenname($transport, &$screenname)
    {
        if($transport == $this->transport)
        {
            $screenname = $this->normalize($screenname);
            return false;
        }
    }

    function onValidateImScreenname($transport, $screenname, &$valid)
    {
        if($transport == $this->transport)
        {
            $valid = $this->validate($screenname);
            return false;
        }
    }

    function onGetImTransports(&$transports)
    {
        $transports[$this->transport] = array(
            'display' => $this->getDisplayName(),
            'daemonScreenname' => $this->daemonScreenname());
    }

    function onSendImConfirmationCode($transport, $screenname, $code, Profile $target)
    {
        if($transport == $this->transport)
        {
            $this->sendConfirmationCode($screenname, $code, $target);
            return false;
        }
    }

    function onUserDeleteRelated($user, &$tables)
    {
        $tables[] = 'User_im_prefs';
        return true;
    }

    function onHaveImPlugin(&$haveImPlugin) {
        $haveImPlugin = true; // set flag true (we're loaded, after all!)
        return false; // stop looking
    }

    function initialize()
    {
        if( ! common_config('queue', 'enabled'))
        {
            // TRANS: Server exception thrown trying to initialise an IM plugin without meeting all prerequisites.
            throw new ServerException(_('Queueing must be enabled to use IM plugins.'));
        }

        if(is_null($this->transport)){
            // TRANS: Server exception thrown trying to initialise an IM plugin without a transport method.
            throw new ServerException(_('Transport cannot be null.'));
        }
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * Superclass for microapp plugin
 *
 * PHP version 5
 *
 * 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  Microapp
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    // This check helps protect against security problems;
    // your code file can't be executed directly from the web.
    exit(1);
}

/**
 * Superclass for microapp plugins
 *
 * This class lets you define micro-applications with different kinds of activities.
 *
 * The applications work more-or-less like other
 *
 * @category  Microapp
 * @package   GNUsocial
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
abstract class MicroAppPlugin extends ActivityHandlerPlugin
{
    /**
     * Returns a localized string which represents this micro-app,
     * to be shown to users selecting what type of post to make.
     * This is paired with the key string in $this->tag().
     *
     * All micro-app classes must override this method.
     *
     * @return string
     */
    abstract function appTitle();

    /**
     * When building the primary notice form, we'll fetch also some
     * alternate forms for specialized types -- that's you!
     *
     * Return a custom Widget or Form object for the given output
     * object, and it'll be included in the HTML output. Beware that
     * your form may be initially hidden.
     *
     * All micro-app classes must override this method.
     *
     * @param HTMLOutputter $out
     * @return Widget
     */
    abstract function entryForm($out);

    /**
     *
     */
    public function newFormAction() {
        // such as 'newbookmark' or 'newevent' route
        return 'new'.$this->tag();
    }

    /**
     * Output the HTML for this kind of object in a list
     *
     * @param NoticeListItem $nli The list item being shown.
     *
     * @return boolean hook value
     */
    function onStartShowNoticeItem(NoticeListItem $nli)
    {
        if (!$this->isMyNotice($nli->notice)) {
            return true;
        }

        // Legacy use was creating a "NoticeListItemAdapter", but
        // nowadays we solve that using event handling for microapps.
        // This section will remain until all plugins are fixed.
        $adapter = $this->adaptNoticeListItem($nli) ?: $nli;

        $adapter->showNotice();
        $adapter->showNoticeAttachments();
        $adapter->showNoticeFooter();

        return false;
    }

    /**
     * Given a notice list item, returns an adapter specific
     * to this plugin.
     *
     * @param NoticeListItem $nli item to adapt
     *
     * @return NoticeListItemAdapter adapter or null
     */
    function adaptNoticeListItem($nli)
    {
      return null;
    }

    function onStartShowEntryForms(&$tabs)
    {
        $tabs[$this->tag()] = array('title' => $this->appTitle(),
                                    'href'  => common_local_url($this->newFormAction()),
                                   );
        return true;
    }

    function onStartMakeEntryForm($tag, $out, &$form)
    {
        if ($tag == $this->tag()) {
            $form = $this->entryForm($out);
            return false;
        }

        return true;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social 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.
//
// GNU social 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 GNU social.  If not, see <http://www.gnu.org/licenses/>.

defined('GNUSOCIAL') || die();

/**
 * Base class for modules
 *
 * A base class for GNU social modules. Mostly a light wrapper around
 * the Event framework.
 *
 * Subclasses of Module will automatically handle an event if they define
 * a method called "onEventName". (Well, OK -- only if they call parent::__construct()
 * in their constructors.)
 *
 * They will also automatically handle the InitializeModule and CleanupModule with the
 * initialize() and cleanup() methods, respectively.
 *
 * @category Module
 * @package  GNUsocial
 * @author   Evan Prodromou <evan@status.net>
 * @copyright 2010-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 *
 * @see      Event
 */
class Module
{
    public function __construct()
    {
        Event::addHandler('InitializeModule', [$this, 'initialize']);
        Event::addHandler('CleanupModule', [$this, 'cleanup']);

        foreach (get_class_methods($this) as $method) {
            if (mb_substr($method, 0, 2) == 'on') {
                Event::addHandler(mb_substr($method, 2), [$this, $method]);
            }
        }

        $this->setupGettext();
    }

    public function initialize()
    {
        return true;
    }

    public function cleanup()
    {
        return true;
    }

    /**
     * Load related components when needed
     *
     * Most non-trivial plugins will require extra components to do their work. Typically
     * these include data classes, action classes, widget classes, or external libraries.
     *
     * This method receives a class name and loads the PHP file related to that class. By
     * tradition, action classes typically have files named for the action, all lower-case.
     * Data classes are in files with the data class name, initial letter capitalized.
     *
     * Note that this method will be called for *all* overloaded classes, not just ones
     * in this plugin! So, make sure to return true by default to let other plugins, and
     * the core code, get a chance.
     *
     * @param string $cls Name of the class to be loaded
     *
     * @return bool hook value; true means continue processing, false means stop.
     */
    public function onAutoload($cls)
    {
        $cls = basename($cls);
        $basedir = INSTALLDIR . '/modules/' . mb_substr(get_called_class(), 0, -6);

        $file = null;

        if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) {
            $type = array_map('strtolower', $type);
            $file = "{$basedir}/{$type[2]}s/{$type[1]}.php";
        }
        if (!file_exists($file)) {
            $file = "{$basedir}/classes/{$cls}.php";

            // library files can be put into subdirs ('_'->'/' conversion)
            // such as LRDDMethod_WebFinger -> lib/lrddmethod/webfinger.php
            if (!file_exists($file)) {
                $type = strtolower($cls);
                $type = str_replace('_', '/', $type);
                $file = "{$basedir}/lib/{$type}.php";
            }
        }

        if (!is_null($file) && file_exists($file)) {
            require_once $file;
            return false;
        }

        return true;
    }

    /**
     * Checks if this plugin has localization that needs to be set up.
     * Gettext localizations can be called via the _m() helper function.
     */
    protected function setupGettext()
    {
        $class = get_class($this);
        if (substr($class, -6) == 'Module') {
            $name = substr($class, 0, -6);
            $path = common_config('plugins', 'locale_path');
            if (!$path) {
                // @fixme this will fail for things installed in local/plugins
                // ... but then so will web links so far.
                $path = INSTALLDIR . "/modules/{$name}/locale";
            }
            if (file_exists($path) && is_dir($path)) {
                bindtextdomain($name, $path);
                bind_textdomain_codeset($name, 'UTF-8');
            }
        }
    }

    protected function log($level, $msg)
    {
        common_log($level, get_class($this) . ': ' . $msg);
    }

    protected function debug($msg)
    {
        $this->log(LOG_DEBUG, $msg);
    }

    public function name()
    {
        $cls = get_class($this);
        return mb_substr($cls, 0, -6);
    }

    public function version()
    {
        return GNUSOCIAL_VERSION;
    }

    protected function userAgent()
    {
        return HTTPClient::userAgent()
            . ' (' . get_class($this) . ' v' . $this->version() . ')';
    }

    public function onModuleVersion(array &$versions): bool
    {
        $name = $this->name();

        $versions[] = [
            'name' => $name,
            // TRANS: Displayed as version information for a plugin if no version information was found.
            'version' => _m('Unknown')
        ];

        return true;
    }

    public static function staticPath($module, $relative)
    {
        if (GNUsocial::useHTTPS()) {
            $server = common_config('plugins', 'sslserver');
        } else {
            $server = common_config('plugins', 'server');
        }

        if (empty($server)) {
            if (GNUsocial::useHTTPS()) {
                $server = common_config('site', 'sslserver');
            }
            if (empty($server)) {
                $server = common_config('site', 'server');
            }
        }

        if (GNUsocial::useHTTPS()) {
            $path = common_config('plugins', 'sslpath');
        } else {
            $path = common_config('plugins', 'path');
        }

        if (empty($path)) {
            $path = common_config('site', 'path') . '/modules/';
        }

        if ($path[strlen($path) - 1] != '/') {
            $path .= '/';
        }

        if ($path[0] != '/') {
            $path = '/' . $path;
        }

        $protocol = GNUsocial::useHTTPS() ? 'https' : 'http';

        return $protocol . '://' . $server . $path . $module . '/' . $relative;
    }

    public function path($relative)
    {
        return self::staticPath($this->name(), $relative);
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social 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.
//
// GNU social 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 GNU social.  If not, see <http://www.gnu.org/licenses/>.

defined('GNUSOCIAL') || die();

/**
 * Base class for plugins
 *
 * A base class for GNU social plugin. Mostly a light wrapper around
 * the Event framework.
 *
 * Subclasses of Plugin will automatically handle an event if they define
 * a method called "onEventName". (Well, OK -- only if they call parent::__construct()
 * in their constructors.)
 *
 * They will also automatically handle the InitializePlugin and CleanupPlugin with the
 * initialize() and cleanup() methods, respectively.
 *
 * @category Module
 * @package  GNUsocial
 * @author   Evan Prodromou <evan@status.net>
 * @copyright 2010-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 *
 * @see      Event
 */
class Plugin extends Module
{
    public function __construct()
    {
        Event::addHandler('InitializePlugin', [$this, 'initialize']);
        Event::addHandler('CleanupPlugin', [$this, 'cleanup']);

        foreach (get_class_methods($this) as $method) {
            if (mb_substr($method, 0, 2) == 'on') {
                Event::addHandler(mb_substr($method, 2), [$this, $method]);
            }
        }

        $this->setupGettext();
    }

    public function initialize()
    {
        return true;
    }

    public function cleanup()
    {
        return true;
    }

    /**
     * Load related components when needed
     *
     * Most non-trivial plugins will require extra components to do their work. Typically
     * these include data classes, action classes, widget classes, or external libraries.
     *
     * This method receives a class name and loads the PHP file related to that class. By
     * tradition, action classes typically have files named for the action, all lower-case.
     * Data classes are in files with the data class name, initial letter capitalized.
     *
     * Note that this method will be called for *all* overloaded classes, not just ones
     * in this plugin! So, make sure to return true by default to let other plugins, and
     * the core code, get a chance.
     *
     * @param string $cls Name of the class to be loaded
     *
     * @return bool hook value; true means continue processing, false means stop.
     */
    public function onAutoload($cls)
    {
        $cls = basename($cls);
        $basedir = INSTALLDIR . '/local/plugins/' . mb_substr(get_called_class(), 0, -6);
        if (!file_exists($basedir)) {
            $basedir = INSTALLDIR . '/plugins/' . mb_substr(get_called_class(), 0, -6);
        }

        $file = null;

        if (preg_match('/^(\w+)(Action|Form)$/', $cls, $type)) {
            $type = array_map('strtolower', $type);
            $file = "{$basedir}/{$type[2]}s/{$type[1]}.php";
        }
        if (!file_exists($file)) {
            $file = "{$basedir}/classes/{$cls}.php";

            // library files can be put into subdirs ('_'->'/' conversion)
            // such as LRDDMethod_WebFinger -> lib/lrddmethod/webfinger.php
            if (!file_exists($file)) {
                $type = strtolower($cls);
                $type = str_replace('_', '/', $type);
                $file = "{$basedir}/lib/{$type}.php";
            }
        }

        if (!is_null($file) && file_exists($file)) {
            require_once $file;
            return false;
        }

        return true;
    }

    /**
     * Checks if this plugin has localization that needs to be set up.
     * Gettext localizations can be called via the _m() helper function.
     */
    protected function setupGettext()
    {
        $class = get_class($this);
        if (substr($class, -6) == 'Plugin') {
            $name = substr($class, 0, -6);
            $path = common_config('plugins', 'locale_path');
            if (!$path) {
                // @fixme this will fail for things installed in local/plugins
                // ... but then so will web links so far.
                $path = INSTALLDIR . "/plugins/{$name}/locale";
                if (!file_exists($path)) {
                    $path = INSTALLDIR . "/local/plugins/{$name}/locale";
                }
            }
            if (file_exists($path) && is_dir($path)) {
                bindtextdomain($name, $path);
                bind_textdomain_codeset($name, 'UTF-8');
            }
        }
    }

    public function onPluginVersion(array &$versions): bool
    {
        $name = $this->name();

        $versions[] = [
            'name' => $name,
            // TRANS: Displayed as version information for a plugin if no version information was found.
            'version' => _m('Unknown')
        ];

        return true;
    }

    public function onModuleVersion(array &$versions): bool
    {
        return true;
    }

    public static function staticPath($plugin, $relative)
    {
        if (GNUsocial::useHTTPS()) {
            $server = common_config('plugins', 'sslserver');
        } else {
            $server = common_config('plugins', 'server');
        }

        if (empty($server)) {
            if (GNUsocial::useHTTPS()) {
                $server = common_config('site', 'sslserver');
            }
            if (empty($server)) {
                $server = common_config('site', 'server');
            }
        }

        if (GNUsocial::useHTTPS()) {
            $path = common_config('plugins', 'sslpath');
        } else {
            $path = common_config('plugins', 'path');
        }

        if (empty($path)) {
            // XXX: extra stat().
            if (@file_exists(PUBLICDIR . '/local/plugins/' . $plugin . '/' . $relative)) {
                $path = common_config('site', 'path') . '/local/plugins/';
            } else {
                $path = common_config('site', 'path') . '/plugins/';
            }
        }

        if ($path[strlen($path) - 1] != '/') {
            $path .= '/';
        }

        if ($path[0] != '/') {
            $path = '/' . $path;
        }

        $protocol = GNUsocial::useHTTPS() ? 'https' : 'http';

        return $protocol . '://' . $server . $path . $plugin . '/' . $relative;
    }

    public function path($relative)
    {
        return self::staticPath($this->name(), $relative);
    }
}


code:
As of StatusNet 1.0.x, actual formatting of the notices is done server-side,
loaded by AJAX after the real-time notification comes in. This has the drawback
that we may make extra HTTP requests and delay incoming notices a little, but
means that formatting and internationalization is consistent.

== TODO ==
* Update mark behaviour (on notice send)
* Pause, Send a notice ~ should not update counter
* Pause ~ retain up to 50-100 most recent notices
* Make it work for Conversation page (perhaps a little tricky)
* IE is updating the counter in document title all the time (Not sure if this
  is still an issue)
* Reconsider the timestamp approach