Example #1
0
/**
 * Removes a contact from the system.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_delete($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $label = $req['label'];
    $contactUserId = $req['contactUserId'];
    Users::canManageContacts($loggedInUserId, $userId, $label, true);
    $contact = new Users_Contact();
    $contact->userId = $userId;
    $contact->label = $label;
    $contact->contactUserId = $contactUserId;
    return $contact->remove();
}
Example #2
0
/**
 * Removes a contact from the system.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_delete($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $label = $req['label'];
    $contactUserId = $req['contactUserId'];
    return !!Users_Contact::removeContact($userId, $label, $contactUserId);
}
Example #3
0
/**
 * Adds contacts to the system. Fills the "contacts" slot.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.nickname] The nickname of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_post($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $contactUserId = $req['contactUserId'];
    $nickname = Q::ifset($req, 'nickname', null);
    $contacts = Users_Contact::addContact($userId, $req['label'], $contactUserId, $nickname);
    Q_Response::setSlot('contacts', Db::exportArray($contacts));
}
Example #4
0
/**
 * Edits a contact in the system. Fills the "contact" slot.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.nickname] The nickname of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_put($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $label = $req['label'];
    $contactUserId = $req['contactUserId'];
    $nickname = Q::ifset($req, 'nickname', null);
    $contact = Users_Contact::updateContact($userId, $label, $contactUserId, compact('nickname'));
    Q_Response::setSlot('contact', $contact->exportArray());
}
Example #5
0
function Q_response_dialogs()
{
    // Here is where you would pre-generate various dialog elements
    // that you might show with Q.Dialogs.push
    if (!Users::roles(null, array('Websites/admins'))) {
        return '';
    }
    $app = Q_Config::expect('Q', 'app');
    $userIds = Users_Contact::select('contactUserId')->where(array('userId' => $app, 'label' => 'Websites/admins'))->fetchAll(PDO::FETCH_COLUMN, 'contactUserId');
    $admins = Users_User::select('*')->where(array('id' => $userIds))->fetchDbRows();
    return Q::view('Trump/dialogs/common.php', compact('admins'));
}
function Streams_before_Users_Contact_saveExecute($params)
{
    $contacts = array($params['row']);
    // the new values about to be written
    if ($params['query']->type === Db_Query::TYPE_UPDATE) {
        // we are updating an existing contact
        $contacts = array_merge($contacts, Users_Contact::select('*')->where($params['where'])->limit(1)->fetchDbRows());
    }
    // Update avatar as viewed by everyone who was in that contacts list
    foreach ($contacts as $contact) {
        Streams::updateAvatar($contact->contactUserId, $contact->userId);
    }
}
Example #7
0
/**
 * Edits a contact in the system. Fills the "contact" slot.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.nickname] The nickname of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_put($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $label = $req['label'];
    $contactUserId = $req['contactUserId'];
    $nickname = Q::ifset($req, 'nickname', null);
    Users::canManageContacts($loggedInUserId, $userId, $label, true);
    $contact = new Users_Contact();
    $contact->userId = $userId;
    $contact->label = $label;
    $contact->contactUserId = $contactUserId;
    if (!$contact->retrieve()) {
        throw new Q_Exception_MissingRow(array('table' => 'Users_Contact', 'criteria' => json_encode($contact->fields)));
    }
    if ($nickname) {
        $contact->nickname = $nickname;
    }
    $contact->save();
    Q_Response::setSlot('contact', $contact->exportArray());
}
Example #8
0
/**
 * Adds contacts to the system. Fills the "contacts" slot.
 * @param {array} $_REQUEST
 * @param {string} $_REQUEST.label The label of the contact
 * @param {string} $_REQUEST.contactUserId The contactUserId of the contact
 * @param {string} [$_REQUEST.nickname] The nickname of the contact
 * @param {string} [$_REQUEST.userId=Users::loggedInUser(true)->id] You can override the user id, if another plugin adds a hook that allows you to do this
 */
