Example #1
0
    async def _a_wait_for_tasks(self, timeout=0.2):
        if not self._tasks:
            return
        done, _ = await asyncio.wait({i['task'] for i in self._tasks}, timeout=timeout)
        for task in done:
            t = None
            for elem in self._tasks:
                if elem['task'] == task:
                    t = elem
                    break
            if t is not None:
                try:
                    result = await t['task']
                except BaseException as e:
                    for line in traceback.format_exc(1000).split('\n'):
                        twitchirc.log('warn', line)

                    if self.command_error_handler is not None:
                        if inspect.iscoroutinefunction(self.command_error_handler):
                            await self.command_error_handler(e, t['command'], t['source_msg'])
                        else:
                            self.command_error_handler(e, t['command'], t['source_msg'])
                    self._tasks.remove(t)
                    continue
                await self._send_if_possible(result, t['source_msg'])
                self._tasks.remove(t)
Example #2
0
    async def _a_wait_for_tasks(self):
        if not self._tasks:
            return
        done, _ = await asyncio.wait({i['task']
                                      for i in self._tasks},
                                     timeout=0.1)
        # don't pause the bot for long unnecessarily.
        for task in done:
            t = None
            for elem in self._tasks:
                if elem['task'] == task:
                    t = elem
                    break
            if t is not None:
                try:
                    result = await t['task']
                except BaseException as e:

                    for line in traceback.format_exc(1000).split('\n'):
                        twitchirc.log('warn', line)

                    if self.command_error_handler is not None:
                        self.command_error_handler(e, t['command'],
                                                   t['source_msg'])
                    self._tasks.remove(t)
                    continue
                self._send_if_possible(result, t['source_msg'])
                self._tasks.remove(t)
Example #3
0
    async def _platform_recv_loop(self, platform):
        while 1:
            # print(f'Wait for {platform!s} to recv')
            try:
                msgs = await self.clients[platform].receive()
            except Reconnect:
                pre_reconnect = 0.5
                print(f'Waiting for {pre_reconnect}s before reconnecting to {platform!s}...')
                await asyncio.sleep(pre_reconnect)
                print(f'Reconnecting to {platform!s}...')
                await self.reconnect_client(platform)
                print(f'Reconnected to {platform!s}...')
                continue

            # print(f'Done waiting for {platform!s} to recv')
            for i in msgs:
                if util_bot.debug:
                    twitchirc.log('debug', str(i))
                self.call_handlers('any_msg', i)
                if isinstance(i, twitchirc.PingMessage):
                    await self.clients[Platform.TWITCH].send(i.reply())
                elif isinstance(i, twitchirc.ReconnectMessage):
                    await self.reconnect_client(Platform.TWITCH)
                elif isinstance(i, (StandardizedMessage, StandardizedWhisperMessage)):
                    should_cont = await self.acall_middleware('receive', {
                        'message': i
                    }, True)
                    if not should_cont:
                        continue

                    if isinstance(i, StandardizedMessage):
                        self.call_handlers('chat_msg', i)
                    await self._acall_command_handlers(i)
                await self.flush_queue(3)
Example #4
0
    async def acall_middleware(self, action, arguments, cancelable) -> typing.Union[bool,
                                                                                    typing.Tuple[bool, typing.Any]]:
        """
        Call all middleware. Shamelessly taken from my IRC library.

        :param action: Action to run.
        :param arguments: Arguments to give, depends on which action you use, for more info see
        AbstractMiddleware.
        :param cancelable: Can the event be canceled?
        :return: False if the event was canceled, True otherwise.
        """
        event = twitchirc.Event(action, arguments, source=self, cancelable=cancelable)
        canceler: typing.Optional[twitchirc.AbstractMiddleware] = None
        for m in self.middleware:
            if hasattr(m, 'aon_action'):
                await m.aon_action(event)
            else:
                m.on_action(event)
            if not canceler and event.canceled:
                canceler = m
        if event.canceled:
            twitchirc.log('debug', f'Event {action!r} was canceled by {canceler.__class__.__name__}.')
            return False
        if event.result is not None:
            return True, event.result
        return True
Example #5
0
 def receive(self):
     """Receive messages from the server and put them in the :py:attr:`receive_buffer`."""
     message = str(self.socket.recv(4096), 'utf-8',
                   errors='ignore').replace('\r\n', '\n')
     if message == '':
         twitchirc.log('warn', 'Empty message')
         return RECONNECT
     self.receive_buffer += message
