RubigramClient 1.7.1__py3-none-any.whl → 1.7.3__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.

Potentially problematic release.


This version of RubigramClient might be problematic. Click here for more details.

Files changed (74) hide show
  1. rubigram/__init__.py +1 -5
  2. rubigram/client.py +102 -154
  3. rubigram/enums.py +4 -3
  4. rubigram/filters.py +600 -139
  5. rubigram/handler.py +24 -0
  6. rubigram/http.py +32 -0
  7. rubigram/logger.py +20 -0
  8. rubigram/method/__init__.py +18 -0
  9. rubigram/method/chat/__init__.py +10 -0
  10. rubigram/method/chat/get_chat.py +26 -0
  11. rubigram/method/chat/get_me.py +22 -0
  12. rubigram/method/chat/get_update.py +32 -0
  13. rubigram/method/decorator/__init__.py +19 -0
  14. rubigram/method/decorator/on_delete_message.py +37 -0
  15. rubigram/method/decorator/on_edit_message.py +37 -0
  16. rubigram/method/decorator/on_inline_message.py +40 -0
  17. rubigram/method/decorator/on_message.py +38 -0
  18. rubigram/method/decorator/on_start.py +30 -0
  19. rubigram/method/decorator/on_stop.py +29 -0
  20. rubigram/method/decorator/register.py +43 -0
  21. rubigram/method/file/__init__.py +32 -0
  22. rubigram/method/file/download_file.py +34 -0
  23. rubigram/method/file/get_bytes.py +25 -0
  24. rubigram/method/file/get_file.py +27 -0
  25. rubigram/method/file/get_file_name.py +29 -0
  26. rubigram/method/file/request_download_file.py +35 -0
  27. rubigram/method/file/request_send_file.py +28 -0
  28. rubigram/method/file/request_upload_file.py +62 -0
  29. rubigram/method/file/send_document.py +58 -0
  30. rubigram/method/file/send_file.py +78 -0
  31. rubigram/method/file/send_gif.py +58 -0
  32. rubigram/method/file/send_music.py +58 -0
  33. rubigram/method/file/send_photo.py +58 -0
  34. rubigram/method/file/send_video.py +58 -0
  35. rubigram/method/file/send_voice.py +55 -0
  36. rubigram/method/messages/__init__.py +29 -0
  37. rubigram/method/messages/delete_message.py +50 -0
  38. rubigram/method/messages/edit_chat_keypad.py +34 -0
  39. rubigram/method/messages/edit_message.py +41 -0
  40. rubigram/method/messages/edit_message_keypad.py +38 -0
  41. rubigram/method/messages/edit_message_text.py +34 -0
  42. rubigram/method/messages/forward_message.py +43 -0
  43. rubigram/method/messages/remove_chat_keypad.py +28 -0
  44. rubigram/method/messages/send_contact.py +74 -0
  45. rubigram/method/messages/send_location.py +70 -0
  46. rubigram/method/messages/send_message.py +67 -0
  47. rubigram/method/messages/send_poll.py +71 -0
  48. rubigram/method/messages/send_sticker.py +66 -0
  49. rubigram/method/network/__init__.py +7 -0
  50. rubigram/method/network/request.py +20 -0
  51. rubigram/method/setting/__init__.py +9 -0
  52. rubigram/method/setting/set_command.py +32 -0
  53. rubigram/method/setting/update_bot_endpoint.py +31 -0
  54. rubigram/method/utilities/__init__.py +11 -0
  55. rubigram/method/utilities/dispatch.py +25 -0
  56. rubigram/method/utilities/setup_endpoint.py +16 -0
  57. rubigram/method/utilities/updater.py +17 -0
  58. rubigram/rubino/client.py +16 -130
  59. rubigram/rubino/network.py +121 -0
  60. rubigram/state.py +14 -19
  61. rubigram/types/__init__.py +3 -0
  62. rubigram/types/messages.py +175 -0
  63. rubigram/types/object.py +112 -0
  64. rubigram/types/types.py +211 -0
  65. rubigram/types/updates.py +572 -0
  66. {rubigramclient-1.7.1.dist-info → rubigramclient-1.7.3.dist-info}/METADATA +4 -3
  67. rubigramclient-1.7.3.dist-info/RECORD +71 -0
  68. rubigram/method.py +0 -354
  69. rubigram/network.py +0 -80
  70. rubigram/types.py +0 -538
  71. rubigramclient-1.7.1.dist-info/RECORD +0 -15
  72. {rubigramclient-1.7.1.dist-info → rubigramclient-1.7.3.dist-info}/WHEEL +0 -0
  73. {rubigramclient-1.7.1.dist-info → rubigramclient-1.7.3.dist-info}/licenses/LICENSE +0 -0
  74. {rubigramclient-1.7.1.dist-info → rubigramclient-1.7.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,25 @@
1
+ from typing import Union
2
+ import rubigram
3
+
4
+
5
+ class Dispatch:
6
+ async def dispatch(
7
+ self: "rubigram.Client",
8
+ update: Union[
9
+ "rubigram.types.Update",
10
+ "rubigram.types.InlineMessage"
11
+ ]
12
+ ):
13
+ if isinstance(update, rubigram.types.InlineMessage):
14
+ event_type = "inline"
15
+ else:
16
+ match update.type:
17
+ case "NewMessage": event_type = "message"
18
+ case "UpdatedMessage": event_type = "edit"
19
+ case "RemovedMessage": event_type = "delete"
20
+ case other:
21
+ return
22
+
23
+ for handler in self.handlers[event_type]:
24
+ if handler.filters is None or await handler.filters(update):
25
+ await handler.func(self, update)
@@ -0,0 +1,16 @@
1
+ import rubigram
2
+ from ...logger import logger
3
+
4
+
5
+ class SetupEndpoints:
6
+ async def setup_endpoints(self: "rubigram.Client"):
7
+ endpoint_types = [
8
+ "ReceiveUpdate",
9
+ "ReceiveInlineMessage"
10
+ ]
11
+ for endpoint_type in endpoint_types:
12
+ setup = await self.update_bot_endpoints(
13
+ "{}/{}".format(self.endpoint, endpoint_type),
14
+ endpoint_type
15
+ )
16
+ logger.info("[<] set endpoint for {} : {}".format(endpoint_type, setup["status"]))
@@ -0,0 +1,17 @@
1
+ import rubigram
2
+
3
+
4
+ class Updater:
5
+ async def updater(
6
+ self: "rubigram.Client",
7
+ data: dict
8
+ ):
9
+ if "inline_message" in data:
10
+ event = rubigram.types.InlineMessage.parse(data["inline_message"])
11
+ elif "update" in data:
12
+ event = rubigram.types.Update.parse(data["update"])
13
+ else:
14
+ return
15
+
16
+ event.client = self
17
+ await self.dispatch(event)
rubigram/rubino/client.py CHANGED
@@ -1,130 +1,16 @@
1
- from typing import Literal, Optional, Union, Any
2
- from aiohttp import ClientSession, FormData
3
- from urllib.parse import urlparse
1
+ from typing import Optional, Literal, Union
2
+ from .network import Network
4
3
  from random import randint
5
- from pathlib import Path
6
- from json import loads
7
- import os
8
-
9
-
10
- class Http:
11
- def __init__(self, auth: str):
12
- self.auth = auth
13
- self.session: Optional[ClientSession] = None
14
- self.api = f"https://rubino{randint(1, 30)}.iranlms.ir"
15
- self.client = {
16
- "app_name": "Main",
17
- "app_version": "3.0.2",
18
- "lang_code": "fa",
19
- "package": "app.rbmain.a",
20
- "platform": "Android"
21
- }
22
-
23
- async def connect(self):
24
- if self.session is None:
25
- self.session = ClientSession()
26
-
27
- async def disconnect(self):
28
- if self.session:
29
- await self.session.close()
30
- self.session = None
31
-
32
- async def __aenter__(self):
33
- await self.connect()
34
- return self
35
-
36
- async def __aexit__(self, exc_type, exc_val, exc_tb):
37
- await self.disconnect()
38
-
39
- async def request(self, method: str, data: dict[str, Any]):
40
- await self.connect()
41
- json = {
42
- "api_version": "0",
43
- "auth": self.auth,
44
- "client": self.client,
45
- "data": data,
46
- "method": method
47
- }
48
- async with self.session.post(self.api, json=json) as response:
49
- response.raise_for_status()
50
- return await response.json()
51
-
52
- async def getBytes(self, url: str) -> bytes:
53
- await self.connect()
54
- async with self.session.get(url) as response:
55
- response.raise_for_status()
56
- return await response.read()
57
-
58
- async def getName(self, url: str) -> str:
59
- parser = urlparse(url)
60
- return os.path.basename(parser.path)
61
-
62
- async def request_upload_file(
63
- self,
64
- file_name: str,
65
- file_size: str,
66
- file_type: str,
67
- profile_id: str,
68
- ):
69
- data = {
70
- "file_name": file_name,
71
- "file_size": file_size,
72
- "file_type": file_type,
73
- "profile_id": profile_id,
74
- }
75
- return await self.request("requestUploadFile", data)
76
4
 
77
- async def requestUpload(
5
+
6
+ class Rubino(Network):
7
+ def __init__(
78
8
  self,
79
- file: str,
80
- file_type: str,
81
- file_name: Optional[str] = None,
82
- profile_id: Optional[str] = None,
9
+ auth: str,
10
+ timeout: float = 30
83
11
  ):
84
- path = Path(file)
85
- if path.is_file():
86
- data = path.read_bytes()
87
- file_name = file_name if file_name else path.name
88
- file_size = path.stat().st_size
89
-
90
- elif file.startswith("http"):
91
- data = await self.getBytes(file)
92
- file_name = file_name if file_name else await self.getName(file)
93
- file_size = len(data)
94
-
95
- else:
96
- raise Exception(f"Can't find this file : {file}")
97
-
98
- request = await self.request_upload_file(file_name, file_size, file_type, profile_id)
99
- request: dict[str, str] = request["data"]
100
-
101
- file_id: str = request["file_id"]
102
- hash_file_request: str = request["hash_file_request"]
103
- server_url: str = request["server_url"]
104
-
105
- headers = {
106
- "auth": self.auth,
107
- "chunk-size": str(file_size),
108
- "file-id": file_id,
109
- "hash-file-request": hash_file_request,
110
- "content-length": str(file_size),
111
- "part-number": "1",
112
- "total-part": "1"
113
- }
114
- form = FormData()
115
- form.add_field(
116
- "file", data, filename=file_name, content_type="application/octet-stream"
117
- )
118
- async with self.session.post(server_url, data=form, headers=headers) as response:
119
- text = await response.text()
120
- hash_file_receive = loads(text)["data"]["hash_file_receive"]
121
- return {"file_id": file_id, "hash_file_receive": hash_file_receive}
122
-
123
-
124
- class Rubino(Http):
125
- def __init__(self, auth: str):
126
- super().__init__(auth)
127
-
12
+ super().__init__(auth, timeout)
13
+
128
14
  def rnd(self):
129
15
  return randint(100000, 999999999)
130
16
 
@@ -193,7 +79,7 @@ class Rubino(Http):
193
79
  "profile_id": profile_id
194
80
  }
195
81
  return self.request("requestFollow", data)
196
-
82
+
197
83
  async def follow(self, followee_id: str, profile_id: Optional[str] = None):
198
84
  return await self.request_follow(followee_id, "Follow", profile_id)
199
85
 
@@ -210,7 +96,7 @@ class Rubino(Http):
210
96
 
211
97
  async def block_profile(self, block_id: str, profile_id: Optional[str] = None):
212
98
  return await self.set_block_profile(block_id, "Block", profile_id)
213
-
99
+
214
100
  async def unblock_profile(self, block_id: str, profile_id: Optional[str] = None):
215
101
  return await self.set_block_profile(block_id, "Unblock", profile_id)
216
102
 
@@ -364,7 +250,7 @@ class Rubino(Http):
364
250
  "profile_id": profile_id
365
251
  }
