-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdelegate.py
More file actions
420 lines (359 loc) · 13.2 KB
/
Copy pathdelegate.py
File metadata and controls
420 lines (359 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
import traceback
from functools import wraps
from . import exception
from . import flavor, peel, is_event, chat_flavors, inline_flavors
def _wrap_none(fn):
def w(*args, **kwargs):
try:
return fn(*args, **kwargs)
except (KeyError, exception.BadFlavor):
return None
return w
def per_chat_id(types='all'):
"""
:param types:
``all`` or a list of chat types (``private``, ``group``, ``channel``)
:return:
a seeder function that returns the chat id only if the chat type is in ``types``.
"""
return _wrap_none(lambda msg:
msg['chat']['id']
if types == 'all' or msg['chat']['type'] in types
else None)
def per_chat_id_in(s, types='all'):
"""
:param s:
a list or set of chat id
:param types:
``all`` or a list of chat types (``private``, ``group``, ``channel``)
:return:
a seeder function that returns the chat id only if the chat id is in ``s``
and chat type is in ``types``.
"""
return _wrap_none(lambda msg:
msg['chat']['id']
if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] in s
else None)
def per_chat_id_except(s, types='all'):
"""
:param s:
a list or set of chat id
:param types:
``all`` or a list of chat types (``private``, ``group``, ``channel``)
:return:
a seeder function that returns the chat id only if the chat id is *not* in ``s``
and chat type is in ``types``.
"""
return _wrap_none(lambda msg:
msg['chat']['id']
if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] not in s
else None)
def per_from_id(flavors=chat_flavors+inline_flavors):
"""
:param flavors:
``all`` or a list of flavors
:return:
a seeder function that returns the from id only if the message flavor is
in ``flavors``.
"""
return _wrap_none(lambda msg:
msg['from']['id']
if flavors == 'all' or flavor(msg) in flavors
else None)
def per_from_id_in(s, flavors=chat_flavors+inline_flavors):
"""
:param s:
a list or set of from id
:param flavors:
``all`` or a list of flavors
:return:
a seeder function that returns the from id only if the from id is in ``s``
and message flavor is in ``flavors``.
"""
return _wrap_none(lambda msg:
msg['from']['id']
if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] in s
else None)
def per_from_id_except(s, flavors=chat_flavors+inline_flavors):
"""
:param s:
a list or set of from id
:param flavors:
``all`` or a list of flavors
:return:
a seeder function that returns the from id only if the from id is *not* in ``s``
and message flavor is in ``flavors``.
"""
return _wrap_none(lambda msg:
msg['from']['id']
if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] not in s
else None)
def per_inline_from_id():
"""
:return:
a seeder function that returns the from id only if the message flavor
is ``inline_query`` or ``chosen_inline_result``
"""
return per_from_id(flavors=inline_flavors)
def per_inline_from_id_in(s):
"""
:param s: a list or set of from id
:return:
a seeder function that returns the from id only if the message flavor
is ``inline_query`` or ``chosen_inline_result`` and the from id is in ``s``.
"""
return per_from_id_in(s, flavors=inline_flavors)
def per_inline_from_id_except(s):
"""
:param s: a list or set of from id
:return:
a seeder function that returns the from id only if the message flavor
is ``inline_query`` or ``chosen_inline_result`` and the from id is *not* in ``s``.
"""
return per_from_id_except(s, flavors=inline_flavors)
def per_application():
"""
:return:
a seeder function that always returns 1, ensuring at most one delegate is ever spawned
for the entire application.
"""
return lambda msg: 1
def per_message(flavors='all'):
"""
:param flavors: ``all`` or a list of flavors
:return:
a seeder function that returns a non-hashable only if the message flavor
is in ``flavors``.
"""
return _wrap_none(lambda msg: [] if flavors == 'all' or flavor(msg) in flavors else None)
def per_event_source_id(event_space):
"""
:return:
a seeder function that returns an event's source id only if that event's
source space equals to ``event_space``.
"""
def f(event):
if is_event(event):
v = peel(event)
if v['source']['space'] == event_space:
return v['source']['id']
else:
return None
else:
return None
return _wrap_none(f)
def per_callback_query_chat_id(types='all'):
"""
:param types:
``all`` or a list of chat types (``private``, ``group``, ``channel``)
:return:
a seeder function that returns a callback query's originating chat id
if the chat type is in ``types``.
"""
def f(msg):
if (flavor(msg) == 'callback_query' and 'message' in msg
and (types == 'all' or msg['message']['chat']['type'] in types)):
return msg['message']['chat']['id']
else:
return None
return f
def per_callback_query_origin(origins='all'):
"""
:param origins:
``all`` or a list of origin types (``chat``, ``inline``)
:return:
a seeder function that returns a callback query's origin identifier if
that origin type is in ``origins``. The origin identifier is guaranteed
to be a tuple.
"""
def f(msg):
def origin_type_ok():
return (origins == 'all'
or ('chat' in origins and 'message' in msg)
or ('inline' in origins and 'inline_message_id' in msg))
if flavor(msg) == 'callback_query' and origin_type_ok():
if 'inline_message_id' in msg:
return msg['inline_message_id'],
else:
return msg['message']['chat']['id'], msg['message']['message_id']
else:
return None
return f
def per_invoice_payload():
"""
:return:
a seeder function that returns the invoice payload.
"""
def f(msg):
if 'successful_payment' in msg:
return msg['successful_payment']['invoice_payload']
else:
return msg['invoice_payload']
return _wrap_none(f)
def call(func, *args, **kwargs):
"""
:return:
a delegator function that returns a tuple (``func``, (seed tuple,)+ ``args``, ``kwargs``).
That is, seed tuple is inserted before supplied positional arguments.
By default, a thread wrapping ``func`` and all those arguments is spawned.
"""
def f(seed_tuple):
return func, (seed_tuple,)+args, kwargs
return f
def create_run(cls, *args, **kwargs):
"""
:return:
a delegator function that calls the ``cls`` constructor whose arguments being
a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
the object's ``run`` method. By default, a thread wrapping that ``run`` method
is spawned.
"""
def f(seed_tuple):
j = cls(seed_tuple, *args, **kwargs)
return j.run
return f
def create_open(cls, *args, **kwargs):
"""
:return:
a delegator function that calls the ``cls`` constructor whose arguments being
a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns
a looping function that uses the object's ``listener`` to wait for messages
and invokes instance method ``open``, ``on_message``, and ``on_close`` accordingly.
By default, a thread wrapping that looping function is spawned.
"""
def f(seed_tuple):
j = cls(seed_tuple, *args, **kwargs)
def wait_loop():
bot, msg, seed = seed_tuple
try:
handled = j.open(msg, seed)
if not handled:
j.on_message(msg)
while 1:
msg = j.listener.wait()
j.on_message(msg)
# These exceptions are "normal" exits.
except (exception.IdleTerminate, exception.StopListening) as e:
j.on_close(e)
# Any other exceptions are accidents. **Print it out.**
# This is to prevent swallowing exceptions in the case that on_close()
# gets overridden but fails to account for unexpected exceptions.
except Exception as e:
traceback.print_exc()
j.on_close(e)
return wait_loop
return f
def until(condition, fns):
"""
Try a list of seeder functions until a condition is met.
:param condition:
a function that takes one argument - a seed - and returns ``True``
or ``False``
:param fns:
a list of seeder functions
:return:
a "composite" seeder function that calls each supplied function in turn,
and returns the first seed where the condition is met. If the condition
is never met, it returns ``None``.
"""
def f(msg):
for fn in fns:
seed = fn(msg)
if condition(seed):
return seed
return None
return f
def chain(*fns):
"""
:return:
a "composite" seeder function that calls each supplied function in turn,
and returns the first seed that is not ``None``.
"""
return until(lambda seed: seed is not None, fns)
def _ensure_seeders_list(fn):
@wraps(fn)
def e(seeders, *aa, **kw):
return fn(seeders if isinstance(seeders, list) else [seeders], *aa, **kw)
return e
@_ensure_seeders_list
def pair(seeders, delegator_factory, *args, **kwargs):
"""
The basic pair producer.
:return:
a (seeder, delegator_factory(\*args, \*\*kwargs)) tuple.
:param seeders:
If it is a seeder function or a list of one seeder function, it is returned
as the final seeder. If it is a list of more than one seeder function, they
are chained together before returned as the final seeder.
"""
return (chain(*seeders) if len(seeders) > 1 else seeders[0],
delegator_factory(*args, **kwargs))
def _natural_numbers():
x = 0
while 1:
x += 1
yield x
_event_space = _natural_numbers()
def pave_event_space(fn=pair):
"""
:return:
a pair producer that ensures the seeder and delegator share the same event space.
"""
global _event_space
event_space = next(_event_space)
@_ensure_seeders_list
def p(seeders, delegator_factory, *args, **kwargs):
return fn(seeders + [per_event_source_id(event_space)],
delegator_factory, *args, event_space=event_space, **kwargs)
return p
def include_callback_query_chat_id(fn=pair, types='all'):
"""
:return:
a pair producer that enables static callback query capturing
across seeder and delegator.
:param types:
``all`` or a list of chat types (``private``, ``group``, ``channel``)
"""
@_ensure_seeders_list
def p(seeders, delegator_factory, *args, **kwargs):
return fn(seeders + [per_callback_query_chat_id(types=types)],
delegator_factory, *args, include_callback_query=True, **kwargs)
return p
from . import helper
def intercept_callback_query_origin(fn=pair, origins='all'):
"""
:return:
a pair producer that enables dynamic callback query origin mapping
across seeder and delegator.
:param origins:
``all`` or a list of origin types (``chat``, ``inline``).
Origin mapping is only enabled for specified origin types.
"""
origin_map = helper.SafeDict()
# For key functions that returns a tuple as key (e.g. per_callback_query_origin()),
# wrap the key in another tuple to prevent router from mistaking it as
# a key followed by some arguments.
def tuplize(fn):
def tp(msg):
return (fn(msg),)
return tp
router = helper.Router(tuplize(per_callback_query_origin(origins=origins)),
origin_map)
def modify_origin_map(origin, dest, set):
if set:
origin_map[origin] = dest
else:
try:
del origin_map[origin]
except KeyError:
pass
if origins == 'all':
intercept = modify_origin_map
else:
intercept = (modify_origin_map if 'chat' in origins else False,
modify_origin_map if 'inline' in origins else False)
@_ensure_seeders_list
def p(seeders, delegator_factory, *args, **kwargs):
return fn(seeders + [_wrap_none(router.map)],
delegator_factory, *args, intercept_callback_query=intercept, **kwargs)
return p