Example #6
0
    def cap_reqs(self, use_membership=True):
        """
        Send CAP REQs.

        :param use_membership: Send the membership capability.
        :return: nothing.
        """
        twitchirc.log('debug', f'Sending CAP REQs. Membership: {use_membership}')
        # await self.force_send(f'CAP REQ :twitch.tv/commands twitch.tv/tags'
        #                       f'{" twitch.tv/membership" if use_membership else ""}\r\n')
        self.clients[Platform.TWITCH].connection.cap_reqs(use_membership)
Example #7
0
    async def acheck_permissions(self, message: twitchirc.ChannelMessage, permissions: typing.List[str],
                                 enable_local_bypass=True, disable_handlers=False):
        """
        Check if the user has the required permissions to run a command

        :param message: Message received.
        :param permissions: Permissions required.
        :param enable_local_bypass: If False this function will ignore the permissions \
        `twitchirc.bypass.permission.local.*`. This is useful when creating a command that can change global \
        settings.
        :param disable_handlers: Disables any events/handlers being fired, essentially hiding the call from other \
        machinery
        :return: A list of missing permissions.

        NOTE `permission_error` handlers are called if this function would return a non-empty list.
        """
        if not disable_handlers:
            o = await self.acall_middleware('permission_check', dict(user=message.user, permissions=permissions,
                                                                     message=message,
                                                                     enable_local_bypass=enable_local_bypass),
                                            cancelable=True)

            if o is False:
                return ['impossible.event_canceled']

            if isinstance(o, tuple):
                return o[1]

        missing_permissions = []
        if message.user not in self.permissions:
            missing_permissions = permissions
        else:
            perms = self.permissions.get_permission_state(message)
            if twitchirc.GLOBAL_BYPASS_PERMISSION in perms or \
                    (enable_local_bypass
                     and twitchirc.LOCAL_BYPASS_PERMISSION_TEMPLATE.format(message.channel) in perms):
                return []
            for p in permissions:
                if p not in perms:
                    missing_permissions.append(p)

        if missing_permissions and not disable_handlers:
            twitchirc.log('warn', f'Missing permissions: {missing_permissions} for message {message}')
            self.call_handlers('permission_error', message, None, missing_permissions)
            await self.acall_middleware(
                'permission_error',
                {
                    'message': message,
                    'missing_permissions': missing_permissions
                },
                False
            )
        return missing_permissions
Example #8
0
    def cap_reqs(self, use_membership=True):
        """
        Send CAP REQs.

        :param use_membership: Send the membership capability.
        :return: nothing.
        """
        twitchirc.log('debug',
                      f'Sending CAP REQs. Membership: {use_membership}')
        self.force_send(
            f'CAP REQ :twitch.tv/commands twitch.tv/tags'
            f'{" twitch.tv/membership" if use_membership else ""}\r\n')
Example #9
0
    def call_handlers(self, event, *args):
        """
        Call handlers for `event`

        :param event: The event that happened. See `handlers`
        :param args: Arguments to give to the handler.

        :return: nothing.
        """
        if event not in ['any_msg', 'chat_msg']:
            twitchirc.log('debug', f'Calling handlers for event {event!r} with args {args!r}')
        for h in self.handlers[event]:
            h(event, *args)
Example #10
0
 def reply_to_thread(self, text: str):
     new = self.reply(text)
     thread_id = self.flags.get(
         'reply-parent-msg-id')  # existing reply thread
     if not thread_id:
         thread_id = self.flags.get('id')
         if not thread_id:
             twitchirc.log(
                 'warn',
                 f'Twitch decided not to return an ID for a message? {self.raw_data}'
             )
             return new
     new.flags = {'reply-parent-msg-id': thread_id}
     return new
Example #11
0
    async def send(self, msg: StandardizedMessage, is_reconnect=False, **kwargs):
        o = await self.acall_middleware('send', dict(message=msg, queue=msg.channel), cancelable=True)
        if o is False:
            twitchirc.log('debug', str(msg), ': canceled')
            return

        if msg.platform in self.clients:
            try:
                await self.clients[msg.platform].send(msg)
            except Reconnect as e:
                await self.reconnect_client(e.platform)
                if is_reconnect:
                    raise RuntimeError('Failed to send message even after reconnect')
                return await self.send(msg, is_reconnect=True)
        else:
            raise RuntimeError(f'Cannot send message without a client being present for platform {msg.platform!r}')