function Users_contact_post($params = array())
{
    $req = array_merge($_REQUEST, $params);
    Q_Request::requireFields(array('label', 'contactUserId'), $req, true);
    $loggedInUserId = Users::loggedInUser(true)->id;
    $userId = Q::ifset($req, 'userId', $loggedInUserId);
    $contactUserId = $req['contactUserId'];
    $nickname = Q::ifset($req, 'nickname', null);
    $l = $req['label'];
    if ($userId !== $loggedInUserId) {
        Users_User::fetch($userId, true);
    }
    Users_User::fetch($contactUserId, true);
    Users::canManageContacts($loggedInUserId, $userId, $l, true);
    $label = new Users_Label();
    $label->userId = $userId;
    $label->label = $l;
    if (!$label->retrieve()) {
        throw new Q_Exception_MissingRow(array('table' => 'Users_Label', 'criteria' => json_encode($label->fields)));
    }
    $contacts = Users_Contact::addContact($userId, $l, $contactUserId, $nickname);
    Q_Response::setSlot('contacts', Db::exportArray($contacts));
}
Example #9
0
 /**
  * Remove contact from label
  * @method removeContact
  * @static
  * @param {string} $userId
  * @param {string} $label
  * @param {string} $contactId
  * @return {boolean}
  */
 static function removeContact($userId, $label, $contactId)
 {
     foreach (array('userId', 'label', 'contactUserId') as $field) {
         if (empty(${$field})) {
             throw new Q_Exception_RequiredField(compact('field'));
         }
     }
     $contact = new Users_Contact();
     $contact->userId = $userId;
     $contact->label = $label;
     $contact->contactUserId = $contactId;
     return !!$contact->remove();
 }
Example #10
0
 /**
  * Inserts some Users_Contact rows for the locally registered users
  * who have added links to this particular contact information.
  * Removes the links after successfully adding the Users_Contact rows.
  * @method saveContactsFromLinks
  * @static
  * @param {array} $contact_info An array of key => value pairs, where keys can be:
  *
  * * "email" => the user's email address
  * * "mobile" => the user's mobile number
  * * "email_hashed" => the standard hash of the user's email address
  * * "mobile_hashed" => the standard hash of the user's mobile number
  * * "facebook" => the user's facebook uid
  * * "twitter" => the user's twitter uid
  *
  * @param {string} $userId The id of the user who has verified these identifiers
  */
 static function saveContactsFromLinks()
 {
     /**
      * @event Users/saveContactsFromLinks {before}
      */
     Q::event('Users/saveContactsFromLinks', array(), 'before');
     $user = self::loggedInUser();
     $contact_info = array();
     foreach (self::$types as $type => $field) {
         if (!empty($user->{$field})) {
             $contact_info[$type] = $user->{$field};
         }
     }
     $links = $contact_info ? Users::links($contact_info) : array();
     $contacts = array();
     foreach ($links as $link) {
         $extraInfo = json_decode($link->extraInfo, true);
         $firstName = Q::ifset($extraInfo, 'firstName', '');
         $lastName = Q::ifset($extraInfo, 'lastName', '');
         $fullName = $firstName ? $lastName ? "{$firstName} {$lastName}" : $firstName : ($lastName ? $lastName : "");
         if (!empty($extraInfo['labels']) and is_array($extraInfo['labels'])) {
             foreach ($extraInfo['labels'] as $label) {
                 // Insert the contacts one by one, so if an error occurs
                 // we can continue right on inserting the rest.
                 $contact = new Users_Contact();
                 $contact->userId = $link->userId;
                 $contact->contactUserId = $user->id;
                 $contact->label = $label;
                 $contact->nickname = $fullName;
                 $contact->save(true);
                 $link->remove();
                 // we don't need this link anymore
                 // TODO: Think about porting this to Node
                 // and setting a flag when done.
                 // Perhaps we should send a custom message through socket.io
                 // which would cause Users.js to add a notice to the interface
             }
         }
     }
     /**
      * @event Users/saveContactsFromLinks {after}
      * @param {array} contacts
      */
     Q::event('Users/saveContactsFromLinks', compact('contacts'), 'after');
     // TODO: Add a handler to this event in the Streams plugin, so that
     // we post this information to a stream on the hub, which will
     // update all its subscribers, who will also run saveContactsFromLinks
     // for their local users.
 }
