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)
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)
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)
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
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
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)
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
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')
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)
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
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}')
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.')
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}')
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)
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)
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))
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)
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)
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)
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.')
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)
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
def close_self(): try: self.disconnect() twitchirc.log('info', 'Automatic disconnect: ok.') except Exception as e: twitchirc.log('info', f'Automatic disconnect: fail ({e})')