Example #12
0
    def connect(self,
                username,
                password: typing.Union[str, None] = None) -> None:
        """
        Connect to the IRC server.

        :param username: Username that will be used.
        :param password: Password to be sent. If None the PASS packet will not be sent.
        """
        self.call_middleware('connect',
                             dict(username=username),
                             cancelable=False)
        twitchirc.info('Connecting...')
        self._connect()
        twitchirc.log('debug', 'Logging in...')
        self._login(username, password)
        twitchirc.log('debug', 'OK.')
Example #13
0
 def _save(self):
     twitchirc.log('debug', f'Saving file {self.file!r}')
     try:
         with open(self.file, 'r') as f:
             try:
                 file_data = json.load(f)
             except json.decoder.JSONDecodeError:
                 pass
             else:
                 if file_data != self._old_data:
                     raise AmbiguousSaveError(
                         'Data in file on disk has changed.')
     except FileNotFoundError:
         pass
     with open(self.file, 'w') as f:
         json.dump(self.data, f)
     self._old_data = copy.deepcopy(self.data)
     twitchirc.log('debug', f'Saved file {self.file!r}')
Example #14
0
    def join(self, channel):
        """
        Join a channel.

        :param channel: Channel you want to join.
        :return: nothing.
        """
        channel = channel.lower().strip('#')
        o = self.call_middleware('join', dict(channel=channel), True)
        if o is False:
            return

        twitchirc.log('debug', 'Joining channel {}'.format(channel))
        self.force_send(f'JOIN #{channel}\r\n')
        self.queue[channel] = []
        self.message_wait[channel] = time.time()
        if channel not in self.channels_connected:
            self.channels_connected.append(channel)
Example #15
0
    async def join(self, channel: str, platform=None):
        """
        Join a channel.

        :param platform: Platform to send this on, depending on this your call maybe ignored by the underlying \
        platform implementation.
        :param channel: Channel you want to join.
        :return: nothing.
        """
        config_name = channel
        channel, platform = self._parse_channel(channel, platform)
        twitchirc.log('info', f'Joining {channel!r} on {platform}')

        o = await self.acall_middleware('join', dict(channel=channel), True)
        if o is False:
            return
        await self.clients[platform].join(channel)
        if config_name not in self.channels_connected:
            self.channels_connected.append(config_name)
Example #16
0
 def call_middleware(self, action, arguments, cancelable) -> typing.Union[bool, typing.Tuple[bool, typing.Any]]:
     if cancelable:
         event = twitchirc.Event(action, arguments, source=self, cancelable=cancelable)
         canceler: typing.Optional[twitchirc.AbstractMiddleware] = None
         for m in self.middleware:
             if hasattr(m, 'aon_action'):
                 warnings.warn('Middleware has async on_action variant, but function was called from sync context')
             else:
                 m.on_action(event)
             if not canceler and event.canceled:
                 canceler = m
         if event.canceled:
             twitchirc.log('debug', f'Event {action!r} was canceled by {canceler.__class__.__name__}.')
             return False
         if event.result is not None:
             return True, event.result
         return True
     else:
         asyncio.get_event_loop().create_task(self.acall_middleware(action, arguments, cancelable))
Example #17
0
    def force_send(self, message: typing.Union[str, twitchirc.Message]):
        """
        Send a message immediately, without making it wait in the queue.
        For queueing a message use :py:meth:`send`.

        :param message: Message to be sent to the server.
        :return: Nothing
        """
        # Call it VIP queue if you wish. :)
        o = self.call_middleware('send',
                                 dict(message=message, queue='__force_send__'),
                                 cancelable=True)
        if o is False:
            return
        if isinstance(o, tuple):
            message = o[1]

        twitchirc.log('debug', 'Force send message: {!r}'.format(message))
        self.queue['misc'].insert(0, to_bytes(message, 'utf-8'))
        self.flush_single_queue('misc', no_cooldown=True)
Example #18
0
    async def _arun(self):
        """
        Brains behind :py:meth:`run`. Doesn't include the `KeyboardInterrupt` handler.

        :return: nothing.
        """
        if self.socket is None:
            self.connect(self.username, self._password)
        self.hold_send = False
        self.call_handlers('start')
        while 1:
            run_result = await self._run_once()
            if run_result is False:
                twitchirc.log('debug', 'break')
                break
            if run_result == RECONNECT:
                self.call_middleware('reconnect', (), False)
                self.disconnect()
                self.connect(self.username, self._password)
            self.scheduler.run(blocking=False)
            await self._a_wait_for_tasks()
            await asyncio.sleep(0)