366
252
  return await self.request("getProfileFollowers", data)
367
-
253
+
368
254
  async def get_profile_followers(self, target_profile_id: str, limit: int = 50, profile_id: Optional[str] = None):
369
255
  data = {
370
256
  "equal": False,
@@ -448,7 +334,7 @@ class Rubino(Http):
448
334
  "profile_id": profile_id
449
335
  }
450
336
  return await self.request("getPostsByHashTag", data)
451
-
337
+
452
338
  async def remove_page(self, profile_id: str, record_id: str):
453
339
  data = {
454
340
  "model": "Profile",
@@ -456,7 +342,7 @@ class Rubino(Http):
456
342
  "profile_id": profile_id
457
343
  }
458
344
  return await self.request("removeRecord", data)
459
-
345
+
460
346
  async def get_new_follow_requests(self, profile_id: Optional[str] = None, limit: Optional[int] = 20):
461
347
  data = {
462
348
  "profile_id": profile_id,
@@ -464,7 +350,7 @@ class Rubino(Http):
464
350
  "sort": "FromMax"
465
351
  }
466
352
  return await self.request("getNewFollowRequests", data)
467
-
353
+
468
354
  async def action_on_request(self, request_id: str, profile_id: Optional[str] = None, action: Literal["Accept", "Decline"] = "Accept"):