function Streams_before_Users_Contact_removeExecute($params)
{
    // Save the contacts list that will be deleted, so we can update the avatars later
    Streams::$cache['contacts_removed'] = Users_Contact::select('*')->where($params['criteria'])->fetchDbRows();
}
Example #12
0
 /**
  * Check label or array of labels and return existing users
  * @method labelsToIds
  * @static
  * @param $asUserId {string} The user id of inviting user
  * @param $labels {string|array}
  * @return {array} The array of user ids
  */
 static function labelsToIds($asUserId, $labels)
 {
     if (empty($labels)) {
         return array();
     }
     if (!is_array($labels)) {
         $labels = array_map('trim', explode(',', $labels));
     }
     $userIds = array();
     foreach ($labels as $label) {
         $userIds = array_merge($userIds, Users_Contact::select('contactUserId')->where(array('userId' => $asUserId, 'label' => $label))->fetchAll(PDO::FETCH_COLUMN));
     }
     return $userIds;
 }
Example #13
0
 /**
  * Updates the publisher's avatars, which may have changed with the taintedAccess.
  * This function should be called during rare events that may cause the
  * publisher's avatar to change appearance for certain users viewing it.<br/>
  *
  * You should rarely have to call this function. It is used internally by the model,
  * in two main situations:
  *
  * 1)  adding, removing or modifying a Streams_Access row for Streams/user/firstName or Streams/user/lastName
  *	In this case, the function is able to update exactly the avatars that need updating.
  * 
  * 2) adding, removing or modifying a Stream row for Streams/user/firstName or Streams/user/lastName
  *	In this case, there may be some avatars which this function will miss.
  *	These correspond to users which are reachable by the access array for one stream,
  *	but not the other. For example, if Streams/user/firstName is being updated, but
  *	a particular user is reachable only by the access array for Streams/user/lastName, then
  *	their avatar will not be updated and contain a stale value for firstName.
  *	To fix this, the Streams_Stream model passes true in the 4th parameter to this function.
  * @method updateAvatars
  * @static
  * @param {string} $publisherId
  *  id of the publisher whose avatar to update
  * @param {array} $taintedAccess
  *  array of Streams_Access objects representing access information that is either
  *  about to be saved, are about to be overwritten, or will be deleted
  * @param {string|Streams_Stream} $streamName
  *  pass the stream name here. You can also pass a Stream_Stream object here,
  *  in which case it will be used, instead of selecting that stream from the database.
  * @param {boolean} $updateToPublicValue=false
  *  if you want to first update all the avatars for this stream
  *  to the what the public would see, to avoid the situation described in 2).
  */
 static function updateAvatars($publisherId, $taintedAccess, $streamName, $updateToPublicValue = false)
 {
     if (!isset($streamName)) {
         $streamAccesses = array();
         foreach ($taintedAccess as $access) {
             $streamAccesses[$access->streamName][] = $access;
         }
         if (count($streamAccesses) > 1) {
             foreach ($streamAccesses as $k => $v) {
                 self::updateAvatars($publisherId, $v, $k);
             }
             return false;
         }
     }
     if ($streamName instanceof Streams_Stream) {
         $stream = $streamName;
         $streamName = $stream->name;
     }
     // If we are here, all the Stream_Access objects have the same streamName
     if ($streamName !== 'Streams/user/firstName' and $streamName !== 'Streams/user/lastName' and $streamName !== 'Streams/user/username') {
         // we don't care about access to other streams being updated
         return false;
     }
     $showToUserIds = array();
     // Select the user corresponding to this publisher
     $user = new Users_User();
     $user->id = $publisherId;
     if (!$user->retrieve(null, null, array('ignoreCache' => true))) {
         throw new Q_Exception_MissingRow(array('table' => 'user', 'criteria' => 'id = ' . $user->id));
     }
     // Obtain the stream object to use
     if (isset($stream)) {
         if (!isset($stream->content)) {
             $stream->content = '';
         }
     } else {
         // If the $stream isn't already defined, select it
         $stream = new Streams_Stream();
         $stream->publisherId = $publisherId;
         $stream->name = $streamName;
         if (!$stream->retrieve()) {
             // Strange, this stream doesn't exist.
             // Well, we will just silently set the content to '' then
             $stream->content = '';
         }
     }
     $content_readLevel = Streams::$READ_LEVEL['content'];
     $readLevels = array();
     $label_readLevels = array();
     $contact_label_list = array();
     $removed_labels = array();
     // First, assign all the readLevels that are directly set for specific users,
     // and aggregate the contact_labels from the other accesses, for an upcoming select.
     foreach ($taintedAccess as $access) {
         if ($userId = $access->ofUserId) {
             $readLevel = $access->readLevel;
             $readLevels[$userId] = $readLevel;
             if ($readLevel < 0) {
                 $showToUserIds[$userId] = null;
                 // not determined yet
             } else {
                 if ($readLevel >= $content_readLevel) {
                     $showToUserIds[$userId] = true;
                 } else {
                     $showToUserIds[$userId] = false;
                 }
             }
         } else {
             if ($access->ofContactLabel) {
                 $ofContactLabel = $access->ofContactLabel;
                 $contact_label_list[] = $ofContactLabel;
                 if ($access->get('removed', false)) {
                     $removed_labels[$ofContactLabel] = true;
                 } else {
                     $label_readLevels[$ofContactLabel] = $access->readLevel;
                 }
             }
         }
     }
     // Now, get all the people affected by this change, and their readLevels
     $readLevels2 = array();
     if ($contact_label_list) {
         $contact_label_list = array_unique($contact_label_list);
         $contacts = Users_Contact::select('*')->where(array('userId' => $publisherId, 'label' => $contact_label_list))->fetchDbRows(null, '', 'contactUserId');
         foreach ($contacts as $contact) {
             $contactUserId = $contact->contactUserId;
             if (isset($showToUserIds[$contactUserId])) {
                 // this user had their read level set directly by the access,
                 // which overrides read levels set by access using ofContactLabel
                 continue;
             }
             if (isset($removed_labels[$ofContactLabel])) {
                 // this label doesn't affect readLevels anymore, since it was deleted
                 // but put this contact's id on a list whose readLevels need to be determined
                 $showToUserIds[$contactUserId] = null;
                 continue;
             }
             if (!isset($label_readLevels[$contact->label])) {
                 continue;
             }
             $readLevel = $label_readLevels[$contact->label];
             if (!isset($readLevels2[$contactUserId])) {
                 $readLevels2[$contactUserId] = $readLevel;
             } else {
                 $readLevels2[$contactUserId] = max($readLevels2[$contactUserId], $readLevel);
             }
         }
     }
     // Now step through all the users we found who were found through ofContactLabel
     // and make sure we update the avatar rows that were meant for them.
     foreach ($readLevels2 as $userId => $rl) {
         if ($rl >= $content_readLevel) {
             $showToUserIds[$userId] = true;
         } else {
             // in order for this to happen, two things had to be true:
             // 1) there was no access that directly set a readLevel >= $content_readLevel
             // 2) there was no access that set a readLevel >= $content_readLevel for any label containing this user
             // therefore, their view should be the public view
             $showToUserIds[$userId] = 'public';
         }
     }
     // Resolve all the undetermined readLevels
     foreach ($showToUserIds as $userId => $v) {
         if (!isset($v)) {
             // if the readLevel hasn't been determined by now, it's the same as the public one
             $showToUserIds[$userId] = 'public';
         }
     }
     // Set up the self avatar:
     $showToUserIds[$publisherId] = true;
     // Finally, set up the public avatar:
     if (!isset($stream->readLevel)) {
         $stream->readLevel = Streams_Stream::$DEFAULTS['readLevel'];
     }
     $showToUserIds[""] = $stream->readLevel >= $content_readLevel;
     // Now, we update the avatars:
     $parts = explode('/', $streamName);
     $field = end($parts);
     $rows_that_show = array();
     $rows_that_hide = array();
     foreach ($showToUserIds as $userId => $show) {
         if ($show === 'public') {
             // If no show is explicitly specified, use the value used for the rest of the public
             $show = $showToUserIds[""];
         }
         if ($show === true) {
             $rows_that_show[] = array('publisherId' => $publisherId, 'toUserId' => $userId, 'username' => $user->username, 'icon' => $user->icon, 'updatedTime' => new Db_Expression("CURRENT_TIMESTAMP"), $field => $stream->content);
         } else {
             if ($show === false) {
                 $rows_that_hide[] = array('publisherId' => $publisherId, 'toUserId' => $userId, 'username' => $user->username, 'icon' => $user->icon, 'updatedTime' => new Db_Expression("CURRENT_TIMESTAMP"), $field => '');
             }
         }
     }
     $updates_that_show = array('username' => $user->username, 'icon' => $user->icon, 'updatedTime' => new Db_Expression("CURRENT_TIMESTAMP"), $field => $stream->content);
     $updates_that_hide = array('username' => $user->username, 'icon' => $user->icon, 'updatedTime' => new Db_Expression("CURRENT_TIMESTAMP"), $field => '');
     // We are now ready to make changes to the database.
     if ($updateToPublicValue) {
         Streams_Avatar::update()->set(array($field => $showToUserIds[""] ? $stream->content : ''))->where(compact('publisherId'))->execute();
     }
     Streams_Avatar::insertManyAndExecute($rows_that_show, array('onDuplicateKeyUpdate' => $updates_that_show));
     Streams_Avatar::insertManyAndExecute($rows_that_hide, array('onDuplicateKeyUpdate' => $updates_that_hide));
 }
