zyncedpy 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. zyncedpy-1.0.3/PKG-INFO +76 -0
  2. zyncedpy-1.0.3/README.md +41 -0
  3. zyncedpy-1.0.3/setup.cfg +4 -0
  4. zyncedpy-1.0.3/setup.py +36 -0
  5. zyncedpy-1.0.3/zyncedpy/__init__.py +7 -0
  6. zyncedpy-1.0.3/zyncedpy/client/__init__.py +6 -0
  7. zyncedpy-1.0.3/zyncedpy/client/base_client.py +25 -0
  8. zyncedpy-1.0.3/zyncedpy/client/client.py +63 -0
  9. zyncedpy-1.0.3/zyncedpy/client/rest_client.py +90 -0
  10. zyncedpy-1.0.3/zyncedpy/client/websocket_client.py +83 -0
  11. zyncedpy-1.0.3/zyncedpy/events/__init__.py +3 -0
  12. zyncedpy-1.0.3/zyncedpy/events/event_emitter.py +44 -0
  13. zyncedpy-1.0.3/zyncedpy/events/handlers/__init__.py +10 -0
  14. zyncedpy-1.0.3/zyncedpy/events/handlers/member_joined.py +8 -0
  15. zyncedpy-1.0.3/zyncedpy/events/handlers/member_left.py +8 -0
  16. zyncedpy-1.0.3/zyncedpy/events/handlers/message_create.py +8 -0
  17. zyncedpy-1.0.3/zyncedpy/events/handlers/private_message_create.py +8 -0
  18. zyncedpy-1.0.3/zyncedpy/events/handlers/user_invite.py +8 -0
  19. zyncedpy-1.0.3/zyncedpy/managers/__init__.py +5 -0
  20. zyncedpy-1.0.3/zyncedpy/managers/base_manager.py +42 -0
  21. zyncedpy-1.0.3/zyncedpy/managers/channel_manager.py +18 -0
  22. zyncedpy-1.0.3/zyncedpy/managers/guild_manager.py +11 -0
  23. zyncedpy-1.0.3/zyncedpy/structures/__init__.py +12 -0
  24. zyncedpy-1.0.3/zyncedpy/structures/base.py +8 -0
  25. zyncedpy-1.0.3/zyncedpy/structures/base_channel.py +44 -0
  26. zyncedpy-1.0.3/zyncedpy/structures/channel.py +123 -0
  27. zyncedpy-1.0.3/zyncedpy/structures/client_user.py +51 -0
  28. zyncedpy-1.0.3/zyncedpy/structures/guild.py +6 -0
  29. zyncedpy-1.0.3/zyncedpy/structures/message.py +26 -0
  30. zyncedpy-1.0.3/zyncedpy/structures/private_message.py +14 -0
  31. zyncedpy-1.0.3/zyncedpy/types.py +55 -0
  32. zyncedpy-1.0.3/zyncedpy/utils/__init__.py +3 -0
  33. zyncedpy-1.0.3/zyncedpy/utils/logger.py +52 -0
  34. zyncedpy-1.0.3/zyncedpy.egg-info/PKG-INFO +76 -0
  35. zyncedpy-1.0.3/zyncedpy.egg-info/SOURCES.txt +36 -0
  36. zyncedpy-1.0.3/zyncedpy.egg-info/dependency_links.txt +1 -0
  37. zyncedpy-1.0.3/zyncedpy.egg-info/requires.txt +4 -0
  38. zyncedpy-1.0.3/zyncedpy.egg-info/top_level.txt +1 -0
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: zyncedpy
3
+ Version: 1.0.3
4
+ Summary: ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.
5
+ Home-page: https://github.com/Emsa001/zyncedpy
6
+ Author: Emanuel Scura
7
+ License: MIT
8
+ Keywords: sdk,bot,websocket,api,chat,zynced
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.25.0
22
+ Requires-Dist: python-socketio>=5.0.0
23
+ Requires-Dist: python-engineio>=4.0.0
24
+ Requires-Dist: dataclasses-json>=0.5.0
25
+ Dynamic: author
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: license
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # ZyncedPy
37
+
38
+ ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install zyncedpy
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from zyncedpy import Client
50
+
51
+ client = Client("your_token_here")
52
+
53
+ @client.on("ready")
54
+ def on_ready(data):
55
+ print(f"Bot is ready! Logged in as {data['user'].username}")
56
+
57
+ @client.on("messageCreate")
58
+ def on_message(message):
59
+ if message.content == "!ping":
60
+ message.reply("Pong!")
61
+
62
+ client.login()
63
+ ```
64
+
65
+ ## Features
66
+
67
+ - WebSocket and REST API support
68
+ - Event-driven architecture
69
+ - Channel and guild management
70
+ - Message handling
71
+ - User management
72
+ - Invite system
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,41 @@
1
+ # ZyncedPy
2
+
3
+ ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install zyncedpy
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from zyncedpy import Client
15
+
16
+ client = Client("your_token_here")
17
+
18
+ @client.on("ready")
19
+ def on_ready(data):
20
+ print(f"Bot is ready! Logged in as {data['user'].username}")
21
+
22
+ @client.on("messageCreate")
23
+ def on_message(message):
24
+ if message.content == "!ping":
25
+ message.reply("Pong!")
26
+
27
+ client.login()
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - WebSocket and REST API support
33
+ - Event-driven architecture
34
+ - Channel and guild management
35
+ - Message handling
36
+ - User management
37
+ - Invite system
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="zyncedpy",
8
+ version="1.0.3",
9
+ author="Emanuel Scura",
10
+ description="ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.",
11
+ long_description=long_description,
12
+ long_description_content_type="text/markdown",
13
+ url="https://github.com/Emsa001/zyncedpy",
14
+ packages=find_packages(),
15
+ classifiers=[
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ ],
27
+ python_requires=">=3.8",
28
+ install_requires=[
29
+ "requests>=2.25.0",
30
+ "python-socketio>=5.0.0",
31
+ "python-engineio>=4.0.0",
32
+ "dataclasses-json>=0.5.0",
33
+ ],
34
+ keywords=["sdk", "bot", "websocket", "api", "chat", "zynced"],
35
+ license="MIT",
36
+ )
@@ -0,0 +1,7 @@
1
+ from .client import Client
2
+ from .types import *
3
+
4
+ __version__ = "1.0.3"
5
+ __author__ = "Emanuel Scura"
6
+
7
+ __all__ = ["Client"]
@@ -0,0 +1,6 @@
1
+ from .base_client import BaseClient
2
+ from .client import Client
3
+ from .rest_client import RestClient
4
+ from .websocket_client import WebSocketClient
5
+
6
+ __all__ = ["BaseClient", "Client", "RestClient", "WebSocketClient"]
@@ -0,0 +1,25 @@
1
+ from ..events import TypedEmitter
2
+ from .rest_client import RestClient
3
+ from .websocket_client import WebSocketClient
4
+ from ..types import ClientOptions, ClientLogger
5
+ from ..utils import resolve_logger
6
+ from typing import Optional
7
+
8
+ class BaseClient(TypedEmitter):
9
+ def __init__(self, token: str, options: Optional[ClientOptions] = None):
10
+ super().__init__()
11
+ if options is None:
12
+ options = ClientOptions()
13
+
14
+ self.logger: ClientLogger = resolve_logger(options.logger)
15
+ self._rest = RestClient(token, 'https://zynced.app/api', self.logger)
16
+ self._ws = WebSocketClient(token, self.logger)
17
+
18
+ def get_rest(self) -> RestClient:
19
+ return self._rest
20
+
21
+ def get_ws(self) -> WebSocketClient:
22
+ return self._ws
23
+
24
+ def destroy(self):
25
+ self._ws.disconnect()
@@ -0,0 +1,63 @@
1
+ from .base_client import BaseClient
2
+ from ..managers import ChannelManager, GuildManager
3
+ from ..structures import ClientUser, Channel
4
+ from ..types import ClientOptions
5
+ from ..events.handlers import (
6
+ handle_private_message_create, handle_message_create,
7
+ handle_member_joined, handle_member_left, handle_user_invite
8
+ )
9
+
10
+ class Client(BaseClient):
11
+ def __init__(self, token: str, options: ClientOptions = None):
12
+ super().__init__(token, options)
13
+
14
+ self.channels = ChannelManager(self)
15
+ self.guilds = GuildManager(self)
16
+ self.user = ClientUser(self)
17
+
18
+ self._setup_event_handlers()
19
+
20
+ def _setup_event_handlers(self):
21
+ self.get_ws().on('privateMessageCreate', lambda data: handle_private_message_create(self, data))
22
+ self.get_ws().on('messageCreate', lambda data: handle_message_create(self, data))
23
+ self.get_ws().on('memberJoined', lambda data: handle_member_joined(self, data))
24
+ self.get_ws().on('memberLeft', lambda data: handle_member_left(self, data))
25
+ self.get_ws().on('userInvite', lambda data: handle_user_invite(self, data))
26
+
27
+ async def login(self):
28
+ self.logger.info('Starting login flow')
29
+ await self.get_ws().connect()
30
+
31
+ response = self.get_rest().get('/data')
32
+ data = response.json()
33
+
34
+ self.user._patch(data['me'])
35
+
36
+ for raw_channel in data['userList']['channels']:
37
+ manager = self.channels if raw_channel['group_id'] else self.guilds
38
+ manager._add(raw_channel)
39
+
40
+ self.emit('ready', {
41
+ 'user': self.user,
42
+ 'channels': self.channels,
43
+ 'guilds': self.guilds,
44
+ })
45
+
46
+ self.logger.info({
47
+ 'channels': self.channels.size,
48
+ 'guilds': self.guilds.size,
49
+ 'userId': self.user.id,
50
+ }, 'Client ready')
51
+
52
+ def channel(self, id: str) -> Channel:
53
+ return self.channels.get(id)
54
+
55
+ async def join_guild(self, guild_id: str):
56
+ response = self.get_rest().post(f'/channels/join/{guild_id}')
57
+ return response.json()
58
+
59
+ async def accept_invite(self, from_user: str):
60
+ response = self.get_rest().post('/invite/accept', json={
61
+ 'from_user': from_user,
62
+ })
63
+ return response.json()
@@ -0,0 +1,90 @@
1
+ import requests
2
+ from typing import Optional, Dict, Any
3
+ from ..types import ClientLogger
4
+
5
+ class RestClient:
6
+ def __init__(self, cookies: str, base_url: Optional[str] = None, logger: Optional[ClientLogger] = None):
7
+ self.cookies = cookies
8
+ self.base_url = base_url or 'https://zynced.app/api'
9
+ self.logger = logger
10
+
11
+ self.session = requests.Session()
12
+ self.session.headers.update({
13
+ 'User-Agent': 'ZyncedPy/1.0',
14
+ 'Accept': '*/*',
15
+ 'Accept-Encoding': 'gzip, deflate, br',
16
+ 'Connection': 'keep-alive',
17
+ 'Cookie': self.cookies,
18
+ 'Sec-Fetch-Dest': 'empty',
19
+ 'Sec-Fetch-Mode': 'cors',
20
+ 'Sec-Fetch-Site': 'same-origin',
21
+ 'Pragma': 'no-cache',
22
+ 'Cache-Control': 'no-cache',
23
+ })
24
+
25
+ def _log_request(self, method: str, url: str):
26
+ if self.logger:
27
+ self.logger.debug(f"REST request: {method} {url}")
28
+
29
+ def _log_response(self, method: str, url: str, status: int):
30
+ if self.logger:
31
+ self.logger.debug(f"REST response: {method} {url} {status}")
32
+
33
+ def _log_error(self, method: str, url: str, status: Optional[int], message: str):
34
+ if self.logger:
35
+ self.logger.error(f"REST request failed: {method} {url} {status} {message}")
36
+
37
+ def get(self, endpoint: str, **kwargs):
38
+ url = f"{self.base_url}{endpoint}"
39
+ self._log_request('GET', url)
40
+ try:
41
+ response = self.session.get(url, **kwargs)
42
+ self._log_response('GET', url, response.status_code)
43
+ return response
44
+ except Exception as e:
45
+ self._log_error('GET', url, None, str(e))
46
+ raise
47
+
48
+ def post(self, endpoint: str, data=None, json=None, **kwargs):
49
+ url = f"{self.base_url}{endpoint}"
50
+ self._log_request('POST', url)
51
+ try:
52
+ response = self.session.post(url, data=data, json=json, **kwargs)
53
+ self._log_response('POST', url, response.status_code)
54
+ return response
55
+ except Exception as e:
56
+ self._log_error('POST', url, None, str(e))
57
+ raise
58
+
59
+ def put(self, endpoint: str, data=None, json=None, **kwargs):
60
+ url = f"{self.base_url}{endpoint}"
61
+ self._log_request('PUT', url)
62
+ try:
63
+ response = self.session.put(url, data=data, json=json, **kwargs)
64
+ self._log_response('PUT', url, response.status_code)
65
+ return response
66
+ except Exception as e:
67
+ self._log_error('PUT', url, None, str(e))
68
+ raise
69
+
70
+ def patch(self, endpoint: str, data=None, json=None, **kwargs):
71
+ url = f"{self.base_url}{endpoint}"
72
+ self._log_request('PATCH', url)
73
+ try:
74
+ response = self.session.patch(url, data=data, json=json, **kwargs)
75
+ self._log_response('PATCH', url, response.status_code)
76
+ return response
77
+ except Exception as e:
78
+ self._log_error('PATCH', url, None, str(e))
79
+ raise
80
+
81
+ def delete(self, endpoint: str, **kwargs):
82
+ url = f"{self.base_url}{endpoint}"
83
+ self._log_request('DELETE', url)
84
+ try:
85
+ response = self.session.delete(url, **kwargs)
86
+ self._log_response('DELETE', url, response.status_code)
87
+ return response
88
+ except Exception as e:
89
+ self._log_error('DELETE', url, None, str(e))
90
+ raise
@@ -0,0 +1,83 @@
1
+ import socketio
2
+ from typing import Optional, Any
3
+ from ..types import ClientLogger
4
+ from ..events import TypedEmitter
5
+
6
+ class WebSocketClient(TypedEmitter):
7
+ def __init__(self, cookies: str, logger: ClientLogger):
8
+ super().__init__()
9
+ self.cookies = cookies
10
+ self.logger = logger
11
+ self.socket: Optional[socketio.AsyncClient] = None
12
+ self._connected = False
13
+
14
+ async def connect(self):
15
+ self.socket = socketio.AsyncClient()
16
+
17
+ # Set up event handlers
18
+ @self.socket.on('connect')
19
+ async def on_connect():
20
+ self._connected = True
21
+ self.logger.info(f"WebSocket connected, socket ID: {self.socket.sid}")
22
+
23
+ @self.socket.on('disconnect')
24
+ async def on_disconnect():
25
+ self._connected = False
26
+ self.logger.info("WebSocket disconnected")
27
+
28
+ @self.socket.on('connect_error')
29
+ async def on_connect_error(err):
30
+ self.logger.error(f"WebSocket connection error: {err}")
31
+
32
+ @self.socket.on('*')
33
+ async def catch_all(event, data):
34
+ await self._handle_message(event, data)
35
+
36
+ # Connect with headers
37
+ await self.socket.connect(
38
+ 'https://zynced.app',
39
+ transports=['websocket'],
40
+ headers={
41
+ 'User-Agent': 'ZyncedPy/1.0',
42
+ 'Accept': '*/*',
43
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
44
+ 'Origin': 'https://zynced.app',
45
+ 'Connection': 'Upgrade',
46
+ 'Cookie': self.cookies,
47
+ 'Sec-Fetch-Dest': 'empty',
48
+ 'Sec-Fetch-Mode': 'websocket',
49
+ 'Sec-Fetch-Site': 'same-origin',
50
+ 'Pragma': 'no-cache',
51
+ 'Cache-Control': 'no-cache',
52
+ 'Upgrade': 'websocket',
53
+ }
54
+ )
55
+
56
+ async def disconnect(self):
57
+ if self.socket:
58
+ await self.socket.disconnect()
59
+ self._connected = False
60
+
61
+ async def send(self, name: str, data: Any):
62
+ if self.socket and self._connected:
63
+ await self.socket.emit(name, data)
64
+ self.logger.info(f"WebSocket sent: {name} {data}")
65
+
66
+ async def _handle_message(self, event: str, payload: Any):
67
+ self.logger.info(f"WebSocket received: {event} {payload}")
68
+
69
+ if event == 'newMessage':
70
+ self.emit('privateMessageCreate', payload)
71
+ elif event == 'newChannelMessage':
72
+ self.emit('messageCreate', payload)
73
+ elif event == 'status-update':
74
+ if payload.get('type') == 'member_joined':
75
+ self.emit('memberJoined', payload)
76
+ elif payload.get('type') == 'member_left':
77
+ self.emit('memberLeft', payload)
78
+ elif event == 'invite-event':
79
+ if payload.get('type') == 'new_invite':
80
+ self.emit('userInvite', payload)
81
+ elif event == 'channelMessages':
82
+ # Internal event
83
+ self.emit('channelMessages', payload)
@@ -0,0 +1,3 @@
1
+ from .event_emitter import TypedEmitter
2
+
3
+ __all__ = ["TypedEmitter"]
@@ -0,0 +1,44 @@
1
+ from typing import Dict, List, Callable, Any, Optional
2
+
3
+ class TypedEmitter:
4
+ def __init__(self):
5
+ self._listeners: Dict[str, List[Callable]] = {}
6
+
7
+ def on(self, event: str, listener: Callable) -> 'TypedEmitter':
8
+ if event not in self._listeners:
9
+ self._listeners[event] = []
10
+ self._listeners[event].append(listener)
11
+ return self
12
+
13
+ def once(self, event: str, listener: Callable) -> 'TypedEmitter':
14
+ def wrapper(*args, **kwargs):
15
+ self.off(event, wrapper)
16
+ listener(*args, **kwargs)
17
+
18
+ self.on(event, wrapper)
19
+ return self
20
+
21
+ def emit(self, event: str, *args, **kwargs) -> bool:
22
+ if event in self._listeners:
23
+ for listener in self._listeners[event]:
24
+ try:
25
+ listener(*args, **kwargs)
26
+ except Exception as e:
27
+ print(f"Error in event listener for {event}: {e}")
28
+ return True
29
+ return False
30
+
31
+ def off(self, event: str, listener: Callable) -> 'TypedEmitter':
32
+ if event in self._listeners:
33
+ try:
34
+ self._listeners[event].remove(listener)
35
+ except ValueError:
36
+ pass
37
+ return self
38
+
39
+ def remove_all_listeners(self, event: Optional[str] = None) -> 'TypedEmitter':
40
+ if event:
41
+ self._listeners.pop(event, None)
42
+ else:
43
+ self._listeners.clear()
44
+ return self
@@ -0,0 +1,10 @@
1
+ from .message_create import handle_message_create
2
+ from .member_joined import handle_member_joined
3
+ from .member_left import handle_member_left
4
+ from .user_invite import handle_user_invite
5
+ from .private_message_create import handle_private_message_create
6
+
7
+ __all__ = [
8
+ "handle_message_create", "handle_member_joined",
9
+ "handle_member_left", "handle_user_invite", "handle_private_message_create"
10
+ ]
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ...client import Client
5
+ from ...types import RawMemberAction
6
+
7
+ def handle_member_joined(client: 'Client', data: RawMemberAction):
8
+ client.emit('memberJoined', data)
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ...client import Client
5
+ from ...types import RawMemberAction
6
+
7
+ def handle_member_left(client: 'Client', data: RawMemberAction):
8
+ client.emit('memberLeft', data)
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ...client import Client
5
+ from ...structures import Message
6
+
7
+ def handle_message_create(client: 'Client', data):
8
+ client.emit('messageCreate', Message(client, data))
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ...client import Client
5
+ from ...structures import PrivateMessage
6
+
7
+ def handle_private_message_create(client: 'Client', data):
8
+ client.emit('privateMessageCreate', PrivateMessage(client, data))
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ...client import Client
5
+ from ...types import RawUserInvite
6
+
7
+ def handle_user_invite(client: 'Client', data: RawUserInvite):
8
+ client.emit('userInvite', data)
@@ -0,0 +1,5 @@
1
+ from .base_manager import BaseManager
2
+ from .channel_manager import ChannelManager
3
+ from .guild_manager import GuildManager
4
+
5
+ __all__ = ["BaseManager", "ChannelManager", "GuildManager"]
@@ -0,0 +1,42 @@
1
+ from typing import Dict, Iterator, List, Optional, TypeVar, Generic, TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ..client import Client
5
+
6
+ T = TypeVar('T')
7
+
8
+ class BaseManager(Generic[T]):
9
+ def __init__(self, client: 'Client'):
10
+ self.cache: Dict[str, T] = {}
11
+ self.client = client
12
+
13
+ def resolve(self, id: str) -> Optional[T]:
14
+ return self.cache.get(id)
15
+
16
+ def get(self, id: str) -> T:
17
+ entity = self.resolve(id)
18
+ if entity is None:
19
+ raise ValueError(f"Entity {id} not in cache")
20
+ return entity
21
+
22
+ def has(self, id: str) -> bool:
23
+ return id in self.cache
24
+
25
+ @property
26
+ def size(self) -> int:
27
+ return len(self.cache)
28
+
29
+ def values(self) -> Iterator[T]:
30
+ return iter(self.cache.values())
31
+
32
+ def find(self, fn) -> Optional[T]:
33
+ for entity in self.cache.values():
34
+ if fn(entity):
35
+ return entity
36
+ return None
37
+
38
+ def filter(self, fn) -> List[T]:
39
+ return [entity for entity in self.cache.values() if fn(entity)]
40
+
41
+ def _add(self, data) -> T:
42
+ raise NotImplementedError
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+ from .base_manager import BaseManager
3
+ from ..structures import Channel, RawChannel
4
+
5
+ class ChannelManager(BaseManager[Channel]):
6
+ def _add(self, data: RawChannel) -> Channel:
7
+ if self.has(data.id):
8
+ return self.cache[data.id]
9
+
10
+ channel = Channel(self.client, data)
11
+ self.cache[channel.id] = channel
12
+ return channel
13
+
14
+ def find_one_by_name(self, name: str) -> Optional[Channel]:
15
+ return self.find(lambda channel: channel.name == name)
16
+
17
+ def find_all_by_name(self, name: str) -> list[Channel]:
18
+ return self.filter(lambda channel: channel.name == name)
@@ -0,0 +1,11 @@
1
+ from .base_manager import BaseManager
2
+ from ..structures import Guild, RawChannel
3
+
4
+ class GuildManager(BaseManager[Guild]):
5
+ def _add(self, data: RawChannel) -> Guild:
6
+ if self.has(data.id):
7
+ return self.cache[data.id]
8
+
9
+ guild = Guild(self.client, data)
10
+ self.cache[guild.id] = guild
11
+ return guild
@@ -0,0 +1,12 @@
1
+ from .base import Base
2
+ from .base_channel import BaseChannel, RawChannel
3
+ from .channel import Channel
4
+ from .client_user import ClientUser, RawClientUser
5
+ from .guild import Guild
6
+ from .message import Message
7
+ from .private_message import PrivateMessage
8
+
9
+ __all__ = [
10
+ "Base", "BaseChannel", "RawChannel", "Channel",
11
+ "ClientUser", "RawClientUser", "Guild", "Message", "PrivateMessage"
12
+ ]
@@ -0,0 +1,8 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ..client import Client
5
+
6
+ class Base:
7
+ def __init__(self, client: 'Client'):
8
+ self.client = client
@@ -0,0 +1,44 @@
1
+ from typing import Optional, TYPE_CHECKING
2
+ from dataclasses import dataclass
3
+ from .base import Base
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import Client
7
+
8
+ @dataclass
9
+ class RawChannel:
10
+ id: str
11
+ name: str
12
+ description: str
13
+ group_id: Optional[str]
14
+ category_id: Optional[str]
15
+ position: Optional[int]
16
+ relative_to_category: Optional[str]
17
+ created_at: str
18
+ blocked: bool
19
+ avatar: str
20
+ accept_joins: bool
21
+ type: str # 'text' | 'voice'
22
+ owner: str
23
+ user_joined_at: str
24
+ lastMessageAt: int
25
+
26
+ class BaseChannel(Base):
27
+ def __init__(self, client: 'Client', data: RawChannel):
28
+ super().__init__(client)
29
+
30
+ self.id = data.id
31
+ self.name = data.name
32
+ self.group_id = data.group_id
33
+ self.description = data.description
34
+ self.category_id = data.category_id
35
+ self.position = data.position
36
+ self.relative_to_category = data.relative_to_category
37
+ self.created_at = data.created_at
38
+ self.blocked = data.blocked
39
+ self.avatar = data.avatar
40
+ self.accept_joins = data.accept_joins
41
+ self.type = data.type
42
+ self.owner = data.owner
43
+ self.user_joined_at = data.user_joined_at
44
+ self.last_message_at = data.lastMessageAt
@@ -0,0 +1,123 @@
1
+ import base64
2
+ import mimetypes
3
+ import os
4
+ from typing import List, Optional, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..client import Client
8
+ from ..types import MessageAttachment
9
+
10
+ from .base_channel import BaseChannel, RawChannel
11
+ from .message import Message
12
+
13
+ class Channel(BaseChannel):
14
+ def __init__(self, client: 'Client', data: RawChannel):
15
+ super().__init__(client, data)
16
+ self.messages: List[Message] = []
17
+
18
+ def _set_messages(self, raw_messages: list):
19
+ self.messages.clear()
20
+ for msg in raw_messages:
21
+ self.messages.append(Message(self.client, msg))
22
+
23
+ def _add_message(self, message: Message) -> int:
24
+ self.messages.append(message)
25
+ return len(self.messages)
26
+
27
+ async def subscribe(self):
28
+ await self.client.get_ws().send('syncChannelRooms', [self.id])
29
+
30
+ def handler(data):
31
+ if data.get('channelId') != self.id:
32
+ return
33
+
34
+ self.client.get_ws().off('channelMessages', handler)
35
+ self._set_messages(data.get('messages', []))
36
+
37
+ self.client.get_ws().on('channelMessages', handler)
38
+ await self.client.get_ws().send('getChannelMessages', [self.id, {
39
+ "channelId": self.id,
40
+ "_t": int(__import__('time').time() * 1000)
41
+ }])
42
+
43
+ async def send(self, message: str, attachments: Optional[List] = None):
44
+ if attachments is None:
45
+ attachments = []
46
+
47
+ if message:
48
+ await self.client.get_ws().send('sendChannelMessage', {
49
+ "channelId": self.id,
50
+ "message": {
51
+ "text": message,
52
+ "from": self.client.user.username if self.client.user else None,
53
+ },
54
+ })
55
+
56
+ for attachment in attachments:
57
+ if attachment.type == 'gif':
58
+ await self._send_gif(attachment.url)
59
+ else:
60
+ await self._send_file(attachment.url)
61
+
62
+ return self
63
+
64
+ async def reply(self, message: str, attachments: Optional[List], reply_to_index: int, reply_author: str, reply_text: str):
65
+ if attachments is None:
66
+ attachments = []
67
+
68
+ await self.client.get_ws().send('sendChannelMessage', {
69
+ "channelId": self.id,
70
+ "message": {
71
+ "text": message,
72
+ "from": self.client.user.username if self.client.user else None,
73
+ "reply_to_index": reply_to_index,
74
+ "reply_author": reply_author,
75
+ "reply_text": reply_text
76
+ },
77
+ })
78
+
79
+ for attachment in attachments:
80
+ if attachment.type == 'gif':
81
+ await self._send_gif(attachment.url)
82
+ else:
83
+ await self._send_file(attachment.url)
84
+
85
+ return self
86
+
87
+ async def _send_gif(self, url: str):
88
+ await self.client.get_ws().send('sendChannelMessage', {
89
+ "channelId": self.id,
90
+ "message": {
91
+ "text": __import__('json').dumps({
92
+ "text": '',
93
+ "messageType": 'gif-link',
94
+ "gifUrl": url,
95
+ "sender": self.client.user.username if self.client.user else None,
96
+ }),
97
+ "from": self.client.user.username if self.client.user else None,
98
+ },
99
+ })
100
+ return self
101
+
102
+ async def _send_file(self, file_path: str):
103
+ if not os.path.exists(file_path):
104
+ raise FileNotFoundError(f"File not found: {file_path}")
105
+
106
+ file_name = os.path.basename(file_path)
107
+
108
+ with open(file_path, 'rb') as f:
109
+ file_buffer = f.read()
110
+
111
+ file_base64 = base64.b64encode(file_buffer).decode('utf-8')
112
+ mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
113
+
114
+ data = {
115
+ "channelId": self.id,
116
+ "text": '',
117
+ "fileName": file_name,
118
+ "mimeType": mime_type,
119
+ "fileBase64": file_base64,
120
+ }
121
+
122
+ await self.client.get_ws().send('uploadChannelMessageFile', data)
123
+ return self
@@ -0,0 +1,51 @@
1
+ from typing import Optional, TYPE_CHECKING
2
+ from dataclasses import dataclass
3
+ from .base import Base
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import Client
7
+
8
+ @dataclass
9
+ class RawClientUser:
10
+ userId: str
11
+ username: str
12
+ avatar: str
13
+ status: str
14
+ bannerUrl: str
15
+ onlineStatus: str
16
+ isPremium: bool
17
+
18
+ class ClientUser(Base):
19
+ def __init__(self, client: 'Client', data: Optional[RawClientUser] = None):
20
+ super().__init__(client)
21
+ self.id = data.userId if data else ''
22
+ self.username = data.username if data else ''
23
+ self.avatar = data.avatar if data else ''
24
+ self.status = data.status if data else ''
25
+ self.banner_url = data.bannerUrl if data else ''
26
+ self.online_status = data.onlineStatus if data else ''
27
+ self.is_premium = data.isPremium if data else False
28
+
29
+ def _patch(self, data: dict):
30
+ if 'userId' in data:
31
+ self.id = data['userId']
32
+ if 'username' in data:
33
+ self.username = data['username']
34
+ if 'avatar' in data:
35
+ self.avatar = data['avatar']
36
+ if 'status' in data:
37
+ self.status = data['status']
38
+ if 'bannerUrl' in data:
39
+ self.banner_url = data['bannerUrl']
40
+ if 'onlineStatus' in data:
41
+ self.online_status = data['onlineStatus']
42
+ if 'isPremium' in data:
43
+ self.is_premium = data['isPremium']
44
+ return self
45
+
46
+ async def set_status(self, status: str):
47
+ await self.client.get_ws().send('updateProfileStatus', {'status': status})
48
+ self.status = status
49
+
50
+ def __str__(self):
51
+ return f"{self.username}#{self.id}"
@@ -0,0 +1,6 @@
1
+ from .base_channel import BaseChannel
2
+
3
+ class Guild(BaseChannel):
4
+ async def leave(self, id: str):
5
+ res = await self.client.get_rest().post(f'/channels/leave/{id}')
6
+ return res.json()
@@ -0,0 +1,26 @@
1
+ from datetime import datetime
2
+ from typing import Optional, List, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import Client
6
+ from ..types import MessageAttachment
7
+
8
+ class Message:
9
+ def __init__(self, client: 'Client', data: dict):
10
+ self._client = client
11
+
12
+ channel = self._client.channel(data['channelId'])
13
+ self.id = channel._add_message(self)
14
+ self.guild_id = data.get('groupId')
15
+ self.channel_id = data['channelId']
16
+ self.content = data['message']['text']
17
+ self.author = data['message']['from']
18
+ self.date = datetime.fromisoformat(data['message']['date'].replace('Z', '+00:00'))
19
+ self.is_reply = bool(data['message'].get('reply_to_index'))
20
+
21
+ def reply(self, content: str, attachments: Optional[List] = None):
22
+ if attachments is None:
23
+ attachments = []
24
+ channel = self._client.channel(self.channel_id)
25
+ if channel:
26
+ channel.reply(content, attachments, self.id, self.author, self.content)
@@ -0,0 +1,14 @@
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import Client
6
+
7
+ class PrivateMessage:
8
+ def __init__(self, client: 'Client', data: dict):
9
+ self._client = client
10
+
11
+ self.chatmode = data['message']['chatmode']
12
+ self.content = data['message']['text']
13
+ self.author = data['from']
14
+ self.date = datetime.fromisoformat(data['message']['timestamp'].replace('Z', '+00:00'))
@@ -0,0 +1,55 @@
1
+ from typing import Optional, Union, Dict, Any, Protocol
2
+ import logging
3
+
4
+ class ClientLogger(Protocol):
5
+ def debug(self, message: str, *args, **kwargs) -> None: ...
6
+ def info(self, message: str, *args, **kwargs) -> None: ...
7
+ def warn(self, message: str, *args, **kwargs) -> None: ...
8
+ def error(self, message: str, *args, **kwargs) -> None: ...
9
+
10
+ ClientLoggerOption = Union[bool, ClientLogger, Dict[str, Any]]
11
+
12
+ class ClientOptions:
13
+ def __init__(self, logger: Optional[ClientLoggerOption] = None):
14
+ self.logger = logger
15
+
16
+ # Event types
17
+ class RawMemberAction:
18
+ def __init__(self, username: str, channelId: str):
19
+ self.username = username
20
+ self.channel_id = channelId
21
+
22
+ class RawSfuCapabilities:
23
+ def __init__(self, codecs: list, headerExtensions: list):
24
+ self.codecs = codecs
25
+ self.header_extensions = headerExtensions
26
+
27
+ class RawUserInvite:
28
+ def __init__(self, type: str, inviteId: str, channelId: str, inviterUsername: str):
29
+ self.type = type
30
+ self.invite_id = inviteId
31
+ self.channel_id = channelId
32
+ self.inviter_username = inviterUsername
33
+
34
+ # Message attachment types
35
+ MessageType = str # 'gif' | 'file'
36
+
37
+ class MessageAttachment:
38
+ def __init__(self, type: MessageType, url: str, name: Optional[str] = None):
39
+ self.type = type
40
+ self.url = url
41
+ self.name = name
42
+
43
+ # Event data types
44
+ from typing import TYPE_CHECKING
45
+ if TYPE_CHECKING:
46
+ from .structures import ClientUser, Message, PrivateMessage
47
+ from .managers import ChannelManager, GuildManager
48
+
49
+ class ClientEvents:
50
+ ready: Dict[str, Any] # {'user': ClientUser, 'channels': ChannelManager, 'guilds': GuildManager}
51
+ messageCreate: 'Message'
52
+ privateMessageCreate: 'PrivateMessage'
53
+ memberJoined: RawMemberAction
54
+ memberLeft: RawMemberAction
55
+ userInvite: RawUserInvite
@@ -0,0 +1,3 @@
1
+ from .logger import resolve_logger
2
+
3
+ __all__ = ["resolve_logger"]
@@ -0,0 +1,52 @@
1
+ import logging
2
+ from typing import Optional, Union, Dict, Any
3
+ from ..types import ClientLogger, ClientLoggerOption
4
+
5
+ def noop(*args, **kwargs):
6
+ pass
7
+
8
+ silent_logger = type('SilentLogger', (), {
9
+ 'debug': noop,
10
+ 'info': noop,
11
+ 'warn': noop,
12
+ 'error': noop,
13
+ })()
14
+
15
+ def is_logger_like(value: Any) -> bool:
16
+ return (
17
+ hasattr(value, 'debug') and callable(getattr(value, 'debug')) and
18
+ hasattr(value, 'info') and callable(getattr(value, 'info')) and
19
+ hasattr(value, 'warn') and callable(getattr(value, 'warn')) and
20
+ hasattr(value, 'error') and callable(getattr(value, 'error'))
21
+ )
22
+
23
+ def resolve_logger(logger: Optional[ClientLoggerOption] = None) -> ClientLogger:
24
+ if logger is False or logger is None:
25
+ return silent_logger
26
+
27
+ if logger is True:
28
+ logger_instance = logging.getLogger('zyncedpy')
29
+ logger_instance.setLevel(logging.DEBUG)
30
+ handler = logging.StreamHandler()
31
+ formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
32
+ handler.setFormatter(formatter)
33
+ logger_instance.addHandler(handler)
34
+ return logger_instance
35
+
36
+ if is_logger_like(logger):
37
+ return logger
38
+
39
+ # If it's a dict, assume it's logging config
40
+ if isinstance(logger, dict):
41
+ logger_instance = logging.getLogger('zyncedpy')
42
+ logger_instance.setLevel(logger.get('level', logging.DEBUG))
43
+ handler = logging.StreamHandler()
44
+ if 'format' in logger:
45
+ formatter = logging.Formatter(logger['format'])
46
+ else:
47
+ formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
48
+ handler.setFormatter(formatter)
49
+ logger_instance.addHandler(handler)
50
+ return logger_instance
51
+
52
+ return silent_logger
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: zyncedpy
3
+ Version: 1.0.3
4
+ Summary: ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.
5
+ Home-page: https://github.com/Emsa001/zyncedpy
6
+ Author: Emanuel Scura
7
+ License: MIT
8
+ Keywords: sdk,bot,websocket,api,chat,zynced
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.25.0
22
+ Requires-Dist: python-socketio>=5.0.0
23
+ Requires-Dist: python-engineio>=4.0.0
24
+ Requires-Dist: dataclasses-json>=0.5.0
25
+ Dynamic: author
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: license
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # ZyncedPy
37
+
38
+ ZyncedPy is a Python module that allows you to create bots for Zynced chat platform.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install zyncedpy
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from zyncedpy import Client
50
+
51
+ client = Client("your_token_here")
52
+
53
+ @client.on("ready")
54
+ def on_ready(data):
55
+ print(f"Bot is ready! Logged in as {data['user'].username}")
56
+
57
+ @client.on("messageCreate")
58
+ def on_message(message):
59
+ if message.content == "!ping":
60
+ message.reply("Pong!")
61
+
62
+ client.login()
63
+ ```
64
+
65
+ ## Features
66
+
67
+ - WebSocket and REST API support
68
+ - Event-driven architecture
69
+ - Channel and guild management
70
+ - Message handling
71
+ - User management
72
+ - Invite system
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,36 @@
1
+ README.md
2
+ setup.py
3
+ zyncedpy/__init__.py
4
+ zyncedpy/types.py
5
+ zyncedpy.egg-info/PKG-INFO
6
+ zyncedpy.egg-info/SOURCES.txt
7
+ zyncedpy.egg-info/dependency_links.txt
8
+ zyncedpy.egg-info/requires.txt
9
+ zyncedpy.egg-info/top_level.txt
10
+ zyncedpy/client/__init__.py
11
+ zyncedpy/client/base_client.py
12
+ zyncedpy/client/client.py
13
+ zyncedpy/client/rest_client.py
14
+ zyncedpy/client/websocket_client.py
15
+ zyncedpy/events/__init__.py
16
+ zyncedpy/events/event_emitter.py
17
+ zyncedpy/events/handlers/__init__.py
18
+ zyncedpy/events/handlers/member_joined.py
19
+ zyncedpy/events/handlers/member_left.py
20
+ zyncedpy/events/handlers/message_create.py
21
+ zyncedpy/events/handlers/private_message_create.py
22
+ zyncedpy/events/handlers/user_invite.py
23
+ zyncedpy/managers/__init__.py
24
+ zyncedpy/managers/base_manager.py
25
+ zyncedpy/managers/channel_manager.py
26
+ zyncedpy/managers/guild_manager.py
27
+ zyncedpy/structures/__init__.py
28
+ zyncedpy/structures/base.py
29
+ zyncedpy/structures/base_channel.py
30
+ zyncedpy/structures/channel.py
31
+ zyncedpy/structures/client_user.py
32
+ zyncedpy/structures/guild.py
33
+ zyncedpy/structures/message.py
34
+ zyncedpy/structures/private_message.py
35
+ zyncedpy/utils/__init__.py
36
+ zyncedpy/utils/logger.py
@@ -0,0 +1,4 @@
1
+ requests>=2.25.0
2
+ python-socketio>=5.0.0
3
+ python-engineio>=4.0.0
4
+ dataclasses-json>=0.5.0
@@ -0,0 +1 @@
1
+ zyncedpy