Skip to content

Commit

Permalink
Merge pull request #13 from gunyu1019/develop
Browse files Browse the repository at this point in the history
[Deploy] bump v1.0.0
  • Loading branch information
gunyu1019 authored Jun 6, 2024
2 parents d9aab12 + 8c95f82 commit f7ec3d1
Show file tree
Hide file tree
Showing 16 changed files with 671 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,5 @@ pip-selfcheck.json
.ionide

# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,venv

main.py
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ This library focused on chat. However, other feature will be developed.
* 사용자 후원 (`on_donation`)
* 메시지 상단 고정하기 (`on_pin`, `on_unpin`)
* 시스템 메시지 (`on_system_message`)
* 메시지 관리
* 로그인 (쿠키 값 `NID_AUT`, `NID_SES` 사용)
* 검색 (채널, 영상, 라이브, 자동완성)
* 방송 상태 조회

## Installation
Expand All @@ -37,6 +39,28 @@ py -3 -m pip install chzzkpy
`chzzkpy`를 사용한 예제는 [Examples](examples)에서 확인하실 수 있습니다.<br/>
아래는 간단한 예제입니다.

#### 방송인 검색

```py
import asyncio
import chzzkpy

loop = asyncio.get_event_loop()
client = chzzkpy.Client(loop=loop)

async def main():
result = await client.search_channel("건유1019")
if len(result) == 0:
print("검색 결과가 없습니다 :(")
return
print(result[0].name)
print(result[0].id)
print(result[0].image)
await client.close()

loop.run_until_complete(main())
```

#### 챗봇 (Chat-Bot)

