async def _get_now_playing(lastfm, formatted_author=None): try: response = (await asyncio.get_event_loop().run_in_executor( None, lambda: requests.get( API_URL, headers={"User-Agent": config["user_agent"]}, params={ "method": "user.getrecenttracks", "api_key": config["lastfm"]["api_key"], "user": lastfm, "limit": 1, "format": "json", }, timeout=5, ), )).json() except JSONDecodeError as e: logger.error(f"Failed to query Last.FM API: {e}.") raise BotError("Failed to query Last.FM API.") if "error" in response: message = (f'Failed to query Last.FM API: {response["error"]} ' f'{response["message"]}.') logger.error(message) raise BotError(message) if not response["recenttracks"]["track"]: raise BotError(f"{formatted_author} has never scrobbled before.") return response["recenttracks"]["track"][0]
async def _get_now_playing(lastfm, author=None): try: response = (await asyncio.get_event_loop().run_in_executor( None, lambda: requests.get( API_URL, headers={'User-Agent': config['user_agent']}, params={ 'method': 'user.getrecenttracks', 'api_key': config['lastfm']['api_key'], 'user': lastfm, 'limit': 1, 'format': 'json', }, timeout=5, ), )).json() except JSONDecodeError as e: logger.error(f'Failed to query Last.FM API: {e}.') raise BotError(f'Failed to query Last.FM API.') if 'error' in response: message = (f'Failed to query Last.FM API: {response["error"]} ' f'{response["message"]}.') logger.error(message) raise BotError(message) if not response['recenttracks']['track']: raise BotError(f'{author} has never scrobbled before.') return response['recenttracks']['track'][0]
def _parse_quote_ids(message): try: quote_ids = [int(qid) for qid in message.split(' ')] except ValueError: raise BotError('Quote IDs must be integers.') if len(quote_ids) > 3: raise BotError('A maximum of three quotes may be queried at once.') return quote_ids
async def call(message): """Hot reload the bot's config and modules.""" try: config.reload() except Exception as e: # noqa: E203 logger.error(f'Error reloading config: {e}') raise BotError('Couldn\'t reload config.') try: load_commands(message.bot, run_setup=False) except Exception as e: # noqa: E203 logger.error(f'Error reloading modules: {e}') raise BotError('Couldn\'t reload modules.') return 'Commands reloaded.'
async def call(message): """Queries the UrbanDictionary API and relays the response.""" entry, search = _parse_args(message.args) response = await _make_request(search) if response["list"]: return trim_message(_get_definition(response, entry), length=400) raise BotError(f'Could not find a definition for {search.rstrip(".")}.')
def _compile_regex(regex, flags): try: if flags["nocase"]: return re.compile(regex, flags=re.IGNORECASE) return re.compile(regex) except re.error: # noqa E722 raise BotError(f"{regex} is not a valid regex.")
def _calculate_time_since_played(track, formatted_author=None): if "@attr" in track: return scrobble_time = datetime.strptime(track["date"]["#text"], "%d %b %Y, %H:%M") diff = datetime.utcnow() - scrobble_time hours, seconds = divmod(diff.seconds, 3600) minutes = divmod(seconds, 60)[0] time_since = [] if diff.days > 0: time_since.append(f"{diff.days} day(s)") if hours > 0: time_since.append(f"{hours} hour(s)") if minutes > 0: time_since.append(f"{minutes} minute(s)") if diff.days > 0 or hours > 0: raise BotError( f"{formatted_author} is not playing anything (last seen: " f'{" ".join(time_since)} ago)') return f"{minutes} minutes"
def wrapper(message): for regex in regexes: match = regex.match(message.contents) if match: message.args = match.groups() return func(message) else: raise BotError("Invalid arguments.")
async def call(message): """Part a channel.""" if message.args: await message.listener.part(message.args[0]) return f"Attempted to part {message.args[0]}." elif message.target != message.author: await message.listener.part(message.target) else: raise BotError("This command must be ran in a channel.")
async def call(message): """Chooses one of many provided options.""" match = re.match(r"(\d+) *- *(\d+)", message.contents) if match: return randint(int(match[1]), int(match[2])) elif "," in message.contents: return choice([c.strip() for c in message.contents.split(",") if c]) elif " " in message.contents: return choice([c.strip() for c in message.contents.split(" ") if c]) raise BotError("No options found.")
async def call(message): """Chooses one of many provided options.""" match = re.match(r'(\d+) *- *(\d+)', message.contents) if match: return randint(int(match[1]), int(match[2])) elif ',' in message.contents: return choice([c.strip() for c in message.contents.split(',') if c]) elif ' ' in message.contents: return choice([c.strip() for c in message.contents.split(' ') if c]) raise BotError('No options found.')
def _get_event_handler(headers): events = { 'push': handle_push, 'issues': handle_issue, 'pull_request': handle_pull_request, } try: return events[headers['X-Github-Event']] except KeyError: logger.info('GitHub request\'s event is unsupported, ignoring.') raise BotError('Unsupported event.')
def _get_event_handler(headers): events = { "push": handle_push, "issues": handle_issue, "pull_request": handle_pull_request, } try: return events[headers["X-Github-Event"]] except KeyError: logger.info("GitHub request's event is unsupported, ignoring.") raise BotError("Unsupported event.")
def _check_signature(payload, headers): secret = config["github_relay"]["secret"] if not secret: return True expected_sig = hmac.new( key=secret.encode(), msg=payload.encode(), digestmod=hashlib.sha1 ).hexdigest() try: return hmac.compare_digest(f"sha1={expected_sig}", headers["X-Hub-Signature"]) except KeyError: raise BotError("Expected signature.")
async def test_handle_message_bot_error(monkeypatch): monkeypatch.setattr("chitanda.bot.config", {"webserver": {"enable": True}}) listener = Mock() listener.message.return_value = Future() listener.message.return_value.set_result(123) chitanda = Chitanda() with patch.object(chitanda, "message_handlers") as msg_handlers: msg_handlers.__iter__.side_effect = BotError("test error") message = Message(chitanda, listener, 2, "azul", 4, 5) await chitanda.handle_message(message) listener.message.assert_called_with(2, "Error: test error")
async def test_handle_message_bot_error(monkeypatch): monkeypatch.setattr('chitanda.bot.config', {'webserver': {'enable': True}}) listener = Mock() listener.message.return_value = Future() listener.message.return_value.set_result(123) chitanda = Chitanda() with patch.object(chitanda, 'message_handlers') as msg_handlers: msg_handlers.__iter__.side_effect = BotError('test error') message = Message(chitanda, listener, 2, 'azul', 4, 5) await chitanda.handle_message(message) listener.message.assert_called_with(2, f'Error: test error')
def _check_signature(payload, headers): secret = config['github_relay']['secret'] if not secret: return True expected_sig = hmac.new(key=secret.encode(), msg=payload.encode(), digestmod=hashlib.sha1).hexdigest() try: return hmac.compare_digest(f'sha1={expected_sig}', headers['X-Hub-Signature']) except KeyError: raise BotError('Expected signature.')
def _get_lastfm_nick(username, listener): with database() as (conn, cursor): cursor.execute( """ SELECT lastfm FROM lastfm WHERE user = ? AND listener = ? """, (username, str(listener)), ) row = cursor.fetchone() if not row: raise BotError("No Last.FM name set.") return row["lastfm"]
async def _make_request(search): future = asyncio.get_event_loop().run_in_executor( None, lambda: requests.get( "https://api.urbandictionary.com/v0/define", headers={"User-Agent": config["user_agent"]}, params={"term": search}, timeout=15, ), ) try: return (await future).json() except (requests.RequestException, JSONDecodeError) as e: logger.error(f"Failed to query UrbanDictionary: {e}") raise BotError("Failed to query UrbanDictionary.")
async def _find_and_replace(message_log, regex, repl, count): parent_connection, child_connection = Pipe() process = Process( target=_sed_and_send_value_over_conn, args=(child_connection, message_log, regex, repl, count), ) process.start() logging.info(f"Started Process {process.pid} for sed command.") for _ in range(int(TIMEOUT / 0.02)): if process.exitcode is not None: logging.info(f"Process {process.pid} has completed its sed.") result = parent_connection.recv() parent_connection.close() return result await asyncio.sleep(0.02) logging.info(f"Process {process.pid}'s sed has timed out, killing...") process.kill() raise BotError("Regex substitution timed out.")
async def _handle_request(bot, request, **kwargs): try: logger.info("Received request from GitHub webhook.") if not _check_signature(await request.text(), request.headers): logger.info("GitHub request contained invalid signature, ignoring.") raise BotError("Invalid signature.") payload = await request.json() repo_cfgs = _get_repo_cfgs(payload["repository"]["id"]) logger.info('Event for repository {payload["repository"]["name"]}.') event_handler = _get_event_handler(request.headers) for cfg in repo_cfgs: asyncio.ensure_future( event_handler(get_listener(bot, cfg["listener"]), payload, cfg) ) return web.Response(body="Received.") except InvalidListener: return web.Response(body="Invalid listener configured.", status=500) except BotError as e: return web.Response(body=e.args[0], status=500)
async def call(message): """Queries the Wolfram|Alpha API and relays the response.""" future = asyncio.get_event_loop().run_in_executor( None, lambda: requests.get( "https://api.wolframalpha.com/v1/result", headers={"User-Agent": config["user_agent"]}, params={ "appid": config["wolframalpha"]["appid"], "i": message.args[0], }, timeout=15, ), ) try: response = (await future).text except requests.RequestException as e: logger.error(f"Failed to query Wolfram|Alpha: {e}") raise BotError("Failed to query Wolfram|Alpha.") return trim_message(response, length=400)
async def call(message): """Queries the Wolfram|Alpha API and relays the response.""" future = asyncio.get_event_loop().run_in_executor( None, lambda: requests.get( 'https://api.wolframalpha.com/v1/result', headers={'User-Agent': config['user_agent']}, params={ 'appid': config['wolframalpha']['appid'], 'i': message.args[0], }, timeout=15, ), ) try: response = (await future).text except requests.RequestException as e: logger.error(f'Failed to query Wolfram|Alpha: {e}') raise BotError('Failed to query Wolfram|Alpha.') return trim_message(response, length=400)
def _calculate_time_since_played(track, author=None): if '@attr' in track: return scrobble_time = datetime.strptime(track['date']['#text'], '%d %b %Y, %H:%M') diff = datetime.utcnow() - scrobble_time hours, seconds = divmod(diff.seconds, 3600) minutes = divmod(seconds, 60)[0] time_since = [] if diff.days > 0: time_since.append(f'{diff.days} day(s)') if hours > 0: time_since.append(f'{hours} hour(s)') if minutes > 0: time_since.append(f'{minutes} minute(s)') if diff.days > 0 or hours > 0: raise BotError(f'{author} is not playing anything (last seen: ' f'{" ".join(time_since)} ago)') return f'{minutes} minutes'
async def wrapper(message): message.username = await message.listener.is_authed(message.author) if not message.username: raise BotError("Identify with NickServ to use this command.") return await func(message)
def confirm_database_is_updated(): if calculate_migrations_needed(): if not len(sys.argv) == 2 or sys.argv[1] != 'migrate': raise BotError( 'The database needs to be migrated. Run `chitanda migrate`.')
def _parse_quote_ids(message): try: quote_ids = [int(qid) for qid in message.split(' ')] except ValueError: raise BotError('Quote IDs must be integers.') return quote_ids
async def wrapper(message): if not await message.listener.is_admin(message.author): raise BotError("Unauthorized.") async for r in func(message): yield r
async def wrapper(message): if not await message.listener.is_admin(message.author): raise BotError("Unauthorized.") return await func(message)
def wrapper(message): if not message.private: return func(message) raise BotError("This command can only be run in a channel.")