wsapi-client 0.1.0__py3-none-any.whl

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 (82) hide show
  1. wsapi_client/__init__.py +12 -0
  2. wsapi_client/api/calls.py +16 -0
  3. wsapi_client/api/chats.py +21 -0
  4. wsapi_client/api/contacts.py +54 -0
  5. wsapi_client/api/groups.py +39 -0
  6. wsapi_client/api/instance.py +38 -0
  7. wsapi_client/api/media.py +15 -0
  8. wsapi_client/api/messages.py +125 -0
  9. wsapi_client/api/session.py +43 -0
  10. wsapi_client/api/users.py +16 -0
  11. wsapi_client/client.py +39 -0
  12. wsapi_client/events/factory.py +82 -0
  13. wsapi_client/exceptions.py +13 -0
  14. wsapi_client/http.py +123 -0
  15. wsapi_client/models/entities/chats/chat_archive.py +8 -0
  16. wsapi_client/models/entities/chats/chat_ephemeral.py +10 -0
  17. wsapi_client/models/entities/chats/chat_info.py +9 -0
  18. wsapi_client/models/entities/chats/chat_mute.py +10 -0
  19. wsapi_client/models/entities/chats/chat_pin.py +9 -0
  20. wsapi_client/models/entities/chats/chat_read.py +8 -0
  21. wsapi_client/models/entities/contacts/contact_business_profile.py +11 -0
  22. wsapi_client/models/entities/contacts/contact_info.py +15 -0
  23. wsapi_client/models/entities/contacts/contact_picture.py +9 -0
  24. wsapi_client/models/entities/groups/group_info.py +24 -0
  25. wsapi_client/models/entities/instance/__init__.py +3 -0
  26. wsapi_client/models/entities/instance/instance_settings.py +14 -0
  27. wsapi_client/models/entities/messages/__init__.py +3 -0
  28. wsapi_client/models/entities/messages/message_contact.py +9 -0
  29. wsapi_client/models/entities/messages/message_created.py +8 -0
  30. wsapi_client/models/entities/messages/message_edit.py +10 -0
  31. wsapi_client/models/entities/messages/message_extended_text.py +11 -0
  32. wsapi_client/models/entities/messages/message_media.py +22 -0
  33. wsapi_client/models/entities/messages/message_pin.py +11 -0
  34. wsapi_client/models/entities/messages/message_reaction.py +9 -0
  35. wsapi_client/models/entities/messages/message_reply_to.py +11 -0
  36. wsapi_client/models/entities/session/session_pair_code.py +9 -0
  37. wsapi_client/models/entities/session/session_status.py +10 -0
  38. wsapi_client/models/entities/users/sender.py +11 -0
  39. wsapi_client/models/entities/users/user_info.py +13 -0
  40. wsapi_client/models/events/base_event.py +11 -0
  41. wsapi_client/models/events/calls/call_accept_event.py +11 -0
  42. wsapi_client/models/events/calls/call_offer_event.py +14 -0
  43. wsapi_client/models/events/calls/call_terminate_event.py +12 -0
  44. wsapi_client/models/events/chats/chat_presence_event.py +12 -0
  45. wsapi_client/models/events/chats/chat_setting_event.py +19 -0
  46. wsapi_client/models/events/contacts/contact_event.py +10 -0
  47. wsapi_client/models/events/messages/message_delete_event.py +18 -0
  48. wsapi_client/models/events/messages/message_event.py +36 -0
  49. wsapi_client/models/events/messages/message_history_sync_event.py +9 -0
  50. wsapi_client/models/events/messages/message_read_event.py +17 -0
  51. wsapi_client/models/events/messages/message_star_event.py +14 -0
  52. wsapi_client/models/events/session/session_logged_error_event.py +9 -0
  53. wsapi_client/models/events/session/session_logged_in_event.py +8 -0
  54. wsapi_client/models/events/session/session_logged_out_event.py +8 -0
  55. wsapi_client/models/events/users/user_picture_event.py +11 -0
  56. wsapi_client/models/events/users/user_presence_event.py +11 -0
  57. wsapi_client/models/events/users/user_push_name_event.py +9 -0
  58. wsapi_client/models/events/users/user_status_event.py +9 -0
  59. wsapi_client/models/problem_details.py +13 -0
  60. wsapi_client/models/requests/calls/reject_call_request.py +8 -0
  61. wsapi_client/models/requests/media/media_download_request.py +17 -0
  62. wsapi_client/models/requests/messages/__init__.py +33 -0
  63. wsapi_client/models/requests/messages/message_delete_for_me_request.py +12 -0
  64. wsapi_client/models/requests/messages/message_delete_request.py +9 -0
  65. wsapi_client/models/requests/messages/message_mark_as_read_request.py +10 -0
  66. wsapi_client/models/requests/messages/message_request_base.py +12 -0
  67. wsapi_client/models/requests/messages/message_send_audio_request.py +14 -0
  68. wsapi_client/models/requests/messages/message_send_contact_request.py +10 -0
  69. wsapi_client/models/requests/messages/message_send_document_request.py +14 -0
  70. wsapi_client/models/requests/messages/message_send_image_request.py +15 -0
  71. wsapi_client/models/requests/messages/message_send_link_request.py +15 -0
  72. wsapi_client/models/requests/messages/message_send_location_request.py +15 -0
  73. wsapi_client/models/requests/messages/message_send_reaction_request.py +11 -0
  74. wsapi_client/models/requests/messages/message_send_sticker_request.py +13 -0
  75. wsapi_client/models/requests/messages/message_send_text_request.py +10 -0
  76. wsapi_client/models/requests/messages/message_send_video_request.py +15 -0
  77. wsapi_client/models/requests/messages/message_send_voice_request.py +13 -0
  78. wsapi_client/models/requests/messages/message_star_request.py +11 -0
  79. wsapi_client/sse/client.py +111 -0
  80. wsapi_client-0.1.0.dist-info/METADATA +166 -0
  81. wsapi_client-0.1.0.dist-info/RECORD +82 -0
  82. wsapi_client-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,12 @@
