def __launch(self): try: # noinspection PyUnresolvedReferences from asyncio import WindowsSelectorEventLoopPolicy # it can be found in windows, sometimes set_event_loop_policy(WindowsSelectorEventLoopPolicy() ) # prevents an error on closing the loop except Exception as e: debug_ex(e, 'set a less error prone event loop policy', LOG, silent=True) try: self.own_eventloop = True self.eventloop = new_event_loop() self.eventloop.set_exception_handler(self.__handle_exception) self.run( hostname=TWITCH_IRC, port=TWITCH_IRC_PORT, password=self.__token, channels=self.__channels, tls=True, tls_verify=False, ) except Exception as e: LOG.warning('IRC bot launch failed.') debug_ex(e, 'launching IRC bot', LOG)
def __handle_exception(self, loop: AbstractEventLoop, context: dict): message = context['message'] e = context.get('exception', None) e = e if e else RuntimeError('Unspecified exception in event loop') debug_ex( e, f'do something in event loop that ended like this: <{message}>', LOG)
def read_dynamic_config(section: str, ignore: List[T], convert_key: Callable[[str], T] = identity, convert_value: Callable[[str], V] = identity, fallback: V = None) -> Dict[T, V]: result = {} if config.has_section(section): for key, value in config.items(section): try: key = convert_key(key.strip()) except Exception as e: debug_ex(e, f'convert config key [{section}]->{key}: {value}', silent=True) key = None if key in ignore: continue try: value = convert_value(value.strip()) except Exception as e: debug_ex(e, f'convert config value [{section}]->{key}: {value}', silent=True) value = fallback if key and value: result[key] = value return result
def __check_elastic(self, nick: str, user_id: int) -> Optional[UserRank]: if user_id and self.use_elastic: try: manual = ManualUserRank.get(user_id, ignore=[404]) if manual: return manual.rank except Exception as e: debug_ex(e, f'get manual rank for <{nick}>', LOG)
def close(self): """ Revokes token if it was acquired. Intended to be invoked when shutting down the application. """ try: self.__revoke_token() except Exception as e: debug_ex(e, 'revoke token', LOG, silent=True)
def try_write(self, cdlc: dict) -> Any: cdlc_id = cdlc.get('id', None) art = cdlc.get('art', '') try: CustomDLC(_id=cdlc_id).update(art=art) except NotFoundError as e: LOG.warning('Art update failed for CDLC #%s, probably because it has not been loaded yet.', cdlc_id) debug_ex(e, 'fix my sins', LOG, silent=True) return None
def __enter__(self): try: f = open(self.__file, 'rb') except Exception as e: debug_ex(e, f'read file <{self.__file}>', LOG, silent=True) else: with f: self.__contents = json.load(f) self.__contents.sort(key=lambda c: c.get('snapshot_timestamp', 0)) self.__writer.start()
def __parse_config(self, s: str) -> Tuple[int, int]: left, colon, right = s.partition(':') try: left = max(int(left), 0) except Exception as e: debug_ex(e, f'convert {left} to int', silent=True) return 0, 0 try: return left, max(int(right), 0) if right else 0 except Exception as e: debug_ex(e, f'convert {right} to int', silent=True) return left, 0
def __with_cookie_jar(self, options: str, on_file: Callable[[IO], T] = identity, trying_to: str = None) -> T: if self.__cookie_jar_file: try: f = open(self.__cookie_jar_file, options) except Exception as e: if trying_to: debug_ex(e, trying_to, LOG) else: with f: return on_file(f)
def execute(self, sender: str, message: str, respond: ResponseHook) -> bool: """ Parses a message by some sender. If it is an available command, executes it. Sender starting with underscore is considered admin (e.g., _console, _test). Such names are not allowed by twitch by default. Before execution, the command will be checked against user rights and timeouts. Hook will be used to send a response, if any. Most generic errors (e.g. no such command, no rights, global timeout) will simply be ignored. :returns true if execution succeeded, false or None if it failed or never executed in the first place """ if self.__is_command(message): name, space, args = message[1:].partition(' ') name = name.lower() command = self.__commands.get(name, None) if not command: return LOG.warning('No command with alias <%s> exists.', name) if not command.is_available(): return LOG.warning('Command !%s is missing a required module and thus cannot be executed.', name) user = self._users.admin() if self.__is_console_like(sender) else self._users.user(sender) required_rank = self.__required_rank(command) if not user.has_right(required_rank): if self.__just_needs_to_follow(user, required_rank): respond.to_sender(f'Please follow the channel to use !{name}') return LOG.warning('<%s> is not authorized to use !%s.', user, name) if self._downtime and user.is_limited: time_to_wait = self._downtime.remaining(command, user) if time_to_wait: time_words = naturaldelta(time_to_wait) if not self._downtime.is_global(command): respond.to_sender(f'You can use !{name} again in {time_words}') return LOG.warning('<%s> has to wait %s before using !%s again.', user, time_words, name) try: failure = command.execute(user, name, args, respond) if not failure and self._downtime and user.is_limited: self._downtime.remember_use(command, user) return not failure except Exception as e: respond.to_sender('Unexpected error occurred. Please try later') debug_ex(e, f'executing <{command}>', LOG)
def read_config(section: str, key: str, convert: Callable[[str], T] = identity, fallback: T = None, allow_empty: bool = False) -> Optional[T]: value = config.get(section, key, fallback=fallback) if value is None or value is fallback or not allow_empty and not value: return fallback try: return convert(value.strip()) except Exception as e: debug_ex(e, f'convert config value [{section}]->{key}: {value}', silent=True) return fallback
def __lazy_all(self, convert: Callable[[Any], Iterator[T]], skip: int = 0, batch: int = None, **call_params) -> Iterator[T]: batch = batch if Verify.batch_size(batch) else self.__batch_size while True: params = call_params.setdefault('params', {}) params['skip'] = skip params['take'] = batch r = self.__call(**call_params) if not r or not r.text: break try: it = convert(r.json()) first = next(it, NON_EXISTENT) if first is NON_EXISTENT: break yield first yield from it except Exception as e: trying_to = call_params['trying_to'] return debug_ex(e, f'parse response of <{trying_to}> as JSON', LOG) skip += batch
def __call(self, trying_to: str, call: Callable[..., Response], url: str, try_login: bool = True, **kwargs) -> Optional[Response]: kwargs.setdefault('cookies', self.__cookies) try: r = call(url=url, timeout=self.__timeout, allow_redirects=False, **kwargs) except Exception as e: return debug_ex(e, trying_to, LOG) if not try_login or not r.is_redirect or not r.headers.get( 'Location', '') == LOGIN_PAGE: return r if not self.login(): LOG.debug('Cannot %s: automatic login to customsforge failed.', trying_to) return None kwargs.pop('cookies', None) return self.__call(trying_to, call, url, try_login=False, **kwargs)
def __collect_detailed_response(self, r: Response, lines: List[str]): lines.append(f'~ {r.elapsed}s elapsed') lines.append('') lines.append(f'< {r.status_code} {r.reason}') for key, value in r.headers.items(): lines.append(f'< {key}: {value}') if not r.text: return lines.append('< EMPTY >') try: if r.text.strip()[:1] in ['[', '{']: return lines.append(json.dumps(r.json(), indent=4)) except Exception as e: if 'json' in r.headers.get('Content-Type', ''): lines.append('< COULD NOT PARSE JSON BODY >') return debug_ex(e, 'parsing JSON response body', HTTP_DUMP, silent=True) size = len(r.text) lines.append(r.text if size <= self.__max_dump else f'< RESPONSE BODY TOO LARGE ({size} bytes) >')
def _with_elastic(do: str, action: Callable[[Elasticsearch], None]) -> bool: try: action(get_connection()) return True except Exception as e: LOG.warning('Could not %s elastic. Perhaps client is down?', do) return debug_ex(e, f'{do} elastic', LOG, silent=True)
def __check_twitch(self, nick: str, user_id: int) -> Optional[UserRank]: if user_id: with self.__rank_lock: try: cached = self.__rank_cache.get(str(user_id)) if cached: return cached['rank'] is_follower = self.__tw.is_following( self.__streamer, user_id) live = UserRank.FLWR if is_follower else UserRank.VWR self.__rank_cache.set( str(user_id), {'rank': live}, duration=self.__cache_duration(is_follower)) return live except Exception as e: debug_ex(e, f'get live twitch rank for <{nick}>', LOG)
def __create_and_cache(self, command_class, **beans): try: command = command_class(tc=self, commands=self.__commands, **beans) except Exception as e: LOG.warning('Command <!%s> (and aliases) not available. See logs.', command_class.__name__.lower()) return debug_ex(e, f'create command for class {command_class}', LOG, silent=True) self._add_command(command)
def __write_from_queue(self): while not self.__is_done.is_set() or not self.__write_queue.empty(): try: cdlc = self.__write_queue.get(timeout=1) except Empty: continue try: f = self.__get_temp_file() if self.__first_dump: self.__first_dump = False else: f.write(',') f.write(json.dumps(cdlc, indent=4)) except Exception as e: self.__is_broken.set() debug_ex(e, 'write to temp file', LOG) break
def remove_manual(self, nick: str) -> bool: """ Removes manual rank for given nick. Only possible if twitch & elastic index are available. :returns true if rank was removed, false otherwise """ user_id = self.id(nick) if self.use_elastic and user_id: try: return ManualUserRank(_id=user_id).delete() except Exception as e: return debug_ex(e, f'remove rank for <{nick}>', LOG)
def set_manual(self, nick: str, rank: UserRank) -> bool: """ Sets manual rank for given nick. Only possible if twitch & elastic index are available. :returns true if rank was updated, false otherwise """ user_id = self.id(nick) if self.use_elastic and user_id: try: return ManualUserRank(_id=user_id).set_rank(rank) except Exception as e: return debug_ex(e, f'set <{nick}> to rank {rank.name}', LOG)
def get_id(self, nick_or_id: Union[str, int]) -> Optional[int]: """ Coerces given nick or twitch id into twitch id, if possible """ try: with self.__user_id_lock: user = self.__user_cache.get(nick_or_id) if not user: user = self.__call(self.__get_user, nick_or_id) self.__user_cache.update({int(user.id): user, user.login: user}) return int(user.id) except Exception as e: return debug_ex(e, f'find twitch user id for <{nick_or_id}>', LOG)
def close(self): self.__is_done.set() self.__writer.join() if self.__is_broken.is_set(): LOG.error('An error occurred while trying to write to temp file.') if self.__temp_dump: try: with self.__temp_dump: self.__temp_dump.write(']') if not self.__is_broken.is_set(): shutil.copy(self.__temp_dump.name, self.__file) LOG.warning('CDLC JSON dump file ready: %s.', self.__file) except Exception as e: LOG.error('Writing to file <%s> failed. Please check temp file if it still exists: %s', self.__file, self.__temp_dump.name) return debug_ex(e, f'write from temp to file <{self.__file}>', LOG, silent=True) try: os.remove(self.__temp_dump.name) except Exception as e: debug_ex(e, f'clean up temp file {self.__temp_dump.name}', LOG, silent=True)
def prepare_doc(es, doc): try: bulk(es, (d.to_dict(True) for d in prepare_index(doc)), refresh=True) except Exception as e: debug_ex(e, 'prepare elasticsearch index for testing') pytest.skip('Elasticsearch setup failed. See logs for exception.')