| /* |
| * libjingle |
| * Copyright 2011, Google Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "talk/xmpp/hangoutpubsubclient.h" |
| |
| #include "talk/base/logging.h" |
| #include "talk/xmpp/constants.h" |
| #include "talk/xmpp/jid.h" |
| #include "talk/xmllite/qname.h" |
| #include "talk/xmllite/xmlelement.h" |
| |
| |
| // Gives a high-level API for MUC call PubSub needs such as |
| // presenter state, recording state, mute state, and remote mute. |
| |
| namespace buzz { |
| |
| namespace { |
| const char kPresenting[] = "s"; |
| const char kNotPresenting[] = "o"; |
| const char kEmpty[] = ""; |
| |
| const std::string GetPublisherNickFromPubSubItem(const XmlElement* item_elem) { |
| if (item_elem == NULL) { |
| return ""; |
| } |
| |
| return Jid(item_elem->Attr(QN_ATTR_PUBLISHER)).resource(); |
| } |
| |
| } // namespace |
| |
| |
| // Knows how to handle specific states and XML. |
| template <typename C> |
| class PubSubStateSerializer { |
| public: |
| virtual ~PubSubStateSerializer() {} |
| virtual XmlElement* Write(const QName& state_name, const C& state) = 0; |
| virtual C Parse(const XmlElement* state_elem) = 0; |
| }; |
| |
| // Knows how to create "keys" for states, which determines their |
| // uniqueness. Most states are per-nick, but block is |
| // per-blocker-and-blockee. This is independent of itemid, especially |
| // in the case of presenter state. |
| class PubSubStateKeySerializer { |
| public: |
| virtual ~PubSubStateKeySerializer() {} |
| virtual std::string GetKey(const std::string& publisher_nick, |
| const std::string& published_nick) = 0; |
| }; |
| |
| class PublishedNickKeySerializer : public PubSubStateKeySerializer { |
| public: |
| virtual std::string GetKey(const std::string& publisher_nick, |
| const std::string& published_nick) { |
| return published_nick; |
| } |
| }; |
| |
| class PublisherAndPublishedNicksKeySerializer |
| : public PubSubStateKeySerializer { |
| public: |
| virtual std::string GetKey(const std::string& publisher_nick, |
| const std::string& published_nick) { |
| return publisher_nick + ":" + published_nick; |
| } |
| }; |
| |
| // A simple serialiazer where presence of item => true, lack of item |
| // => false. |
| class BoolStateSerializer : public PubSubStateSerializer<bool> { |
| virtual XmlElement* Write(const QName& state_name, const bool& state) { |
| if (!state) { |
| return NULL; |
| } |
| |
| return new XmlElement(state_name, true); |
| } |
| |
| virtual bool Parse(const XmlElement* state_elem) { |
| return state_elem != NULL; |
| } |
| }; |
| |
| // Adapts PubSubClient to be specifically suited for pub sub call |
| // states. Signals state changes and keeps track of keys, which are |
| // normally nicks. |
| // TODO: Expose this as a generally useful class, not just |
| // private to hangouts. |
| template <typename C> |
| class PubSubStateClient : public sigslot::has_slots<> { |
| public: |
| // Gets ownership of the serializers, but not the client. |
| PubSubStateClient(const std::string& publisher_nick, |
| PubSubClient* client, |
| const QName& state_name, |
| C default_state, |
| PubSubStateKeySerializer* key_serializer, |
| PubSubStateSerializer<C>* state_serializer) |
| : publisher_nick_(publisher_nick), |
| client_(client), |
| state_name_(state_name), |
| default_state_(default_state) { |
| key_serializer_.reset(key_serializer); |
| state_serializer_.reset(state_serializer); |
| client_->SignalItems.connect( |
| this, &PubSubStateClient<C>::OnItems); |
| client_->SignalPublishResult.connect( |
| this, &PubSubStateClient<C>::OnPublishResult); |
| client_->SignalPublishError.connect( |
| this, &PubSubStateClient<C>::OnPublishError); |
| client_->SignalRetractResult.connect( |
| this, &PubSubStateClient<C>::OnRetractResult); |
| client_->SignalRetractError.connect( |
| this, &PubSubStateClient<C>::OnRetractError); |
| } |
| |
| virtual ~PubSubStateClient() {} |
| |
| virtual void Publish(const std::string& published_nick, |
| const C& state, |
| std::string* task_id_out) { |
| std::string key = key_serializer_->GetKey(publisher_nick_, published_nick); |
| std::string itemid = state_name_.LocalPart() + ":" + key; |
| if (StatesEqual(state, default_state_)) { |
| client_->RetractItem(itemid, task_id_out); |
| } else { |
| XmlElement* state_elem = state_serializer_->Write(state_name_, state); |
| state_elem->AddAttr(QN_NICK, published_nick); |
| client_->PublishItem(itemid, state_elem, task_id_out); |
| } |
| }; |
| |
| sigslot::signal1<const PubSubStateChange<C>&> SignalStateChange; |
| // Signal (task_id, item). item is NULL for retract. |
| sigslot::signal2<const std::string&, |
| const XmlElement*> SignalPublishResult; |
| // Signal (task_id, item, error stanza). item is NULL for retract. |
| sigslot::signal3<const std::string&, |
| const XmlElement*, |
| const XmlElement*> SignalPublishError; |
| |
| protected: |
| // return false if retracted item (no info or state given) |
| virtual bool ParseStateItem(const PubSubItem& item, |
| StateItemInfo* info_out, |
| bool* state_out) { |
| const XmlElement* state_elem = item.elem->FirstNamed(state_name_); |
| if (state_elem == NULL) { |
| return false; |
| } |
| |
| info_out->publisher_nick = GetPublisherNickFromPubSubItem(item.elem); |
| info_out->published_nick = state_elem->Attr(QN_NICK); |
| *state_out = state_serializer_->Parse(state_elem); |
| return true; |
| }; |
| |
| virtual bool StatesEqual(C state1, C state2) { |
| return state1 == state2; |
| } |
| |
| PubSubClient* client() { return client_; } |
| |
| private: |
| void OnItems(PubSubClient* pub_sub_client, |
| const std::vector<PubSubItem>& items) { |
| for (std::vector<PubSubItem>::const_iterator item = items.begin(); |
| item != items.end(); ++item) { |
| OnItem(*item); |
| } |
| } |
| |
| void OnItem(const PubSubItem& item) { |
| const std::string& itemid = item.itemid; |
| StateItemInfo info; |
| C new_state; |
| |
| bool retracted = !ParseStateItem(item, &info, &new_state); |
| if (retracted) { |
| bool known_itemid = |
| (info_by_itemid_.find(itemid) != info_by_itemid_.end()); |
| if (!known_itemid) { |
| // Nothing to retract, and nothing to publish. |
| // Probably a different state type. |
| return; |
| } else { |
| info = info_by_itemid_[itemid]; |
| info_by_itemid_.erase(itemid); |
| new_state = default_state_; |
| } |
| } else { |
| // TODO: Assert new key matches the known key. It |
| // shouldn't change! |
| info_by_itemid_[itemid] = info; |
| } |
| |
| std::string key = key_serializer_->GetKey( |
| info.publisher_nick, info.published_nick); |
| bool has_old_state = (state_by_key_.find(key) != state_by_key_.end()); |
| const C& old_state = has_old_state ? state_by_key_[key] : default_state_; |
| if ((retracted && !has_old_state) || StatesEqual(new_state, old_state)) { |
| // Nothing change, so don't bother signalling. |
| return; |
| } |
| |
| if (retracted || StatesEqual(new_state, default_state_)) { |
| // We treat a default state similar to a retract. |
| state_by_key_.erase(key); |
| } else { |
| state_by_key_[key] = new_state; |
| } |
| |
| PubSubStateChange<C> change; |
| change.publisher_nick = info.publisher_nick; |
| change.published_nick = info.published_nick; |
| change.old_state = old_state; |
| change.new_state = new_state; |
| SignalStateChange(change); |
| } |
| |
| void OnPublishResult(PubSubClient* pub_sub_client, |
| const std::string& task_id, |
| const XmlElement* item) { |
| SignalPublishResult(task_id, item); |
| } |
| |
| void OnPublishError(PubSubClient* pub_sub_client, |
| const std::string& task_id, |
| const buzz::XmlElement* item, |
| const buzz::XmlElement* stanza) { |
| SignalPublishError(task_id, item, stanza); |
| } |
| |
| void OnRetractResult(PubSubClient* pub_sub_client, |
| const std::string& task_id) { |
| // There's no point in differentiating between publish and retract |
| // errors, so we simplify by making them both signal a publish |
| // result. |
| const XmlElement* item = NULL; |
| SignalPublishResult(task_id, item); |
| } |
| |
| void OnRetractError(PubSubClient* pub_sub_client, |
| const std::string& task_id, |
| const buzz::XmlElement* stanza) { |
| // There's no point in differentiating between publish and retract |
| // errors, so we simplify by making them both signal a publish |
| // error. |
| const XmlElement* item = NULL; |
| SignalPublishError(task_id, item, stanza); |
| } |
| |
| std::string publisher_nick_; |
| PubSubClient* client_; |
| const QName state_name_; |
| C default_state_; |
| talk_base::scoped_ptr<PubSubStateKeySerializer> key_serializer_; |
| talk_base::scoped_ptr<PubSubStateSerializer<C> > state_serializer_; |
| // key => state |
| std::map<std::string, C> state_by_key_; |
| // itemid => StateItemInfo |
| std::map<std::string, StateItemInfo> info_by_itemid_; |
| }; |
| |
| class PresenterStateClient : public PubSubStateClient<bool> { |
| public: |
| PresenterStateClient(const std::string& publisher_nick, |
| PubSubClient* client, |
| const QName& state_name, |
| bool default_state) |
| : PubSubStateClient<bool>( |
| publisher_nick, client, state_name, default_state, |
| new PublishedNickKeySerializer(), NULL) { |
| } |
| |
| virtual void Publish(const std::string& published_nick, |
| const bool& state, |
| std::string* task_id_out) { |
| XmlElement* presenter_elem = new XmlElement(QN_PRESENTER_PRESENTER, true); |
| // There's a dummy value, not used, but required. |
| presenter_elem->AddAttr(QN_JID, "dummy@value.net"); |
| presenter_elem->AddAttr(QN_NICK, published_nick); |
| |
| XmlElement* presentation_item_elem = |
| new XmlElement(QN_PRESENTER_PRESENTATION_ITEM, false); |
| const std::string& presentation_type = state ? kPresenting : kNotPresenting; |
| presentation_item_elem->AddAttr( |
| QN_PRESENTER_PRESENTATION_TYPE, presentation_type); |
| |
| // The Presenter state is kind of dumb in that it doesn't use |
| // retracts. It relies on setting the "type" to a special value. |
| std::string itemid = published_nick; |
| std::vector<XmlElement*> children; |
| children.push_back(presenter_elem); |
| children.push_back(presentation_item_elem); |
| client()->PublishItem(itemid, children, task_id_out); |
| } |
| |
| protected: |
| virtual bool ParseStateItem(const PubSubItem& item, |
| StateItemInfo* info_out, |
| bool* state_out) { |
| const XmlElement* presenter_elem = |
| item.elem->FirstNamed(QN_PRESENTER_PRESENTER); |
| const XmlElement* presentation_item_elem = |
| item.elem->FirstNamed(QN_PRESENTER_PRESENTATION_ITEM); |
| if (presentation_item_elem == NULL || presenter_elem == NULL) { |
| return false; |
| } |
| |
| info_out->publisher_nick = GetPublisherNickFromPubSubItem(item.elem); |
| info_out->published_nick = presenter_elem->Attr(QN_NICK); |
| *state_out = (presentation_item_elem->Attr( |
| QN_PRESENTER_PRESENTATION_TYPE) != kNotPresenting); |
| return true; |
| } |
| }; |
| |
| HangoutPubSubClient::HangoutPubSubClient(XmppTaskParentInterface* parent, |
| const Jid& mucjid, |
| const std::string& nick) |
| : mucjid_(mucjid), |
| nick_(nick) { |
| presenter_client_.reset(new PubSubClient(parent, mucjid, NS_PRESENTER)); |
| presenter_client_->SignalRequestError.connect( |
| this, &HangoutPubSubClient::OnPresenterRequestError); |
| |
| media_client_.reset(new PubSubClient(parent, mucjid, NS_GOOGLE_MUC_MEDIA)); |
| media_client_->SignalRequestError.connect( |
| this, &HangoutPubSubClient::OnMediaRequestError); |
| |
| presenter_state_client_.reset(new PresenterStateClient( |
| nick_, presenter_client_.get(), QN_PRESENTER_PRESENTER, false)); |
| presenter_state_client_->SignalStateChange.connect( |
| this, &HangoutPubSubClient::OnPresenterStateChange); |
| presenter_state_client_->SignalPublishResult.connect( |
| this, &HangoutPubSubClient::OnPresenterPublishResult); |
| presenter_state_client_->SignalPublishError.connect( |
| this, &HangoutPubSubClient::OnPresenterPublishError); |
| |
| audio_mute_state_client_.reset(new PubSubStateClient<bool>( |
| nick_, media_client_.get(), QN_GOOGLE_MUC_AUDIO_MUTE, false, |
| new PublishedNickKeySerializer(), new BoolStateSerializer())); |
| // Can't just repeat because we need to watch for remote mutes. |
| audio_mute_state_client_->SignalStateChange.connect( |
| this, &HangoutPubSubClient::OnAudioMuteStateChange); |
| audio_mute_state_client_->SignalPublishResult.connect( |
| this, &HangoutPubSubClient::OnAudioMutePublishResult); |
| audio_mute_state_client_->SignalPublishError.connect( |
| this, &HangoutPubSubClient::OnAudioMutePublishError); |
| |
| recording_state_client_.reset(new PubSubStateClient<bool>( |
| nick_, media_client_.get(), QN_GOOGLE_MUC_RECORDING, false, |
| new PublishedNickKeySerializer(), new BoolStateSerializer())); |
| recording_state_client_->SignalStateChange.connect( |
| this, &HangoutPubSubClient::OnRecordingStateChange); |
| recording_state_client_->SignalPublishResult.connect( |
| this, &HangoutPubSubClient::OnRecordingPublishResult); |
| recording_state_client_->SignalPublishError.connect( |
| this, &HangoutPubSubClient::OnRecordingPublishError); |
| |
| media_block_state_client_.reset(new PubSubStateClient<bool>( |
| nick_, media_client_.get(), QN_GOOGLE_MUC_MEDIA_BLOCK, false, |
| new PublisherAndPublishedNicksKeySerializer(), |
| new BoolStateSerializer())); |
| media_block_state_client_->SignalStateChange.connect( |
| this, &HangoutPubSubClient::OnMediaBlockStateChange); |
| media_block_state_client_->SignalPublishResult.connect( |
| this, &HangoutPubSubClient::OnMediaBlockPublishResult); |
| media_block_state_client_->SignalPublishError.connect( |
| this, &HangoutPubSubClient::OnMediaBlockPublishError); |
| } |
| |
| HangoutPubSubClient::~HangoutPubSubClient() { |
| } |
| |
| void HangoutPubSubClient::RequestAll() { |
| presenter_client_->RequestItems(); |
| media_client_->RequestItems(); |
| } |
| |
| void HangoutPubSubClient::OnPresenterRequestError( |
| PubSubClient* client, const XmlElement* stanza) { |
| SignalRequestError(client->node(), stanza); |
| } |
| |
| void HangoutPubSubClient::OnMediaRequestError( |
| PubSubClient* client, const XmlElement* stanza) { |
| SignalRequestError(client->node(), stanza); |
| } |
| |
| void HangoutPubSubClient::PublishPresenterState( |
| bool presenting, std::string* task_id_out) { |
| presenter_state_client_->Publish(nick_, presenting, task_id_out); |
| } |
| |
| void HangoutPubSubClient::PublishAudioMuteState( |
| bool muted, std::string* task_id_out) { |
| audio_mute_state_client_->Publish(nick_, muted, task_id_out); |
| } |
| |
| void HangoutPubSubClient::PublishRecordingState( |
| bool recording, std::string* task_id_out) { |
| recording_state_client_->Publish(nick_, recording, task_id_out); |
| } |
| |
| // Remote mute is accomplished by setting another client's mute state. |
| void HangoutPubSubClient::RemoteMute( |
| const std::string& mutee_nick, std::string* task_id_out) { |
| audio_mute_state_client_->Publish(mutee_nick, true, task_id_out); |
| } |
| |
| // Block media is accomplished by setting another client's block |
| // state, kind of like remote mute. |
| void HangoutPubSubClient::BlockMedia( |
| const std::string& blockee_nick, std::string* task_id_out) { |
| media_block_state_client_->Publish(blockee_nick, true, task_id_out); |
| } |
| |
| void HangoutPubSubClient::OnPresenterStateChange( |
| const PubSubStateChange<bool>& change) { |
| SignalPresenterStateChange( |
| change.published_nick, change.old_state, change.new_state); |
| } |
| |
| void HangoutPubSubClient::OnPresenterPublishResult( |
| const std::string& task_id, const XmlElement* item) { |
| SignalPublishPresenterResult(task_id); |
| } |
| |
| void HangoutPubSubClient::OnPresenterPublishError( |
| const std::string& task_id, const XmlElement* item, |
| const XmlElement* stanza) { |
| SignalPublishPresenterError(task_id, stanza); |
| } |
| |
| // Since a remote mute is accomplished by another client setting our |
| // mute state, if our state changes to muted, we should mute |
| // ourselves. Note that we never remote un-mute, though. |
| void HangoutPubSubClient::OnAudioMuteStateChange( |
| const PubSubStateChange<bool>& change) { |
| bool was_muted = change.old_state; |
| bool is_muted = change.new_state; |
| bool remote_action = (!change.publisher_nick.empty() && |
| (change.publisher_nick != change.published_nick)); |
| if (is_muted && remote_action) { |
| const std::string& mutee_nick = change.published_nick; |
| const std::string& muter_nick = change.publisher_nick; |
| bool should_mute_locally = (mutee_nick == nick_); |
| SignalRemoteMute(mutee_nick, muter_nick, should_mute_locally); |
| } else { |
| SignalAudioMuteStateChange(change.published_nick, was_muted, is_muted); |
| } |
| } |
| |
| const std::string GetAudioMuteNickFromItem(const XmlElement* item) { |
| if (item != NULL) { |
| const XmlElement* audio_mute_state = |
| item->FirstNamed(QN_GOOGLE_MUC_AUDIO_MUTE); |
| if (audio_mute_state != NULL) { |
| return audio_mute_state->Attr(QN_NICK); |
| } |
| } |
| return std::string(); |
| } |
| |
| const std::string GetBlockeeNickFromItem(const XmlElement* item) { |
| if (item != NULL) { |
| const XmlElement* media_block_state = |
| item->FirstNamed(QN_GOOGLE_MUC_MEDIA_BLOCK); |
| if (media_block_state != NULL) { |
| return media_block_state->Attr(QN_NICK); |
| } |
| } |
| return std::string(); |
| } |
| |
| void HangoutPubSubClient::OnAudioMutePublishResult( |
| const std::string& task_id, const XmlElement* item) { |
| const std::string& mutee_nick = GetAudioMuteNickFromItem(item); |
| if (mutee_nick != nick_) { |
| SignalRemoteMuteResult(task_id, mutee_nick); |
| } else { |
| SignalPublishAudioMuteResult(task_id); |
| } |
| } |
| |
| void HangoutPubSubClient::OnAudioMutePublishError( |
| const std::string& task_id, const XmlElement* item, |
| const XmlElement* stanza) { |
| const std::string& mutee_nick = GetAudioMuteNickFromItem(item); |
| if (mutee_nick != nick_) { |
| SignalRemoteMuteError(task_id, mutee_nick, stanza); |
| } else { |
| SignalPublishAudioMuteError(task_id, stanza); |
| } |
| } |
| |
| void HangoutPubSubClient::OnRecordingStateChange( |
| const PubSubStateChange<bool>& change) { |
| SignalRecordingStateChange( |
| change.published_nick, change.old_state, change.new_state); |
| } |
| |
| void HangoutPubSubClient::OnRecordingPublishResult( |
| const std::string& task_id, const XmlElement* item) { |
| SignalPublishRecordingResult(task_id); |
| } |
| |
| void HangoutPubSubClient::OnRecordingPublishError( |
| const std::string& task_id, const XmlElement* item, |
| const XmlElement* stanza) { |
| SignalPublishRecordingError(task_id, stanza); |
| } |
| |
| void HangoutPubSubClient::OnMediaBlockStateChange( |
| const PubSubStateChange<bool>& change) { |
| const std::string& blockee_nick = change.published_nick; |
| const std::string& blocker_nick = change.publisher_nick; |
| |
| bool was_blockee = change.old_state; |
| bool is_blockee = change.new_state; |
| if (!was_blockee && is_blockee) { |
| SignalMediaBlock(blockee_nick, blocker_nick); |
| } |
| // TODO: Should we bother signaling unblock? Currently |
| // it isn't allowed, but it might happen when a participant leaves |
| // the room and the item is retracted. |
| } |
| |
| void HangoutPubSubClient::OnMediaBlockPublishResult( |
| const std::string& task_id, const XmlElement* item) { |
| const std::string& blockee_nick = GetBlockeeNickFromItem(item); |
| SignalMediaBlockResult(task_id, blockee_nick); |
| } |
| |
| void HangoutPubSubClient::OnMediaBlockPublishError( |
| const std::string& task_id, const XmlElement* item, |
| const XmlElement* stanza) { |
| const std::string& blockee_nick = GetBlockeeNickFromItem(item); |
| SignalMediaBlockError(task_id, blockee_nick, stanza); |
| } |
| |
| } // namespace buzz |