/** * Used to post messages to EXISTING stream * $_REQUEST shall contain the content of the message. Also may include 'streamNames' * field which is an array of additional names of the streams to post message to. * * @param string $params * publisher id and stream name of existing stream shall be supplied * @return {void} */ function Streams_message_post() { $user = Users::loggedInUser(true); $publisherId = Streams::requestedPublisherId(true); $streamName = Streams::requestedName(true); // check if type is allowed $streams = Streams::fetch($user->id, $publisherId, $streamName); if (empty($streams)) { throw new Streams_Exception_NoSuchStream(); } $stream = reset($streams); if (empty($_REQUEST['type'])) { throw new Q_Exception_RequiredField(array('field' => 'type'), 'type'); } $type = $_REQUEST['type']; if (!Q_Config::get("Streams", "types", $stream->type, "messages", $type, 'post', false)) { throw new Q_Exception("This app doesn't support directly posting messages of type '{$type}' for streams of type '{$stream->type}'"); } $result = Streams_Message::post($user->id, $publisherId, $streamName, $_REQUEST); if (is_array($result)) { Streams::$cache['messages'] = $result; } else { Streams::$cache['message'] = $result; } }
function Broadcast_control_response_content($params) { $user = Users::loggedInUser(true); $organizations = Broadcast_Agreement::select('a.userId, a.publisherId, u.organization_title, u.organization_domain', 'a')->join(Broadcast_User::table() . ' u', array('a.publisherId' => 'u.userId'))->where(array('a.userId' => $user->id))->fetchAll(PDO::FETCH_ASSOC); foreach ($organizations as $k => $org) { $messages = Streams_Message::select('content')->where(array('publisherId' => $org['publisherId'], 'streamName' => 'Broadcast/main'))->orderBy('sentTime')->fetchAll(PDO::FETCH_ASSOC); $organizations[$k]['messages'] = array(); foreach ($messages as $msg) { $content = json_decode($msg['content'], true); if (isset($content['link'])) { $ch = curl_init(); $timeout = 5; curl_setopt($ch, CURLOPT_URL, $content['link']); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.7.12) Gecko/20050929"); $page_contents = curl_exec($ch); curl_close($ch); preg_match('/<title>([^<]+)<\\/title>/', $page_contents, $matches); if (isset($matches[1])) { $content['link_title'] = $matches[1]; } } $organizations[$k]['messages'][] = $content; } } Q_Config::set('Q', 'response', 'Broadcast', 'layout_html', 'Broadcast/layout/canvas.php'); Q_Response::addStylesheet('css/canvas.css'); Q_Response::addScript('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'); Q_Response::addScript('js/canvas.js'); return Q::view('Broadcast/content/control.php', compact('organizations')); }
function Streams_after_Users_Contact_removeExecute($params) { // Update avatar as viewed by everyone who was in that contacts list $contacts = Streams::$cache['contacts_removed']; foreach ($contacts as $contact) { Streams::updateAvatar($contact->contactUserId, $contact->userId); } Streams_Message::post(null, $contact->userId, 'Streams/contacts', array('type' => 'Streams/contacts/removed', 'instructions' => array('contacts' => Db::exportArray($contacts))), true); }
function Streams_after_Users_Label_saveExecute($params) { // The icon or title might have been modified $modifiedFields = $params['modifiedFields']; $label = $params['row']; $updates = Q::take($modifiedFields, array('icon', 'title')); $updates['userId'] = $label->userId; $updates['label'] = $label->label; return Streams_Message::post(null, $label->userId, "Streams/labels", array('type' => 'Streams/labels/updated', 'instructions' => compact('updates')), true); }
function Streams_after_Users_Contact_saveExecute($params) { $inserted = $params['inserted']; $modifiedFields = $params['modifiedFields']; $contact = $params['row']; if ($inserted) { Streams_Message::post(null, $contact->userId, 'Streams/contacts', array('type' => 'Streams/contacts/inserted', 'instructions' => array('contact' => $contact->exportArray())), true); } else { $updates = Q::take($modifiedFields, array('nickname')); $updates = array_merge($contact->toArray(), $updates); Streams_Message::post(null, $contact->userId, 'Streams/contacts', array('type' => 'Streams/contacts/updated', 'instructions' => compact('updates')), true); } }
function Streams_after_Users_Label_saveExecute($params) { // The icon or title might have been modified $inserted = $params['inserted']; $modifiedFields = $params['modifiedFields']; $label = $params['row']; if ($inserted) { Streams_Message::post(null, $label->userId, 'Streams/labels', array('type' => 'Streams/labels/inserted', 'instructions' => array('label' => $label->exportArray())), true); } else { $updates = Q::take($modifiedFields, array('icon', 'title')); $updates = array_merge($label->toArray(), $updates); Streams_Message::post(null, $label->userId, "Streams/labels", array('type' => 'Streams/labels/updated', 'instructions' => compact('updates')), true); } }
function Streams_after_Users_Label_removeExecute($params) { $label = $params['row']; Streams_Message::post(null, $label->userId, 'Streams/labels', array('type' => 'Streams/labels/removed', 'instructions' => array('label' => $label->toArray())), true); }
/** * Fetch messages of the stream. * @method getMessages * @param {array} [options=array()] An array of options determining how messages will be fetched, which can include: * "min" => Minimum ordinal of the message to select from (inclusive). Defaults to minimum ordinal of existing messages (if any). * "max" => Maximum ordinal of the message to select to (inclusive). Defaults to maximum ordinal of existing messages (if any). * Can also be negative, then the value will be substracted from maximum number of existing messages and +1 will be added * to guarantee that $max = -1 means highest message ordinal. * "limit" => Number of the messages to be selected. Defaults to 1000. * "ascending" => Sorting of fetched messages by ordinal. If true, sorting is ascending, if false - descending. * Defaults to true, but in case if 'min' option not given and only 'max' and 'limit' are given, we assuming * fetching in reverse order, so 'ascending' will default to false. * "type" => Optional string specifying the particular type of messages to get */ function getMessages($options) { // preparing default query $criteria = array('publisherId' => $this->publisherId, 'streamName' => $this->name); if (!empty($options['type'])) { $criteria['type'] = $options['type']; } $q = Streams_Message::select('*')->where($criteria); // getting $min and $max $result = Streams_Message::select("MIN(ordinal) AS min, MAX(ordinal) AS max")->where($criteria)->fetchAll(PDO::FETCH_ASSOC); if (!$result[0]) { return array(); } $min = (int) $result[0]['min']; $max = (int) $result[0]['max']; // default sorting is 'ORDER BY `ordinal` ASC', but it can be changed depending on options $ascending = true; if (!isset($options['min'])) { $options['min'] = $min; // if 'min' is not given, assume 'reverse' fetching, so $ascending is false $ascending = false; } if (!isset($options['max'])) { $options['max'] = $max; } else { if ($options['max'] < 0) { // if 'max' is negative, substract value from existing maximum $options['max'] = $max + $options['max'] + 1; } } if (empty($options['limit'])) { $options['limit'] = self::getConfigField($this->type, 'getMessagesLimit', 100); } if ($options['min'] > $options['max']) { return array(); } $q->where(array('ordinal >=' => $options['min'], 'ordinal <=' => $options['max'])); $q->limit($options['limit']); $q->orderBy('ordinal', isset($options['ascending']) ? $options['ascending'] : $ascending); return $q->fetchDbRows(null, '', 'ordinal'); }
/** * Updates the weight on a relation * @param {string} $asUserId * The id of the user on whose behalf the app will be updating the relation * @param {string} $toPublisherId * The publisher of the stream on the 'to' end of the reltion * @param {string} $toStreamName * The name of the stream on the 'to' end of the relation * @param {string} $type * The type of the relation * @param {string} $fromPublisherId * The publisher of the stream on the 'from' end of the reltion * @param {string} $fromStreamName * The name of the stream on the 'from' end of the reltion * @param {double} $weight * The new weight * @param {double} $adjustWeights=null * The amount to move the other weights by, to make room for this one * @param {array} $options=array() * An array of options that can include: * "skipAccess" => Defaults to false. If true, skips the access checks and just updates the weight on the relation * @return {array|boolean} * Returns false if the operation was canceled by a hook * Otherwise returns array with key "to" and value of type Streams_Message */ static function updateRelation($asUserId, $toPublisherId, $toStreamName, $type, $fromPublisherId, $fromStreamName, $weight, $adjustWeights = null, $options = array()) { self::getRelation($asUserId, $toPublisherId, $toStreamName, $type, $fromPublisherId, $fromStreamName, $relatedTo, $relatedFrom, $category, $stream, $options); if (!$relatedTo->retrieve()) { throw new Q_Exception_MissingRow(array('table' => 'relatedTo', 'criteria' => 'with those fields'), array('publisherId', 'name', 'type', 'toPublisherId', 'to_name')); } // if (!$relatedFrom->retrieve()) { // throw new Q_Exception_MissingRow( // array('table' => 'relatedFrom', 'criteria' => 'those fields'), // array('publisherId', 'name', 'type', 'fromPublisherId', 'from_name') // ); // } if (empty($options['skipAccess'])) { if (!$category->testWriteLevel('relations')) { throw new Users_Exception_NotAuthorized(); } } /** * @event Streams/updateRelation/$streamType {before} * @param {string} relatedTo * @param {string} relatedFrom * @param {string} asUserId * @param {double} weight * @param {double} previousWeight */ $previousWeight = $relatedTo->weight; $adjustWeightsBy = $weight < $previousWeight ? $adjustWeights : -$adjustWeights; if (Q::event("Streams/updateRelation/{$stream->type}", compact('relatedTo', 'relatedFrom', 'type', 'weight', 'previousWeight', 'adjustWeightsBy', 'asUserId'), 'before') === false) { return false; } if (!empty($adjustWeights) and is_numeric($adjustWeights) and $weight !== $previousWeight) { $criteria = array('toPublisherId' => $toPublisherId, 'toStreamName' => $toStreamName, 'type' => $type, 'weight' => $weight < $previousWeight ? new Db_Range($weight, true, false, $previousWeight) : new Db_Range($previousWeight, false, true, $weight)); Streams_RelatedTo::update()->set(array('weight' => new Db_Expression("weight + " . $adjustWeightsBy)))->where($criteria)->execute(); } $relatedTo->weight = $weight; $relatedTo->save(); // Send Streams/updatedRelateTo message to the category stream // node server will be notified by Streams_Message::post $message = Streams_Message::post($asUserId, $toPublisherId, $toStreamName, array('type' => 'Streams/updatedRelateTo', 'instructions' => Q::json_encode(compact('fromPublisherId', 'fromStreamName', 'type', 'weight', 'previousWeight', 'adjustWeightsBy', 'asUserId'))), true); // TODO: We are not yet sending Streams/updatedRelateFrom message to the other stream // because we might be changing a lot of weights, and we'd have to message a lot of streams. // This is better done in the background using Node.js after selecting using $criteria // When we implement this, we can introduce weight again in the relatedFrom table. /** * @event Streams/updateRelation/$streamType {after} * @param {string} relatedTo * @param {string} relatedFrom * @param {string} asUserId * @param {double} weight * @param {double} previousWeight */ Q::event("Streams/updateRelation/{$stream->type}", compact('relatedTo', 'relatedFrom', 'type', 'weight', 'previousWeight', 'adjustWeightsBy', 'asUserId'), 'after'); return $message; }
/** * Post (potentially) multiple messages to multiple streams. * With one call to this function you can post at most one message per stream. * @static * @param {string} $asUserId * The user to post the message as * @param {string} $messages * Array indexed as follows: * array($publisherId => array($streamName => $message)) * where $message are either Streams_Message objects, * or arrays containing all the fields of messages that will need to be posted. * @param {booleam} $skipAccess=false * If true, skips the access checks and just posts the message. * @return {array} * Returns an array(array(Streams_Message), array(Streams_Stream)) */ static function postMessages($asUserId, $messages, $skipAccess = false) { if (!isset($asUserId)) { $asUserId = Users::loggedInUser(); if (!$asUserId) { $asUserId = ""; } } if ($asUserId instanceof Users_User) { $asUserId = $asUserId->id; } // Build arrays we will need foreach ($messages as $publisherId => $arr) { if (!is_array($arr)) { throw new Q_Exception_WrongType(array('field' => "messages", 'type' => 'array of publisherId => streamName => message')); } foreach ($arr as $streamName => &$message) { if (!is_array($message)) { if (!$message instanceof Streams_Message) { throw new Q_Exception_WrongType(array('field' => "message under {$publisherId} => {$streamName}", 'type' => 'array or Streams_Message')); } $message = $message->fields; } } } // Start posting messages, publisher by publisher $eventParams = array(); $posted = array(); $streams = array(); $messages2 = array(); $totals2 = array(); $clientId = Q_Request::special('clientId', ''); $sendToNode = true; foreach ($messages as $publisherId => $arr) { $streamNames = array_keys($messages[$publisherId]); $streams[$publisherId] = $fetched = Streams::fetch($asUserId, $publisherId, $streamNames, '*', array('refetch' => true, 'begin' => true)); foreach ($arr as $streamName => $message) { $p =& $posted[$publisherId][$streamName]; $p = false; $type = isset($message['type']) ? $message['type'] : 'text/small'; $content = isset($message['content']) ? $message['content'] : ''; $instructions = isset($message['instructions']) ? $message['instructions'] : ''; $weight = isset($message['weight']) ? $message['weight'] : 1; if (!isset($message['byClientId'])) { $message['byClientId'] = $clientId ? substr($clientId, 0, 255) : ''; } if (is_array($instructions)) { $instructions = Q::json_encode($instructions); } $byClientId = $message['byClientId']; // Get the Streams_Stream object if (!isset($fetched[$streamName])) { $p = new Q_Exception_MissingRow(array('table' => 'stream', 'criteria' => "publisherId {$publisherId} and name {$streamName}")); continue; } $stream = $fetched[$streamName]; // Make a Streams_Message object $message = new Streams_Message(); $message->publisherId = $publisherId; $message->streamName = $streamName; $message->insertedTime = new Db_Expression("CURRENT_TIMESTAMP"); $message->sentTime = new Db_Expression("CURRENT_TIMESTAMP"); $message->byUserId = $asUserId; $message->byClientId = $byClientId ? substr($byClientId, 0, 31) : ''; $message->type = $type; $message->content = $content; $message->instructions = $instructions; $message->weight = $weight; $message->ordinal = $stream->messageCount + 1; // thanks to transaction // Set up some parameters for the event hooks $eventParams[$publisherId][$streamName] = array('publisherId' => $publisherId, 'message' => $message, 'skipAccess' => $skipAccess, 'sendToNode' => &$sendToNode, 'stream' => $stream); $params = $eventParams[$publisherId][$streamName]; /** * @event Streams/post/$streamType {before} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} message * @return {false} To cancel further processing */ if (Q::event("Streams/post/{$stream->type}", $params, 'before') === false) { $results[$stream->name] = false; continue; } /** * @event Streams/message/$messageType {before} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} message * @return {false} To cancel further processing */ if (Q::event("Streams/message/{$type}", $params, 'before') === false) { $results[$stream->name] = false; continue; } if (!$skipAccess && !$stream->testWriteLevel('post')) { $p = new Users_Exception_NotAuthorized(); /** * @event Streams/notAuthorized {before} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} message */ Q::event("Streams/notAuthorized", $params, 'after'); continue; } // if we are still here, mark the message as "in the database" $message->wasRetrieved(true); $posted[$publisherId][$streamName] = $message; // build the arrays of rows to insert $messages2[] = $mf = $message->fields; $totals2[] = array('publisherId' => $mf['publisherId'], 'streamName' => $mf['streamName'], 'messageType' => $mf['type'], 'messageCount' => 1); } } if ($totals2) { Streams_Total::insertManyAndExecute($totals2, array('onDuplicateKeyUpdate' => array('messageCount' => new Db_Expression('messageCount + 1')))); } if ($messages2) { Streams_Message::insertManyAndExecute($messages2); } // time to update the stream rows and commit the transaction // on all the shards where the streams were fetched. Streams_Stream::update()->set(array('messageCount' => new Db_Expression("messageCount+1")))->where(array('publisherId' => $publisherId, 'name' => $streamNames))->commit()->execute(); // handle all the events for successfully posting foreach ($posted as $publisherId => $arr) { foreach ($arr as $streamName => $m) { $message = $posted[$publisherId][$streamName]; $params =& $eventParams[$publisherId][$streamName]; /** * @event Streams/message/$messageType {after} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} message */ Q::event("Streams/message/{$message->type}", $params, 'after', false); /** * @event Streams/post/$streamType {after} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} message */ Q::event("Streams/post/{$stream->type}", $params, 'after', false); } } /** * @event Streams/postMessages {after} * @param {string} publisherId * @param {Streams_Stream} stream * @param {string} posted */ Q::event("Streams/postMessages", array('streams' => $streams, 'messages' => $messages, 'skipAccess' => $skipAccess, 'posted' => $posted), 'after', false); if ($sendToNode) { Q_Utils::sendToNode(array("Q/method" => "Streams/Message/postMessages", "posted" => Q::json_encode($messages2), "streams" => Q::json_encode($streams))); } return array($posted, $streams); }
/** * Send credits, as the logged-in user, to another user * @method send * @static * @param {integer} $amount The amount of credits to send. * @param {string} $toUserId The id of the user to whom you will send the credits * @param {array} $more An array supplying more info, including * "reason" => Identifies the reason for sending, if any */ static function send($amount, $toUserId, $more = array()) { if (!is_int($amount) or $amount <= 0) { throw new Q_Exception_WrongType(array('field' => 'amount', 'type' => 'integer')); } $user = Users::loggedInUser(true); $from_stream = new Streams_Stream(); $from_stream->publisherId = $user->id; $from_stream->name = 'Awards/credits'; if (!$from_stream->retrieve()) { $from_stream = self::createStream($user); } $existing_amount = $from_stream->getAttribute('amount'); if ($existing_amount < $amount) { throw new Awards_Exception_NotEnoughCredits(array('missing' => $amount - $existing_amount)); } $to_user = Users_User::fetch($toUserId, true); $to_stream = new Streams_Stream(); $to_stream->publisherId = $toUserId; $to_stream->name = 'Awards/credits'; if (!$to_stream->retrieve()) { $to_stream = self::createStream($to_user); } $to_stream->setAttribute('amount', $to_stream->getAttribute('amount') - $amount); $to_stream->save(); // NOTE: we are not doing transactions here mainly because of sharding. // If if we reached this point without exceptions, that means everything worked. // But if the following statement fails, then someone will get free credits. $from_stream->setAttribute('amount', $from_stream->getAttribute('amount') - $amount); $from_stream->save(); $instructions_json = Q::json_encode(array_merge(array('app' => Q_Config::expect('Q', 'app')), $more)); Streams_Message::post($user->id, $userId, array('type' => 'Awards/credits/sent', 'content' => $amount, 'instructions' => $instructions_json)); Streams_Message::post($user->id, $toUserId, array('type' => 'Awards/credits/received', 'content' => $amount, 'instructions' => $instructions_json)); }
/** * Unsubscribe from one or more streams, to stop receiving notifications. * Pooststs "Streams/unsubscribe" message to the streams. * Also posts "Streams/unsubscribed" messages to user's "Streams/participating" stream. * Does not change the actual subscription, but only the participant row. * (When subscribing again, the existing subscription will be used.) * @method unsubscribe * @static * @param {string} $asUserId The id of the user that is joining. Pass null here to use the logged-in user's id. * @param {string} $publisherId The id of the user publishing all the streams * @param {array} $streams An array of Streams_Stream objects or stream names * @param {array} [$options=array()] * @param {boolean} [$options.leave] set to true to also leave the streams * @param {boolean} [$options.skipAccess] if true, skip access check for whether user can join and subscribe * @return {array} Returns an array of Streams_Participant rows, if any were in the database. */ static function unsubscribe($asUserId, $publisherId, $streams, $options = array()) { $streams2 = self::_getStreams($asUserId, $publisherId, $streams); $streamNames = array(); foreach ($streams2 as $s) { $streamNames[] = $s->name; } if (empty($options['skipAccess'])) { self::_accessExceptions($streams2, $streamNames, 'join'); } $skipAccess = Q::ifset($options, 'skipAccess', false); if (empty($options['leave'])) { $criteria = array('publisherId' => $publisherId, 'streamName' => $streamNames, 'userId' => $asUserId); Streams_Participant::update()->set(array('subscribed' => 'no'))->where($criteria)->execute(); $participants = Streams_Participant::select('*')->where($criteria)->fetchDbRows(); } else { $participants = Streams::leave($asUserId, $publisherId, $streams2, compact('skipAccess')); } $messages = array(); $pMessages = array(); foreach ($streamNames as $sn) { $stream = $streams2[$sn]; if ($participant = Q::ifset($participants, $sn, null)) { if ($participant instanceof Streams_Participant) { $participant = $participant->toArray(); } } // Send a message to Node Q_Utils::sendToNode(array("Q/method" => "Streams/Stream/unsubscribe", "participant" => Q::json_encode($participant), "stream" => Q::json_encode($stream->toArray()))); // Stream messages to post $messages[$publisherId][$sn] = array('type' => 'Streams/unsubscribe'); $pMessages[] = array('type' => 'Streams/unsubscribed', 'instructions' => array('publisherId' => $publisherId, 'streamName' => $sn)); } Streams_Message::postMessages($asUserId, $messages, true); Streams_Message::postMessages($asUserId, array($asUserId => array('Streams/participating' => $pMessages)), true); return $participants; }