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