1
1
"""Create and run conversations applications."""
2
2
import asyncio
3
3
import logging
4
- import os
5
4
6
5
from .channels import ChannelsCollection
7
6
from .dialog_manager import DialogManager
8
7
from .errors import BotError
9
8
from .resources import Resources
10
- from .user_locks import AsyncioLocks
9
+ from .user_locks import AsyncioLocks , UnixSocketStreams
11
10
12
11
logger = logging .getLogger (__name__ )
13
12
@@ -20,22 +19,28 @@ def __init__(
20
19
dialog_manager = None ,
21
20
channels = None ,
22
21
user_locks = None ,
23
- state_store = None ,
22
+ persistence_manager = None ,
24
23
resources = None ,
24
+ history_tracked = False ,
25
25
):
26
26
"""Create new class instance.
27
27
28
28
:param DialogManager dialog_manager: Dialog manager.
29
29
:param ChannelsCollection channels: Channels for communication with users.
30
- :param StateStore state_store: State store .
30
+ :param PersistenceManager persistence_manager: Persistence manager .
31
31
:param Resources resources: Resources for tracking and reloading changes.
32
32
"""
33
33
self .dialog_manager = dialog_manager or DialogManager ()
34
34
self .channels = channels or ChannelsCollection .empty ()
35
- self ._state_store = state_store # the default value is initialized lazily
36
- self .user_locks = user_locks or AsyncioLocks ()
35
+ self ._persistence_manager = persistence_manager # the default value is initialized lazily
36
+ self ._history_tracked = history_tracked
37
+ self ._user_locks = user_locks
37
38
self .resources = resources or Resources .empty ()
38
39
40
+ SocketStreams = UnixSocketStreams
41
+ SUFFIX_LOCKS = "-locks.sock"
42
+ SUFFIX_DB = ".db"
43
+
39
44
@classmethod
40
45
def builder (cls , ** kwargs ):
41
46
"""Create a :class:`~BotBuilder` in a convenient way.
@@ -84,6 +89,23 @@ def from_directory(cls, bot_dir, **kwargs):
84
89
builder .use_directory_resources (bot_dir )
85
90
return builder .build ()
86
91
92
+ @property
93
+ def user_locks (self ):
94
+ """Get user locks implementation."""
95
+ if self ._user_locks is None :
96
+ self ._user_locks = AsyncioLocks ()
97
+ return self ._user_locks
98
+
99
+ def setdefault_user_locks (self , value ):
100
+ """Set .user_locks field value if it is not set.
101
+
102
+ :param AsyncioLocks value: User locks object.
103
+ :return AsyncioLocks: .user_locks field value
104
+ """
105
+ if self ._user_locks is None :
106
+ self ._user_locks = value
107
+ return self ._user_locks
108
+
87
109
@property
88
110
def rpc (self ):
89
111
"""Get RPC manager used by the bot.
@@ -93,14 +115,24 @@ def rpc(self):
93
115
return self .dialog_manager .rpc
94
116
95
117
@property
96
- def state_store (self ):
97
- """State store used to maintain state variables ."""
98
- if self ._state_store is None :
118
+ def persistence_manager (self ):
119
+ """Return persistence manager ."""
120
+ if self ._persistence_manager is None :
99
121
# lazy import to speed up load time
100
- from .state_store import SQLAlchemyStateStore
122
+ from .persistence_manager import SQLAlchemyManager
123
+
124
+ self ._persistence_manager = SQLAlchemyManager ()
125
+ return self ._persistence_manager
101
126
102
- self ._state_store = SQLAlchemyStateStore ()
103
- return self ._state_store
127
+ def setdefault_persistence_manager (self , factory ):
128
+ """Set .persistence_manager field value if it is not set.
129
+
130
+ :param callable factory: Persistence manager factory.
131
+ :return SQLAlchemyStateStore: .persistence_manager field value.
132
+ """
133
+ if self ._persistence_manager is None :
134
+ self ._persistence_manager = factory ()
135
+ return self ._persistence_manager
104
136
105
137
def process_message (self , message , dialog = None ):
106
138
"""Process user message.
@@ -115,8 +147,10 @@ def process_message(self, message, dialog=None):
115
147
"""
116
148
if dialog is None :
117
149
dialog = self ._default_dialog ()
118
- with self .state_store (dialog ) as state :
119
- return asyncio .run (self .dialog_manager .process_message (message , dialog , state ))
150
+ with self .persistence_manager (dialog ) as tracker :
151
+ return asyncio .run (
152
+ self .dialog_manager .process_message (message , dialog , tracker .get_state ())
153
+ )
120
154
121
155
def process_rpc (self , request , dialog = None ):
122
156
"""Process RPC request.
@@ -131,8 +165,10 @@ def process_rpc(self, request, dialog=None):
131
165
"""
132
166
if dialog is None :
133
167
dialog = self ._default_dialog ()
134
- with self .state_store (dialog ) as state :
135
- return asyncio .run (self .dialog_manager .process_rpc (request , dialog , state ))
168
+ with self .persistence_manager (dialog ) as tracker :
169
+ return asyncio .run (
170
+ self .dialog_manager .process_rpc (request , dialog , tracker .get_state ())
171
+ )
136
172
137
173
def _default_dialog (self ):
138
174
return {"channel_name" : "builtin" , "user_id" : "1" }
@@ -148,10 +184,14 @@ async def default_channel_adapter(self, data, channel):
148
184
message = await channel .call_receivers (data )
149
185
if message is None :
150
186
return
151
- with self .state_store (dialog ) as state :
152
- commands = await self .dialog_manager .process_message (message , dialog , state )
187
+ with self .persistence_manager (dialog ) as tracker :
188
+ commands = await self .dialog_manager .process_message (
189
+ message , dialog , tracker .get_state ()
190
+ )
153
191
for command in commands :
154
192
await channel .call_senders (command , dialog )
193
+ if self ._history_tracked :
194
+ tracker .set_message_history (message , commands )
155
195
156
196
async def default_rpc_adapter (self , request , channel , user_id ):
157
197
"""Handle RPC request for specific channel.
@@ -162,80 +202,32 @@ async def default_rpc_adapter(self, request, channel, user_id):
162
202
"""
163
203
dialog = {"channel_name" : channel .name , "user_id" : str (user_id )}
164
204
async with self .user_locks (dialog ):
165
- with self .state_store (dialog ) as state :
166
- commands = await self .dialog_manager .process_rpc (request , dialog , state )
205
+ with self .persistence_manager (dialog ) as tracker :
206
+ commands = await self .dialog_manager .process_rpc (
207
+ request , dialog , tracker .get_state ()
208
+ )
167
209
for command in commands :
168
210
await channel .call_senders (command , dialog )
169
-
170
- def run_webapp (self , host = "localhost" , port = "8080" , * , public_url = None , autoreload = False ):
171
- """Run web application.
172
-
173
- :param str host: Hostname or IP address on which to listen.
174
- :param int port: TCP port on which to listen.
175
- :param str public_url: Base url to register webhook.
176
- :param bool autoreload: Enable tracking and reloading bot resource changes.
177
- """
178
- # lazy import to speed up load time
179
- import sanic
180
-
181
- self ._validate_at_least_one_channel ()
182
-
183
- app = sanic .Sanic ("maxbot" , configure_logging = False )
184
- app .config .FALLBACK_ERROR_FORMAT = "text"
185
-
186
- for channel in self .channels :
187
- if public_url is None :
188
- logger .warning (
189
- "Make sure you have a public URL that is forwarded to -> "
190
- f"http://{ host } :{ port } /{ channel .name } and register webhook for it."
191
- )
192
-
193
- app .blueprint (
194
- channel .blueprint (
195
- self .default_channel_adapter ,
196
- public_url = public_url ,
197
- webhook_path = f"/{ channel .name } " ,
198
- )
199
- )
200
-
201
- if self .rpc :
202
- app .blueprint (self .rpc .blueprint (self .channels , self .default_rpc_adapter ))
203
-
204
- if autoreload :
205
-
206
- @app .after_server_start
207
- async def start_autoreloader (app , loop ):
208
- app .add_task (self .autoreloader , name = "autoreloader" )
209
-
210
- @app .before_server_stop
211
- async def stop_autoreloader (app , loop ):
212
- await app .cancel_task ("autoreloader" )
213
-
214
- @app .after_server_start
215
- async def report_started (app , loop ):
216
- logger .info (
217
- f"Started webhooks updater on http://{ host } :{ port } . Press 'Ctrl-C' to exit."
218
- )
219
-
220
- if sanic .__version__ .startswith ("21." ):
221
- app .run (host , port , motd = False , workers = 1 )
222
- else :
223
- os .environ ["SANIC_IGNORE_PRODUCTION_WARNING" ] = "true"
224
- app .run (host , port , motd = False , single_process = True )
211
+ if self ._history_tracked :
212
+ tracker .set_rpc_history (request , commands )
225
213
226
214
def run_polling (self , autoreload = False ):
227
215
"""Run polling application.
228
216
229
217
:param bool autoreload: Enable tracking and reloading bot resource changes.
230
218
"""
231
219
# lazy import to speed up load time
232
- from telegram .ext import ApplicationBuilder , MessageHandler , filters
220
+ from telegram .ext import ApplicationBuilder , CallbackQueryHandler , MessageHandler , filters
233
221
234
- self ._validate_at_least_one_channel ()
222
+ self .validate_at_least_one_channel ()
235
223
self ._validate_polling_support ()
236
224
237
225
builder = ApplicationBuilder ()
238
226
builder .token (self .channels .telegram .config ["api_token" ])
227
+
228
+ builder .request (self .channels .telegram .create_request ())
229
+ builder .get_updates_request (self .channels .telegram .create_request ())
230
+
239
231
background_tasks = []
240
232
241
233
@builder .post_init
@@ -263,6 +255,7 @@ async def error_handler(update, context):
263
255
264
256
app = builder .build ()
265
257
app .add_handler (MessageHandler (filters .ALL , callback ))
258
+ app .add_handler (CallbackQueryHandler (callback = callback , pattern = None ))
266
259
app .add_error_handler (error_handler )
267
260
app .run_polling ()
268
261
@@ -311,7 +304,8 @@ def _exclude_unsupported_changes(self, changes):
311
304
)
312
305
return changes - unsupported
313
306
314
- def _validate_at_least_one_channel (self ):
307
+ def validate_at_least_one_channel (self ):
308
+ """Raise BotError if at least one channel is missing."""
315
309
if not self .channels :
316
310
raise BotError (
317
311
"At least one channel is required to run a bot. "
0 commit comments