kurimod 3.2.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.
kurimod/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """
2
+ kurimod - A monkeypatched add-on for Kurigram/Pyrogram
3
+ Copyright (C) 2020 Cezar H. <https://github.com/usernein>
4
+
5
+ This file is part of kurimod.
6
+
7
+ kurimod is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ kurimod is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with kurimod. If not, see <https://www.gnu.org/licenses/>.
19
+ """
20
+
21
+ from .config import config
22
+ from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply
23
+ from .listen import Client, MessageHandler, CallbackQueryHandler, Message, Chat, User
24
+ from .nav import Pagination
25
+ from .utils import patch_into, should_patch
26
+
27
+ __all__ = [
28
+ "config",
29
+ "Client",
30
+ "MessageHandler",
31
+ "Message",
32
+ "Chat",
33
+ "User",
34
+ "CallbackQueryHandler",
35
+ "patch_into",
36
+ "should_patch",
37
+ "ikb",
38
+ "bki",
39
+ "ntb",
40
+ "btn",
41
+ "kb",
42
+ "kbtn",
43
+ "array_chunk",
44
+ "force_reply",
45
+ "Pagination",
46
+ ]
@@ -0,0 +1,12 @@
1
+ from types import SimpleNamespace
2
+
3
+ config = SimpleNamespace(
4
+ timeout_handler=None,
5
+ stopped_handler=None,
6
+ throw_exceptions=True,
7
+ unallowed_click_alert=True,
8
+ unallowed_click_alert_text=("[kurimod] You're not expected to click this button."),
9
+ disable_startup_logs=False,
10
+ )
11
+
12
+ __all__ = ["config"]
kurimod/config.py ADDED
@@ -0,0 +1,9 @@
1
+ from types import SimpleNamespace
2
+
3
+ config = SimpleNamespace(
4
+ timeout_handler=None,
5
+ stopped_handler=None,
6
+ throw_exceptions=True,
7
+ unallowed_click_alert=True,
8
+ unallowed_click_alert_text=("[kurimod] You're not expected to click this button."),
9
+ )
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ from .listener_stopped import ListenerStopped
2
+ from .listener_timeout import ListenerTimeout
3
+
4
+ __all__ = ["ListenerStopped", "ListenerTimeout"]
@@ -0,0 +1,2 @@
1
+ class ListenerStopped(Exception):
2
+ pass
@@ -0,0 +1,2 @@
1
+ class ListenerTimeout(Exception):
2
+ pass
@@ -0,0 +1,22 @@
1
+ """
2
+ kurimod - A monkeypatcher add-on for Pyrogram
3
+ Copyright (C) 2020 Cezar H. <https://github.com/usernein>
4
+
5
+ This file is part of kurimod.
6
+
7
+ kurimod is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ kurimod is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with kurimod. If not, see <https://www.gnu.org/licenses/>.
19
+ """
20
+ from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply
21
+
22
+ __all__ = ["ikb", "bki", "ntb", "btn", "kb", "kbtn", "array_chunk", "force_reply"]
@@ -0,0 +1,90 @@
1
+ from pyrogram.types import (
2
+ InlineKeyboardButton,
3
+ InlineKeyboardMarkup,
4
+ KeyboardButton,
5
+ ReplyKeyboardMarkup,
6
+ ForceReply,
7
+ )
8
+
9
+
10
+ def ikb(rows=None):
11
+ if rows is None:
12
+ rows = []
13
+
14
+ lines = []
15
+ for row in rows:
16
+ line = []
17
+ for button in row:
18
+ button = (
19
+ btn(button, button) if isinstance(button, str) else btn(*button)
20
+ ) # InlineKeyboardButton
21
+ line.append(button)
22
+ lines.append(line)
23
+ return InlineKeyboardMarkup(inline_keyboard=lines)
24
+ # return {'inline_keyboard': lines}
25
+
26
+
27
+ def btn(text, value, type="callback_data"):
28
+ return InlineKeyboardButton(text, **{type: value})
29
+ # return {'text': text, type: value}
30
+
31
+
32
+ # The inverse of above
33
+ def bki(keyboard):
34
+ lines = []
35
+ for row in keyboard.inline_keyboard:
36
+ line = []
37
+ for button in row:
38
+ button = ntb(button) # btn() format
39
+ line.append(button)
40
+ lines.append(line)
41
+ return lines
42
+ # return ikb() format
43
+
44
+
45
+ def ntb(button):
46
+ for btn_type in [
47
+ "callback_data",
48
+ "url",
49
+ "switch_inline_query",
50
+ "switch_inline_query_current_chat",
51
+ "callback_game",
52
+ ]:
53
+ value = getattr(button, btn_type)
54
+ if value:
55
+ break
56
+ button = [button.text, value]
57
+ if btn_type != "callback_data":
58
+ button.append(btn_type)
59
+ return button
60
+ # return {'text': text, type: value}
61
+
62
+
63
+ def kb(rows=None, **kwargs):
64
+ if rows is None:
65
+ rows = []
66
+
67
+ lines = []
68
+ for row in rows:
69
+ line = []
70
+ for button in row:
71
+ button_type = type(button)
72
+ if button_type == str:
73
+ button = KeyboardButton(button)
74
+ elif button_type == dict:
75
+ button = KeyboardButton(**button)
76
+
77
+ line.append(button)
78
+ lines.append(line)
79
+ return ReplyKeyboardMarkup(keyboard=lines, **kwargs)
80
+
81
+
82
+ kbtn = KeyboardButton
83
+
84
+
85
+ def force_reply(selective=True):
86
+ return ForceReply(selective=selective)
87
+
88
+
89
+ def array_chunk(input_array, size):
90
+ return [input_array[i : i + size] for i in range(0, len(input_array), size)]
@@ -0,0 +1,35 @@
1
+ """
2
+ kurimod - A monkeypatcher add-on for Pyrogram
3
+ Copyright (C) 2020 Cezar H. <https://github.com/usernein>
4
+
5
+ This file is part of kurimod.
6
+
7
+ kurimod is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ kurimod is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with kurimod. If not, see <https://www.gnu.org/licenses/>.
19
+ """
20
+
21
+ from .callback_query_handler import CallbackQueryHandler
22
+ from .chat import Chat
23
+ from .client import Client
24
+ from .message import Message
25
+ from .message_handler import MessageHandler
26
+ from .user import User
27
+
28
+ __all__ = [
29
+ "Client",
30
+ "MessageHandler",
31
+ "Message",
32
+ "Chat",
33
+ "User",
34
+ "CallbackQueryHandler",
35
+ ]
@@ -0,0 +1,146 @@
1
+ from inspect import iscoroutinefunction
2
+ from typing import Callable, Tuple
3
+
4
+ import pyrogram
5
+ from pyrogram.filters import Filter
6
+ from pyrogram.types import CallbackQuery
7
+
8
+ from .client import Client
9
+ from ..config import config
10
+ from ..types import ListenerTypes, Identifier, Listener
11
+ from ..utils import patch_into, should_patch
12
+
13
+
14
+ @patch_into(pyrogram.handlers.callback_query_handler.CallbackQueryHandler)
15
+ class CallbackQueryHandler(
16
+ pyrogram.handlers.callback_query_handler.CallbackQueryHandler
17
+ ):
18
+ old__init__: Callable
19
+
20
+ @should_patch()
21
+ def __init__(self, callback: Callable, filters: Filter = None):
22
+ self.original_callback = callback
23
+ self.old__init__(self.resolve_future_or_callback, filters)
24
+
25
+ @should_patch()
26
+ def compose_data_identifier(self, query: CallbackQuery):
27
+ from_user = query.from_user
28
+ from_user_id = from_user.id if from_user else None
29
+ from_user_username = from_user.username if from_user else None
30
+
31
+ chat_id = None
32
+ message_id = None
33
+
34
+ if query.message:
35
+ message_id = getattr(
36
+ query.message, "id", getattr(query.message, "message_id", None)
37
+ )
38
+
39
+ if query.message.chat:
40
+ chat_id = [query.message.chat.id, query.message.chat.username]
41
+
42
+ return Identifier(
43
+ message_id=message_id,
44
+ chat_id=chat_id,
45
+ from_user_id=[from_user_id, from_user_username],
46
+ inline_message_id=query.inline_message_id,
47
+ )
48
+
49
+ @should_patch()
50
+ async def check_if_has_matching_listener(
51
+ self, client: Client, query: CallbackQuery
52
+ ) -> Tuple[bool, Listener]:
53
+ data = self.compose_data_identifier(query)
54
+
55
+ listener = client.get_listener_matching_with_data(
56
+ data, ListenerTypes.CALLBACK_QUERY
57
+ )
58
+
59
+ listener_does_match = False
60
+
61
+ if listener:
62
+ filters = listener.filters
63
+ if callable(filters):
64
+ if iscoroutinefunction(filters.__call__):
65
+ listener_does_match = await filters(client, query)
66
+ else:
67
+ listener_does_match = await client.loop.run_in_executor(
68
+ None, filters, client, query
69
+ )
70
+ else:
71
+ listener_does_match = True
72
+
73
+ return listener_does_match, listener
74
+
75
+ @should_patch()
76
+ async def check(self, client: Client, query: CallbackQuery):
77
+ listener_does_match, listener = await self.check_if_has_matching_listener(
78
+ client, query
79
+ )
80
+
81
+ if callable(self.filters):
82
+ if iscoroutinefunction(self.filters.__call__):
83
+ handler_does_match = await self.filters(client, query)
84
+ else:
85
+ handler_does_match = await client.loop.run_in_executor(
86
+ None, self.filters, client, query
87
+ )
88
+ else:
89
+ handler_does_match = True
90
+
91
+ data = self.compose_data_identifier(query)
92
+
93
+ if config.unallowed_click_alert:
94
+ # matches with the current query but from any user
95
+ permissive_identifier = Identifier(
96
+ chat_id=data.chat_id,
97
+ message_id=data.message_id,
98
+ inline_message_id=data.inline_message_id,
99
+ from_user_id=None,
100
+ )
101
+
102
+ matches = permissive_identifier.matches(data)
103
+
104
+ if (
105
+ listener
106
+ and (matches and not listener_does_match)
107
+ and listener.unallowed_click_alert
108
+ ):
109
+ alert = (
110
+ listener.unallowed_click_alert
111
+ if isinstance(listener.unallowed_click_alert, str)
112
+ else config.unallowed_click_alert_text
113
+ )
114
+ await query.answer(alert)
115
+ return False
116
+
117
+ # let handler get the chance to handle if listener
118
+ # exists but its filters doesn't match
119
+ return listener_does_match or handler_does_match
120
+
121
+ @should_patch()
122
+ async def resolve_future_or_callback(
123
+ self, client: Client, query: CallbackQuery, *args
124
+ ):
125
+ listener_does_match, listener = await self.check_if_has_matching_listener(
126
+ client, query
127
+ )
128
+
129
+ if listener and listener_does_match:
130
+ client.remove_listener(listener)
131
+
132
+ if listener.future and not listener.future.done():
133
+ listener.future.set_result(query)
134
+
135
+ raise pyrogram.StopPropagation
136
+ elif listener.callback:
137
+ if iscoroutinefunction(listener.callback):
138
+ await listener.callback(client, query, *args)
139
+ else:
140
+ listener.callback(client, query, *args)
141
+
142
+ raise pyrogram.StopPropagation
143
+ else:
144
+ raise ValueError("Listener must have either a future or a callback")
145
+ else:
146
+ await self.original_callback(client, query, *args)
kurimod/listen/chat.py ADDED
@@ -0,0 +1,21 @@
1
+ import pyrogram
2
+
3
+ from .client import Client
4
+ from ..utils import patch_into, should_patch
5
+
6
+
7
+ @patch_into(pyrogram.types.user_and_chats.chat.Chat)
8
+ class Chat(pyrogram.types.user_and_chats.chat.Chat):
9
+ _client: Client
10
+
11
+ @should_patch()
12
+ def listen(self, *args, **kwargs):
13
+ return self._client.listen(*args, chat_id=self.id, **kwargs)
14
+
15
+ @should_patch()
16
+ def ask(self, text, *args, **kwargs):
17
+ return self._client.ask(self.id, text, *args, **kwargs)
18
+
19
+ @should_patch()
20
+ def stop_listening(self, *args, **kwargs):
21
+ return self._client.stop_listening(*args, chat_id=self.id, **kwargs)
@@ -0,0 +1,232 @@
1
+ import asyncio
2
+ from inspect import iscoroutinefunction
3
+ from typing import Optional, Callable, Dict, List, Union
4
+
5
+ import pyrogram
6
+ from pyrogram.filters import Filter
7
+
8
+ from ..config import config
9
+ from ..exceptions import ListenerTimeout, ListenerStopped
10
+ from ..types import ListenerTypes, Identifier, Listener
11
+ from ..utils import should_patch, patch_into
12
+
13
+
14
+
15
+ @patch_into(pyrogram.client.Client)
16
+ class Client(pyrogram.client.Client.on_callback_query()):
17
+ listeners: Dict[ListenerTypes, List[Listener]]
18
+ old__init__: Callable
19
+
20
+ @should_patch()
21
+ def __init__(self, *args, **kwargs):
22
+ self.listeners = {listener_type: [] for listener_type in ListenerTypes}
23
+ self.old__init__(*args, **kwargs)
24
+
25
+ @should_patch()
26
+ async def listen(
27
+ self,
28
+ filters: Optional[Filter] = None,
29
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
30
+ timeout: Optional[int] = None,
31
+ unallowed_click_alert: bool = True,
32
+ chat_id: Union[Union[int, str], List[Union[int, str]]] = None,
33
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
34
+ message_id: Union[int, List[int]] = None,
35
+ inline_message_id: Union[str, List[str]] = None,
36
+ ):
37
+ pattern = Identifier(
38
+ from_user_id=user_id,
39
+ chat_id=chat_id,
40
+ message_id=message_id,
41
+ inline_message_id=inline_message_id,
42
+ )
43
+
44
+ loop = asyncio.get_event_loop()
45
+ future = loop.create_future()
46
+
47
+ listener = Listener(
48
+ future=future,
49
+ filters=filters,
50
+ unallowed_click_alert=unallowed_click_alert,
51
+ identifier=pattern,
52
+ listener_type=listener_type,
53
+ )
54
+
55
+ future.add_done_callback(lambda _future: self.remove_listener(listener))
56
+
57
+ self.listeners[listener_type].append(listener)
58
+
59
+ try:
60
+ return await asyncio.wait_for(future, timeout)
61
+ except asyncio.exceptions.TimeoutError:
62
+ if callable(config.timeout_handler):
63
+ if iscoroutinefunction(config.timeout_handler.__call__):
64
+ await config.timeout_handler(pattern, listener, timeout)
65
+ else:
66
+ await self.loop.run_in_executor(
67
+ None, config.timeout_handler, pattern, listener, timeout
68
+ )
69
+ elif config.throw_exceptions:
70
+ raise ListenerTimeout(timeout)
71
+
72
+ @should_patch()
73
+ async def ask(
74
+ self,
75
+ chat_id: Union[Union[int, str], List[Union[int, str]]],
76
+ text: str,
77
+ filters: Optional[Filter] = None,
78
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
79
+ timeout: Optional[int] = None,
80
+ unallowed_click_alert: bool = True,
81
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
82
+ message_id: Union[int, List[int]] = None,
83
+ inline_message_id: Union[str, List[str]] = None,
84
+ *args,
85
+ **kwargs,
86
+ ):
87
+ sent_message = None
88
+ if text.strip() != "":
89
+ chat_to_ask = chat_id[0] if isinstance(chat_id, list) else chat_id
90
+ sent_message = await self.send_message(chat_to_ask, text, *args, **kwargs)
91
+
92
+ response = await self.listen(
93
+ filters=filters,
94
+ listener_type=listener_type,
95
+ timeout=timeout,
96
+ unallowed_click_alert=unallowed_click_alert,
97
+ chat_id=chat_id,
98
+ user_id=user_id,
99
+ message_id=message_id,
100
+ inline_message_id=inline_message_id,
101
+ )
102
+ if response:
103
+ response.sent_message = sent_message
104
+
105
+ return response
106
+
107
+ @should_patch()
108
+ def remove_listener(self, listener: Listener):
109
+ try:
110
+ self.listeners[listener.listener_type].remove(listener)
111
+ except ValueError:
112
+ pass
113
+
114
+ @should_patch()
115
+ def get_listener_matching_with_data(
116
+ self, data: Identifier, listener_type: ListenerTypes
117
+ ) -> Optional[Listener]:
118
+ matching = []
119
+ for listener in self.listeners[listener_type]:
120
+ if listener.identifier.matches(data):
121
+ matching.append(listener)
122
+
123
+ # in case of multiple matching listeners, the most specific should be returned
124
+ def count_populated_attributes(listener_item: Listener):
125
+ return listener_item.identifier.count_populated()
126
+
127
+ return max(matching, key=count_populated_attributes, default=None)
128
+
129
+ def get_listener_matching_with_identifier_pattern(
130
+ self, pattern: Identifier, listener_type: ListenerTypes
131
+ ) -> Optional[Listener]:
132
+ matching = []
133
+ for listener in self.listeners[listener_type]:
134
+ if pattern.matches(listener.identifier):
135
+ matching.append(listener)
136
+
137
+ # in case of multiple matching listeners, the most specific should be returned
138
+
139
+ def count_populated_attributes(listener_item: Listener):
140
+ return listener_item.identifier.count_populated()
141
+
142
+ return max(matching, key=count_populated_attributes, default=None)
143
+
144
+ @should_patch()
145
+ def get_many_listeners_matching_with_data(
146
+ self,
147
+ data: Identifier,
148
+ listener_type: ListenerTypes,
149
+ ) -> List[Listener]:
150
+ listeners = []
151
+ for listener in self.listeners[listener_type]:
152
+ if listener.identifier.matches(data):
153
+ listeners.append(listener)
154
+ return listeners
155
+
156
+ @should_patch()
157
+ def get_many_listeners_matching_with_identifier_pattern(
158
+ self,
159
+ pattern: Identifier,
160
+ listener_type: ListenerTypes,
161
+ ) -> List[Listener]:
162
+ listeners = []
163
+ for listener in self.listeners[listener_type]:
164
+ if pattern.matches(listener.identifier):
165
+ listeners.append(listener)
166
+ return listeners
167
+
168
+ @should_patch()
169
+ async def stop_listening(
170
+ self,
171
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
172
+ chat_id: Union[Union[int, str], List[Union[int, str]]] = None,
173
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
174
+ message_id: Union[int, List[int]] = None,
175
+ inline_message_id: Union[str, List[str]] = None,
176
+ ):
177
+ pattern = Identifier(
178
+ from_user_id=user_id,
179
+ chat_id=chat_id,
180
+ message_id=message_id,
181
+ inline_message_id=inline_message_id,
182
+ )
183
+ listeners = self.get_many_listeners_matching_with_identifier_pattern(pattern, listener_type)
184
+
185
+ for listener in listeners:
186
+ await self.stop_listener(listener)
187
+
188
+ @should_patch()
189
+ async def stop_listener(self, listener: Listener):
190
+ self.remove_listener(listener)
191
+
192
+ if listener.future.done():
193
+ return
194
+
195
+ if callable(config.stopped_handler):
196
+ if iscoroutinefunction(config.stopped_handler.__call__):
197
+ await config.stopped_handler(None, listener)
198
+ else:
199
+ await self.loop.run_in_executor(
200
+ None, config.stopped_handler, None, listener
201
+ )
202
+ elif config.throw_exceptions:
203
+ listener.future.set_exception(ListenerStopped())
204
+
205
+ @should_patch()
206
+ def register_next_step_handler(
207
+ self,
208
+ callback: Callable,
209
+ filters: Optional[Filter] = None,
210
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
211
+ unallowed_click_alert: bool = True,
212
+ chat_id: Union[Union[int, str], List[Union[int, str]]] = None,
213
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
214
+ message_id: Union[int, List[int]] = None,
215
+ inline_message_id: Union[str, List[str]] = None,
216
+ ):
217
+ pattern = Identifier(
218
+ from_user_id=user_id,
219
+ chat_id=chat_id,
220
+ message_id=message_id,
221
+ inline_message_id=inline_message_id,
222
+ )
223
+
224
+ listener = Listener(
225
+ callback=callback,
226
+ filters=filters,
227
+ unallowed_click_alert=unallowed_click_alert,
228
+ identifier=pattern,
229
+ listener_type=listener_type,
230
+ )
231
+
232
+ self.listeners[listener_type].append(listener)
@@ -0,0 +1,32 @@
1
+ from typing import Optional, Union, List
2
+
3
+ import pyrogram
4
+
5
+ from .client import Client
6
+ from ..types import ListenerTypes
7
+ from ..utils import patch_into, should_patch
8
+
9
+
10
+ @patch_into(pyrogram.types.messages_and_media.message.Message)
11
+ class Message(pyrogram.types.messages_and_media.message.Message):
12
+ _client = Client
13
+
14
+ @should_patch()
15
+ async def wait_for_click(
16
+ self,
17
+ from_user_id: Optional[Union[Union[int, str], List[Union[int, str]]]] = None,
18
+ timeout: Optional[int] = None,
19
+ filters=None,
20
+ alert: Union[str, bool] = True,
21
+ ):
22
+ message_id = getattr(self, "id", getattr(self, "message_id", None))
23
+
24
+ return await self._client.listen(
25
+ listener_type=ListenerTypes.CALLBACK_QUERY,
26
+ timeout=timeout,
27
+ filters=filters,
28
+ unallowed_click_alert=alert,
29
+ chat_id=self.chat.id,
30
+ user_id=from_user_id,
31
+ message_id=message_id,
32
+ )