def __init__(self, message: BaseMessage, register: Register): self.message = message self.platform = message.get_platform() self.conversation = message.get_conversation() self.user = message.get_user() self.stack = Stack(message.get_layers()) self.register = register self.custom_content = {}
def accept(self, stack: Stack): """ Checks that the stack can be accepted according to the `PATTERNS`. If the pattern is found, then its name is stored in the `annotation` attribute of the stack. """ for name, pattern in self.PATTERNS.items(): if stack.match_exp(pattern): stack.annotation = name return True return False
async def _send_text(self, request: Request, stack: Stack, parse_mode: Optional[Text] = None): """ Base function for sending text """ parts = [] chat_id = request.message.get_chat_id() for layer in stack.layers: if isinstance(layer, (lyr.Text, lyr.RawText, lyr.Markdown)): text = await render(layer.text, request) parts.append(text) for part in parts[:-1]: await self.call( 'sendMessage', text=part, chat_id=chat_id, ) msg = { 'text': parts[-1], 'chat_id': chat_id, } if parse_mode is not None: msg['parse_mode'] = parse_mode await set_reply_markup(msg, request, stack) if stack.has_layer(Reply): reply = stack.get_layer(Reply) if reply.message: msg['reply_to_message_id'] = reply.message['message_id'] if stack.has_layer(Update): update = stack.get_layer(Update) if update.inline_message_id: msg['inline_message_id'] = update.inline_message_id del msg['chat_id'] else: msg['message_id'] = update.message_id await self.call('editMessageText', {'Bad Request: message is not modified'}, **msg) else: await self.call('sendMessage', **msg)
async def _send_typing(self, request: Request, stack: Stack): """ Send to Facebook typing indications """ active = stack.get_layer(lyr.Typing).active msg = ujson.dumps({ 'recipient': { 'id': request.conversation.fbid, }, 'sender_action': 'typing_on' if active else 'typing_off', }) headers = { 'content-type': 'application/json', } params = { 'access_token': self._access_token(request), } post = self.session.post( MESSAGES_ENDPOINT, params=params, data=msg, headers=headers, ) logger.debug('Sending: %s', msg) async with post as r: await self._handle_fb_response(r)
async def _send_generic_template(self, request: Request, stack: Stack): """ Generates and send a generic template. """ gt = stack.get_layer(lyr.FbGenericTemplate) # noinspection PyUnresolvedReferences payload = { 'template_type': 'generic', 'elements': [await e.serialize(request) for e in gt.elements], 'sharable': gt.is_sharable(), } if gt.aspect_ratio: payload['image_aspect_ratio'] = gt.aspect_ratio.value msg = { 'attachment': { 'type': 'template', 'payload': payload } } await self._send(request, msg)
async def _send_sleep(self, request: Request, stack: Stack): """ Sleep for the amount of time specified in the Sleep layer """ duration = stack.get_layer(lyr.Sleep).duration await asyncio.sleep(duration)
class Request(object): """ The request object is generated after each message. Its goal is to provide a comprehensive access to the received message and its context to be used by the transitions and the handlers. """ def __init__(self, message: BaseMessage, register: Register): self.message = message self.platform = message.get_platform() self.conversation = message.get_conversation() self.user = message.get_user() self.stack = Stack(message.get_layers()) self.register = register self.custom_content = {} async def transform(self): await self.stack.transform(self) def get_trans_reg(self, name: Text, default: Any = None) -> Any: """ Convenience function to access the transition register of a specific kind. :param name: Name of the register you want to see :param default: What to return by default """ tr = self.register.get(Register.TRANSITION, {}) return tr.get(name, default) def has_layer(self, class_: Type[L], became: bool = True) -> bool: """ Proxy to stack """ return self.stack.has_layer(class_, became) def get_layer(self, class_: Type[L], became: bool = True) -> L: """ Proxy to stack """ return self.stack.get_layer(class_, became) def get_layers(self, class_: Type[L], became: bool = True) -> List[L]: """ Proxy to stack """ return self.stack.get_layers(class_, became)
def send(self, stack: Layers): """ Intercept any potential "AnswerCallbackQuery" before adding the stack to the output buffer. """ if not isinstance(stack, Stack): stack = Stack(stack) if 'callback_query' in self._update and stack.has_layer(Update): layer = stack.get_layer(Update) try: msg = self._update['callback_query']['message'] except KeyError: layer.inline_message_id = \ self._update['callback_query']['inline_message_id'] else: layer.chat_id = msg['chat']['id'] layer.message_id = msg['message_id'] if stack.has_layer(AnswerCallbackQuery): self._acq = stack.get_layer(AnswerCallbackQuery) stack = Stack([ l for l in stack.layers if not isinstance(l, AnswerCallbackQuery) ]) if stack.has_layer(Reply): layer = stack.get_layer(Reply) if 'message' in self._update: layer.message = self._update['message'] elif 'callback_query' in self._update: layer.message = self._update['callback_query']['message'] if 'inline_query' in self._update \ and stack.has_layer(AnswerInlineQuery): a = stack.get_layer(AnswerInlineQuery) a.inline_query_id = self._update['inline_query']['id'] if stack.layers: return super(TelegramResponder, self).send(stack)
def typify(self, stack: Stack) -> List[Stack]: """ Appends a typing stack after the given stack, but only if required (aka don't have two typing layers following each other). """ if len(stack.layers) == 1 and isinstance(stack.layers[0], lyr.Typing): return [stack] return [stack, Stack([lyr.Typing()])]
async def flush(self, request: Request, stacks: List[Stack]): """ For all stacks to be sent, append a pause after each text layer. """ ns = await self.expand_stacks(request, stacks) ns = self.split_stacks(ns) ns = self.clean_stacks(ns) await self.next(request, [Stack(x) for x in ns])
def handle(self, *layers: BaseLayer): """ Call this method to send a test message. Call it OUTSIDE the async loop. It will return when the message is fully handled. """ self.sent = [] stack = Stack(list(layers)) message = TestMessage(stack) responder = TestResponder(self) run(self._notify(message, responder))
async def _send_generic_template(self, request: Request, stack: Stack): """ Generates and send a generic template. """ gt = stack.get_layer(GenericTemplate) payload = await gt.serialize(request) msg = {'attachment': {'type': 'template', 'payload': payload}} await self._add_qr(stack, msg, request) await self._send(request, msg, stack)
async def _send(self, request: Request, content: Dict[Text, Any], stack: Stack): """ Actually proceed to sending the message to the Facebook API. """ msg = { 'recipient': { 'id': request.conversation.fbid, }, 'message': content, } if stack and stack.has_layer(MessagingType): mt = stack.get_layer(MessagingType) else: mt = MessagingType(response=True) msg.update(mt.serialize()) msg_json = ujson.dumps(msg) headers = { 'content-type': 'application/json', } params = { 'access_token': self._access_token(request), } post = self.session.post( MESSAGES_ENDPOINT, params=params, data=msg_json, headers=headers, ) logger.debug('Sending: %s', msg_json) async with post as r: await self._handle_fb_response(r)
def send(self, stack: Layers): """ Add a message stack to the send list. """ if not isinstance(stack, Stack): stack = Stack(stack) if not self.platform.accept(stack): raise UnacceptableStack('The platform does not allow "{}"'.format( stack.describe())) self._stacks.append(stack)
async def flush(self, request: Request, stacks: List[Stack]): """ Add a typing stack after each stack. """ ns: List[Stack] = [] for stack in stacks: ns.extend(self.typify(stack)) if len(ns) > 1 and ns[-1] == Stack([lyr.Typing()]): ns[-1].get_layer(lyr.Typing).active = False await self.next(request, ns)
async def _send_typing(self, request: Request, stack: Stack): """ In telegram, the typing stops when the message is received. Thus, there is no "typing stops" messages to send. The API is only called when typing must start. """ t = stack.get_layer(lyr.Typing) if t.active: await self.call( 'sendChatAction', chat_id=request.message.get_chat_id(), action='typing', )
async def _send_button_template(self, request: Request, stack: Stack): """ Generates and send a button template. """ gt = stack.get_layer(ButtonTemplate) payload = { 'template_type': 'button', 'text': await render(gt.text, request), 'buttons': [await b.serialize(request) for b in gt.buttons], } msg = {'attachment': {'type': 'template', 'payload': payload}} await self._add_qr(stack, msg, request) await self._send(request, msg, stack)
async def _send_text(self, request: Request, stack: Stack): """ Send text layers to the user. Each layer will go in its own bubble. Also, Facebook limits messages to 320 chars, so if any message is longer than that it will be split into as many messages as needed to be accepted by Facebook. """ parts = [] for layer in stack.layers: if isinstance(layer, (lyr.Text, lyr.RawText)): text = await render(layer.text, request) for part in wrap(text, 320): parts.append(part) for part in parts[:-1]: await self._send(request, { 'text': part, }) part = parts[-1] msg = { 'text': part, } try: qr = stack.get_layer(lyr.QuickRepliesList) except KeyError: pass else: # noinspection PyUnresolvedReferences msg['quick_replies'] = [ await self._make_qr(o, request) for o in qr.options ] await self._send(request, msg)
def __init__(self, message: 'BaseMessage'): from bernard.layers import Stack self.message = message self.stack: Stack = Stack(message.get_layers())
class Request(object): """ The request object is generated after each message. Its goal is to provide a comprehensive access to the received message and its context to be used by the transitions and the handlers. """ QUERY = 'query' HASH = 'hash' def __init__(self, message: BaseMessage, register: Register): self.message = message self.platform = message.get_platform() self.conversation = message.get_conversation() self.user = message.get_user() self.stack = Stack(message.get_layers()) self.register = register self.custom_content = {} self._locale_override = None async def transform(self): await self.stack.transform(self) def get_trans_reg(self, name: Text, default: Any = None) -> Any: """ Convenience function to access the transition register of a specific kind. :param name: Name of the register you want to see :param default: What to return by default """ tr = self.register.get(Register.TRANSITION, {}) return tr.get(name, default) def has_layer(self, class_: Type[L], became: bool = True) -> bool: """ Proxy to stack """ return self.stack.has_layer(class_, became) def get_layer(self, class_: Type[L], became: bool = True) -> L: """ Proxy to stack """ return self.stack.get_layer(class_, became) def get_layers(self, class_: Type[L], became: bool = True) -> List[L]: """ Proxy to stack """ return self.stack.get_layers(class_, became) def set_locale_override(self, locale: Text) -> None: """ This allows to override manually the locale that will be used in replies. :param locale: Name of the locale (format 'fr' or 'fr_FR') """ self._locale_override = locale async def get_locale(self) -> Text: """ Get the locale to use for this request. It's either the overridden locale or the locale provided by the platform. :return: Locale to use for this request """ if self._locale_override: return self._locale_override else: return await self.user.get_locale() async def get_trans_flags(self) -> 'Flags': """ Gives a chance to middlewares to make the translation flags """ from bernard.middleware import MiddlewareManager async def make_flags(request: Request) -> 'Flags': return {} mf = MiddlewareManager.instance().get('make_trans_flags', make_flags) return await mf(self) async def get_token(self) -> Text: """ Returns the auth token from the message """ return await self.message.get_token() async def sign_url(self, url, method=HASH): """ Sign an URL with this request's auth token """ token = await self.get_token() if method == self.QUERY: return patch_qs(url, { settings.WEBVIEW_TOKEN_KEY: token, }) elif method == self.HASH: hash_id = 5 p = list(urlparse(url)) p[hash_id] = quote(token) return urlunparse(p) else: raise ValueError(f'Invalid signing method "{method}"')
def __repr__(self): stack = Stack(self.get_layers()) return f'{self.__class__.__name__}({stack})'
async def _send_inline_answer(self, request: Request, stack: Stack): aiq = stack.get_layer(AnswerInlineQuery) answer = await aiq.serialize(request) await self.call('answerInlineQuery', **answer)