1
+ from .client import WSApiClient
2
+ from .sse.client import SSEClient, SSEConnectionState
3
+ from .exceptions import ApiException
4
+ from .http import ApiResponse
5
+
6
+ __all__ = [
7
+ "WSApiClient",
8
+ "SSEClient",
9
+ "SSEConnectionState",
10
+ "ApiException",
11
+ "ApiResponse",
12
+ ]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import WSApiHttp, ApiResponse
4
+ from ..models.requests.calls.reject_call_request import RejectCallRequest
5
+
6
+
7
+ class CallsClient:
8
+ def __init__(self, http: WSApiHttp) -> None:
9
+ self._http = http
10
+
11
+ def reject(self, call_id: str, request: RejectCallRequest) -> None:
12
+ self._http.send_json("PUT", f"/calls/{call_id}/reject", model=None, json=request.model_dump(by_alias=True))
13
+ return None
14
+
15
+ def try_reject(self, call_id: str, request: RejectCallRequest) -> ApiResponse[None]:
16
+ return self._http.try_send_json("PUT", f"/calls/{call_id}/reject", model=None, json=request.model_dump(by_alias=True))
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+ from ..http import WSApiHttp, ApiResponse
3
+ from ..models.entities.chats.chat_info import ChatInfo
4
+
5
+
6
+ class ChatsClient:
7
+ def __init__(self, http: WSApiHttp) -> None:
8
+ self._http = http
9
+
10
+ def list(self) -> list[ChatInfo]:
11
+ return self._http.send_json("GET", "/chats", model=list[ChatInfo])
12
+
13
+ def get(self, chat_id: str) -> ChatInfo:
14
+ return self._http.send_json("GET", f"/chats/{chat_id}", model=ChatInfo)
15
+
16
+ # Try
17
+ def try_list(self) -> ApiResponse[list[ChatInfo]]:
18
+ return self._http.try_send_json("GET", "/chats", model=list[ChatInfo])
19
+
20
+ def try_get(self, chat_id: str) -> ApiResponse[ChatInfo]:
21
+ return self._http.try_send_json("GET", f"/chats/{chat_id}", model=ChatInfo)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+ from ..http import WSApiHttp, ApiResponse
3
+ from ..models.entities.contacts.contact_info import ContactInfo
4
+ from ..models.entities.contacts.contact_picture import ContactPicture
5
+ from ..models.entities.contacts.contact_business_profile import ContactBusinessProfile
6
+
7
+
8
+ class ContactsClient:
9
+ def __init__(self, http: WSApiHttp) -> None:
10
+ self._http = http
11
+
12
+ # Standard methods
13
+ def list(self) -> list[ContactInfo]:
14
+ return self._http.send_json("GET", "/contacts", model=list[ContactInfo])
15
+
16
+ def get(self, contact_id: str) -> ContactInfo:
17
+ return self._http.send_json("GET", f"/contacts/{contact_id}", model=ContactInfo)
18
+
19
+ def get_picture(self, contact_id: str) -> ContactPicture:
20
+ return self._http.send_json("GET", f"/contacts/{contact_id}/picture", model=ContactPicture)
21
+
22
+ def get_business_profile(self, contact_id: str) -> ContactBusinessProfile:
23
+ return self._http.send_json("GET", f"/contacts/{contact_id}/business", model=ContactBusinessProfile)
24
+
25
+ def create(self, payload: dict) -> None:
26
+ self._http.send_json("POST", "/contacts", model=dict, json=payload)
27
+
28
+ def update(self, contact_id: str, payload: dict) -> None:
29
+ self._http.send_json("PUT", f"/contacts/{contact_id}", model=dict, json=payload)
30
+
31
+ def subscribe_presence(self, contact_id: str) -> None:
32
+ self._http.send_json("POST", f"/contacts/{contact_id}/presence", model=dict, json={})
33
+
34
+ # Try methods
35
+ def try_list(self) -> ApiResponse[list[ContactInfo]]:
36
+ return self._http.try_send_json("GET", "/contacts", model=list[ContactInfo])
37
+
38
+ def try_get(self, contact_id: str) -> ApiResponse[ContactInfo]:
39
+ return self._http.try_send_json("GET", f"/contacts/{contact_id}", model=ContactInfo)
40
+
41
+ def try_get_picture(self, contact_id: str) -> ApiResponse[ContactPicture]:
42
+ return self._http.try_send_json("GET", f"/contacts/{contact_id}/picture", model=ContactPicture)
43
+
44
+ def try_get_business_profile(self, contact_id: str) -> ApiResponse[ContactBusinessProfile]:
45
+ return self._http.try_send_json("GET", f"/contacts/{contact_id}/business", model=ContactBusinessProfile)
46
+
47
+ def try_create(self, payload: dict) -> ApiResponse[object]:
48
+ return self._http.try_send_json("POST", "/contacts", model=dict, json=payload)
49
+
50
+ def try_update(self, contact_id: str, payload: dict) -> ApiResponse[object]:
51
+ return self._http.try_send_json("PUT", f"/contacts/{contact_id}", model=dict, json=payload)
52
+
53
+ def try_subscribe_presence(self, contact_id: str) -> ApiResponse[object]:
54
+ return self._http.try_send_json("POST", f"/contacts/{contact_id}/presence", model=dict, json={})
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from ..http import WSApiHttp, ApiResponse
3
+ from ..models.entities.groups.group_info import GroupInfo
4
+
5
+
6
+ class GroupsClient:
7
+ def __init__(self, http: WSApiHttp) -> None:
8
+ self._http = http
9
+
10
+ def list(self) -> list[GroupInfo]:
11
+ return self._http.send_json("GET", "/groups", model=list[GroupInfo])
12
+
13
+ def get(self, group_id: str) -> GroupInfo:
14
+ return self._http.send_json("GET", f"/groups/{group_id}", model=GroupInfo)
15
+
16
+ def update_description(self, group_id: str, payload: dict) -> None:
17
+ self._http.send_json("PUT", f"/groups/{group_id}/description", model=dict, json=payload)
18
+
19
+ def update_name(self, group_id: str, payload: dict) -> None:
20
+ self._http.send_json("PUT", f"/groups/{group_id}/name", model=dict, json=payload)
21
+
22
+ def leave(self, group_id: str) -> None:
23
+ self._http.send_json("PUT", f"/groups/{group_id}/leave", model=dict)
24
+
25
+ # Try variants
26
+ def try_list(self) -> ApiResponse[list[GroupInfo]]:
27
+ return self._http.try_send_json("GET", "/groups", model=list[GroupInfo])
28
+
29
+ def try_get(self, group_id: str) -> ApiResponse[GroupInfo]:
30
+ return self._http.try_send_json("GET", f"/groups/{group_id}", model=GroupInfo)
31
+
32
+ def try_update_description(self, group_id: str, payload: dict) -> ApiResponse[object]:
33
+ return self._http.try_send_json("PUT", f"/groups/{group_id}/description", model=dict, json=payload)
34
+
35
+ def try_update_name(self, group_id: str, payload: dict) -> ApiResponse[object]:
36
+ return self._http.try_send_json("PUT", f"/groups/{group_id}/name", model=dict, json=payload)
37
+
38
+ def try_leave(self, group_id: str) -> ApiResponse[object]:
39
+ return self._http.try_send_json("PUT", f"/groups/{group_id}/leave", model=dict)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from ..http import WSApiHttp, ApiResponse
7
+ from ..models.entities.instance.instance_settings import InstanceSettings
8
+
9
+
10
+ class InstanceClient:
11
+ def __init__(self, http: WSApiHttp) -> None:
12
+ self._http = http
13
+
14
+ # Standard methods (raise ApiException)
15
+ def get_settings(self) -> InstanceSettings:
16
+ return self._http.send_json("GET", "/instance/settings", model=InstanceSettings)
17
+
18
+ def update_settings(self, settings: InstanceSettings) -> None:
19
+ self._http.send_json("PUT", "/instance/settings", model=dict, json=settings.model_dump(by_alias=True))
20
+
21
+ def restart(self) -> None:
22
+ self._http.send_json("PUT", "/instance/restart", model=dict)
23
+
24
+ def update_api_key(self) -> str:
25
+ return self._http.send_json("PUT", "/instance/apikey", model=str)
26
+
27
+ # Try methods (no exceptions)
28
+ def try_get_settings(self) -> ApiResponse[InstanceSettings]:
29
+ return self._http.try_send_json("GET", "/instance/settings", model=InstanceSettings)
30
+
31
+ def try_update_settings(self, settings: InstanceSettings) -> ApiResponse[object]:
32
+ return self._http.try_send_json("PUT", "/instance/settings", model=dict, json=settings.model_dump(by_alias=True))
33
+
34
+ def try_restart(self) -> ApiResponse[object]:
35
+ return self._http.try_send_json("PUT", "/instance/restart", model=dict)
36
+
37
+ def try_update_api_key(self) -> ApiResponse[str]:
38
+ return self._http.try_send_json("PUT", "/instance/apikey", model=str)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import WSApiHttp, ApiResponse
4
+ from ..models.requests.media.media_download_request import MediaDownloadRequest
5
+
6
+
7
+ class MediaClient:
8
+ def __init__(self, http: WSApiHttp) -> None:
9
+ self._http = http
10
+
11
+ def download(self, req: MediaDownloadRequest) -> bytes:
12
+ return self._http.send_bytes("POST", "/media/download", json=req.model_dump(by_alias=True))
13
+
14
+ def try_download(self, req: MediaDownloadRequest) -> ApiResponse[bytes]:
15
+ return self._http.try_send_bytes("POST", "/media/download", json=req.model_dump(by_alias=True))
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from ..http import WSApiHttp, ApiResponse
5
+ from ..models.entities.messages.message_created import MessageCreated
6
+ from ..models.requests.messages import (
7
+ MessageSendTextRequest,
8
+ MessageSendImageRequest,
9
+ MessageSendVideoRequest,
10
+ MessageSendAudioRequest,
11
+ MessageSendVoiceRequest,
12
+ MessageSendStickerRequest,
13
+ MessageSendDocumentRequest,
14
+ MessageSendContactRequest,
15
+ MessageSendLocationRequest,
16
+ MessageSendLinkRequest,
17
+ MessageSendReactionRequest,
18
+ MessageMarkAsReadRequest,
19
+ MessageDeleteRequest,
20
+ MessageDeleteForMeRequest,
21
+ MessageStarRequest,
22
+ )
23
+
24
+
25
+ class MessagesClient:
26
+ def __init__(self, http: WSApiHttp) -> None:
27
+ self._http = http
28
+
29
+ # Standard methods
30
+ def send_text(self, req: MessageSendTextRequest) -> MessageCreated:
31
+ return self._http.send_json("POST", "messages/text", model=MessageCreated, json=req.model_dump(by_alias=True))
32
+
33
+ def send_image(self, req: MessageSendImageRequest) -> MessageCreated:
34
+ return self._http.send_json("POST", "messages/image", model=MessageCreated, json=req.model_dump(by_alias=True))
35
+
36
+ def send_video(self, req: MessageSendVideoRequest) -> MessageCreated:
37
+ return self._http.send_json("POST", "messages/video", model=MessageCreated, json=req.model_dump(by_alias=True))
38
+
39
+ def send_audio(self, req: MessageSendAudioRequest) -> MessageCreated:
40
+ return self._http.send_json("POST", "messages/audio", model=MessageCreated, json=req.model_dump(by_alias=True))
41
+
42
+ def send_voice(self, req: MessageSendVoiceRequest) -> MessageCreated:
43
+ return self._http.send_json("POST", "messages/voice", model=MessageCreated, json=req.model_dump(by_alias=True))
44
+
45
+ def send_sticker(self, req: MessageSendStickerRequest) -> MessageCreated:
46
+ return self._http.send_json("POST", "messages/sticker", model=MessageCreated, json=req.model_dump(by_alias=True))
47
+
48
+ def send_document(self, req: MessageSendDocumentRequest) -> MessageCreated:
49
+ return self._http.send_json("POST", "messages/document", model=MessageCreated, json=req.model_dump(by_alias=True))
50
+
51
+ def send_contact(self, req: MessageSendContactRequest) -> MessageCreated:
52
+ return self._http.send_json("POST", "messages/contact", model=MessageCreated, json=req.model_dump(by_alias=True))
53
+
54
+ def send_location(self, req: MessageSendLocationRequest) -> MessageCreated:
55
+ return self._http.send_json("POST", "messages/location", model=MessageCreated, json=req.model_dump(by_alias=True))
56
+
57
+ def send_link(self, req: MessageSendLinkRequest) -> MessageCreated:
58
+ return self._http.send_json("POST", "messages/link", model=MessageCreated, json=req.model_dump(by_alias=True))
59
+
60
+ def send_reaction(self, message_id: str, req: MessageSendReactionRequest) -> MessageCreated:
61
+ return self._http.send_json("POST", f"messages/{message_id}/reaction", model=MessageCreated, json=req.model_dump(by_alias=True))
62
+
63
+ def edit_text(self, message_id: str, req: MessageSendTextRequest) -> MessageCreated:
64
+ return self._http.send_json("PUT", f"messages/{message_id}/text", model=MessageCreated, json=req.model_dump(by_alias=True))
65
+
66
+ def mark_as_read(self, message_id: str, req: MessageMarkAsReadRequest) -> None:
67
+ self._http.send_json("PUT", f"messages/{message_id}/read", model=None, json=req.model_dump(by_alias=True))
68
+
69
+ def star(self, message_id: str, req: MessageStarRequest) -> None:
70
+ self._http.send_json("PUT", f"messages/{message_id}/star", model=None, json=req.model_dump(by_alias=True))
71
+
72
+ def delete(self, message_id: str, req: MessageDeleteRequest) -> None:
73
+ self._http.send_json("PUT", f"messages/{message_id}/delete", model=None, json=req.model_dump(by_alias=True))
74
+
75
+ def delete_for_me(self, message_id: str, req: MessageDeleteForMeRequest) -> None:
76
+ self._http.send_json("PUT", f"messages/{message_id}/delete/forme", model=None, json=req.model_dump(by_alias=True))
77
+
78
+ # Try variants
79
+ def try_send_text(self, req: MessageSendTextRequest) -> ApiResponse[MessageCreated]:
80
+ return self._http.try_send_json("POST", "messages/text", model=MessageCreated, json=req.model_dump(by_alias=True))
81
+
82
+ def try_send_image(self, req: MessageSendImageRequest) -> ApiResponse[MessageCreated]:
83
+ return self._http.try_send_json("POST", "messages/image", model=MessageCreated, json=req.model_dump(by_alias=True))
84
+
85
+ def try_send_video(self, req: MessageSendVideoRequest) -> ApiResponse[MessageCreated]:
86
+ return self._http.try_send_json("POST", "messages/video", model=MessageCreated, json=req.model_dump(by_alias=True))
87
+
88
+ def try_send_audio(self, req: MessageSendAudioRequest) -> ApiResponse[MessageCreated]:
89
+ return self._http.try_send_json("POST", "messages/audio", model=MessageCreated, json=req.model_dump(by_alias=True))
90
+
91
+ def try_send_voice(self, req: MessageSendVoiceRequest) -> ApiResponse[MessageCreated]:
92
+ return self._http.try_send_json("POST", "messages/voice", model=MessageCreated, json=req.model_dump(by_alias=True))
93
+
94
+ def try_send_sticker(self, req: MessageSendStickerRequest) -> ApiResponse[MessageCreated]:
95
+ return self._http.try_send_json("POST", "messages/sticker", model=MessageCreated, json=req.model_dump(by_alias=True))
96
+
97
+ def try_send_document(self, req: MessageSendDocumentRequest) -> ApiResponse[MessageCreated]:
98
+ return self._http.try_send_json("POST", "messages/document", model=MessageCreated, json=req.model_dump(by_alias=True))
99
+
100
+ def try_send_contact(self, req: MessageSendContactRequest) -> ApiResponse[MessageCreated]:
101
+ return self._http.try_send_json("POST", "messages/contact", model=MessageCreated, json=req.model_dump(by_alias=True))
102
+
103
+ def try_send_location(self, req: MessageSendLocationRequest) -> ApiResponse[MessageCreated]:
104
+ return self._http.try_send_json("POST", "messages/location", model=MessageCreated, json=req.model_dump(by_alias=True))
105
+
106
+ def try_send_link(self, req: MessageSendLinkRequest) -> ApiResponse[MessageCreated]:
107
+ return self._http.try_send_json("POST", "messages/link", model=MessageCreated, json=req.model_dump(by_alias=True))
108
+
109
+ def try_send_reaction(self, message_id: str, req: MessageSendReactionRequest) -> ApiResponse[MessageCreated]:
110
+ return self._http.try_send_json("POST", f"messages/{message_id}/reaction", model=MessageCreated, json=req.model_dump(by_alias=True))
111
+
112
+ def try_edit_text(self, message_id: str, req: MessageSendTextRequest) -> ApiResponse[MessageCreated]:
113
+ return self._http.try_send_json("PUT", f"messages/{message_id}/text", model=MessageCreated, json=req.model_dump(by_alias=True))
114
+
115
+ def try_mark_as_read(self, message_id: str, req: MessageMarkAsReadRequest) -> ApiResponse[object]:
116
+ return self._http.try_send_json("PUT", f"messages/{message_id}/read", model=None, json=req.model_dump(by_alias=True))
117
+
118
+ def try_star(self, message_id: str, req: MessageStarRequest) -> ApiResponse[object]:
119
+ return self._http.try_send_json("PUT", f"messages/{message_id}/star", model=None, json=req.model_dump(by_alias=True))
120
+
121
+ def try_delete(self, message_id: str, req: MessageDeleteRequest) -> ApiResponse[object]:
122
+ return self._http.try_send_json("PUT", f"messages/{message_id}/delete", model=None, json=req.model_dump(by_alias=True))
123
+
124
+ def try_delete_for_me(self, message_id: str, req: MessageDeleteForMeRequest) -> ApiResponse[object]:
125
+ return self._http.try_send_json("PUT", f"messages/{message_id}/delete/forme", model=None, json=req.model_dump(by_alias=True))
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+ from ..http import WSApiHttp, ApiResponse
3
+ from ..models.entities.session.session_status import SessionStatus
4
+ from ..models.entities.session.session_pair_code import SessionPairCode
5
+
6
+
7
+ class SessionClient:
8
+ def __init__(self, http: WSApiHttp) -> None:
9
+ self._http = http
10
+
11
+ # Standard methods
12
+ def get_login_qr_image(self) -> bytes:
13
+ # C# returned byte[] via JSON helper, but Python will treat it as bytes if endpoint returns bytes.
14
+ # If API actually returns base64 JSON, we can add decoding later.
15
+ return self._http.send_bytes("GET", "/session/login/qr/image")
16
+
17
+ def get_login_qr_code(self) -> str:
18
+ return self._http.send_json("GET", "/session/login/qr/code", model=str)
19
+
20
+ def get_login_pair_code(self, phone_number: str) -> SessionPairCode:
21
+ return self._http.send_json("GET", f"/session/login/code/{phone_number}", model=SessionPairCode)
22
+
23
+ def logout(self) -> None:
24
+ self._http.send_json("POST", "/session/logout", model=dict)
25
+
26
+ def get_session_status(self) -> SessionStatus:
27
+ return self._http.send_json("GET", "/session/status", model=SessionStatus)
28
+
29
+ # Try methods
30
+ def try_get_login_qr_image(self) -> ApiResponse[bytes]:
31
+ return self._http.try_send_bytes("GET", "/session/login/qr/image")
32
+
33
+ def try_get_login_qr_code(self) -> ApiResponse[str]:
34
+ return self._http.try_send_json("GET", "/session/login/qr/code", model=str)
35
+
36
+ def try_get_login_pair_code(self, phone_number: str) -> ApiResponse[SessionPairCode]:
37
+ return self._http.try_send_json("GET", f"/session/login/code/{phone_number}", model=SessionPairCode)
38
+
39
+ def try_logout(self) -> ApiResponse[object]:
40
+ return self._http.try_send_json("POST", "/session/logout", model=dict)
41
+
42
+ def try_get_session_status(self) -> ApiResponse[SessionStatus]:
43
+ return self._http.try_send_json("GET", "/session/status", model=SessionStatus)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from ..http import WSApiHttp
5
+ from ..models.entities.users.user_info import UserInfo
6
+
7
+
8
+ class UsersClient:
9
+ def __init__(self, http: WSApiHttp) -> None:
10
+ self._http = http
11
+
12
+ def get_by_id(self, user_id: str) -> Optional[UserInfo]:
13
+ return self._http.send_json("GET", f"/users/{user_id}", model=UserInfo)
14
+
15
+ def try_get_by_id(self, user_id: str):
16
+ return self._http.try_send_json("GET", f"/users/{user_id}", model=UserInfo)
wsapi_client/client.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from .http import WSApiHttp
5
+ from .api.instance import InstanceClient
6
+ from .api.messages import MessagesClient
7
+ from .api.media import MediaClient
8
+ from .api.session import SessionClient
9
+ from .api.contacts import ContactsClient
10
+ from .api.groups import GroupsClient
11
+ from .api.chats import ChatsClient
12
+ from .api.users import UsersClient
13
+ from .api.calls import CallsClient
14
+
15
+
16
+ class WSApiClient:
17
+ def __init__(
18
+ self,
19
+ api_key: str,
20
+ instance_id: str,
21
+ *,
22
+ base_url: str = "https://api.wsapi.chat",
23
+ timeout: float = 30.0,
24
+ ) -> None:
25
+ self._http = WSApiHttp(api_key=api_key, instance_id=instance_id, base_url=base_url, timeout=timeout)
26
+
27
+ # Surface parity with .NET: expose sub-clients
28
+ self.instance = InstanceClient(self._http)
29
+ self.messages = MessagesClient(self._http)
30
+ self.media = MediaClient(self._http)
31
+ self.session = SessionClient(self._http)
32
+ self.contacts = ContactsClient(self._http)
33
+ self.groups = GroupsClient(self._http)
34
+ self.chats = ChatsClient(self._http)
35
+ self.users = UsersClient(self._http)
36
+ self.calls = CallsClient(self._http)
37
+
38
+ def close(self) -> None:
39
+ self._http.close()
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Type
4
+
5
+ from pydantic import TypeAdapter
6
+
7
+ from ..models.events.base_event import BaseEvent
8
+ from ..models.events.messages.message_star_event import MessageStarEvent
9
+ from ..models.events.messages.message_read_event import MessageReadEvent
10
+ from ..models.events.messages.message_event import MessageEvent
11
+ from ..models.events.messages.message_delete_event import MessageDeleteEvent
12
+ from ..models.events.messages.message_history_sync_event import MessageHistorySyncEvent
13
+ from ..models.events.contacts.contact_event import ContactEvent
14
+ from ..models.events.users.user_push_name_event import UserPushNameEvent
15
+ from ..models.events.users.user_picture_event import UserPictureEvent
16
+ from ..models.events.users.user_presence_event import UserPresenceEvent
17
+ from ..models.events.users.user_status_event import UserStatusEvent
18
+ from ..models.events.calls.call_offer_event import CallOfferEvent
19
+ from ..models.events.calls.call_accept_event import CallAcceptEvent
20
+ from ..models.events.calls.call_terminate_event import CallTerminateEvent
21
+ from ..models.events.chats.chat_presence_event import ChatPresenceEvent
22
+ from ..models.events.chats.chat_setting_event import ChatSettingEvent
23
+ from ..models.events.session.session_logged_in_event import SessionLoggedInEvent
24
+ from ..models.events.session.session_logged_out_event import SessionLoggedOutEvent
25
+ from ..models.events.session.session_logged_error_event import SessionLoggedErrorEvent
26
+
27
+
28
+ _EVENT_TYPES: Dict[str, Type[Any]] = {
29
+ "logged_in": SessionLoggedInEvent,
30
+ "logged_out": SessionLoggedOutEvent,
31
+ "logged_error": SessionLoggedErrorEvent,
32
+ "chat_presence": ChatPresenceEvent,
33
+ "chat_setting": ChatSettingEvent,
34
+ "message": MessageEvent,
35
+ "message_delete": MessageDeleteEvent,
36
+ "message_history_sync": MessageHistorySyncEvent,
37
+ "message_read": MessageReadEvent,
38
+ "message_star": MessageStarEvent,
39
+ "contact": ContactEvent,
40
+ "user_push_name": UserPushNameEvent,
41
+ "user_picture": UserPictureEvent,
42
+ "user_presence": UserPresenceEvent,
43
+ "user_status": UserStatusEvent,
44
+ "call_offer": CallOfferEvent,
45
+ "call_accept": CallAcceptEvent,
46
+ "call_terminate": CallTerminateEvent,
47
+ }
48
+
49
+
50
+ def parse_event(raw_json: str) -> BaseEvent:
51
+ import json
52
+
53
+ if not raw_json:
54
+ raise ValueError("JSON cannot be empty")
55
+ obj = json.loads(raw_json)
56
+
57
+ # Validate base props
58
+ if "receivedAt" not in obj or "instanceId" not in obj or "eventType" not in obj or "eventData" not in obj:
59
+ raise ValueError("Missing required event fields")
60
+
61
+ event_type = obj["eventType"]
62
+ if event_type not in _EVENT_TYPES:
63
+ raise ValueError(f"Unknown event type: {event_type}")
64
+
65
+ model = _EVENT_TYPES[event_type]
66
+ data = obj["eventData"]
67
+
68
+ # Deserialize payload
69
+ payload = TypeAdapter(model).validate_python(data)
70
+
71
+ # Inject base props for convenience if payload doesn’t derive BaseEvent
72
+ base = BaseEvent.model_validate({
73
+ "instanceId": obj["instanceId"],
74
+ "receivedAt": obj["receivedAt"],
75
+ "eventType": event_type,
76
+ })
77
+
78
+ # Attach base info dynamically
79
+ # For now, return the payload; callers can use base separately if needed.
80
+ # Future: create a wrapper type with .base and .data
81
+ payload._base = base # type: ignore[attr-defined]
82
+ return payload # type: ignore[return-value]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from .models.problem_details import ProblemDetails
5
+
6
+
7
+ class ApiException(Exception):
8
+ def __init__(self, problem: ProblemDetails):
9
+ super().__init__(problem.detail or problem.title or "API error")
10
+ self.problem: ProblemDetails = problem
11
+
12
+ def __repr__(self) -> str:
13
+ return f"ApiException(status={self.problem.status}, title={self.problem.title!r})"
wsapi_client/http.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any, Generic, Optional, Type, TypeVar, cast
4
+
5
+ import httpx
6
+ from pydantic import BaseModel, ValidationError, TypeAdapter
7
+
8
+ from .exceptions import ApiException
9
+ from .models.problem_details import ProblemDetails
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ @dataclass
15
+ class ApiResponse(Generic[T]):
16
+ result: Optional[T] = None
17
+ error: Optional[ProblemDetails] = None
18
+
19
+ @property
20
+ def is_success(self) -> bool:
21
+ return self.error is None
22
+
23
+
24
+ class WSApiHttp:
25
+ def __init__(
26
+ self,
27
+ api_key: str,
28
+ instance_id: str,
29
+ base_url: str = "https://api.wsapi.chat",
30
+ timeout: float = 30.0,
31
+ client: Optional[httpx.Client] = None,
32
+ ) -> None:
33
+ headers = {
34
+ "X-Api-Key": api_key,
35
+ "X-Instance-Id": instance_id,
36
+ }
37
+ self._client = client or httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
38
+
39
+ def close(self) -> None:
40
+ self._client.close()
41
+
42
+ # --- core request helpers ---
43
+ def _handle_error(self, resp: httpx.Response) -> None:
44
+ # Try to parse ProblemDetails
45
+ problem: Optional[ProblemDetails] = None
46
+ try:
47
+ data = resp.json()
48
+ problem = ProblemDetails.model_validate(data)
49
+ except Exception:
50
+ # non-JSON body
51
+ problem = ProblemDetails(status=resp.status_code, detail=resp.text)
52
+ raise ApiException(problem)
53
+
54
+ def _handle_exception_to_api_response(self, exc: Exception) -> ProblemDetails:
55
+ # Map timeouts to 408, others to 500-like behavior
56
+ if isinstance(exc, httpx.TimeoutException):
57
+ return ProblemDetails(status=408, title="Request Timeout", detail="The request timed out")
58
+ if isinstance(exc, httpx.TransportError):
59
+ return ProblemDetails(status=500, title="Request Failed", detail=str(exc))
60
+ return ProblemDetails(status=500, title="Request Failed", detail=str(exc))
61
+
62
+ def _parse_json(self, data: Any, model: Type[T] | Any) -> T:
63
+ if isinstance(model, type) and issubclass(model, BaseModel):
64
+ return cast(T, model.model_validate(data))
65
+ adapter = TypeAdapter(model) # type: ignore[arg-type]
66
+ return adapter.validate_python(data) # type: ignore[return-value]
67
+
68
+ def _has_body(self, resp: httpx.Response) -> bool:
69
+ if resp.status_code == 204:
70
+ return False
71
+ # Some servers return 200 with empty body; if content attr is missing (mock), assume body exists
72
+ if hasattr(resp, "content"):
73
+ return resp.content is not None and len(resp.content) > 0
74
+ return True
75
+
76
+ # JSON response
77
+ def send_json(self, method: str, url: str, *, model: Type[T] | Any | None, json: Any | None = None) -> Optional[T]:
78
+ resp = self._client.request(method, url, json=json)
79
+ if 200 <= resp.status_code < 300:
80
+ if model is None or not self._has_body(resp):
81
+ return None
82
+ data = resp.json()
83
+ return self._parse_json(data, model)
84
+ self._handle_error(resp)
85
+ raise AssertionError("Unreachable")
86
+
87
+ def try_send_json(self, method: str, url: str, *, model: Type[T] | Any | None, json: Any | None = None) -> ApiResponse[Optional[T]]:
88
+ try:
89
+ resp = self._client.request(method, url, json=json)
90
+ if 200 <= resp.status_code < 300:
91
+ if model is None or not self._has_body(resp):
92
+ return ApiResponse(result=None)
93
+ data = resp.json()
94
+ return ApiResponse(result=self._parse_json(data, model))
95
+ # !2xx -> map to ProblemDetails
96
+ try:
97
+ problem = ProblemDetails.model_validate(resp.json())
98
+ except Exception:
99
+ problem = ProblemDetails(status=resp.status_code, detail=resp.text)
100
+ return ApiResponse(error=problem)
101
+ except Exception as exc:
102
+ return ApiResponse(error=self._handle_exception_to_api_response(exc))
103
+
104
+ # Bytes response
105
+ def send_bytes(self, method: str, url: str, *, json: Any | None = None) -> bytes:
106
+ resp = self._client.request(method, url, json=json)
107
+ if 200 <= resp.status_code < 300:
108
+ return resp.content
109
+ self._handle_error(resp)
110
+ raise AssertionError("Unreachable")
111
+
112
+ def try_send_bytes(self, method: str, url: str, *, json: Any | None = None) -> ApiResponse[bytes]:
113
+ try:
114
+ resp = self._client.request(method, url, json=json)
115
+ if 200 <= resp.status_code < 300:
116
+ return ApiResponse(result=resp.content)
117
+ try:
118
+ problem = ProblemDetails.model_validate(resp.json())
119
+ except Exception:
120
+ problem = ProblemDetails(status=resp.status_code, detail=resp.text)
121
+ return ApiResponse(error=problem)
122
+ except Exception as exc:
123
+ return ApiResponse(error=self._handle_exception_to_api_response(exc))
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, Field, ConfigDict
3
+
4
+
5
+ class ChatArchive(BaseModel):
6
+ model_config = ConfigDict(populate_by_name=True)
7
+
8
+ is_archived: bool = Field(alias="isArchived")