telefeeds-sdk 0.2.4__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.
telefeeds/__init__.py ADDED
@@ -0,0 +1,77 @@
1
+ from ._core import (
2
+ AuthorizationAttemptExpiredError,
3
+ AuthorizationAttemptForbiddenError,
4
+ AuthorizationAttemptNotFoundError,
5
+ AuthorizationChallenge,
6
+ AuthorizationCodeProvider,
7
+ AuthorizationCodeProviderKind,
8
+ AuthorizationCredentialsRequiredError,
9
+ AuthorizationError,
10
+ AuthorizationErrorCode,
11
+ AuthorizationKind,
12
+ AuthorizationPasswordNotExpectedError,
13
+ AuthorizationPermissionDeniedError,
14
+ AuthorizationProviderUnavailableError,
15
+ AuthorizationRateLimitedError,
16
+ AuthorizationResult,
17
+ AuthorizationSignUpRequiredError,
18
+ AuthorizationState,
19
+ BannedAuthorizationPhoneError,
20
+ ExpiredAuthorizationCodeError,
21
+ GatewayError,
22
+ GatewayErrorCode,
23
+ GatewayInvokeError,
24
+ InvalidAuthorizationAttemptError,
25
+ InvalidAuthorizationCodeError,
26
+ InvalidAuthorizationCredentialsError,
27
+ InvalidAuthorizationPasswordError,
28
+ InvalidAuthorizationPhoneError,
29
+ SessionRegistration,
30
+ SessionSnapshot,
31
+ SubscriptionReplacedError,
32
+ TelefeedsClient,
33
+ TelefeedsError,
34
+ TelegramRPCError,
35
+ UpdateEnvelope,
36
+ UserSessionAccessDisabledError,
37
+ )
38
+
39
+ __all__ = [
40
+ "AuthorizationAttemptExpiredError",
41
+ "AuthorizationAttemptForbiddenError",
42
+ "AuthorizationAttemptNotFoundError",
43
+ "AuthorizationChallenge",
44
+ "AuthorizationCodeProvider",
45
+ "AuthorizationCodeProviderKind",
46
+ "AuthorizationCredentialsRequiredError",
47
+ "AuthorizationError",
48
+ "AuthorizationErrorCode",
49
+ "AuthorizationKind",
50
+ "AuthorizationPasswordNotExpectedError",
51
+ "AuthorizationPermissionDeniedError",
52
+ "AuthorizationProviderUnavailableError",
53
+ "AuthorizationRateLimitedError",
54
+ "AuthorizationResult",
55
+ "AuthorizationSignUpRequiredError",
56
+ "AuthorizationState",
57
+ "BannedAuthorizationPhoneError",
58
+ "ExpiredAuthorizationCodeError",
59
+ "GatewayError",
60
+ "GatewayErrorCode",
61
+ "GatewayInvokeError",
62
+ "InvalidAuthorizationAttemptError",
63
+ "InvalidAuthorizationCodeError",
64
+ "InvalidAuthorizationCredentialsError",
65
+ "InvalidAuthorizationPasswordError",
66
+ "InvalidAuthorizationPhoneError",
67
+ "SessionRegistration",
68
+ "SessionSnapshot",
69
+ "SubscriptionReplacedError",
70
+ "TelefeedsClient",
71
+ "TelefeedsError",
72
+ "TelegramRPCError",
73
+ "UpdateEnvelope",
74
+ "UserSessionAccessDisabledError",
75
+ ]
76
+
77
+ __version__ = "0.2.4"
@@ -0,0 +1,77 @@
1
+ from .client import TelefeedsClient
2
+ from .errors import (
3
+ AuthorizationAttemptExpiredError,
4
+ AuthorizationAttemptForbiddenError,
5
+ AuthorizationAttemptNotFoundError,
6
+ AuthorizationCredentialsRequiredError,
7
+ AuthorizationError,
8
+ AuthorizationErrorCode,
9
+ AuthorizationPasswordNotExpectedError,
10
+ AuthorizationPermissionDeniedError,
11
+ AuthorizationProviderUnavailableError,
12
+ AuthorizationRateLimitedError,
13
+ AuthorizationSignUpRequiredError,
14
+ BannedAuthorizationPhoneError,
15
+ ExpiredAuthorizationCodeError,
16
+ GatewayError,
17
+ GatewayErrorCode,
18
+ GatewayInvokeError,
19
+ InvalidAuthorizationAttemptError,
20
+ InvalidAuthorizationCodeError,
21
+ InvalidAuthorizationCredentialsError,
22
+ InvalidAuthorizationPasswordError,
23
+ InvalidAuthorizationPhoneError,
24
+ SubscriptionReplacedError,
25
+ TelefeedsError,
26
+ TelegramRPCError,
27
+ UserSessionAccessDisabledError,
28
+ )
29
+ from .models import (
30
+ AuthorizationChallenge,
31
+ AuthorizationCodeProvider,
32
+ AuthorizationCodeProviderKind,
33
+ AuthorizationKind,
34
+ AuthorizationResult,
35
+ AuthorizationState,
36
+ SessionRegistration,
37
+ SessionSnapshot,
38
+ UpdateEnvelope,
39
+ )
40
+
41
+ __all__ = [
42
+ "AuthorizationAttemptExpiredError",
43
+ "AuthorizationAttemptForbiddenError",
44
+ "AuthorizationAttemptNotFoundError",
45
+ "AuthorizationChallenge",
46
+ "AuthorizationCodeProvider",
47
+ "AuthorizationCodeProviderKind",
48
+ "AuthorizationCredentialsRequiredError",
49
+ "AuthorizationError",
50
+ "AuthorizationErrorCode",
51
+ "AuthorizationKind",
52
+ "AuthorizationPasswordNotExpectedError",
53
+ "AuthorizationPermissionDeniedError",
54
+ "AuthorizationProviderUnavailableError",
55
+ "AuthorizationRateLimitedError",
56
+ "AuthorizationResult",
57
+ "AuthorizationSignUpRequiredError",
58
+ "AuthorizationState",
59
+ "BannedAuthorizationPhoneError",
60
+ "ExpiredAuthorizationCodeError",
61
+ "GatewayError",
62
+ "GatewayErrorCode",
63
+ "GatewayInvokeError",
64
+ "InvalidAuthorizationAttemptError",
65
+ "InvalidAuthorizationCodeError",
66
+ "InvalidAuthorizationCredentialsError",
67
+ "InvalidAuthorizationPasswordError",
68
+ "InvalidAuthorizationPhoneError",
69
+ "SessionRegistration",
70
+ "SessionSnapshot",
71
+ "SubscriptionReplacedError",
72
+ "TelefeedsClient",
73
+ "TelefeedsError",
74
+ "TelegramRPCError",
75
+ "UpdateEnvelope",
76
+ "UserSessionAccessDisabledError",
77
+ ]
@@ -0,0 +1,407 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncIterator, Awaitable, Sequence
5
+ from datetime import timezone
6
+ from typing import TypeVar
7
+
8
+ import grpc
9
+ from typing_extensions import Self
10
+
11
+ from telefeeds._generated import telefeeds_gateway_v1_pb2 as gateway
12
+ from telefeeds._generated import telefeeds_gateway_v1_pb2_grpc as gateway_grpc
13
+
14
+ from .errors import (
15
+ AUTHORIZATION_ERROR_TYPES,
16
+ GatewayError,
17
+ GatewayErrorCode,
18
+ GatewayInvokeError,
19
+ TelegramRPCError,
20
+ )
21
+ from .models import (
22
+ AuthorizationChallenge,
23
+ AuthorizationCodeProvider,
24
+ AuthorizationCodeProviderKind,
25
+ AuthorizationKind,
26
+ AuthorizationResult,
27
+ AuthorizationState,
28
+ SessionRegistration,
29
+ SessionSnapshot,
30
+ UpdateEnvelope,
31
+ )
32
+
33
+ ResponseType = TypeVar("ResponseType")
34
+
35
+
36
+ def gateway_error(error: grpc.aio.AioRpcError) -> GatewayError:
37
+ error_type: type[GatewayError] = GatewayError
38
+ trailing_metadata = error.trailing_metadata()
39
+ if trailing_metadata is not None:
40
+ for key, value in trailing_metadata:
41
+ if key == "telefeeds-error-code":
42
+ error_type = AUTHORIZATION_ERROR_TYPES.get(str(value), GatewayError)
43
+ break
44
+ return error_type(
45
+ code=GatewayErrorCode(error.code().name),
46
+ details=error.details() or "",
47
+ )
48
+
49
+
50
+ async def rpc_result(call: Awaitable[ResponseType]) -> ResponseType:
51
+ try:
52
+ return await call
53
+ except grpc.aio.AioRpcError as error:
54
+ raise gateway_error(error) from error
55
+
56
+
57
+ class TelefeedsClient:
58
+ """Low-level asynchronous client for the versioned Telefeeds gRPC API."""
59
+
60
+ def __init__(
61
+ self,
62
+ token: str,
63
+ endpoint: str = "telegram.telefeeds.ru:443",
64
+ *,
65
+ secure: bool = True,
66
+ root_certificates: bytes | None = None,
67
+ default_timeout: float = 30.0,
68
+ max_message_bytes: int = 16 * 1024 * 1024,
69
+ ) -> None:
70
+ if not token:
71
+ raise ValueError("token must not be empty")
72
+ self.token = token
73
+ self.endpoint = (
74
+ endpoint.removeprefix("https://").removeprefix("http://").rstrip("/")
75
+ )
76
+ self.secure = secure
77
+ self.root_certificates = root_certificates
78
+ self.default_timeout = default_timeout
79
+ self.max_message_bytes = max_message_bytes
80
+ self.channel: grpc.aio.Channel | None = None
81
+ self.stub: gateway_grpc.TelegramGatewayStub | None = None
82
+
83
+ @property
84
+ def metadata(self) -> tuple[tuple[str, str], ...]:
85
+ return (("authorization", f"Bearer {self.token}"),)
86
+
87
+ async def open(self, *, timeout: float | None = 15.0) -> Self:
88
+ if self.channel is not None:
89
+ return self
90
+ options = (
91
+ ("grpc.max_send_message_length", self.max_message_bytes),
92
+ ("grpc.max_receive_message_length", self.max_message_bytes),
93
+ ("grpc.keepalive_time_ms", 30_000),
94
+ ("grpc.keepalive_timeout_ms", 10_000),
95
+ ("grpc.keepalive_permit_without_calls", 1),
96
+ )
97
+ if self.secure:
98
+ credentials = grpc.ssl_channel_credentials(
99
+ root_certificates=self.root_certificates
100
+ )
101
+ channel = grpc.aio.secure_channel(
102
+ self.endpoint, credentials, options=options
103
+ )
104
+ else:
105
+ channel = grpc.aio.insecure_channel(self.endpoint, options=options)
106
+ try:
107
+ ready = channel.channel_ready()
108
+ if timeout is None:
109
+ await ready
110
+ else:
111
+ await asyncio.wait_for(ready, timeout)
112
+ except grpc.aio.AioRpcError as error:
113
+ await channel.close()
114
+ raise gateway_error(error) from error
115
+ except BaseException:
116
+ await channel.close()
117
+ raise
118
+ self.channel = channel
119
+ self.stub = gateway_grpc.TelegramGatewayStub(channel)
120
+ return self
121
+
122
+ async def close(self) -> None:
123
+ if self.channel is None:
124
+ return
125
+ await self.channel.close()
126
+ self.channel = None
127
+ self.stub = None
128
+
129
+ async def __aenter__(self) -> Self:
130
+ return await self.open()
131
+
132
+ async def __aexit__(self, *exc_info: object) -> None:
133
+ await self.close()
134
+
135
+ def require_stub(self) -> gateway_grpc.TelegramGatewayStub:
136
+ if self.stub is None:
137
+ raise RuntimeError("TelefeedsClient is not open")
138
+ return self.stub
139
+
140
+ async def subscribe(
141
+ self,
142
+ *,
143
+ interface: int | None = None,
144
+ close_other: bool | None = None,
145
+ tl_layer: int,
146
+ ) -> AsyncIterator[UpdateEnvelope]:
147
+ request = gateway.SubscribeRequest(tl_layer=tl_layer)
148
+ if interface is not None:
149
+ request.interface = interface
150
+ if close_other is not None:
151
+ request.close_other = close_other
152
+ try:
153
+ call = self.require_stub().Subscribe(request, metadata=self.metadata)
154
+ await call.initial_metadata()
155
+ async for event in call:
156
+ received_at = (
157
+ event.received_at.ToDatetime(tzinfo=timezone.utc)
158
+ if event.HasField("received_at")
159
+ else None
160
+ )
161
+ yield UpdateEnvelope(
162
+ session_peer_id=event.session_peer_id,
163
+ session_kind=event.session_kind,
164
+ body=event.body,
165
+ received_at=received_at,
166
+ tl_layer=event.tl_layer,
167
+ )
168
+ except grpc.aio.AioRpcError as error:
169
+ raise gateway_error(error) from error
170
+
171
+ async def invoke_raw(
172
+ self,
173
+ session_peer_id: int,
174
+ body: bytes,
175
+ *,
176
+ dc_id: int | None = None,
177
+ tl_layer: int,
178
+ timeout: float | None = None,
179
+ ) -> bytes:
180
+ request = gateway.InvokeRequest(
181
+ session_peer_id=session_peer_id,
182
+ body=body,
183
+ tl_layer=tl_layer,
184
+ )
185
+ if dc_id is not None:
186
+ request.dc_id = dc_id
187
+ response = await rpc_result(
188
+ self.require_stub().Invoke(
189
+ request,
190
+ metadata=self.metadata,
191
+ timeout=self.default_timeout if timeout is None else timeout,
192
+ )
193
+ )
194
+ if response.HasField("rpc_error"):
195
+ error = response.rpc_error
196
+ raise TelegramRPCError(
197
+ code=error.code,
198
+ name=error.name,
199
+ value=error.value if error.HasField("value") else None,
200
+ caused_by=error.caused_by if error.HasField("caused_by") else None,
201
+ )
202
+ if response.HasField("error"):
203
+ raise GatewayInvokeError(response.error)
204
+ return response.body
205
+
206
+ async def get_session_snapshots(
207
+ self,
208
+ session_peer_ids: Sequence[int] = (),
209
+ *,
210
+ timeout: float | None = None,
211
+ ) -> list[SessionSnapshot]:
212
+ response = await rpc_result(
213
+ self.require_stub().GetSessionSnapshots(
214
+ gateway.GetSessionSnapshotsRequest(session_peer_ids=session_peer_ids),
215
+ metadata=self.metadata,
216
+ timeout=self.default_timeout if timeout is None else timeout,
217
+ )
218
+ )
219
+ return [
220
+ SessionSnapshot(
221
+ session_peer_id=session.session_peer_id,
222
+ state=session.state,
223
+ alive=session.alive,
224
+ fatal=session.fatal,
225
+ proxy_broken=session.proxy_broken,
226
+ uptime_ms=session.uptime_ms,
227
+ updates_per_second=session.updates_per_second,
228
+ invokes_per_second=session.invokes_per_second,
229
+ invokes_in_flight=session.invokes_in_flight,
230
+ memory_bytes=session.memory_bytes,
231
+ updates_total=session.updates_total,
232
+ invokes_total=session.invokes_total,
233
+ last_error=session.last_error
234
+ if session.HasField("last_error")
235
+ else None,
236
+ updated_at=(
237
+ session.updated_at.ToDatetime(tzinfo=timezone.utc)
238
+ if session.HasField("updated_at")
239
+ else None
240
+ ),
241
+ media_upload_bytes=session.media_upload_bytes,
242
+ media_download_bytes=session.media_download_bytes,
243
+ media_requests_total=session.media_requests_total,
244
+ media_requests_in_flight=session.media_requests_in_flight,
245
+ tl_layer=session.tl_layer if session.HasField("tl_layer") else None,
246
+ usage_days=session.usage_days,
247
+ last_usage_at=(
248
+ session.last_usage_at.ToDatetime(tzinfo=timezone.utc)
249
+ if session.HasField("last_usage_at")
250
+ else None
251
+ ),
252
+ )
253
+ for session in response.sessions
254
+ ]
255
+
256
+ async def begin_phone_authorization(
257
+ self,
258
+ phone_number: str,
259
+ *,
260
+ timeout: float | None = None,
261
+ ) -> AuthorizationChallenge:
262
+ response = await rpc_result(
263
+ self.require_stub().BeginPhoneAuthorization(
264
+ gateway.BeginPhoneAuthorizationRequest(phone_number=phone_number),
265
+ metadata=self.metadata,
266
+ timeout=self.default_timeout if timeout is None else timeout,
267
+ )
268
+ )
269
+ return AuthorizationChallenge(
270
+ authorization_id=response.authorization_id,
271
+ expires_at=(
272
+ response.expires_at.ToDatetime(tzinfo=timezone.utc)
273
+ if response.HasField("expires_at")
274
+ else None
275
+ ),
276
+ code_length=response.code_length
277
+ if response.HasField("code_length")
278
+ else None,
279
+ authorization_kind=AuthorizationKind(response.authorization_kind),
280
+ code_provider=(
281
+ AuthorizationCodeProvider(
282
+ kind=AuthorizationCodeProviderKind(response.code_provider.kind),
283
+ url=response.code_provider.url
284
+ if response.code_provider.HasField("url")
285
+ else None,
286
+ code_secret=response.code_provider.code_secret
287
+ if response.code_provider.HasField("code_secret")
288
+ else None,
289
+ requires_open_url=response.code_provider.requires_open_url,
290
+ )
291
+ if response.HasField("code_provider")
292
+ else None
293
+ ),
294
+ )
295
+
296
+ async def begin_existing_session_authorization(
297
+ self,
298
+ session_peer_id: int,
299
+ *,
300
+ timeout: float | None = None,
301
+ ) -> AuthorizationChallenge:
302
+ response = await rpc_result(
303
+ self.require_stub().BeginExistingSessionAuthorization(
304
+ gateway.BeginExistingSessionAuthorizationRequest(
305
+ session_peer_id=session_peer_id
306
+ ),
307
+ metadata=self.metadata,
308
+ timeout=self.default_timeout if timeout is None else timeout,
309
+ )
310
+ )
311
+ return AuthorizationChallenge(
312
+ authorization_id=response.authorization_id,
313
+ expires_at=(
314
+ response.expires_at.ToDatetime(tzinfo=timezone.utc)
315
+ if response.HasField("expires_at")
316
+ else None
317
+ ),
318
+ code_length=response.code_length,
319
+ authorization_kind=AuthorizationKind(response.authorization_kind),
320
+ code_provider=AuthorizationCodeProvider(
321
+ kind=AuthorizationCodeProviderKind(response.code_provider.kind),
322
+ url=response.code_provider.url
323
+ if response.code_provider.HasField("url")
324
+ else None,
325
+ code_secret=response.code_provider.code_secret
326
+ if response.code_provider.HasField("code_secret")
327
+ else None,
328
+ requires_open_url=response.code_provider.requires_open_url,
329
+ ),
330
+ )
331
+
332
+ async def complete_existing_session_authorization(
333
+ self,
334
+ authorization_id: str,
335
+ code: str,
336
+ *,
337
+ timeout: float | None = None,
338
+ ) -> SessionRegistration:
339
+ response = await rpc_result(
340
+ self.require_stub().CompleteExistingSessionAuthorization(
341
+ gateway.CompleteExistingSessionAuthorizationRequest(
342
+ authorization_id=authorization_id,
343
+ code=code,
344
+ ),
345
+ metadata=self.metadata,
346
+ timeout=self.default_timeout if timeout is None else timeout,
347
+ )
348
+ )
349
+ return SessionRegistration(
350
+ session_peer_id=response.session.session_peer_id,
351
+ state=response.session.state,
352
+ )
353
+
354
+ async def complete_phone_authorization(
355
+ self,
356
+ authorization_id: str,
357
+ code: str,
358
+ *,
359
+ timeout: float | None = None,
360
+ ) -> AuthorizationResult:
361
+ response = await rpc_result(
362
+ self.require_stub().CompletePhoneAuthorization(
363
+ gateway.CompletePhoneAuthorizationRequest(
364
+ authorization_id=authorization_id,
365
+ code=code,
366
+ ),
367
+ metadata=self.metadata,
368
+ timeout=self.default_timeout if timeout is None else timeout,
369
+ )
370
+ )
371
+ session = (
372
+ SessionRegistration(
373
+ session_peer_id=response.session.session_peer_id,
374
+ state=response.session.state,
375
+ )
376
+ if response.HasField("session")
377
+ else None
378
+ )
379
+ return AuthorizationResult(
380
+ state=AuthorizationState(response.state),
381
+ password_hint=response.password_hint
382
+ if response.HasField("password_hint")
383
+ else None,
384
+ session=session,
385
+ )
386
+
387
+ async def complete_password_authorization(
388
+ self,
389
+ authorization_id: str,
390
+ password: str,
391
+ *,
392
+ timeout: float | None = None,
393
+ ) -> SessionRegistration:
394
+ response = await rpc_result(
395
+ self.require_stub().CompletePasswordAuthorization(
396
+ gateway.CompletePasswordAuthorizationRequest(
397
+ authorization_id=authorization_id,
398
+ password=password,
399
+ ),
400
+ metadata=self.metadata,
401
+ timeout=self.default_timeout if timeout is None else timeout,
402
+ )
403
+ )
404
+ return SessionRegistration(
405
+ session_peer_id=response.session.session_peer_id,
406
+ state=response.session.state,
407
+ )