maxkit 2.13.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.
aiomax/types.py ADDED
@@ -0,0 +1,1512 @@
1
+ import logging
2
+ from typing import Callable, Literal, Optional
3
+
4
+ from . import buttons, exceptions, utils
5
+
6
+ type_logger = logging.getLogger("aiomax.types")
7
+
8
+
9
+ class BotCommand:
10
+ def __init__(self, name: str, description: str, **kwargs):
11
+ self.name = name
12
+ self.description = description
13
+
14
+ def as_dict(self):
15
+ return {"name": self.name, "description": self.description}
16
+
17
+
18
+ class User:
19
+ def __init__(
20
+ self,
21
+ user_id: int,
22
+ first_name: str,
23
+ name: str,
24
+ is_bot: bool,
25
+ last_activity_time: int,
26
+ last_name: "str | None" = None,
27
+ username: "str | None" = None,
28
+ description: "str | None" = None,
29
+ avatar_url: "str | None" = None,
30
+ full_avatar_url: "str | None" = None,
31
+ commands: "list[BotCommand] | None" = None,
32
+ last_access_time: "int | None" = None,
33
+ is_owner: "bool | None" = None,
34
+ is_admin: "bool | None" = None,
35
+ join_time: "int | None" = None,
36
+ permissions: "list[str] | None" = None,
37
+ **kwargs,
38
+ ):
39
+ self.user_id: int = user_id
40
+ self.first_name: str = first_name
41
+ self.last_name: str = last_name
42
+ self.name: str = name
43
+ self.username: "str | None" = username
44
+ self.is_bot: bool = is_bot
45
+ self.last_activity_time: float | None = (
46
+ last_activity_time / 1000 if last_activity_time else None
47
+ )
48
+ self.description: "str | None" = description
49
+ self.avatar_url: "str | None" = avatar_url
50
+ self.full_avatar_url: "str | None" = full_avatar_url
51
+ self.commands: "list[BotCommand] | None" = (
52
+ [BotCommand(**i) for i in commands] if commands else None
53
+ )
54
+ self.last_access_time: "int | None" = (
55
+ last_access_time / 1000 if last_access_time else None
56
+ )
57
+ self.is_owner: "bool | None" = is_owner
58
+ self.is_admin: "bool | None" = is_admin
59
+ self.join_time: "float | None" = (
60
+ join_time / 1000 if join_time else None
61
+ )
62
+ self.permissions: "list[str] | None" = permissions
63
+
64
+ def __repr__(self):
65
+ return (
66
+ f"{type(self).__name__}(user_id={self.user_id!r},"
67
+ f"name={self.name!r})"
68
+ )
69
+
70
+ def __eq__(self, other):
71
+ if isinstance(other, User):
72
+ return self.user_id == other.user_id
73
+ return False
74
+
75
+ @staticmethod
76
+ def from_json(data: dict) -> "User | None":
77
+ if data is None:
78
+ return None
79
+
80
+ return User(**data)
81
+
82
+
83
+ class Attachment:
84
+ def __init__(self, type: str):
85
+ self.type: str = type
86
+
87
+ @staticmethod
88
+ def from_json(data: dict) -> "Attachment | None":
89
+ if data["type"] == "image":
90
+ return PhotoAttachment.from_json(data)
91
+ elif data["type"] == "video":
92
+ return VideoAttachment.from_json(data)
93
+ elif data["type"] == "audio":
94
+ return AudioAttachment.from_json(data)
95
+ elif data["type"] == "file":
96
+ return FileAttachment.from_json(data)
97
+ elif data["type"] == "sticker":
98
+ return StickerAttachment.from_json(data)
99
+ elif data["type"] == "contact":
100
+ return ContactAttachment.from_json(data)
101
+ elif data["type"] == "share":
102
+ return ShareAttachment.from_json(data)
103
+ elif data["type"] == "location":
104
+ return LocationAttachment.from_json(data)
105
+ elif data["type"] == "inline_keyboard":
106
+ return InlineKeyboardAttachment.from_json(data)
107
+ else:
108
+ # Unknown/newly-added attachment type: keep parsing the rest of the
109
+ # update instead of crashing handle_update (and losing the batch).
110
+ type_logger.warning(
111
+ "Unknown attachment type: %s", data.get("type")
112
+ )
113
+ return Attachment(data["type"])
114
+
115
+
116
+ class PhotoAttachment(Attachment):
117
+ def __init__(
118
+ self,
119
+ url: "str | None" = None,
120
+ token: "str | None" = None,
121
+ photo_id: "int | None" = None,
122
+ ):
123
+ """
124
+ A photo attachment. Use either `url` or `token` when uploading.
125
+
126
+ :param url: Image URL
127
+ :param token: Attachment token got while uploading the image
128
+ :param photo_id: Unique photo ID. Not used when sending the attachment
129
+ """
130
+ super().__init__("image")
131
+ self.url: "str | None" = url
132
+ self.token: "str | None" = token
133
+ self.photo_id: "int | None" = photo_id
134
+
135
+ @staticmethod
136
+ def from_json(data: dict) -> "PhotoAttachment | None":
137
+ photo = PhotoAttachment(
138
+ url=data["payload"].get("url"),
139
+ token=data["payload"].get("token"),
140
+ photo_id=data["payload"].get("photo_id"),
141
+ )
142
+ return photo
143
+
144
+ def as_dict(self):
145
+ data = {"type": self.type, "payload": {}}
146
+ if self.token:
147
+ data["payload"]["token"] = self.token
148
+ if self.url:
149
+ data["payload"]["url"] = self.url
150
+ return data
151
+
152
+
153
+ class VideoAttachment(Attachment):
154
+ def __init__(
155
+ self,
156
+ token: "str | None" = None,
157
+ url: "str | None" = None,
158
+ thumbnail: "str | None" = None,
159
+ width: "int | None" = None,
160
+ height: "int | None" = None,
161
+ duration: "int | None" = None,
162
+ ):
163
+ """
164
+ A video attachment. Use `token` when uploading.
165
+
166
+ :param token: Attachment token got while uploading video
167
+ :param url: Video URL that can be used for downloading the video
168
+ :param thumbnail: Video thumbnail URL
169
+ :param width: Video width
170
+ :param height: Video height
171
+ :param duration: Video duration
172
+ """
173
+ super().__init__("video")
174
+ self.token: "str | None" = token
175
+ self.url: "str | None" = url
176
+ self.thumbnail: "str | None" = thumbnail
177
+ self.width: "int | None" = width
178
+ self.height: "int | None" = height
179
+ self.duration: "int | None" = duration
180
+
181
+ @staticmethod
182
+ def from_json(data: dict) -> "VideoAttachment | None":
183
+ return VideoAttachment(
184
+ data["payload"].get("token", None),
185
+ data["payload"].get("url", None),
186
+ data.get("thumbnail", {}).get("url"),
187
+ data.get("width"),
188
+ data.get("height"),
189
+ data.get("duration"),
190
+ )
191
+
192
+ def as_dict(self):
193
+ return {"type": self.type, "payload": {"token": self.token}}
194
+
195
+
196
+ class AudioAttachment(Attachment):
197
+ def __init__(
198
+ self,
199
+ url: "str | None" = None,
200
+ token: "str | None" = None,
201
+ transcription: "str | None" = None,
202
+ ):
203
+ """
204
+ An audio attachment. Use `token` when uploading.
205
+
206
+ :param token: Attachment token got while uploading audio
207
+ :param transcription: Audio transcription
208
+ """
209
+ super().__init__("audio")
210
+ self.url: str = url
211
+ self.token: str = token
212
+ self.transcription: "str | None" = transcription
213
+
214
+ @staticmethod
215
+ def from_json(data: dict) -> "AudioAttachment | None":
216
+ payload = data.get("payload") or {}
217
+ return AudioAttachment(
218
+ payload.get("url"),
219
+ payload.get("token"),
220
+ data.get("transcription"),
221
+ )
222
+
223
+ def as_dict(self):
224
+ return {"type": self.type, "payload": {"token": self.token}}
225
+
226
+
227
+ class FileAttachment(Attachment):
228
+ def __init__(
229
+ self,
230
+ token: str,
231
+ url: "str | None" = None,
232
+ filename: "str | None" = None,
233
+ size: "int | None" = None,
234
+ ):
235
+ """
236
+ A file attachment. Use `token` when uploading.
237
+
238
+ :param token: Attachment token got while uploading the file
239
+ :param url: File URL that can be used for downloading the file
240
+ :param filename: File name
241
+ :param size: File size
242
+ """
243
+ super().__init__("file")
244
+ self.url: "str | None" = url
245
+ self.token: str = token
246
+ self.filename: "str | None" = filename
247
+ self.size: "int | None" = size
248
+
249
+ @staticmethod
250
+ def from_json(data: dict) -> "FileAttachment | None":
251
+ return FileAttachment(
252
+ data["payload"]["token"],
253
+ data["payload"].get("url"),
254
+ data.get("filename"),
255
+ data.get("size"),
256
+ )
257
+
258
+ def as_dict(self):
259
+ return {"type": self.type, "payload": {"token": self.token}}
260
+
261
+
262
+ class StickerAttachment(Attachment):
263
+ def __init__(
264
+ self,
265
+ code: str,
266
+ url: "str | None" = None,
267
+ width: "int | None" = None,
268
+ height: "int | None" = None,
269
+ ):
270
+ """
271
+ A sticker attachment. Use `code` when uploading.
272
+
273
+ :param code: Sticker code
274
+ :param url: Sticker URL that can be used for downloading the sticker
275
+ :param width: Sticker width
276
+ :param height: Sticker height
277
+ """
278
+ super().__init__("sticker")
279
+ self.code: str = code
280
+ self.url: "str | None" = url
281
+ self.width: int = width
282
+ self.height: int = height
283
+
284
+ @staticmethod
285
+ def from_json(data: dict) -> "StickerAttachment | None":
286
+ sticker = StickerAttachment(
287
+ data["payload"]["code"],
288
+ data["payload"].get("url"),
289
+ data.get("width"),
290
+ data.get("height"),
291
+ )
292
+ return sticker
293
+
294
+ def as_dict(self) -> dict:
295
+ return {"type": "sticker", "payload": {"code": self.code}}
296
+
297
+
298
+ class ContactAttachment(Attachment):
299
+ def __init__(
300
+ self,
301
+ name: "str | None" = None,
302
+ contact_id: "int | None" = None,
303
+ vcf_info: "str | None" = None,
304
+ vcf_phone: "str | None" = None,
305
+ max_info: "User | None" = None,
306
+ ):
307
+ """
308
+ A contact attachment.
309
+
310
+ :param name: Contact name. Only used when sending
311
+ :param contact_id: Contact user ID (if sending a Max user).
312
+ Only used when sending contacts
313
+ :param vcf_info: Contact's information in vCard format
314
+ :param vcf_phone: Contact's phone number.
315
+ Only used when sending contacts
316
+ :param max_info: User object if contact is a user.
317
+ Only used when receiving contacts
318
+ """
319
+ super().__init__("contact")
320
+ self.name: "str | None" = name
321
+ self.contact_id: "int | None" = contact_id
322
+ self.vcf_info: "str | None" = vcf_info
323
+ self.vcf_phone: "str | None" = vcf_phone
324
+ self.max_info: "User | None" = max_info
325
+
326
+ @staticmethod
327
+ def from_json(data: dict) -> "ContactAttachment | None":
328
+ if not data:
329
+ return None
330
+
331
+ payload = data.get("payload") or {}
332
+
333
+ return ContactAttachment(
334
+ name=payload.get("name"),
335
+ contact_id=payload.get("contact_id"),
336
+ vcf_info=payload.get("vcf_info"),
337
+ vcf_phone=payload.get("vcf_phone"),
338
+ max_info=User.from_json(payload.get("max_info")),
339
+ )
340
+
341
+ def as_dict(self) -> dict:
342
+ return {
343
+ "type": self.type,
344
+ "payload": {
345
+ "name": self.name,
346
+ "contact_id": self.contact_id,
347
+ "vcf_info": self.vcf_info,
348
+ "vcf_phone": self.vcf_phone,
349
+ },
350
+ }
351
+
352
+
353
+ class ShareAttachment(Attachment):
354
+ def __init__(
355
+ self,
356
+ url: "str | None" = None,
357
+ token: "str | None" = None,
358
+ title: "str | None" = None,
359
+ description: "str | None" = None,
360
+ image_url: "str | None" = None,
361
+ ):
362
+ """
363
+ Link preview. Use `url` and `token` when uploading
364
+
365
+ :param url: Link URL
366
+ :param token: Attachment token
367
+ :param title: Preview title
368
+ :param description: Preview description
369
+ :param image_url: Preview image URL
370
+ """
371
+ super().__init__("share")
372
+ self.url: "str | None" = url
373
+ self.token: "str | None" = token
374
+ self.title: "str | None" = title
375
+ self.description: "str | None" = description
376
+ self.image_url: "str | None" = image_url
377
+
378
+ @staticmethod
379
+ def from_json(data: dict) -> "ShareAttachment | None":
380
+ return ShareAttachment(
381
+ data["payload"].get("url", None),
382
+ data["payload"].get("token", None),
383
+ data.get("title"),
384
+ data.get("description"),
385
+ data.get("image_url"),
386
+ )
387
+
388
+ def as_dict(self) -> dict:
389
+ return {
390
+ "type": self.type,
391
+ "payload": {"url": self.url, "token": self.token},
392
+ }
393
+
394
+
395
+ class LocationAttachment(Attachment):
396
+ def __init__(
397
+ self,
398
+ latitude: float,
399
+ longitude: float,
400
+ ):
401
+ super().__init__("location")
402
+ self.latitude: float = latitude
403
+ self.longitude: float = longitude
404
+
405
+ @staticmethod
406
+ def from_json(data: dict) -> "LocationAttachment | None":
407
+ return LocationAttachment(data["latitude"], data["longitude"])
408
+
409
+ def as_dict(self) -> dict:
410
+ return {
411
+ "type": "location",
412
+ "latitude": self.latitude,
413
+ "longitude": self.longitude,
414
+ }
415
+
416
+
417
+ class InlineKeyboardAttachment(Attachment):
418
+ def __init__(
419
+ self,
420
+ payload: list[list[buttons.Button]],
421
+ ):
422
+ super().__init__("inline_keyboard")
423
+ self.payload: list[list[buttons.Button]] = payload
424
+
425
+ @staticmethod
426
+ def from_json(data: dict) -> "InlineKeyboardAttachment | None":
427
+ return InlineKeyboardAttachment(
428
+ [
429
+ [buttons.Button.from_json(j) for j in i]
430
+ for i in data["payload"]["buttons"]
431
+ ]
432
+ )
433
+
434
+
435
+ class MessageRecipient:
436
+ def __init__(self, chat_id: "int | None", chat_type: str):
437
+ self.chat_id: "int | None" = chat_id
438
+ self.chat_type: str = chat_type
439
+
440
+ def __repr__(self):
441
+ return (
442
+ f"{type(self).__name__}(chat_id={self.chat_id!r},"
443
+ f"chat_type={self.chat_type!r})"
444
+ )
445
+
446
+ def __eq__(self, other):
447
+ if isinstance(other, MessageRecipient):
448
+ return self.chat_id == other.chat_id
449
+ return False
450
+
451
+ @staticmethod
452
+ def from_json(data: dict) -> "MessageRecipient | None":
453
+ if data is None:
454
+ return None
455
+
456
+ return MessageRecipient(
457
+ chat_id=data["chat_id"], chat_type=data["chat_type"]
458
+ )
459
+
460
+
461
+ class Markup:
462
+ def __init__(
463
+ self,
464
+ type: Literal[
465
+ "strong",
466
+ "emphasized",
467
+ "monospaced",
468
+ "link",
469
+ "strikethrough",
470
+ "underline",
471
+ "user_mention",
472
+ "heading",
473
+ "highlighted",
474
+ ],
475
+ start: int,
476
+ length: int,
477
+ user_link: "str | None" = None,
478
+ user_id: "int | None" = None,
479
+ url: "str | None" = None,
480
+ ):
481
+ """
482
+ A markup element
483
+
484
+ :param type: Markup type
485
+ :param start: Start position
486
+ :param length: Length
487
+ :param user_link: Username. `None` if markup type is not `user_link`
488
+ :param user_id: User ID. `None` if markup type is not `user_link`
489
+ :param url: URL. `None` if markup type is not `link`
490
+ """
491
+ self.type: Literal[
492
+ "strong",
493
+ "emphasized",
494
+ "monospaced",
495
+ "link",
496
+ "strikethrough",
497
+ "underline",
498
+ "user_mention",
499
+ "heading",
500
+ "highlighted",
501
+ ] = type
502
+ self.start: int = start
503
+ self.length: int = length
504
+
505
+ self.user_link: "str | None" = user_link
506
+ self.user_id: "int | None" = user_id
507
+ self.url: "str | None" = url
508
+
509
+ @staticmethod
510
+ def from_json(data: dict) -> "Markup | None":
511
+ if data is None:
512
+ return None
513
+
514
+ if data["type"] == "user_mention":
515
+ return Markup(
516
+ data["type"],
517
+ data["from"],
518
+ data["length"],
519
+ user_link=data.get("user_link"),
520
+ user_id=data.get("user_id"),
521
+ )
522
+ elif data["type"] == "link":
523
+ return Markup(
524
+ data["type"], data["from"], data["length"], url=data["url"]
525
+ )
526
+
527
+ return Markup(data["type"], data["from"], data["length"])
528
+
529
+
530
+ class MessageBody:
531
+ def __init__(
532
+ self,
533
+ mid: str,
534
+ seq: int,
535
+ text: "str | None",
536
+ attachments: "list[Attachment] | None",
537
+ markup: "list[Markup] | None" = None,
538
+ ):
539
+ self.message_id: str = mid
540
+ self.seq: int = seq
541
+ self.text: "str | None" = text
542
+ self.attachments: "list[Attachment] | None" = attachments
543
+ self.markup: "list[Markup] | None" = markup
544
+
545
+ @staticmethod
546
+ def from_json(data: dict) -> "MessageBody | None":
547
+ if data is None:
548
+ return None
549
+
550
+ return MessageBody(
551
+ mid=data["mid"],
552
+ seq=data["seq"],
553
+ text=data["text"],
554
+ attachments=[
555
+ Attachment.from_json(x) for x in data.get("attachments", [])
556
+ ],
557
+ markup=[Markup.from_json(x) for x in data.get("markup", [])],
558
+ )
559
+
560
+
561
+ class LinkedMessage:
562
+ def __init__(
563
+ self,
564
+ type: str,
565
+ message: MessageBody,
566
+ sender: User,
567
+ chat_id: "int | None" = None,
568
+ ):
569
+ self.type: str = type
570
+ self.message: "MessageBody | None" = message
571
+ self.sender: User = sender
572
+ self.chat_id: "int | None" = chat_id
573
+
574
+ @staticmethod
575
+ def from_json(data: dict) -> "LinkedMessage | None":
576
+ if data is None:
577
+ return None
578
+
579
+ return LinkedMessage(
580
+ type=data["type"],
581
+ message=MessageBody.from_json(data.get("message")),
582
+ sender=User.from_json(data.get("sender")),
583
+ chat_id=data.get("chat_id"),
584
+ )
585
+
586
+ @property
587
+ def user_id(self):
588
+ return self.sender.user_id
589
+
590
+
591
+ class Message:
592
+ def __init__(
593
+ self,
594
+ recipient: MessageRecipient,
595
+ body: MessageBody,
596
+ timestamp: float,
597
+ sender: User,
598
+ link: "LinkedMessage | None" = None,
599
+ views: "int | None" = None,
600
+ url: "str | None" = None,
601
+ bot=None,
602
+ ):
603
+ self.recipient: MessageRecipient = recipient
604
+ self.body: "MessageBody | None" = body
605
+ self.timestamp: "float | None" = (
606
+ timestamp / 1000 if timestamp else None
607
+ )
608
+ self.sender: "User | None" = sender
609
+ self.link: "LinkedMessage | None" = link
610
+ self.views: "int | None" = views
611
+ self.url: "str | None" = url
612
+ self.user_locale: "str | None" = None
613
+ self.bot = bot
614
+
615
+ def __repr__(self):
616
+ return f"{type(self).__name__}(text={self.body.text!r})"
617
+
618
+ def __str__(self):
619
+ return self.body.text
620
+
621
+ def __eq__(self, other):
622
+ if isinstance(other, Message):
623
+ return self.id == other.id
624
+ return False
625
+
626
+ @property
627
+ def id(self) -> str:
628
+ return self.body.message_id
629
+
630
+ @property
631
+ def content(self) -> str:
632
+ return self.body.text
633
+
634
+ @property
635
+ def user_id(self):
636
+ return self.sender.user_id
637
+
638
+ @staticmethod
639
+ def from_json(data: dict) -> "Message":
640
+ return Message(
641
+ recipient=MessageRecipient.from_json(data.get("recipient")),
642
+ body=MessageBody.from_json(data.get("body")),
643
+ timestamp=data.get("timestamp"),
644
+ sender=User.from_json(data.get("sender")),
645
+ link=LinkedMessage.from_json(data.get("link")),
646
+ views=data.get("stat", {}).get("views", None),
647
+ url=data.get("url"),
648
+ )
649
+
650
+ def resolve_mention(
651
+ self,
652
+ replies: bool = True,
653
+ message_text: bool = True,
654
+ skip_bot: bool = True,
655
+ ) -> "int | None":
656
+ """
657
+ Finds who was mentioned in this message
658
+ and returns the user ID if found.
659
+
660
+ :param replies: Whether to check for this message's link author.
661
+ :param message_text: Whether to check for mentions in message text.
662
+ :param skip_bot: Whether to ignore mentions of the bot.
663
+ """
664
+ if replies and self.link and self.link.type == "reply":
665
+ if (
666
+ skip_bot
667
+ and self.bot
668
+ and self.link.sender.user_id == self.bot.id
669
+ ):
670
+ pass
671
+ else:
672
+ return self.link.sender.user_id
673
+
674
+ if message_text and self.body.markup:
675
+ for i in self.body.markup:
676
+ if i.type != "user_mention":
677
+ continue
678
+ if skip_bot and self.bot and i.user_id == self.bot.id:
679
+ continue
680
+ if skip_bot and self.bot and i.user_link == self.bot.username:
681
+ continue
682
+ return i.user_id
683
+
684
+ async def send(
685
+ self,
686
+ text: "str | None" = None,
687
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
688
+ notify: bool = True,
689
+ disable_link_preview: bool = False,
690
+ keyboard: """list[list[buttons.Button]] \
691
+ | buttons.KeyboardBuilder \
692
+ | None""" = None,
693
+ attachments: "list[Attachment] | Attachment | None" = None,
694
+ ) -> "Message":
695
+ """
696
+ Send a message to the chat that the message is sent.
697
+
698
+ :param text: Message text. Up to 4000 characters
699
+ :param format: Message format. Bot.default_format by default
700
+ :param notify: Whether to notify users about the message.
701
+ True by default.
702
+ :param disable_link_preview: Whether to disable link preview.
703
+ False by default
704
+ :param keyboard: An inline keyboard to attach to the message
705
+ :param attachments: List of attachments
706
+ """
707
+ if self.bot is None:
708
+ return
709
+ return await self.bot.send_message(
710
+ text,
711
+ chat_id=self.recipient.chat_id,
712
+ format=format,
713
+ notify=notify,
714
+ disable_link_preview=disable_link_preview,
715
+ keyboard=keyboard,
716
+ attachments=attachments,
717
+ )
718
+
719
+ async def reply(
720
+ self,
721
+ text: "str | None" = None,
722
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
723
+ notify: bool = True,
724
+ disable_link_preview: bool = False,
725
+ keyboard: """list[list[buttons.Button]] \
726
+ | buttons.KeyboardBuilder \
727
+ | None""" = None,
728
+ attachments: "list[Attachment] | Attachment | None" = None,
729
+ ) -> "Message | None":
730
+ """
731
+ Reply to this message.
732
+
733
+ :param text: Message text. Up to 4000 characters
734
+ :param format: Message format. Bot.default_format by default
735
+ :param notify: Whether to notify users about the message.
736
+ True by default.
737
+ :param disable_link_preview: Whether to disable link preview.
738
+ False by default
739
+ :param keyboard: An inline keyboard to attach to the message
740
+ :param attachments: List of attachments
741
+ """
742
+ if self.bot is None:
743
+ return
744
+ return await self.bot.send_message(
745
+ text,
746
+ chat_id=self.recipient.chat_id,
747
+ format=format,
748
+ notify=notify,
749
+ disable_link_preview=disable_link_preview,
750
+ keyboard=keyboard,
751
+ attachments=attachments,
752
+ reply_to=self.id,
753
+ )
754
+
755
+ async def edit(
756
+ self,
757
+ text: "str | None" = None,
758
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
759
+ reply_to: "int | None" = None,
760
+ notify: bool = True,
761
+ keyboard: """list[list[buttons.Button]] \
762
+ | buttons.KeyboardBuilder \
763
+ | None""" = None,
764
+ attachments: "list[Attachment] | Attachment | None" = None,
765
+ ) -> "Message | None":
766
+ """
767
+ Edit a message
768
+
769
+ :param text: Message text. Up to 4000 characters
770
+ :param format: Message format. Bot.default_format by default
771
+ :param notify: Whether to notify users about the message.
772
+ True by default.
773
+ :param disable_link_preview: Whether to disable link preview.
774
+ False by default
775
+ :param keyboard: An inline keyboard to attach to the message
776
+ :param attachments: List of attachments
777
+ """
778
+ if self.bot is None:
779
+ return
780
+ return await self.bot.edit_message(
781
+ self.id,
782
+ text,
783
+ format=format,
784
+ notify=notify,
785
+ keyboard=keyboard,
786
+ reply_to=reply_to,
787
+ attachments=attachments,
788
+ )
789
+
790
+ async def delete(self):
791
+ if self.bot is None:
792
+ return
793
+ return await self.bot.delete_message(self.id)
794
+
795
+
796
+ class BotStartPayload:
797
+ def __init__(
798
+ self,
799
+ chat_id: int,
800
+ user: User,
801
+ payload: "str | None",
802
+ user_locale: "str | None",
803
+ bot=None,
804
+ ):
805
+ self.chat_id: int = chat_id
806
+ self.user: User = user
807
+ self.payload: "str | None" = payload
808
+ self.user_locale: "str | None" = user_locale
809
+ self.bot = bot
810
+
811
+ @staticmethod
812
+ def from_json(data: dict, bot) -> "BotStartPayload":
813
+ return BotStartPayload(
814
+ chat_id=data["chat_id"],
815
+ user=User.from_json(data["user"]),
816
+ payload=data.get("payload"),
817
+ user_locale=data.get("user_locale"),
818
+ bot=bot,
819
+ )
820
+
821
+ @property
822
+ def user_id(self):
823
+ return self.user.user_id
824
+
825
+ async def send(
826
+ self,
827
+ text: "str | None" = None,
828
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
829
+ notify: bool = True,
830
+ disable_link_preview: bool = False,
831
+ keyboard: """list[list[buttons.Button]] \
832
+ | buttons.KeyboardBuilder \
833
+ | None""" = None,
834
+ attachments: "list[Attachment] | Attachment | None" = None,
835
+ ) -> "Message | None":
836
+ """
837
+ Send a message to the chat where bot was started.
838
+
839
+ :param text: Message text. Up to 4000 characters
840
+ :param format: Message format. Bot.default_format by default
841
+ :param notify: Whether to notify users about the message.
842
+ True by default.
843
+ :param disable_link_preview: Whether to disable link preview.
844
+ False by default
845
+ :param keyboard: An inline keyboard to attach to the message
846
+ :param attachments: List of attachments
847
+ """
848
+ if self.bot is None:
849
+ return
850
+ return await self.bot.send_message(
851
+ text,
852
+ chat_id=self.chat_id,
853
+ format=format,
854
+ notify=notify,
855
+ disable_link_preview=disable_link_preview,
856
+ keyboard=keyboard,
857
+ attachments=attachments,
858
+ )
859
+
860
+
861
+ class CommandContext:
862
+ def __init__(self, bot, message: Message, command_name: str, args: str):
863
+ self.bot = bot
864
+ self.message: Message = message
865
+ self.sender: User = message.sender
866
+ self.recipient: MessageRecipient = message.recipient
867
+ self.command_name: str = command_name
868
+ self.args_raw: str = args
869
+ self.args: list[str] = args.split()
870
+
871
+ async def send(
872
+ self,
873
+ text: "str | None" = None,
874
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
875
+ notify: bool = True,
876
+ disable_link_preview: bool = False,
877
+ keyboard: """list[list[buttons.Button]] \
878
+ | buttons.KeyboardBuilder \
879
+ | None""" = None,
880
+ attachments: "list[Attachment] | Attachment | None" = None,
881
+ ) -> Message:
882
+ """
883
+ Send a message to the chat that the user sent the command.
884
+
885
+ :param text: Message text. Up to 4000 characters
886
+ :param format: Message format. Bot.default_format by default
887
+ :param notify: Whether to notify users about the message.
888
+ True by default.
889
+ :param disable_link_preview: Whether to disable link preview.
890
+ False by default
891
+ :param keyboard: An inline keyboard to attach to the message
892
+ :param attachments: List of attachments
893
+ """
894
+ return await self.bot.send_message(
895
+ text,
896
+ chat_id=self.message.recipient.chat_id,
897
+ format=format,
898
+ notify=notify,
899
+ disable_link_preview=disable_link_preview,
900
+ keyboard=keyboard,
901
+ attachments=attachments,
902
+ )
903
+
904
+ async def reply(
905
+ self,
906
+ text: "str | None" = None,
907
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
908
+ notify: bool = True,
909
+ disable_link_preview: bool = False,
910
+ keyboard: """list[list[buttons.Button]] \
911
+ | buttons.KeyboardBuilder \
912
+ | None""" = None,
913
+ attachments: "list[Attachment] | Attachment | None" = None,
914
+ ) -> Message:
915
+ """
916
+ Reply to the message that the user sent.
917
+
918
+ :param text: Message text. Up to 4000 characters
919
+ :param format: Message format. Bot.default_format by default
920
+ :param notify: Whether to notify users about the message.
921
+ True by default.
922
+ :param disable_link_preview: Whether to disable link preview.
923
+ False by default
924
+ :param keyboard: An inline keyboard to attach to the message
925
+ :param attachments: List of attachments
926
+ """
927
+ return await self.bot.send_message(
928
+ text,
929
+ chat_id=self.message.recipient.chat_id,
930
+ format=format,
931
+ notify=notify,
932
+ disable_link_preview=disable_link_preview,
933
+ keyboard=keyboard,
934
+ attachments=attachments,
935
+ reply_to=self.message.id,
936
+ )
937
+
938
+ @property
939
+ def user_id(self):
940
+ return self.sender.user_id
941
+
942
+
943
+ class CommandHandler:
944
+ def __init__(
945
+ self,
946
+ call: Callable,
947
+ as_message: bool = False,
948
+ ):
949
+ self.call = call
950
+ self.as_message: bool = as_message
951
+
952
+
953
+ class Handler:
954
+ def __init__(
955
+ self,
956
+ call: Callable,
957
+ deco_filter: "Callable | None" = None,
958
+ router_filters: Optional[list[Callable]] = None,
959
+ ):
960
+ if router_filters is None:
961
+ router_filters = []
962
+
963
+ self.call = call
964
+ self.deco_filter: "Callable | None" = deco_filter
965
+ self.router_filters: list[Callable] = router_filters
966
+
967
+ @property
968
+ def filters(self) -> list[Callable]:
969
+ if self.deco_filter:
970
+ return [self.deco_filter, *self.router_filters]
971
+ return self.router_filters
972
+
973
+
974
+ class MessageHandler(Handler):
975
+ def __init__(
976
+ self,
977
+ call: Callable,
978
+ deco_filter: "Callable | None" = None,
979
+ router_filters: Optional[list[Callable]] = None,
980
+ detect_commands: bool = False,
981
+ ):
982
+ if router_filters is None:
983
+ router_filters = []
984
+
985
+ super().__init__(call, deco_filter, router_filters)
986
+ self.detect_commands: bool = detect_commands
987
+
988
+
989
+ class Image:
990
+ def __init__(
991
+ self,
992
+ url: str,
993
+ **kwargs,
994
+ ):
995
+ """
996
+ An image.
997
+
998
+ :param url: Image URL
999
+ """
1000
+ self.url: str = url
1001
+
1002
+ @staticmethod
1003
+ def from_json(data: dict) -> "Image | None":
1004
+ if data is None:
1005
+ return None
1006
+
1007
+ return Image(**data)
1008
+
1009
+
1010
+ class ImageRequestPayload:
1011
+ def __init__(
1012
+ self,
1013
+ url: "str | None" = None,
1014
+ token: "str | None" = None,
1015
+ **kwargs,
1016
+ ):
1017
+ """
1018
+ A payload with the info about an image or avatar to send to the bot.
1019
+
1020
+ Only url or token must be specified.
1021
+
1022
+ :param url: Image URL
1023
+ :param token: Attachment token generated by Bot.upload_image().token
1024
+ """
1025
+ if url is None and token is None:
1026
+ raise exceptions.AiomaxException("Token or URL must be specified")
1027
+ if not (url is None or token is None):
1028
+ raise exceptions.AiomaxException(
1029
+ "Token and URL cannot be specified at the same time"
1030
+ )
1031
+
1032
+ self.url: "str | None" = url
1033
+ self.token: "str | None" = token
1034
+
1035
+ @staticmethod
1036
+ def from_json(data: dict) -> "ImageRequestPayload | None":
1037
+ if data is None:
1038
+ return None
1039
+
1040
+ return ImageRequestPayload(**data)
1041
+
1042
+ def as_dict(self):
1043
+ return {"url": self.url} if self.url else {"token": self.token}
1044
+
1045
+
1046
+ class Chat:
1047
+ def __init__(
1048
+ self,
1049
+ chat_id: int,
1050
+ type: str,
1051
+ status: str,
1052
+ last_event_time: int,
1053
+ participants_count: int,
1054
+ is_public: bool,
1055
+ title: "str | None" = None,
1056
+ icon: "Image | None" = None,
1057
+ description: "str | None" = None,
1058
+ pinned_message: "Message | None" = None,
1059
+ owner_id: "int | None" = None,
1060
+ participants: "dict[str, int] | None" = None,
1061
+ link: "str | None" = None,
1062
+ messages_count: "str | None" = None,
1063
+ chat_message_id: "str | None" = None,
1064
+ dialog_with_user: "User | None" = None,
1065
+ **kwargs,
1066
+ ):
1067
+ self.chat_id: int = chat_id
1068
+ self.type: str = type
1069
+ self.status: str = status
1070
+ self.last_event_time: float | None = (
1071
+ last_event_time / 1000 if last_event_time else None
1072
+ )
1073
+ self.participants_count: int = participants_count
1074
+ self.title: "str | None" = title
1075
+ self.icon: "Image | None" = icon
1076
+ self.is_public: bool = is_public
1077
+ self.dialog_with_user: "User | None" = dialog_with_user
1078
+ self.description: "str | None" = description
1079
+ self.pinned_message: "Message | None" = pinned_message
1080
+ self.owner_id: "int | None" = owner_id
1081
+ self.participants: "dict[int, int] | None" = (
1082
+ {int(k): v for k, v in participants.items()}
1083
+ if participants
1084
+ else None
1085
+ )
1086
+ self.link: "str | None" = link
1087
+ self.messages_count: "str | None" = messages_count
1088
+ self.chat_message_id: "str | None" = chat_message_id
1089
+
1090
+ def __eq__(self, other):
1091
+ if isinstance(other, Chat):
1092
+ return self.chat_id == other.chat_id
1093
+ return False
1094
+
1095
+ def __repr__(self):
1096
+ return (
1097
+ f"{self.__class__.__name__}(chat_id={self.chat_id!r},"
1098
+ f"title={self.title!r})"
1099
+ )
1100
+
1101
+ @staticmethod
1102
+ def from_json(data: dict) -> "Chat | None":
1103
+ if data is None:
1104
+ return None
1105
+
1106
+ data = dict(data)
1107
+ if data.get("icon") is not None:
1108
+ data["icon"] = Image.from_json(data["icon"])
1109
+ if data.get("pinned_message") is not None:
1110
+ data["pinned_message"] = Message.from_json(data["pinned_message"])
1111
+ if data.get("dialog_with_user") is not None:
1112
+ data["dialog_with_user"] = User.from_json(data["dialog_with_user"])
1113
+
1114
+ return Chat(**data)
1115
+
1116
+
1117
+ class Callback:
1118
+ def __init__(
1119
+ self,
1120
+ bot,
1121
+ timestamp: int,
1122
+ callback_id: str,
1123
+ message: "Message | None",
1124
+ user: User,
1125
+ user_locale: "str | None",
1126
+ payload: "str | None" = None,
1127
+ ):
1128
+ self.bot = bot
1129
+ self.timestamp: float = timestamp / 1000
1130
+ self.callback_id: str = callback_id
1131
+ self.message: "Message | None" = message
1132
+ self.user: User = user
1133
+ self.payload: "str | None" = payload
1134
+ self.user_locale: "str | None" = user_locale
1135
+
1136
+ if self.message is not None:
1137
+ self.message.bot = bot
1138
+
1139
+ @property
1140
+ def content(self) -> str:
1141
+ return self.payload
1142
+
1143
+ async def send(
1144
+ self,
1145
+ text: "str | None" = None,
1146
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
1147
+ notify: bool = True,
1148
+ disable_link_preview: bool = False,
1149
+ keyboard: """list[list[buttons.Button]] \
1150
+ | buttons.KeyboardBuilder \
1151
+ | None""" = None,
1152
+ attachments: "list[Attachment] | Attachment | None" = None,
1153
+ ) -> "Message | None":
1154
+ """
1155
+ Send a message to the chat that contains the message
1156
+ with the pressed button.
1157
+
1158
+ :param text: Message text. Up to 4000 characters
1159
+ :param format: Message format. Bot.default_format by default
1160
+ :param notify: Whether to notify users about the message.
1161
+ True by default.
1162
+ :param disable_link_preview: Whether to disable link preview.
1163
+ False by default
1164
+ :param keyboard: An inline keyboard to attach to the message
1165
+ :param attachments: List of attachments
1166
+ """
1167
+ if self.bot is None:
1168
+ return
1169
+
1170
+ if self.message is None:
1171
+ raise exceptions.AiomaxException("Original message not found")
1172
+
1173
+ return await self.bot.send_message(
1174
+ text,
1175
+ chat_id=self.message.recipient.chat_id,
1176
+ format=format,
1177
+ notify=notify,
1178
+ disable_link_preview=disable_link_preview,
1179
+ keyboard=keyboard,
1180
+ attachments=attachments,
1181
+ )
1182
+
1183
+ async def reply(
1184
+ self,
1185
+ text: "str | None" = None,
1186
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
1187
+ notify: bool = True,
1188
+ disable_link_preview: bool = False,
1189
+ keyboard: """list[list[buttons.Button]] \
1190
+ | buttons.KeyboardBuilder \
1191
+ | None""" = None,
1192
+ attachments: "list[Attachment] | Attachment | None" = None,
1193
+ ) -> "Message | None":
1194
+ """
1195
+ Reply to the message with the button.
1196
+
1197
+ :param text: Message text. Up to 4000 characters
1198
+ :param format: Message format. Bot.default_format by default
1199
+ :param notify: Whether to notify users about the message.
1200
+ True by default.
1201
+ :param disable_link_preview: Whether to disable link preview.
1202
+ False by default
1203
+ :param keyboard: An inline keyboard to attach to the message
1204
+ :param attachments: List of attachments
1205
+ """
1206
+ if self.bot is None:
1207
+ return
1208
+
1209
+ if self.message is None:
1210
+ raise exceptions.AiomaxException("Original message not found")
1211
+
1212
+ return await self.bot.send_message(
1213
+ text,
1214
+ chat_id=self.message.recipient.chat_id,
1215
+ format=format,
1216
+ notify=notify,
1217
+ disable_link_preview=disable_link_preview,
1218
+ keyboard=keyboard,
1219
+ attachments=attachments,
1220
+ reply_to=self.message.id,
1221
+ )
1222
+
1223
+ async def answer(
1224
+ self,
1225
+ notification: "str | None" = None,
1226
+ text: "str | None" = None,
1227
+ format: "Literal['html', 'markdown', 'default'] | None" = "default",
1228
+ notify: bool = True,
1229
+ keyboard: """list[list[buttons.Button]] \
1230
+ | buttons.KeyboardBuilder \
1231
+ | None""" = None,
1232
+ attachments: "list[Attachment] | Attachment | None" = None,
1233
+ ):
1234
+ """
1235
+ Answer the callback.
1236
+
1237
+ :param notification: Notification to display to the user
1238
+ :param text: Message text. Up to 4000 characters
1239
+ :param format: Message format. Bot.default_format by default
1240
+ :param notify: Whether to notify users about the message.
1241
+ True by default.
1242
+ :param keyboard: An inline keyboard to attach to the message
1243
+ :param attachments: List of attachments
1244
+ """
1245
+ if (
1246
+ notification is None
1247
+ and text is None
1248
+ and attachments is None
1249
+ and keyboard is None
1250
+ ):
1251
+ raise exceptions.AiomaxException(
1252
+ "Either notification, text or attachments must be specified"
1253
+ )
1254
+ body = {"notification": notification, "message": None}
1255
+ if keyboard is None and self.message is not None:
1256
+ keyboard = [
1257
+ i
1258
+ for i in self.message.body.attachments
1259
+ if i.type == "inline_keyboard"
1260
+ ]
1261
+ keyboard = None if len(keyboard) == 0 else keyboard[0].payload
1262
+
1263
+ if text is not None or attachments is not None or keyboard is not None:
1264
+ format = self.bot.default_format if format == "default" else format
1265
+ body["message"] = utils.get_message_body(
1266
+ text,
1267
+ format,
1268
+ notify=notify,
1269
+ keyboard=keyboard,
1270
+ attachments=attachments,
1271
+ )
1272
+
1273
+ out = await self.bot.post(
1274
+ "answers",
1275
+ params={"callback_id": self.callback_id},
1276
+ json=body,
1277
+ )
1278
+ return await out.json()
1279
+
1280
+ @property
1281
+ def user_id(self):
1282
+ return self.user.user_id
1283
+
1284
+ @staticmethod
1285
+ def from_json(
1286
+ data: dict,
1287
+ message: "dict | None",
1288
+ user_locale: "str | None" = None,
1289
+ bot=None,
1290
+ ) -> "Callback | None":
1291
+ if data is None:
1292
+ return None
1293
+
1294
+ return Callback(
1295
+ bot,
1296
+ data["timestamp"],
1297
+ data["callback_id"],
1298
+ Message.from_json(message) if message is not None else None,
1299
+ User.from_json(data["user"]),
1300
+ user_locale,
1301
+ data.get("payload"),
1302
+ )
1303
+
1304
+
1305
+ class ChatCreatePayload:
1306
+ def __init__(
1307
+ self,
1308
+ timestamp: int,
1309
+ chat: Chat,
1310
+ message_id: "str | None" = None,
1311
+ start_payload: "str | None" = None,
1312
+ ):
1313
+ """
1314
+ Payload that is sent to the `Bot.on_button_chat_create` decorator.
1315
+
1316
+ :param timestamp: Timestamp of the button press
1317
+ :param chat: Created chat
1318
+ :param message_id: Message ID on which the button was
1319
+ :param start_payload: Start payload specified by the button
1320
+ """
1321
+ self.timestamp: float = timestamp / 1000
1322
+ self.chat: Chat = chat
1323
+ self.message_id: "str | None" = message_id
1324
+ self.start_payload: "str | None" = start_payload
1325
+
1326
+ @staticmethod
1327
+ def from_json(data: dict) -> "ChatCreatePayload | None":
1328
+ if data is None:
1329
+ return None
1330
+
1331
+ return ChatCreatePayload(
1332
+ data["timestamp"],
1333
+ Chat.from_json(data["chat"]),
1334
+ data.get("message_id"),
1335
+ data.get("start_payload"),
1336
+ )
1337
+
1338
+
1339
+ class MessageDeletePayload:
1340
+ def __init__(
1341
+ self,
1342
+ timestamp: int,
1343
+ message: "Message | None" = None,
1344
+ message_id: "str | None" = None,
1345
+ chat_id: "int | None" = None,
1346
+ user_id: "int | None" = None,
1347
+ bot=None,
1348
+ ):
1349
+ """
1350
+ Payload that is sent to the `Bot.on_message_delete` decorator.
1351
+
1352
+ :param timestamp: Timestamp of the message deletion.
1353
+ :param message: Cached Message object.
1354
+ May be None if message was not cached
1355
+ :param message_id: ID of the deleted message
1356
+ :param chat_id: ID of the chat the message was deleted in
1357
+ :param user_id: ID of the user who deleted the message
1358
+ """
1359
+ self.timestamp: float = timestamp / 1000
1360
+ self.message: "Message | None" = message
1361
+ self.message_id: "str | None" = message_id
1362
+ self.chat_id: "int | None" = chat_id
1363
+ self.user_id: "int | None" = user_id
1364
+ self.bot = bot
1365
+
1366
+ @staticmethod
1367
+ def from_json(data: dict, bot) -> "MessageDeletePayload | None":
1368
+ if data is None:
1369
+ return None
1370
+
1371
+ return MessageDeletePayload(
1372
+ data["timestamp"],
1373
+ (
1374
+ bot.cache.get_message(data.get("message_id"))
1375
+ if bot.cache
1376
+ else None
1377
+ ),
1378
+ data.get("message_id"),
1379
+ data.get("chat_id"),
1380
+ data.get("user_id"),
1381
+ bot=bot,
1382
+ )
1383
+
1384
+ @property
1385
+ def content(self) -> "str | None":
1386
+ if self.message is None:
1387
+ return None
1388
+
1389
+ return self.message.content
1390
+
1391
+
1392
+ class ChatTitleEditPayload:
1393
+ def __init__(
1394
+ self,
1395
+ timestamp: int,
1396
+ user: User,
1397
+ chat_id: "int | None" = None,
1398
+ title: "str | None" = None,
1399
+ ):
1400
+ """
1401
+ Payload that is sent to the `Bot.on_chat_title_change` decorator.
1402
+
1403
+ :param timestamp: Timestamp of the title edit.
1404
+ :param user: User that edited the chat name.
1405
+ :param chat_id: Chat ID that had its title edited.
1406
+ :param title: New chat title
1407
+ """
1408
+ self.timestamp: float = timestamp / 1000
1409
+ self.user: User = user
1410
+ self.chat_id: "int | None" = chat_id
1411
+ self.title: "str | None" = title
1412
+
1413
+ @property
1414
+ def user_id(self):
1415
+ return self.user.user_id
1416
+
1417
+ @staticmethod
1418
+ def from_json(data: dict) -> "ChatTitleEditPayload | None":
1419
+ if data is None:
1420
+ return None
1421
+
1422
+ return ChatTitleEditPayload(
1423
+ data["timestamp"],
1424
+ User.from_json(data["user"]),
1425
+ data.get("chat_id"),
1426
+ data.get("title"),
1427
+ )
1428
+
1429
+
1430
+ class ChatMembershipPayload:
1431
+ def __init__(
1432
+ self,
1433
+ timestamp: int,
1434
+ user: User,
1435
+ chat_id: "int | None" = None,
1436
+ is_channel: bool = False,
1437
+ ):
1438
+ """
1439
+ Payload that is sent to the `Bot.on_bot_add`
1440
+ or `Bot.on_bot_remove` decorator.
1441
+
1442
+ :param timestamp: Timestamp of the action.
1443
+ :param user: User that invited or kicked the bot.
1444
+ :param chat_id: Chat ID that the bot was invited to / kicked from.
1445
+ :param is_channel: Whether the bot got added to / kicked
1446
+ from a channel or not
1447
+ """
1448
+ self.timestamp: float = timestamp / 1000
1449
+ self.user: User = user
1450
+ self.chat_id: "int | None" = chat_id
1451
+ self.is_channel: bool = is_channel
1452
+
1453
+ @property
1454
+ def user_id(self):
1455
+ return self.user.user_id
1456
+
1457
+ @staticmethod
1458
+ def from_json(data: dict) -> "ChatMembershipPayload | None":
1459
+ if data is None:
1460
+ return None
1461
+
1462
+ return ChatMembershipPayload(
1463
+ data["timestamp"],
1464
+ User.from_json(data["user"]),
1465
+ data.get("chat_id"),
1466
+ data.get("is_channel", False),
1467
+ )
1468
+
1469
+
1470
+ class UserMembershipPayload:
1471
+ def __init__(
1472
+ self,
1473
+ timestamp: int,
1474
+ user: User,
1475
+ chat_id: "int | None" = None,
1476
+ is_channel: bool = False,
1477
+ initiator: "int | None" = None,
1478
+ ):
1479
+ """
1480
+ Payload that is sent to the `Bot.on_user_add` or
1481
+ `Bot.on_user_remove` decorator.
1482
+
1483
+ :param timestamp: Timestamp of the action.
1484
+ :param user: User that joined or left the chat.
1485
+ :param chat_id: Chat ID that the user joined / left.
1486
+ :param is_channel: Whether the user was added to / kicked
1487
+ from a channel or not.
1488
+ :param initiator: User ID of the inviter / kicker,
1489
+ if the user got invited by another user or kicked by an admin.
1490
+ """
1491
+ self.timestamp: float = timestamp / 1000
1492
+ self.user: User = user
1493
+ self.chat_id: "int | None" = chat_id
1494
+ self.is_channel: bool = is_channel
1495
+ self.initiator: "int | None" = initiator
1496
+
1497
+ @property
1498
+ def user_id(self):
1499
+ return self.user.user_id
1500
+
1501
+ @staticmethod
1502
+ def from_json(data: dict) -> "UserMembershipPayload | None":
1503
+ if data is None:
1504
+ return None
1505
+
1506
+ return UserMembershipPayload(
1507
+ data["timestamp"],
1508
+ User.from_json(data["user"]),
1509
+ data.get("chat_id"),
1510
+ data.get("is_channel", False),
1511
+ data.get("inviter_id", data.get("admin_id")),
1512
+ )