469
355
  data = {
470
356
  "action": action,
@@ -472,7 +358,7 @@ class Rubino(Http):
472
358
  "profile_id": profile_id
473
359
  }
474
360
  return await self.request("actionOnRequest", data)
475
-
361
+
476
362
  async def accept_request(self, request_id: str, profile_id: Optional[str] = None):
477
363
  return await self.action_on_request(request_id, profile_id)
478
364
 
@@ -0,0 +1,121 @@
1
+ from typing import Optional, Any
2
+ from aiohttp import FormData
3
+ from ..http import Http
4
+ from urllib.parse import urlparse
5
+ from random import randint
6
+ from pathlib import Path
7
+ from json import loads
8
+ import os
9
+
10
+
11
+ class Network:
12
+ def __init__(
13
+ self,
14
+ auth: str,
15
+ timeout: Optional[float] = 30
16
+ ):
17
+ self.auth = auth
18
+ self.http = Http(timeout)
19
+ self.api = f"https://rubino{randint(1, 30)}.iranlms.ir"
20
+ self.client = {
21
+ "app_name": "Main",
22
+ "app_version": "3.0.2",
23
+ "lang_code": "fa",
24
+ "package": "app.rbmain.a",
25
+ "platform": "Android"
26
+ }
27
+
28
+ async def start(self):
29
+ await self.http.connect()
30
+
31
+ async def stop(self):
32
+ await self.http.disconnect()
33
+
34
+ async def __aenter__(self):
35
+ await self.start()
36
+ return self
37
+
38
+ async def __aexit__(self, *args):
39
+ await self.stop()
40
+
41
+ async def request(self, method: str, data: dict[str, Any]):
42
+ json = {
43
+ "api_version": "0",
44
+ "auth": self.auth,
45
+ "client": self.client,
46
+ "data": data,
47
+ "method": method
48
+ }
49
+ async with self.http.session.post(self.api, json=json) as response:
50
+ response.raise_for_status()
51
+ return await response.json()
52
+
53
+ async def getBytes(self, url: str) -> bytes:
54
+ async with self.http.session.get(url) as response:
55
+ response.raise_for_status()
56
+ return await response.read()
57
+
58
+ async def getName(self, url: str) -> str:
59
+ parser = urlparse(url)
60
+ return os.path.basename(parser.path)
61
+
62
+ async def request_upload_file(
63
+ self,
64
+ file_name: str,
65
+ file_size: str,
66
+ file_type: str,
67
+ profile_id: str,
68
+ ):
69
+ data = {
70
+ "file_name": file_name,
71
+ "file_size": file_size,
72
+ "file_type": file_type,
73
+ "profile_id": profile_id,
74
+ }
75
+ return await self.request("requestUploadFile", data)
76
+
77
+ async def requestUpload(
78
+ self,
79
+ file: str,
80
+ file_type: str,
81
+ file_name: Optional[str] = None,
82
+ profile_id: Optional[str] = None,
83
+ ):
84
+ path = Path(file)
85
+ if path.is_file():
86
+ data = path.read_bytes()
87
+ file_name = file_name if file_name else path.name
88
+ file_size = path.stat().st_size
89
+
90
+ elif file.startswith("http"):
91
+ data = await self.getBytes(file)
92
+ file_name = file_name if file_name else await self.getName(file)
93
+ file_size = len(data)
94
+
95
+ else:
96
+ raise Exception(f"Can't find this file : {file}")
97
+
98
+ request = await self.request_upload_file(file_name, file_size, file_type, profile_id)
99
+ request: dict[str, str] = request["data"]
100
+
101
+ file_id: str = request["file_id"]
102
+ hash_file_request: str = request["hash_file_request"]
103
+ server_url: str = request["server_url"]
104
+
105
+ headers = {
106
+ "auth": self.auth,
107
+ "chunk-size": str(file_size),
108
+ "file-id": file_id,
109
+ "hash-file-request": hash_file_request,
110
+ "content-length": str(file_size),
111
+ "part-number": "1",
112
+ "total-part": "1"
113
+ }
114
+ form = FormData()
115
+ form.add_field(
116
+ "file", data, filename=file_name, content_type="application/octet-stream"
117
+ )
118
+ async with self.http.session.post(server_url, data=form, headers=headers) as response:
119
+ text = await response.text()
120
+ hash_file_receive = loads(text)["data"]["hash_file_receive"]
121
+ return {"file_id": file_id, "hash_file_receive": hash_file_receive}
rubigram/state.py CHANGED
@@ -1,35 +1,30 @@
1
1
  from typing import Any