Example #14
0
 /**
  * Fetch an array of labels. By default, returns all the labels.
  * @method fetch
  * @param {string} [$userId=null] The id of the user whose contact labels should be fetched
  * @param {string|Db_Expression} [$filter=''] Pass a string prefix such as "Users/", or some db expression, to get only a particular subset of labels.
  * @param {boolean} [$checkContacts=false] Whether to also look in the Users_Contact table and only return labels that have at least one contact.
  * @return {array} An array of array(label => title) pairs
  */
 static function fetch($userId = null, $filter = '', $checkContacts = false)
 {
     if (!isset($userId)) {
         $user = Users::loggedInUser(true);
         $userId = $user->id;
     }
     $criteria = array('userId' => $userId);
     if ($filter) {
         $criteria['label'] = is_string($filter) ? new Db_Range($filter, true, false, null) : $filter;
     }
     if ($checkContacts) {
         $contact_array = Users_Contact::select('*')->where($criteria)->groupBy('userId, label')->fetchDbRows();
     }
     $labels = Users_Label::select('*')->where($criteria)->fetchDbRows(null, null, 'label');
     $icons = array();
     if (!$checkContacts) {
         return $labels;
     }
     $contacts = array();
     foreach ($contact_array as $contact) {
         $contacts[$contact->label] = $contact->label;
     }
     foreach ($labels as $label) {
         if (!isset($contacts[$label->label])) {
             unset($labels[$label->label]);
         }
     }
     return $labels;
 }
