Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion streamer/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,32 @@ async def user_list(self, **kwargs):
response_data = UserList(user_list=user_list)
await self.send(text_data=json.dumps(response_data.model_dump()))

@action()
async def delete_room(self, **kwargs):
"""ホスト(オーナー)がルームを削除するアクション。

ルーム内の全員(送信者を含む)へ ``room_deleted`` を通知してから、
ルームと参加者をまとめて論理削除する。ホスト以外からの要求は無視する。
"""
if self.anime_room is None or self.anime_user is None:
return
await self.database_renew_state()
if not self.anime_user.is_host:
return
room_id_str = str(self.anime_room.room_id)
# 猟予期間の自動削除が予約されていれば取り消す(ここで明示的に削除するため)。
_cancel_pending_room_delete(room_id_str)
server_message = ServerMessage(message_type="room_deleted")
response_data = RoomSend(
response=server_message,
sender_channel_name=self.channel_name,
)
await self.channel_layer.group_send(
room_id_str,
json.loads(json.dumps(response_data.model_dump())),
)
await self.database_delete_room_and_users()

async def room_send(self, data: dict):
"""自分を含むのグループに所属するユーザーへの一斉送信

Expand Down Expand Up @@ -454,6 +480,17 @@ def database_delete_room(self):
self.anime_room.delete()
self.anime_room.save()

@database_sync_to_async
def database_delete_room_and_users(self):
"""ルームと、その中の生存ユーザーをまとめて論理削除する。

ホストがルームを削除したときに呼ぶ。QuerySet の ``delete()`` は
``LogicalDeletionMixin`` により論理削除(``deleted_at`` 付与)。
"""
room_id = self.anime_room.room_id
AnimeUser.objects.alive().filter(room_id=room_id).delete()
AnimeRoom.objects.alive().filter(room_id=room_id).delete()

@database_sync_to_async
def database_increase_num_people(self):
"""人が増えた場合にデータベースのnum_peopleとsum_peopleを加算する"""
Expand Down Expand Up @@ -516,7 +553,7 @@ def database_renew_state(self):
def database_user_list(self):
"""ルーム内のユーザーを取得する"""
ar = AnimeRoom.objects.get(room_id=self.anime_room.room_id)
user_list = ar.inroom.alive().values("user_name", "user_id")
user_list = ar.inroom.alive().values("user_name", "user_id", "is_host")
return list(user_list)

@database_sync_to_async
Expand Down
2 changes: 2 additions & 0 deletions streamer/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ class User(BaseModel):
Attributes:
user_id: UUID identifying the user.
user_name: display name chosen by the user.
is_host: whether this participant is the room host (owner).
"""

user_id: UUID
user_name: str
is_host: bool = False


class ResponseBaseFormat(BaseModel):
Expand Down
117 changes: 117 additions & 0 deletions streamer/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ async def test_anime_party_consumer_video_action(self):
response = await communicator1.receive_json_from()
assert response["action"] == "user_list"
assert len(response["user_list"]) == 2
# user_list はホスト(オーナー)判定のため is_host を含む。
hosts = {u["user_name"]: u["is_host"] for u in response["user_list"]}
assert hosts[user_name1] is True
assert hosts[user_name2] is False
await communicator2.send_json_to(
{
"action": "video_operation",
Expand All @@ -163,6 +167,119 @@ async def test_anime_party_consumer_video_action(self):
assert response["room_id"] == join_room_id
await communicator1.disconnect()

@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_anime_party_consumer_delete_room_ok(self):
"""ホストが delete_room を送ると、ルームが論理削除され、ルーム内の
全員(ホスト自身を含む)へ room_deleted が通知されるテスト"""
host = WebsocketCommunicator(
AnimePartyConsumer.as_asgi(), "/anime-store/party/"
)
await host.connect()
await host.send_json_to(
{
"action": "create",
"user_name": "host_user",
"part_id": "123456",
"request_id": 100,
}
)
create_response = await host.receive_json_from()
room_id = create_response["room_id"]
# create 直後の user_list を読み飛ばす。
await host.receive_json_from()

guest = WebsocketCommunicator(
AnimePartyConsumer.as_asgi(), "/anime-store/party/"
)
await guest.connect()
await guest.send_json_to(
{
"action": "join",
"user_name": "guest_user",
"room_id": room_id,
"request_id": 100,
}
)
# guest: join 応答 + user_list、host: user_add + user_list を読み飛ばす。
await guest.receive_json_from()
await guest.receive_json_from()
await host.receive_json_from()
await host.receive_json_from()

# ホストがルーム削除を要求する。
await host.send_json_to({"action": "delete_room", "request_id": 100})

host_msg = await host.receive_json_from()
assert host_msg["action"] == "server_message"
assert host_msg["message_type"] == "room_deleted"

guest_msg = await guest.receive_json_from()
assert guest_msg["action"] == "server_message"
assert guest_msg["message_type"] == "room_deleted"

assert await self.room_alive(room_id) is False
assert await self.alive_user_count(room_id) == 0

# 既存テストにならい片方のみ切断する(num_people は作成者を数えないため、
# 2 回 decrement すると CHECK 制約 >= 0 を割る既知の会計上の癖を避ける)。
await host.disconnect()

@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_anime_party_consumer_delete_room_non_host_ignored(self):
"""ホスト以外が delete_room を送っても無視され、ルームは残るテスト"""
host = WebsocketCommunicator(
AnimePartyConsumer.as_asgi(), "/anime-store/party/"
)
await host.connect()
await host.send_json_to(
{
"action": "create",
"user_name": "host_user",
"part_id": "123456",
"request_id": 100,
}
)
create_response = await host.receive_json_from()
room_id = create_response["room_id"]
await host.receive_json_from()

guest = WebsocketCommunicator(
AnimePartyConsumer.as_asgi(), "/anime-store/party/"
)
await guest.connect()
await guest.send_json_to(
{
"action": "join",
"user_name": "guest_user",
"room_id": room_id,
"request_id": 100,
}
)
await guest.receive_json_from()
await guest.receive_json_from()
await host.receive_json_from()
await host.receive_json_from()

# 非ホスト(ゲスト)が削除を要求しても無視される。
await guest.send_json_to({"action": "delete_room", "request_id": 100})

assert await guest.receive_nothing() is True
assert await self.room_alive(room_id) is True
assert await self.alive_user_count(room_id) == 2

# 片方のみ切断する(num_people の二重 decrement による CHECK 制約違反を避ける)。
await host.disconnect()

@database_sync_to_async
def room_alive(self, room_id):
return AnimeRoom.objects.alive().filter(room_id=room_id).exists()

@database_sync_to_async
def alive_user_count(self, room_id):
return AnimeUser.objects.alive().filter(room_id=room_id).count()

@database_sync_to_async
def anime_user_exist(self, user_id):
return AnimeUser.objects.filter(user_id=user_id).exists()
Expand Down
Loading