Skip to content

Commit

Permalink
Merge pull request #34 from gunyu1019/develop
Browse files Browse the repository at this point in the history
[Deploy] Deploy v1.1.0
  • Loading branch information
gunyu1019 authored Jan 1, 2025
2 parents f98f54e + bb35df2 commit fa35e58
Show file tree
Hide file tree
Showing 24 changed files with 1,601 additions and 132 deletions.
56 changes: 46 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@
![PyPI - License](https://img.shields.io/pypi/l/chzzkpy?style=flat)

파이썬 기반의 치지직(네이버 라이브 스트리밍 서비스)의 비공식 라이브러리 입니다.<br/>
채팅 기능을 중점으로 개발하였으며, 다른 기능도 개발될 예정입니다.

An unofficial python library for [Chzzk(Naver Live Streaming Service)](https://chzzk.naver.com/).<br/>
This library focused on chat. However, other feature will be developed.

* [공식 문서(한국어)](https://gunyu1019.github.io/chzzkpy/ko/)
* [Offical Documentation(English)](https://gunyu1019.github.io/chzzkpy/en/)

#### Available Features

* 채팅
* 사용자 상호 채팅 (`on_chat` <-> `client.send_chat`)
* 사용자 후원 (`on_donation`)
* 메시지 상단 고정하기 (`on_pin`, `on_unpin`)
* 시스템 메시지 (`on_system_message`)
* 사용자 상호 채팅
* 사용자 후원
* 메시지 상단 고정하기
* 시스템 메시지
* 메시지 관리
* 채널 관리
* 금칙어 설정
* 활동 제한
* 채널 규칙 설정
* 영상/팔로워/구독자 관리
* 로그인 (쿠키 값 `NID_AUT`, `NID_SES` 사용)
* 검색 (채널, 영상, 라이브, 자동완성)
* 방송 상태 조회
Expand Down Expand Up @@ -52,20 +57,21 @@ $ python3 -m pip install -U .
import asyncio
import chzzkpy

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

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

print(result[0].name)
print(result[0].id)
print(result[0].image)
await client.close()

loop.run_until_complete(main())
asyncio.run(main())
```

#### 챗봇 (Chat-Bot)
Expand All @@ -87,9 +93,39 @@ async def on_donation(message: DonationMessage):
await client.send_chat("%s님, %d원 후원 감사합니다." % (message.profile.nickname, message.extras.pay_amount))


# 챗봇 기능을 이용하기 위해서는 네이버 사용자 인증이 필요합니다.
# 웹브라우저의 쿠키 값에 있는 NID_AUT와 NID_SES 값으로 로그인을 대체할 수 있습니다.
client.run("NID_AUT", "NID_SES")
```

#### 팔로워 불러오기 (Get followers)

```py
import asyncio
import chzzkpy


async def main():
client = chzzkpy.Client()

# 채널 관리 기능을 이용하기 위해서는 네이버 사용자 인증이 필요합니다.
# 웹브라우저의 쿠키 값에 있는 NID_AUT와 NID_SES 값으로 로그인을 대체할 수 있습니다.
client.login("NID_AUT", "NID_SES")
manage_client = client.manage("channel_id")

followers = await manage_client.followers()
if len(result) == 0:
print("팔로워가 없습니다. :(")
await client.close()
return

for user in result.data:
print(f"{user.user.nickname}: {user.following.follow_date}부터 팔로우 중.")
await client.close()

asyncio.run(main())
```

## Contributions
`chzzkpy`의 기여는 언제든지 환영합니다!<br/>
버그 또는 새로운 기능은 `Pull Request`로 진행해주세요.
7 changes: 4 additions & 3 deletions chzzkpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@

from .client import Client
from .error import *
from .manage import *
from .live import LiveStatus, LiveDetail, LivePollingStatus
from .user import User
from .user import User, UserRole

# Extension Package
try:
Expand All @@ -39,7 +40,7 @@
__author__ = "gunyu1019"
__license__ = "MIT"
__copyright__ = "Copyright 2024-present gunyu1019"
__version__ = "1.0.4" # version_info.to_string()
__version__ = "1.1.0" # version_info.to_string()


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


version_info: VersionInfo = VersionInfo(
major=1, minor=0, micro=4, release_level=None, serial=0
major=1, minor=1, micro=0, release_level=None, serial=0
)
45 changes: 43 additions & 2 deletions chzzkpy/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@
SOFTWARE.
"""

from typing import Generic, TypeVar, Optional
from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Extra
from functools import wraps
from typing import Generic, TypeVar, Optional, TYPE_CHECKING

from pydantic import BaseModel, ConfigDict, Extra, PrivateAttr
from pydantic.alias_generators import to_camel

if TYPE_CHECKING:
from .manage.manage_client import ManageClient

T = TypeVar("T")


Expand All @@ -45,3 +51,38 @@ class Content(ChzzkModel, Generic[T]):
code: int
message: Optional[str]
content: Optional[T]


class ManagerClientAccessable(BaseModel):
_manage_client: Optional[ManageClient] = PrivateAttr(default=None)

@staticmethod
def based_manage_client(func):
@wraps(func)
async def wrapper(self, *args, **kwargs):
if not self.is_interactable:
raise RuntimeError(
f"This {self.__class__.__name__} is intended to store data only."
)
return await func(self, *args, **kwargs)

return wrapper

@property
def channel_id(self) -> str:
if not self.is_interactable:
raise RuntimeError(
f"This {self.__class__.__name__} is intended to store data only."
)
return self._manage_client.channel_id

@property
def is_interactable(self):
"""Ensure this model has access to interact with manage client."""
return self._manage_client is not None

def _set_manage_client(self, client: ManageClient):
if self.is_interactable:
raise ValueError("Manage Client already set.")
self._manage_client = client
return self
2 changes: 1 addition & 1 deletion chzzkpy/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
VideoDonation,
MissionDonation,
)
from .enums import ChatType, ChatCmd, UserRole
from .enums import ChatType, ChatCmd
from .error import *
from .message import (
Message,
Expand Down
132 changes: 122 additions & 10 deletions chzzkpy/chat/chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@
from .enums import ChatCmd
from .error import ChatConnectFailed
from .gateway import ChzzkWebSocket, ReconnectWebsocket
from .http import ChzzkChatSession
from .http import ChzzkAPIChatSession, NaverGameChatSession
from .state import ConnectionState
from ..client import Client
from ..error import LoginRequired
from ..manage.manage_client import ManageClient
from ..user import PartialUser
from ..live import LiveDetail, LiveStatus
from ..http import ChzzkAPISession

if TYPE_CHECKING:
from .access_token import AccessToken
from .message import ChatMessage
from .profile import Profile
from .recent_chat import RecentChat

_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -88,8 +91,8 @@ def __init__(
self._status: Literal["OPEN", "CLOSE"] = None

def _session_initial_set(self):
self._api_session = ChzzkAPISession(loop=self.loop)
self._game_session = ChzzkChatSession(loop=self.loop)
self._api_session = ChzzkAPIChatSession(loop=self.loop)
self._game_session = NaverGameChatSession(loop=self.loop)

@property
def is_connected(self) -> bool:
Expand All @@ -111,6 +114,20 @@ async def start(self, authorization_key: str = None, session_key: str = None):
finally:
await self.close()

def login(self, authorization_key: str, session_key: str):
"""Login at Chzzk.
Used for features that require a login. (ex. user method)
Parameters
----------
authorization_key : str
A `NID_AUT` value in the cookie.
session_key : str
A `NID_SES` value in the cookie.
"""
super().login(authorization_key=authorization_key, session_key=session_key)
self._manage_client[self.channel_id] = ManageClient(self.channel_id, self)

async def connect(self) -> None:
if self.chat_channel_id is None:
status = await self.live_status(channel_id=self.channel_id)
Expand Down Expand Up @@ -372,7 +389,7 @@ async def request_recent_chat(self, count: int = 50):
Parameters
----------
count : int, optional
count : Optional[int]
Number of messages to fetch from the most recent, by default 50
Raises
Expand Down Expand Up @@ -448,16 +465,111 @@ async def blind_message(self, message: ChatMessage) -> None:
)
return

async def live_status(
self, channel_id: Optional[str] = None
) -> Optional[LiveStatus]:
async def temporary_restrict(self, user: PartialUser | str) -> PartialUser:
"""Give temporary restrict to user.
A temporary restriction cannot be lifted arbitrarily.
Parameters
----------
user : ParticleUser | str
A user object to give temporary restrict activity.
Instead, it can be user id.
Returns
-------
ParticleUser
Returns an user temporary restricted in chat.
"""
user_id = user
if isinstance(user, PartialUser):
user_id = user.user_id_hash

response = await self._api_session.temporary_restrict(
channel_id=self.channel_id,
chat_channel_id=self.chat_channel_id,
target_id=user_id,
)
return response

async def live_status(self, channel_id: Optional[str] = None):
"""Get a live status info of broadcaster.
Parameters
----------
channel_id : Optional[str]
The channel ID of broadcaster, default by channel id of ChatClient.
Returns
-------
Optional[LiveStatus]
Return LiveStatus info. Sometimes the broadcaster is not broadcasting, returns None.
"""
if channel_id is None:
channel_id = self.channel_id
return await super().live_status(channel_id)

async def live_detail(
self, channel_id: Optional[str] = None
) -> Optional[LiveDetail]:
async def live_detail(self, channel_id: Optional[str] = None):
"""Get a live detail info of broadcaster.
Parameters
----------
channel_id : Optional[str]
The channel ID of broadcaster, default by channel id of ChatClient.
Returns
-------
Optional[LiveDetail]
Return LiveDetail info. Sometimes the broadcaster is not broadcasting, returns None.
"""
if channel_id is None:
channel_id = self.channel_id
return await super().live_detail(channel_id)

def manage(self, channel_id: Optional[str] = None) -> ManageClient:
"""Get a client provided broadcast management functionality.
Parameters
----------
channel_id : Optional[str]
A channel id to manage broadcasts.
The default value is the last channel id used.
If initally use the manage method and don't have a channel_id argument,
the default value is channel id of ChatClient.
Returns
-------
ManageClient
Return a client provided broadcast management functionality.
"""
if channel_id is None and self._latest_manage_client_id is None:
channel_id = self.channel_id
return super().manage(channel_id=channel_id)

@property
def manage_self(self) -> ManageClient:
"""Get a client provided self-channel management functionally."""
return self.manage(channel_id=self.channel_id)

async def profile_card(self, user: PartialUser | str) -> Profile:
"""Get a profile card.
Parameters
----------
user : ParticleUser | str
A user object to get profile card.
Instead, it can be user id.
Returns
-------
Profile
Returns a profile card with this channel information (include following info).
"""
user_id = user
if isinstance(user, PartialUser):
user_id = user.user_id_hash

data = await self._game_session.profile_card(
chat_channel_id=self.chat_channel_id, user_id=user_id
)
data.content._set_manage_client(self.manage_self)
return data.content
8 changes: 0 additions & 8 deletions chzzkpy/chat/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ class ChatType(IntEnum):
OPEN = 121


class UserRole(Enum):
common_user = "common_user"
streamer = "streamer"
chat_manager = "streaming_chat_manager"
channel_manager = "streaming_channel_manager"
manager = "manager"


def get_enum(cls: type[E], val: Any) -> E:
enum_val = [i for i in cls if i.value == val]
if len(enum_val) == 0:
Expand Down
Loading

0 comments on commit fa35e58

Please sign in to comment.