```py
Expand Down
4 changes: 2 additions & 2 deletions chzzkpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
__author__ = "gunyu1019"
__license__ = "MIT"
__copyright__ = "Copyright 2024-present gunyu1019"
__version__ = "0.0.5-alpha1" # version_info.to_string()
__version__ = "1.0.0" # version_info.to_string()


class VersionInfo(NamedTuple):
Expand All @@ -57,5 +57,5 @@ def to_string(self) -> str:


version_info: VersionInfo = VersionInfo(
major=0, minor=0, micro=5, release_level="alpha", serial=1
major=1, minor=0, micro=0, release_level=None, serial=0
)
2 changes: 1 addition & 1 deletion chzzkpy/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ def special_date_parsing_validator(value: T) -> T:
class Content(ChzzkModel, Generic[T]):
code: int
message: Optional[str]
content: T
content: Optional[T]
46 changes: 46 additions & 0 deletions chzzkpy/channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""MIT License
Copyright (c) 2024 gunyu1019
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from typing import Optional

from pydantic import BeforeValidator, Field

from .base_model import ChzzkModel


class ChannelPersonalData(ChzzkModel):
private_user_block: bool = False


class PartialChannel(ChzzkModel):
id: str = Field(alias="channelId")
name: str = Field(alias="channelName")
image: Optional[str] = Field(alias="channelImageUrl")
verified_mark: bool = False
personal_data: Optional[ChannelPersonalData] = None


class Channel(PartialChannel):
description: str = Field(alias="channelDescription")
follower: int = Field("followerCount")
open_live: bool
5 changes: 4 additions & 1 deletion chzzkpy/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
ChatMessage,
NoticeMessage,
DonationMessage,
SubscriptionMessage,
SystemMessage,
Extra,
NoticeExtra,
SystemExtra,
DonationExtra,
ChatDonationExtra,
VideoDonationExtra,
MissionDonationExtra,
)
from .profile import Profile, ActivityBadge, StreamingProperty, Badge
from .recent_chat import RecentChat
132 changes: 130 additions & 2 deletions chzzkpy/chat/chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@


class ChatClient(Client):
"""Represents a client to connect Chzzk (Naver Live Streaming).
Addition, this class includes chat feature.
"""

def __init__(
self,
channel_id: str,
Expand Down Expand Up @@ -75,7 +79,9 @@ def __init__(
self._ready = asyncio.Event()

handler = {ChatCmd.CONNECTED: self._ready.set}
self._connection = ConnectionState(dispatch=self.dispatch, handler=handler)
self._connection = ConnectionState(
dispatch=self.dispatch, handler=handler, client=self
)
self._gateway: Optional[ChzzkWebSocket] = None

def _session_initial_set(self):
Expand All @@ -84,6 +90,7 @@ def _session_initial_set(self):

@property
def is_connected(self) -> bool:
"""Specifies if the client successfully connected with chzzk."""
return self._ready.is_set()

def run(self, authorization_key: str = None, session_key: str = None) -> None:
Expand Down Expand Up @@ -116,6 +123,7 @@ async def connect(self) -> None:
await self.polling()

async def close(self):
"""Close the connection to chzzk."""
self._ready.clear()

if self._gateway is not None:
Expand Down Expand Up @@ -149,6 +157,7 @@ async def polling(self) -> None:

# Event Handler
async def wait_until_connected(self) -> None:
"""Waits until the client's internal cache is all ready."""
await self._ready.wait()

def wait_for(
Expand All @@ -157,6 +166,21 @@ def wait_for(
check: Optional[Callable[..., bool]] = None,
timeout: Optional[float] = None,
):
"""Waits for a WebSocket event to be dispatched.
Parameters
----------
event : str
The event name.
For a list of events, read :method:`event`
check : Optional[Callable[..., bool]],
A predicate to check what to wait for. The arguments must meet the
parameters of the event being waited for.
timeout : Optional[float]
The number of seconds to wait before timing out and raising
:exc:`asyncio.TimeoutError`.
"""
future = self.loop.create_future()

if check is None:
Expand All @@ -175,6 +199,28 @@ def _check(*_):
def event(
self, coro: Callable[..., Coroutine[Any, Any, Any]]
) -> Callable[..., Coroutine[Any, Any, Any]]:
"""A decorator that registers an event to listen to.
The function must be corutine. Else client cause TypeError
A list of events that the client can listen to.
* `on_chat`: Called when a ChatMessage is created and sent.
* `on_connect`: Called when the client is done preparing the data received from Chzzk.
* `on_donation`: Called when a listener donates
* `on_system_message`: Called when a system message is created and sent.
(Example. notice/blind message)
* `on_recent_chat`: Called when a recent chat received.
This event called when `request_recent_chat` method called.
* `on_pin` / `on_unpin`: Called when a message pinned or unpinned.
* `on_blind`: Called when a message blocked.
* `on_client_error`: Called when client cause exception.
Example
-------
>>> @client.event
... async def on_chat(message: ChatMessage):
... print(message.content)
"""
if not asyncio.iscoroutinefunction(coro):
raise TypeError("function must be a coroutine.")

Expand Down Expand Up @@ -260,6 +306,18 @@ async def _generate_access_token(self) -> AccessToken:

# Chat Method
async def send_chat(self, message: str) -> None:
"""Send a message.
Parameters
----------
message : str
Message to Broadcasters
Raises
------
RuntimeError
Occurs when the client can't connect to a broadcaster's chat
"""
if not self.is_connected:
raise RuntimeError("Not connected to server. Please connect first.")

Expand All @@ -269,14 +327,84 @@ async def send_chat(self, message: str) -> None:
await self._gateway.send_chat(message, self.chat_channel_id)

async def request_recent_chat(self, count: int = 50):
"""Send a request recent chat to chzzk.
This method only makes a “request”.
If you want to get the recent chats of participants, use the `history` method.
Parameters
----------
count : int, optional
Number of messages to fetch from the most recent, by default 50
Raises
------
RuntimeError
Occurs when the client can't connect to a broadcaster's chat
"""
if not self.is_connected:
raise RuntimeError("Not connected to server. Please connect first.")

await self._gateway.request_recent_chat(count, self.chat_channel_id)

async def history(self, count: int = 50) -> list[ChatMessage]:
"""Get messages the user has previously sent.
Parameters
----------
count : Optional[int]
Number of messages to fetch from the most recent, by default 50
Returns
-------
list[ChatMessage]
Returns the user's most recently sent messages, in order of appearance
"""
await self.request_recent_chat(count)
recent_chat: RecentChat = await self.wait_for(
"recent_chat", lambda x: len(recent_chat.message_list) <= count
"recent_chat", lambda x: len(x.message_list) <= count
)
return recent_chat.message_list

async def set_notice_message(self, message: ChatMessage) -> None:
"""Set a pinned messsage.
Parameters
----------
message : ChatMessage
A Chat to pin.
"""
await self._game_session.set_notice_message(
channel_id=self.chat_channel_id,
extras=(
message.extras.model_dump_json(by_alias=True)
if message.extras is not None
else "{}"
),
message=message.content,
message_time=int(message.created_time.timestamp() * 1000),
message_user_id_hash=message.user_id,
streaming_channel_id=message.extras.streaming_channel_id,
)
return

async def delete_notice_message(self) -> None:
"""Delete a pinned message."""
await self._game_session.delete_notice_message(channel_id=self.chat_channel_id)
return

async def blind_message(self, message: ChatMessage) -> None:
"""Blinds a chat.
Parameters
----------
message : ChatMessage
A Chat to blind.
"""
await self._game_session.blind_message(
channel_id=self.chat_channel_id,
message=message.content,
message_time=int(message.created_time.timestamp() * 1000),
message_user_id_hash=message.user_id,
streaming_channel_id=message.extras.streaming_channel_id,
)
return
1 change: 1 addition & 0 deletions chzzkpy/chat/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ChatType(IntEnum):
VIDEO = 4
RICH = 5
DONATION = 10
SUBSCRIPTION = 11
SYSTEM_MESSAGE = 30
OPEN = 121

Expand Down
Loading

0 comments on commit f7ec3d1

Please sign in to comment.