2
2
 
3
- class StateManager:
3
+
4
+ class state:
4
5
  def __init__(self):
5
- self.DATA: dict[str, dict[str, Any]] = {}
6
- self.STATE: dict[str, str] = {}
6
+ self.data: dict[str, dict[str, Any]] = {}
7
+ self.state: dict[str, str] = {}
7
8
 
8
-
9
9
  async def set_state(self, user_id: str, state: str):
10
- self.STATE[user_id] = state
11
-
12
-
10
+ self.state[user_id] = state
11
+
13
12
  async def get_state(self, user_id: str):
14
- return self.STATE.get(user_id)
13
+ return self.state.get(user_id)
15
14
 
16
-
17
15
  async def remove_state(self, user_id: str):
18
- self.STATE.pop(user_id, None)
16
+ self.state.pop(user_id, None)
19
17
 
20
-
21
18
  async def set_data(self, user_id: str, **data):
22
- if user_id not in self.DATA:
23
- self.DATA[user_id] = {}
24
- self.DATA[user_id].update(data)
19
+ if user_id not in self.data:
20
+ self.data[user_id] = {}
21
+ self.data[user_id].update(data)
25
22
 
26
-
27
23
  async def get_data(self, user_id: str, key: str = None):
28
- data = self.DATA.get(user_id, {})
24
+ data = self.data.get(user_id, {})
29
25
  return data.get(key) if key else data
