11"""Create and run conversations applications."""
22import asyncio
33import logging
4- import os
54
65from .channels import ChannelsCollection
76from .dialog_manager import DialogManager
87from .errors import BotError
98from .resources import Resources
10- from .user_locks import AsyncioLocks
9+ from .user_locks import AsyncioLocks , UnixSocketStreams
1110
1211logger = logging .getLogger (__name__ )
1312
@@ -20,22 +19,28 @@ def __init__(
2019 dialog_manager = None ,
2120 channels = None ,
2221 user_locks = None ,
23- state_store = None ,
22+ persistence_manager = None ,
2423 resources = None ,
24+ history_tracked = False ,
2525 ):
2626 """Create new class instance.
2727
2828 :param DialogManager dialog_manager: Dialog manager.
2929 :param ChannelsCollection channels: Channels for communication with users.
30- :param StateStore state_store: State store .
30+ :param PersistenceManager persistence_manager: Persistence manager .
3131 :param Resources resources: Resources for tracking and reloading changes.
3232 """
3333 self .dialog_manager = dialog_manager or DialogManager ()
3434 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
3738 self .resources = resources or Resources .empty ()
3839
40+ SocketStreams = UnixSocketStreams
41+ SUFFIX_LOCKS = "-locks.sock"
42+ SUFFIX_DB = ".db"
43+
3944 @classmethod
4045 def builder (cls , ** kwargs ):
4146 """Create a :class:`~BotBuilder` in a convenient way.
@@ -84,6 +89,23 @@ def from_directory(cls, bot_dir, **kwargs):
8489 builder .use_directory_resources (bot_dir )
8590 return builder .build ()
8691
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+
87109 @property
88110 def rpc (self ):
89111 """Get RPC manager used by the bot.
@@ -93,14 +115,24 @@ def rpc(self):
93115 return self .dialog_manager .rpc
94116
95117 @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 :
99121 # 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
101126
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
104136
105137 def process_message (self , message , dialog = None ):
106138 """Process user message.
@@ -115,8 +147,10 @@ def process_message(self, message, dialog=None):
115147 """
116148 if dialog is None :
117149 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+ )
120154
121155 def process_rpc (self , request , dialog = None ):
122156 """Process RPC request.
@@ -131,8 +165,10 @@ def process_rpc(self, request, dialog=None):
131165 """
132166 if dialog is None :
133167 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+ )
136172
137173 def _default_dialog (self ):
138174 return {"channel_name" : "builtin" , "user_id" : "1" }
@@ -148,10 +184,14 @@ async def default_channel_adapter(self, data, channel):
148184 message = await channel .call_receivers (data )
149185 if message is None :
150186 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+ )
153191 for command in commands :
154192 await channel .call_senders (command , dialog )
193+ if self ._history_tracked :
194+ tracker .set_message_history (message , commands )
155195
156196 async def default_rpc_adapter (self , request , channel , user_id ):
157197 """Handle RPC request for specific channel.
@@ -162,80 +202,32 @@ async def default_rpc_adapter(self, request, channel, user_id):
162202 """
163203 dialog = {"channel_name" : channel .name , "user_id" : str (user_id )}
164204 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+ )
167209 for command in commands :
168210 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 )
225213
226214 def run_polling (self , autoreload = False ):
227215 """Run polling application.
228216
229217 :param bool autoreload: Enable tracking and reloading bot resource changes.
230218 """
231219 # lazy import to speed up load time
232- from telegram .ext import ApplicationBuilder , MessageHandler , filters
220+ from telegram .ext import ApplicationBuilder , CallbackQueryHandler , MessageHandler , filters
233221
234- self ._validate_at_least_one_channel ()
222+ self .validate_at_least_one_channel ()
235223 self ._validate_polling_support ()
236224
237225 builder = ApplicationBuilder ()
238226 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+
239231 background_tasks = []
240232
241233 @builder .post_init
@@ -263,6 +255,7 @@ async def error_handler(update, context):
263255
264256 app = builder .build ()
265257 app .add_handler (MessageHandler (filters .ALL , callback ))
258+ app .add_handler (CallbackQueryHandler (callback = callback , pattern = None ))
266259 app .add_error_handler (error_handler )
267260 app .run_polling ()
268261
@@ -311,7 +304,8 @@ def _exclude_unsupported_changes(self, changes):
311304 )
312305 return changes - unsupported
313306
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."""
315309 if not self .channels :
316310 raise BotError (
317311 "At least one channel is required to run a bot. "
0 commit comments