/ src / network / room_member.cpp
room_member.cpp
  1  // Copyright 2017 Citra Emulator Project
  2  // Licensed under GPLv2 or any later version
  3  // Refer to the license.txt file included.
  4  
  5  #include <algorithm>
  6  #include <atomic>
  7  #include <list>
  8  #include <mutex>
  9  #include <set>
 10  #include <thread>
 11  #include "common/assert.h"
 12  #include "enet/enet.h"
 13  #include "network/packet.h"
 14  #include "network/room_member.h"
 15  
 16  namespace Network {
 17  
 18  constexpr u32 ConnectionTimeoutMs = 5000;
 19  
 20  class RoomMember::RoomMemberImpl {
 21  public:
 22      ENetHost* client = nullptr; ///< ENet network interface.
 23      ENetPeer* server = nullptr; ///< The server peer the client is connected to
 24  
 25      /// Information about the clients connected to the same room as us.
 26      MemberList member_information;
 27      /// Information about the room we're connected to.
 28      RoomInformation room_information;
 29  
 30      /// The current game name, id and version
 31      GameInfo current_game_info;
 32  
 33      std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
 34      void SetState(const State new_state);
 35      void SetError(const Error new_error);
 36      bool IsConnected() const;
 37  
 38      std::string nickname; ///< The nickname of this member.
 39  
 40      std::string username;              ///< The username of this member.
 41      mutable std::mutex username_mutex; ///< Mutex for locking username.
 42  
 43      MacAddress mac_address; ///< The mac_address of this member.
 44  
 45      std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
 46      /// Thread that receives and dispatches network packets
 47      std::unique_ptr<std::thread> loop_thread;
 48      std::mutex send_list_mutex;  ///< Mutex that controls access to the `send_list` variable.
 49      std::list<Packet> send_list; ///< A list that stores all packets to send the async
 50  
 51      template <typename T>
 52      using CallbackSet = std::set<CallbackHandle<T>>;
 53      std::mutex callback_mutex; ///< The mutex used for handling callbacks
 54  
 55      class Callbacks {
 56      public:
 57          template <typename T>
 58          CallbackSet<T>& Get();
 59  
 60      private:
 61          CallbackSet<WifiPacket> callback_set_wifi_packet;
 62          CallbackSet<ChatEntry> callback_set_chat_messages;
 63          CallbackSet<StatusMessageEntry> callback_set_status_messages;
 64          CallbackSet<RoomInformation> callback_set_room_information;
 65          CallbackSet<State> callback_set_state;
 66          CallbackSet<Error> callback_set_error;
 67          CallbackSet<Room::BanList> callback_set_ban_list;
 68      };
 69      Callbacks callbacks; ///< All CallbackSets to all events
 70  
 71      void MemberLoop();
 72  
 73      void StartLoop();
 74  
 75      /**
 76       * Sends data to the room. It will be send on channel 0 with flag RELIABLE
 77       * @param packet The data to send
 78       */
 79      void Send(Packet&& packet);
 80  
 81      /**
 82       * Sends a request to the server, asking for permission to join a room with the specified
 83       * nickname and preferred mac.
 84       * @params nickname The desired nickname.
 85       * @params console_id_hash A hash of the Console ID.
 86       * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells
 87       * @params password The password for the room
 88       * the server to assign one for us.
 89       */
 90      void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash,
 91                           const MacAddress& preferred_mac = NoPreferredMac,
 92                           const std::string& password = "", const std::string& token = "");
 93  
 94      /**
 95       * Extracts a MAC Address from a received ENet packet.
 96       * @param event The ENet event that was received.
 97       */
 98      void HandleJoinPacket(const ENetEvent* event);
 99      /**
100       * Extracts RoomInformation and MemberInformation from a received ENet packet.
101       * @param event The ENet event that was received.
102       */
103      void HandleRoomInformationPacket(const ENetEvent* event);
104  
105      /**
106       * Extracts a WifiPacket from a received ENet packet.
107       * @param event The  ENet event that was received.
108       */
109      void HandleWifiPackets(const ENetEvent* event);
110  
111      /**
112       * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
113       * @param event The ENet event that was received.
114       */
115      void HandleChatPacket(const ENetEvent* event);
116  
117      /**
118       * Extracts a system message entry from a received ENet packet and adds it to the system message
119       * queue.
120       * @param event The ENet event that was received.
121       */
122      void HandleStatusMessagePacket(const ENetEvent* event);
123  
124      /**
125       * Extracts a ban list request response from a received ENet packet.
126       * @param event The ENet event that was received.
127       */
128      void HandleModBanListResponsePacket(const ENetEvent* event);
129  
130      /**
131       * Disconnects the RoomMember from the Room
132       */
133      void Disconnect();
134  
135      template <typename T>
136      void Invoke(const T& data);
137  
138      template <typename T>
139      CallbackHandle<T> Bind(std::function<void(const T&)> callback);
140  };
141  
142  // RoomMemberImpl
143  void RoomMember::RoomMemberImpl::SetState(const State new_state) {
144      if (state != new_state) {
145          state = new_state;
146          Invoke<State>(state);
147      }
148  }
149  
150  void RoomMember::RoomMemberImpl::SetError(const Error new_error) {
151      Invoke<Error>(new_error);
152  }
153  
154  bool RoomMember::RoomMemberImpl::IsConnected() const {
155      return state == State::Joining || state == State::Joined || state == State::Moderator;
156  }
157  
158  void RoomMember::RoomMemberImpl::MemberLoop() {
159      // Receive packets while the connection is open
160      while (IsConnected()) {
161          std::lock_guard network_lock(network_mutex);
162          ENetEvent event;
163          if (enet_host_service(client, &event, 16) > 0) {
164              switch (event.type) {
165              case ENET_EVENT_TYPE_RECEIVE:
166                  switch (event.packet->data[0]) {
167                  case IdWifiPacket:
168                      HandleWifiPackets(&event);
169                      break;
170                  case IdChatMessage:
171                      HandleChatPacket(&event);
172                      break;
173                  case IdStatusMessage:
174                      HandleStatusMessagePacket(&event);
175                      break;
176                  case IdRoomInformation:
177                      HandleRoomInformationPacket(&event);
178                      break;
179                  case IdJoinSuccess:
180                  case IdJoinSuccessAsMod:
181                      // The join request was successful, we are now in the room.
182                      // If we joined successfully, there must be at least one client in the room: us.
183                      ASSERT_MSG(member_information.size() > 0,
184                                 "We have not yet received member information.");
185                      HandleJoinPacket(&event); // Get the MAC Address for the client
186                      if (event.packet->data[0] == IdJoinSuccessAsMod) {
187                          SetState(State::Moderator);
188                      } else {
189                          SetState(State::Joined);
190                      }
191                      break;
192                  case IdModBanListResponse:
193                      HandleModBanListResponsePacket(&event);
194                      break;
195                  case IdRoomIsFull:
196                      SetState(State::Idle);
197                      SetError(Error::RoomIsFull);
198                      break;
199                  case IdNameCollision:
200                      SetState(State::Idle);
201                      SetError(Error::NameCollision);
202                      break;
203                  case IdMacCollision:
204                      SetState(State::Idle);
205                      SetError(Error::MacCollision);
206                      break;
207                  case IdConsoleIdCollision:
208                      SetState(State::Idle);
209                      SetError(Error::ConsoleIdCollision);
210                      break;
211                  case IdVersionMismatch:
212                      SetState(State::Idle);
213                      SetError(Error::WrongVersion);
214                      break;
215                  case IdWrongPassword:
216                      SetState(State::Idle);
217                      SetError(Error::WrongPassword);
218                      break;
219                  case IdCloseRoom:
220                      SetState(State::Idle);
221                      SetError(Error::LostConnection);
222                      break;
223                  case IdHostKicked:
224                      SetState(State::Idle);
225                      SetError(Error::HostKicked);
226                      break;
227                  case IdHostBanned:
228                      SetState(State::Idle);
229                      SetError(Error::HostBanned);
230                      break;
231                  case IdModPermissionDenied:
232                      SetError(Error::PermissionDenied);
233                      break;
234                  case IdModNoSuchUser:
235                      SetError(Error::NoSuchUser);
236                      break;
237                  }
238                  enet_packet_destroy(event.packet);
239                  break;
240              case ENET_EVENT_TYPE_DISCONNECT:
241                  if (state == State::Joined || state == State::Moderator) {
242                      SetState(State::Idle);
243                      SetError(Error::LostConnection);
244                  }
245                  break;
246              case ENET_EVENT_TYPE_NONE:
247                  break;
248              case ENET_EVENT_TYPE_CONNECT:
249                  // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
250                  // already connected
251                  ASSERT_MSG(false, "Received unexpected connect event while already connected");
252                  break;
253              }
254          }
255  
256          std::list<Packet> packets;
257          {
258              std::lock_guard send_list_lock(send_list_mutex);
259              packets.swap(send_list);
260          }
261          for (const auto& packet : packets) {
262              ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(),
263                                                          ENET_PACKET_FLAG_RELIABLE);
264              enet_peer_send(server, 0, enetPacket);
265          }
266          enet_host_flush(client);
267      }
268      Disconnect();
269  };
270  
271  void RoomMember::RoomMemberImpl::StartLoop() {
272      loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this);
273  }
274  
275  void RoomMember::RoomMemberImpl::Send(Packet&& packet) {
276      std::lock_guard lock(send_list_mutex);
277      send_list.push_back(std::move(packet));
278  }
279  
280  void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname,
281                                                   const std::string& console_id_hash,
282                                                   const MacAddress& preferred_mac,
283                                                   const std::string& password,
284                                                   const std::string& token) {
285      Packet packet;
286      packet << static_cast<u8>(IdJoinRequest);
287      packet << nickname;
288      packet << console_id_hash;
289      packet << preferred_mac;
290      packet << network_version;
291      packet << password;
292      packet << token;
293      Send(std::move(packet));
294  }
295  
296  void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) {
297      Packet packet;
298      packet.Append(event->packet->data, event->packet->dataLength);
299  
300      // Ignore the first byte, which is the message id.
301      packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
302  
303      RoomInformation info{};
304      packet >> info.name;
305      packet >> info.description;
306      packet >> info.member_slots;
307      packet >> info.port;
308      packet >> info.preferred_game;
309      packet >> info.host_username;
310      room_information.name = info.name;
311      room_information.description = info.description;
312      room_information.member_slots = info.member_slots;
313      room_information.port = info.port;
314      room_information.preferred_game = info.preferred_game;
315      room_information.host_username = info.host_username;
316  
317      u32 num_members;
318      packet >> num_members;
319      member_information.resize(num_members);
320  
321      for (auto& member : member_information) {
322          packet >> member.nickname;
323          packet >> member.mac_address;
324          packet >> member.game_info.name;
325          packet >> member.game_info.id;
326          packet >> member.username;
327          packet >> member.display_name;
328          packet >> member.avatar_url;
329  
330          {
331              std::lock_guard lock(username_mutex);
332              if (member.nickname == nickname) {
333                  username = member.username;
334              }
335          }
336      }
337      Invoke(room_information);
338  }
339  
340  void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
341      Packet packet;
342      packet.Append(event->packet->data, event->packet->dataLength);
343  
344      // Ignore the first byte, which is the message id.
345      packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
346  
347      // Parse the MAC Address from the packet
348      packet >> mac_address;
349  }
350  
351  void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) {
352      WifiPacket wifi_packet{};
353      Packet packet;
354      packet.Append(event->packet->data, event->packet->dataLength);
355  
356      // Ignore the first byte, which is the message id.
357      packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
358  
359      // Parse the WifiPacket from the packet
360      u8 frame_type;
361      packet >> frame_type;
362      WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type);
363  
364      wifi_packet.type = type;
365      packet >> wifi_packet.channel;
366      packet >> wifi_packet.transmitter_address;
367      packet >> wifi_packet.destination_address;
368      packet >> wifi_packet.data;
369  
370      Invoke<WifiPacket>(wifi_packet);
371  }
372  
373  void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
374      Packet packet;
375      packet.Append(event->packet->data, event->packet->dataLength);
376  
377      // Ignore the first byte, which is the message id.
378      packet.IgnoreBytes(sizeof(u8));
379  
380      ChatEntry chat_entry{};
381      packet >> chat_entry.nickname;
382      packet >> chat_entry.username;
383      packet >> chat_entry.message;
384      chat_entry.message.resize(std::min(chat_entry.message.find('\0'), chat_entry.message.size()));
385      Invoke<ChatEntry>(chat_entry);
386  }
387  
388  void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
389      Packet packet;
390      packet.Append(event->packet->data, event->packet->dataLength);
391  
392      // Ignore the first byte, which is the message id.
393      packet.IgnoreBytes(sizeof(u8));
394  
395      StatusMessageEntry status_message_entry{};
396      u8 type{};
397      packet >> type;
398      status_message_entry.type = static_cast<StatusMessageTypes>(type);
399      packet >> status_message_entry.nickname;
400      packet >> status_message_entry.username;
401      Invoke<StatusMessageEntry>(status_message_entry);
402  }
403  
404  void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) {
405      Packet packet;
406      packet.Append(event->packet->data, event->packet->dataLength);
407  
408      // Ignore the first byte, which is the message id.
409      packet.IgnoreBytes(sizeof(u8));
410  
411      Room::BanList ban_list = {};
412      packet >> ban_list.first;
413      packet >> ban_list.second;
414      Invoke<Room::BanList>(ban_list);
415  }
416  
417  void RoomMember::RoomMemberImpl::Disconnect() {
418      member_information.clear();
419      room_information.member_slots = 0;
420      room_information.name.clear();
421  
422      if (!server)
423          return;
424      enet_peer_disconnect(server, 0);
425  
426      ENetEvent event;
427      while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) {
428          switch (event.type) {
429          case ENET_EVENT_TYPE_RECEIVE:
430              enet_packet_destroy(event.packet); // Ignore all incoming data
431              break;
432          case ENET_EVENT_TYPE_DISCONNECT:
433              server = nullptr;
434              return;
435          case ENET_EVENT_TYPE_NONE:
436          case ENET_EVENT_TYPE_CONNECT:
437              break;
438          }
439      }
440      // didn't disconnect gracefully force disconnect
441      enet_peer_reset(server);
442      server = nullptr;
443  }
444  
445  template <>
446  RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
447      return callback_set_wifi_packet;
448  }
449  
450  template <>
451  RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
452  RoomMember::RoomMemberImpl::Callbacks::Get() {
453      return callback_set_state;
454  }
455  
456  template <>
457  RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>&
458  RoomMember::RoomMemberImpl::Callbacks::Get() {
459      return callback_set_error;
460  }
461  
462  template <>
463  RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>&
464  RoomMember::RoomMemberImpl::Callbacks::Get() {
465      return callback_set_room_information;
466  }
467  
468  template <>
469  RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() {
470      return callback_set_chat_messages;
471  }
472  
473  template <>
474  RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>&
475  RoomMember::RoomMemberImpl::Callbacks::Get() {
476      return callback_set_status_messages;
477  }
478  
479  template <>
480  RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>&
481  RoomMember::RoomMemberImpl::Callbacks::Get() {
482      return callback_set_ban_list;
483  }
484  
485  template <typename T>
486  void RoomMember::RoomMemberImpl::Invoke(const T& data) {
487      std::lock_guard lock(callback_mutex);
488      CallbackSet<T> callback_set = callbacks.Get<T>();
489      for (auto const& callback : callback_set)
490          (*callback)(data);
491  }
492  
493  template <typename T>
494  RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind(
495      std::function<void(const T&)> callback) {
496      std::lock_guard lock(callback_mutex);
497      CallbackHandle<T> handle;
498      handle = std::make_shared<std::function<void(const T&)>>(callback);
499      callbacks.Get<T>().insert(handle);
500      return handle;
501  }
502  
503  // RoomMember
504  RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {}
505  
506  RoomMember::~RoomMember() {
507      ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected");
508      if (room_member_impl->loop_thread) {
509          Leave();
510      }
511  }
512  
513  RoomMember::State RoomMember::GetState() const {
514      return room_member_impl->state;
515  }
516  
517  const RoomMember::MemberList& RoomMember::GetMemberInformation() const {
518      return room_member_impl->member_information;
519  }
520  
521  const std::string& RoomMember::GetNickname() const {
522      return room_member_impl->nickname;
523  }
524  
525  const std::string& RoomMember::GetUsername() const {
526      std::lock_guard lock(room_member_impl->username_mutex);
527      return room_member_impl->username;
528  }
529  
530  const MacAddress& RoomMember::GetMacAddress() const {
531      ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected");
532      return room_member_impl->mac_address;
533  }
534  
535  RoomInformation RoomMember::GetRoomInformation() const {
536      return room_member_impl->room_information;
537  }
538  
539  void RoomMember::Join(const std::string& nick, const std::string& console_id_hash,
540                        const char* server_addr, u16 server_port, u16 client_port,
541                        const MacAddress& preferred_mac, const std::string& password,
542                        const std::string& token) {
543      // If the member is connected, kill the connection first
544      if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) {
545          Leave();
546      }
547      // If the thread isn't running but the ptr still exists, reset it
548      else if (room_member_impl->loop_thread) {
549          room_member_impl->loop_thread.reset();
550      }
551  
552      if (!room_member_impl->client) {
553          room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
554          ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
555      }
556  
557      room_member_impl->SetState(State::Joining);
558  
559      ENetAddress address{};
560      enet_address_set_host(&address, server_addr);
561      address.port = server_port;
562      room_member_impl->server =
563          enet_host_connect(room_member_impl->client, &address, NumChannels, 0);
564  
565      if (!room_member_impl->server) {
566          room_member_impl->SetState(State::Idle);
567          room_member_impl->SetError(Error::UnknownError);
568          return;
569      }
570  
571      ENetEvent event{};
572      int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
573      if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
574          room_member_impl->nickname = nick;
575          room_member_impl->StartLoop();
576          room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token);
577          SendGameInfo(room_member_impl->current_game_info);
578      } else {
579          enet_peer_disconnect(room_member_impl->server, 0);
580          room_member_impl->SetState(State::Idle);
581          room_member_impl->SetError(Error::CouldNotConnect);
582      }
583  }
584  
585  bool RoomMember::IsConnected() const {
586      return room_member_impl->IsConnected();
587  }
588  
589  void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) {
590      Packet packet;
591      packet << static_cast<u8>(IdWifiPacket);
592      packet << static_cast<u8>(wifi_packet.type);
593      packet << wifi_packet.channel;
594      packet << wifi_packet.transmitter_address;
595      packet << wifi_packet.destination_address;
596      packet << wifi_packet.data;
597      room_member_impl->Send(std::move(packet));
598  }
599  
600  void RoomMember::SendChatMessage(const std::string& message) {
601      Packet packet;
602      packet << static_cast<u8>(IdChatMessage);
603      packet << message;
604      room_member_impl->Send(std::move(packet));
605  }
606  
607  void RoomMember::SendGameInfo(const GameInfo& game_info) {
608      room_member_impl->current_game_info = game_info;
609      if (!IsConnected())
610          return;
611  
612      Packet packet;
613      packet << static_cast<u8>(IdSetGameInfo);
614      packet << game_info.name;
615      packet << game_info.id;
616      room_member_impl->Send(std::move(packet));
617  }
618  
619  void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) {
620      ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban,
621                 "type is not a moderation request");
622      if (!IsConnected())
623          return;
624  
625      Packet packet;
626      packet << static_cast<u8>(type);
627      packet << nickname;
628      room_member_impl->Send(std::move(packet));
629  }
630  
631  void RoomMember::RequestBanList() {
632      if (!IsConnected())
633          return;
634  
635      Packet packet;
636      packet << static_cast<u8>(IdModGetBanList);
637      room_member_impl->Send(std::move(packet));
638  }
639  
640  RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged(
641      std::function<void(const RoomMember::State&)> callback) {
642      return room_member_impl->Bind(callback);
643  }
644  
645  RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError(
646      std::function<void(const RoomMember::Error&)> callback) {
647      return room_member_impl->Bind(callback);
648  }
649  
650  RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived(
651      std::function<void(const WifiPacket&)> callback) {
652      return room_member_impl->Bind(callback);
653  }
654  
655  RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
656      std::function<void(const RoomInformation&)> callback) {
657      return room_member_impl->Bind(callback);
658  }
659  
660  RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved(
661      std::function<void(const ChatEntry&)> callback) {
662      return room_member_impl->Bind(callback);
663  }
664  
665  RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived(
666      std::function<void(const StatusMessageEntry&)> callback) {
667      return room_member_impl->Bind(callback);
668  }
669  
670  RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived(
671      std::function<void(const Room::BanList&)> callback) {
672      return room_member_impl->Bind(callback);
673  }
674  
675  template <typename T>
676  void RoomMember::Unbind(CallbackHandle<T> handle) {
677      std::lock_guard lock(room_member_impl->callback_mutex);
678      room_member_impl->callbacks.Get<T>().erase(handle);
679  }
680  
681  void RoomMember::Leave() {
682      room_member_impl->SetState(State::Idle);
683      room_member_impl->loop_thread->join();
684      room_member_impl->loop_thread.reset();
685  
686      enet_host_destroy(room_member_impl->client);
687      room_member_impl->client = nullptr;
688  }
689  
690  template void RoomMember::Unbind(CallbackHandle<WifiPacket>);
691  template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
692  template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
693  template void RoomMember::Unbind(CallbackHandle<RoomInformation>);
694  template void RoomMember::Unbind(CallbackHandle<ChatEntry>);
695  template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>);
696  template void RoomMember::Unbind(CallbackHandle<Room::BanList>);
697  
698  } // namespace Network