30
26
 
31
-
32
27
  async def remove_data(self, user_id: str, key: str = None):
33
28
  if key:
34
- return self.DATA.get(user_id, {}).pop(key, None)
35
- return self.DATA.pop(user_id, None)
29
+ return self.data.get(user_id, {}).pop(key, None)
30
+ return self.data.pop(user_id, None)
@@ -0,0 +1,3 @@
1
+ from .types import *
2
+ from .messages import *
3
+ from .updates import *
@@ -0,0 +1,175 @@
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+ from .object import Object
4
+ from .types import *
5
+ import rubigram
6
+
7
+
8
+ @dataclass
9
+ class Message(Object):
10
+ message_id: Optional[str] = None
11
+ text: Optional[str] = None
12
+ time: Optional[str] = None
13
+ is_edited: Optional[bool] = None
14
+ sender_type: Optional[enums.MessageSender] = None
15
+ sender_id: Optional[str] = None
16
+ aux_data: Optional[AuxData] = None
17
+ file: Optional[File] = None
18
+ reply_to_message_id: Optional[str] = None
19
+ forwarded_from: Optional[ForwardedFrom] = None
20
+ forwarded_no_link: Optional[str] = None
21
+ location: Optional[Location] = None
22
+ sticker: Optional[Sticker] = None
23
+ contact_message: Optional[ContactMessage] = None
24
+ poll: Optional[Poll] = None
25
+ live_location: Optional[LiveLocation] = None
26
+ client: Optional["rubigram.Client"] = None
27
+
28
+
29
+ @dataclass
30
+ class InlineMessage(Object):
31
+ sender_id: Optional[str] = None
32
+ text: Optional[str] = None
33
+ message_id: Optional[str] = None
34
+ chat_id: Optional[str] = None
35
+ file: Optional[File] = None
36
+ location: Optional[Location] = None
37
+ aux_data: Optional[AuxData] = None
38
+ client: Optional["rubigram.Client"] = None
39
+
40
+
41
+ @dataclass
42
+ class UMessage(Object):
43
+ message_id: Optional[str] = None
44
+ file_id: Optional[str] = None
45
+ chat_id: Optional[str] = None
46
+ client: Optional["rubigram.Client"] = None
47
+
48
+ async def delete(self):
49
+ """Delete this message from the chat.
50
+
51
+ Sends a request to Rubigram to remove the message identified
52
+ by this object's `message_id` from its chat.
53
+
54
+ Args:
55
+ self (UMessage): The message instance to be deleted.
56
+
57
+ Returns:
58
+ bool: True if the message was successfully deleted, False otherwise.
59
+
60
+ Example:
61
+ >>> message = await client.send_message(chat_id="b0X123", text="Hello!")
62
+ >>> await message.delete()
63
+ """
64
+ return await self.client.delete_message(
65
+ self.chat_id, self.message_id
66
+ )
67
+
68
+ async def edit(
69
+ self,
70
+ text: Optional[str] = None,
71
+ inline: Optional[Keypad] = None,
72
+ keypad: Optional[Keypad] = None
73
+ ):
74
+ """Edit this message's content, inline keyboard, or chat keypad.
75
+
76
+ This method allows modifying the text, inline keyboard, or chat keypad
77
+ of the current message. You can use any combination of parameters to
78
+ update different parts of the message.
79
+
80
+ Args:
81
+ self (UMessage): The message instance to edit.
82
+ text (Optional[str], optional): New text content for the message. Defaults to None.
83
+ inline (Optional[Keypad], optional): Inline keyboard to attach to the message. Defaults to None.
84
+ keypad (Optional[Keypad], optional): Chat keypad to attach to the message. Defaults to None.
85
+
86
+ Returns:
87
+ None
88
+
89
+ Example:
90
+ >>> message = await client.send_message(chat_id="b0X123", text="Hello!")
91
+ >>> await message.edit(text="Updated text")
92
+ >>> await message.edit(inline=my_inline_keypad)
93
+ >>> await message.edit(keypad=my_chat_keypad)
94
+ """
95
+ if text:
96
+ await self.edit_text(text)
97
+ if inline:
98
+ await self.edit_inline(inline)
99
+ if keypad:
100
+ await self.edit_keypad(keypad)
101
+
102
+ async def edit_text(self, text: str):
103
+ """Edit the text content of this message.
104
+
105
+ Args:
106
+ self (UMessage): The message instance to edit.
107
+ text (str): New text to replace the current message content.
108
+
109
+ Returns:
110
+ UMessage: The updated message object returned by the client.
111
+
112
+ Example:
113
+ >>> await message.edit_text("Updated text content")
114
+ """
115
+ return await self.client.edit_message_text(
116
+ self.chat_id,
117
+ self.message_id,
118
+ text
119
+ )
120
+
121
+ async def edit_inline(self, inline: Keypad):
122
+ """Edit the inline keyboard of this message.
123
+
124
+ Args:
125
+ self (UMessage): The message instance to edit.
126
+ inline (Keypad): New inline keyboard to attach to the message.
127
+
128
+ Returns:
129
+ UMessage: The updated message object returned by the client.
130
+
131
+ Example:
132
+ >>> await message.edit_inline(my_inline_keypad)
133
+ """
134
+ return await self.client.edit_message_keypad(
135
+ self.chat_id,
136
+ self.message_id,
137
+ inline
138
+ )
139
+
140
+ async def edit_keypad(self, keypad: Keypad):
141
+ """Edit the chat keypad for this message's chat.
142
+
143
+ Args:
144
+ self (UMessage): The message instance whose chat keypad will be edited.
145
+ keypad (Keypad): New chat keypad to attach.
146
+
147
+ Returns:
148
+ UMessage: The updated message object returned by the client.
149
+
150
+ Example:
151
+ >>> await message.edit_keypad(my_chat_keypad)
152
+ """
153
+ return await self.client.edit_chat_keypad(
154
+ self.chat_id,
155
+ keypad
156
+ )
157
+
158
+ async def forward(self, chat_id: str):
159
+ """Forward this message to another chat.
160
+
161
+ Args:
162
+ self (UMessage): The message instance to forward.
163
+ chat_id (str): ID of the chat to forward the message to.
164
+
165
+ Returns:
166
+ UMessage: The forwarded message object returned by the client.
167
+
168
+ Example:
169
+ >>> await message.forward("b0AnotherChat")
170
+ """
171
+ return await self.client.forward_message(
172
+ self.chat_id,
173
+ self.message_id,
174
+ chat_id
175
+ )