Example #15
0
 /**
  * Check if a contact with this label exists
  * @method checkLabel
  * @static
  * @param {string} $userId
  * @param {string} $label
  * @param {string} $contactId
  * @return {Db_Row|false}
  */
 static function checkLabel($userId, $label, $contactId)
 {
     if (!$userId or !$contactId) {
         return null;
     }
     if ($userId instanceof Users_User) {
         $userId = $userId->id;
     }
     if ($contactId instanceof Users_User) {
         $contactId = $contactId->id;
     }
     $contact = new Users_Contact();
     $contact->userId = $userId;
     $contact->label = $label;
     $contact->contactUserId = $contactId;
     return $contact->retrieve();
 }
Example #16
0
 /**
  * Invites a user (or a future user) to a stream .
  * @method invite
  * @static
  * @param {string} $publisherId The id of the stream publisher
  * @param {string} $streamName The name of the stream the user will be invited to
  * @param {array} $who Array that can contain the following keys:
  * @param {string|array} [$who.userId] user id or an array of user ids
  * @param {string|array} [$who.fb_uid]  fb user id or array of fb user ids
  * @param {string|array} [$who.label]  label or an array of labels, or tab-delimited string
  * @param {string|array} [$who.identifier]  identifier or an array of identifiers, or tab-delimited string
  * @param {integer} [$who.newFutureUsers] the number of new Users_User objects to create via Users::futureUser in order to invite them to this stream. This typically is used in conjunction with passing the "html" option to this function.
  * @param {array} [$options=array()]
  *  @param {string|array} [$options.addLabel] label or an array of labels for adding publisher's contacts
  *  @param {string|array} [$options.addMyLabel] label or an array of labels for adding asUserId's contacts
  *  @param {integer} [$options.readLevel] => the read level to grant those who are invited
  *  @param {integer} [$options.writeLevel] => the write level to grant those who are invited
  *  @param {integer} [$options.adminLevel] => the admin level to grant those who are invited
  *	@param {string} [$options.displayName] => the display name to use to represent the inviting user
  *  @param {string} [$options.appUrl] => Can be used to override the URL to which the invited user will be redirected and receive "Q.Streams.token" in the querystring.
  *	@param {array} [$options.html] => an array of ($template, $batchName) such as ("MyApp/foo.handlebars", "foo") for generating html snippets which can then be viewed from and printed via the action Streams/invitations?batchName=$batchName&invitingUserId=$asUserId&limit=$limit&offset=$offset
  * @param {string} [$options.asUserId=Users::loggedInUser(true)->id] Invite as this user id, defaults to logged-in user
  * @param {boolean} [$options.skipAccess] whether to skip access checks when adding labels and contacts
  * @see Users::addLink()
  * @return {array} returns array with keys "success", "invited", "statuses", "identifierTypes", "alreadyParticipating"
  */
 static function invite($publisherId, $streamName, $who, $options = array())
 {
     if (isset($options['asUserId'])) {
         $asUserId = $options['asUserId'];
         $asUser = Users_User::fetch($asUserId);
     } else {
         $asUser = Users::loggedInUser(true);
         $asUserId = $asUser->id;
     }
     // Fetch the stream as the logged-in user
     $stream = Streams::fetchOne($asUserId, $publisherId, $streamName);
     if (!$stream) {
         throw new Q_Exception_MissingRow(array('table' => 'stream', 'criteria' => 'with that name'), 'streamName');
     }
     // Do we have enough admin rights to invite others to this stream?
     if (!$stream->testAdminLevel('invite') || !$stream->testWriteLevel('join')) {
         throw new Users_Exception_NotAuthorized();
     }
     if (isset($options['html'])) {
         $html = $options['html'];
         if (!is_array($html) or count($html) < 2) {
             throw new Q_Exception_WrongType(array('field' => "options.html", 'type' => 'array of 2 strings'));
         }
         list($template, $batchName) = $html;
         // validate these paths
         $filename = APP_VIEWS_DIR . DS . $template;
         if (!Q::realPath($filename)) {
             throw new Q_Exception_MissingFile(compact('filename'));
         }
         $ext = $pathinfo = pathinfo($template, PATHINFO_EXTENSION);
         if ($ext !== 'handlebars') {
             throw new Q_Exception_WrongValue(array('field' => 'options.html[0]', 'range' => 'a filename with extension .handlebars'));
         }
         $path = Streams::invitationsPath($asUserId) . DS . $batchName;
         Q_Utils::canWriteToPath($path, true, true);
     }
     // get user ids if any to array, throw if user not found
     $raw_userIds = isset($who['userId']) ? Users_User::verifyUserIds($who['userId'], true) : array();
     // merge labels if any
     if (isset($who['label'])) {
         $label = $who['label'];
         if (is_string($label)) {
             $label = array_map('trim', explode("\t", $label));
         }
         $raw_userIds = array_merge($raw_userIds, Users_User::labelsToIds($asUserId, $label));
     }
     // merge identifiers if any
     $identifierType = null;
     $statuses = null;
     if (isset($who['identifier'])) {
         $identifier = $who['identifier'];
         if (is_string($identifier)) {
             if (Q_Valid::email($who['identifier'])) {
                 $identifierType = 'email';
             } else {
                 if (Q_Valid::phone($who['identifier'])) {
                     $identifierType = 'mobile';
                 }
             }
             $identifier = array_map('trim', explode("\t", $identifier));
         }
         $statuses = array();
         $identifier_ids = Users_User::idsFromIdentifiers($identifier, $statuses);
         $raw_userIds = array_merge($raw_userIds, $identifier_ids);
     }
     // merge fb uids if any
     if (isset($who['fb_uid'])) {
         $fb_uids = $who['fb_uid'];
         if (is_string($fb_uids)) {
             $fb_uids = array_map('trim', explode("\t", $fb_uids));
         }
         $raw_userIds = array_merge($raw_userIds, Users_User::idsFromFacebook($fb_uids));
     }
     if (!empty($who['newFutureUsers'])) {
         $nfu = $who['newFutureUsers'];
         for ($i = 0; $i < $nfu; ++$i) {
             $raw_userIds[] = Users::futureUser('none', null)->id;
         }
     }
     // ensure that each userId is included only once
     // and remove already participating users
     $raw_userIds = array_unique($raw_userIds);
     $total = count($raw_userIds);
     $userIds = Streams_Participant::filter($raw_userIds, $stream);
     $to_invite = count($userIds);
     $appUrl = !empty($options['appUrl']) ? $options['appUrl'] : Q_Request::baseUrl() . '/' . Q_Config::get("Streams", "types", $stream->type, "invite", "url", "plugins/Streams/stream");
     // now check and define levels for invited user
     $readLevel = isset($options['readLevel']) ? $options['readLevel'] : null;
     if (isset($readLevel)) {
         $readLevel = Streams_Stream::numericReadLevel($readLevel);
         if (!$stream->testReadLevel($readLevel)) {
             // We can't assign greater read level to other people than we have ourselves!
             throw new Users_Exception_NotAuthorized();
         }
     }
     $writeLevel = isset($options['writeLevel']) ? $options['writeLevel'] : null;
     if (isset($writeLevel)) {
         $writeLevel = Streams_Stream::numericWriteLevel($writeLevel);
         if (!$stream->testWriteLevel($writeLevel)) {
             // We can't assign greater write level to other people than we have ourselves!
             throw new Users_Exception_NotAuthorized();
         }
     }
     $adminLevel = isset($options['adminLevel']) ? $options['adminLevel'] : null;
     if (isset($adminLevel)) {
         $adminLevel = Streams_Stream::numericAdminLevel($adminLevel);
         if (!$stream->testAdminLevel($adminLevel + 1)) {
             // We can't assign an admin level greater, or equal, to our own!
             // A stream's publisher can assign owners. Owners can assign admins.
             // Admins can confer powers to invite others, to some people.
             // Those people can confer the privilege to publish a message re this stream.
             // But admins can't assign other admins, and even stream owners
             // can't assign other owners.
             throw new Users_Exception_NotAuthorized();
         }
     }
     // calculate expiry time
     $duration = Q_Config::get("Streams", "types", $stream->type, "invite", "duration", false);
     $expiry = $duration ? strtotime($duration) : null;
     $asUserId2 = empty($options['skipAccess']) ? $asUserId : false;
     if ($label = Q::ifset($options, 'addLabel', null)) {
         if (is_string($label)) {
             $label = explode("\t", $label);
         }
         Users_Label::addLabel($label, $publisherId, null, null, $asUserId2, true);
     }
     if ($myLabel = Q::ifset($options, 'addMyLabel', null)) {
         if (is_string($myLabel)) {
             $myLabel = explode("\t", $myLabel);
         }
         Users_Label::addLabel($myLabel, $asUserId, null, null, $asUserId2, true);
     }
     foreach ($raw_userIds as $userId) {
         Users_Contact::addContact($asUserId, "Streams/invited", $userId, null, false, true);
         Users_Contact::addContact($asUserId, "Streams/invited/{$stream->type}", $userId, null, false, true);
         Users_Contact::addContact($userId, "Streams/invitedMe", $asUserId, null, false, true);
         Users_Contact::addContact($userId, "Streams/invitedMe/{$stream->type}", $asUserId, null, false, true);
         if ($label) {
             Users_Contact::addContact($publisherId, $label, $userId, null, $asUserId2, true);
         }
         if ($myLabel) {
             Users_Contact::addContact($asUserId, $label, $userId, null, $asUserId2, true);
         }
     }
     // let node handle the rest, and get the result
     $displayName = isset($options['displayName']) ? $options['displayName'] : Streams::displayName($asUser);
     $params = array("Q/method" => "Streams/Stream/invite", "invitingUserId" => $asUserId, "username" => $asUser->username, "userIds" => Q::json_encode($userIds), "stream" => Q::json_encode($stream->toArray()), "appUrl" => $appUrl, "label" => $label, "myLabel" => $myLabel, "readLevel" => $readLevel, "writeLevel" => $writeLevel, "adminLevel" => $adminLevel, "displayName" => $displayName, "expiry" => $expiry);
     if (!empty($template)) {
         $params['template'] = $template;
         $params['batchName'] = $batchName;
     }
     $result = Q_Utils::queryInternal('Q/node', $params);
     return array('success' => $result, 'invited' => $userIds, 'statuses' => $statuses, 'identifierType' => $identifierType, 'alreadyParticipating' => $total - $to_invite);
 }