Example #19
0
    def part(self, channel):
        """
        Leave a channel

        :param channel: Channel you want to leave.
        :return: nothing.
        """
        channel = channel.lower().strip('#')

        o = self.call_middleware('part',
                                 dict(channel=channel),
                                 cancelable=True)
        if o is False:
            return

        twitchirc.log('debug', f'Departing from channel {channel}')
        self.force_send(f'PART #{channel}\r\n')

        self.channels_to_remove.append(channel)

        while channel in self.channels_connected:
            self.channels_connected.remove(channel)
Example #20
0
    def send(self,
             message: typing.Union[str, twitchirc.ChannelMessage],
             queue='misc') -> None:
        """
        Queue a packet to be sent to the server.

        For sending a packet immediately use :py:meth:`force_send`.

        :param message: Message to be sent to the server.
        :param queue: Queue name. This will be automatically overridden if the message is a ChannelMessage. It will \
        be set to `message.channel`.
        :return: Nothing
        """
        o = self.call_middleware('send',
                                 dict(message=message, queue=queue),
                                 cancelable=True)
        if o is False:
            twitchirc.log('debug', str(message), ': canceled')
            return

        if isinstance(message, twitchirc.ChannelMessage):
            if message.user == 'rcfile':
                twitchirc.info(str(message))
                return
            queue = message.channel
            if message.channel in self.last_sent_messages and self.last_sent_messages[
                    message.channel] == message.text:
                message.text += ' \U000e0000'
            self.last_sent_messages[message.channel] = message.text

        if self.socket is not None or self.hold_send:
            twitchirc.log('debug', 'Queued message: {}'.format(message))
            if queue not in self.queue:
                self.queue[queue] = []
                self.message_wait[queue] = time.time()
            self.queue[queue].append(to_bytes(message, 'utf-8'))
        else:
            twitchirc.warn(
                f'Cannot queue message: {message!r}: Not connected.')
Example #21
0
    def send(self,
             message: typing.Union[str, twitchirc.ChannelMessage],
             queue='misc') -> None:
        """
        Send a message to the server.

        :param message: message to send
        :param queue: Queue for the message to be in. This will be automatically overridden if the message is a \
        ChannelMessage. It will be set to :py:meth:`ChannelMessage.channel`.

        :return: nothing

        NOTE The message will not be sent instantly and this is intended. If you would send lots of messages Twitch
        will not forward any of them to the chat clients.
        """
        if self._in_rc_mode:
            if isinstance(message, twitchirc.ChannelMessage):
                twitchirc.log(
                    'info', f'[OUT/{message.channel}, {queue}] {message.text}')
            else:
                twitchirc.log('info', f'[OUT/?, {queue}] {message}')
        else:
            super().send(message, queue)
Example #22
0
    async def _run_once(self):
        """
        Do everything needed to run, but don't loop. This can be used as a non-blocking version of :py:meth:`run`.

        :return: False if the bot should quit, True if everything went right, RECONNECT if the bot needs to reconnect.
        """
        if self.socket is None:  # self.disconnect() was called.
            return False
        if not self._select_socket(
        ):  # no data in socket, assume all messages where handled last time and return
            return True
        twitchirc.log('debug', 'Receiving.')
        o = self.receive()
        if o == RECONNECT:
            return RECONNECT
        twitchirc.log('debug', 'Processing.')
        self.process_messages(100, mode=2)  # process all the messages.
        twitchirc.log('debug', 'Calling handlers.')
        for i in self.receive_queue.copy():
            twitchirc.log('debug', '<', repr(i))
            self.call_handlers('any_msg', i)
            if isinstance(i, twitchirc.PingMessage):
                self.force_send('PONG {}\r\n'.format(i.host))
                if i in self.receive_queue:
                    self.receive_queue.remove(i)
                continue
            elif isinstance(i, twitchirc.ReconnectMessage):
                self.receive_queue.remove(i)
                return RECONNECT
            elif isinstance(i, twitchirc.ChannelMessage):
                self.call_handlers('chat_msg', i)
                await self._acall_command_handlers(i)
            elif isinstance(i, twitchirc.WhisperMessage):
                await self._acall_command_handlers(i)
            if i in self.receive_queue:  # this check may fail if self.part() was called.
                self.receive_queue.remove(i)
        if not self.channels_connected:  # if the bot left every channel, stop processing messages.
            return False
        self.flush_queue(max_messages=100)
        return True
Example #23
0
 def close_self():
     try:
         self.disconnect()
         twitchirc.log('info', 'Automatic disconnect: ok.')
     except Exception as e:
         twitchirc.log('info', f'Automatic disconnect: fail ({e})')