async def test_voice_unban_user_not_found(self): """Should include info to return dict when user was not found from guild.""" self.guild.get_member.return_value = None self.guild.fetch_member.side_effect = NotFound(Mock(status=404), "Not found") result = await self.cog.pardon_voice_ban(self.user.id, self.guild) self.assertEqual(result, {"Info": "User was not found in the guild."})
async def test_empty_diff_for_db_users_not_in_guild(self): """When the DB knows a user, but the guild doesn't, no difference is found.""" self.bot.api_client.get.return_value = { "count": 3, "next_page_no": None, "previous_page_no": None, "results": [fake_user(), fake_user(id=63, in_guild=False)] } guild = self.get_guild(fake_user()) guild.get_member.side_effect = [ self.get_mock_member(fake_user()), None ] guild.fetch_member.side_effect = NotFound(mock.Mock(status=404), "Not found") actual_diff = await UserSyncer._get_diff(guild) expected_diff = ([], [], None) self.assertEqual(actual_diff, expected_diff)
async def test_diff_sets_in_guild_false_for_leaving_users(self): """When a user leaves the guild, the `in_guild` flag is updated to `False`.""" self.bot.api_client.get.return_value = { "count": 3, "next_page_no": None, "previous_page_no": None, "results": [fake_user(), fake_user(id=63)] } guild = self.get_guild(fake_user()) guild.get_member.side_effect = [ self.get_mock_member(fake_user()), None ] guild.fetch_member.side_effect = NotFound(mock.Mock(status=404), "Not found") actual_diff = await UserSyncer._get_diff(guild) expected_diff = ([], [{"id": 63, "in_guild": False}], None) self.assertEqual(actual_diff, expected_diff)
async def test_diff_for_new_updated_and_leaving_users(self): """When users are added, updated, and removed, all of them are returned properly.""" new_user = fake_user(id=99, name="new") updated_user = fake_user(id=55, name="updated") self.bot.api_client.get.return_value = { "count": 3, "next_page_no": None, "previous_page_no": None, "results": [fake_user(), fake_user(id=55), fake_user(id=63)] } guild = self.get_guild(fake_user(), new_user, updated_user) guild.get_member.side_effect = [ self.get_mock_member(fake_user()), self.get_mock_member(updated_user), None ] guild.fetch_member.side_effect = NotFound(mock.Mock(status=404), "Not found") actual_diff = await UserSyncer._get_diff(guild) expected_diff = ([new_user], [{"id": 55, "name": "updated"}, {"id": 63, "in_guild": False}], None) self.assertEqual(actual_diff, expected_diff)
def request(self, verb, url, payload=None, multipart=None, *, files=None, reason=None): headers = {} data = None files = files or [] if payload: headers['Content-Type'] = 'application/json' data = utils.to_json(payload) if reason: headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') if multipart is not None: data = {'payload_json': multipart.pop('payload_json')} base_url = url.replace(self._request_url, '/') or '/' _id = self._webhook_id for tries in range(5): for file in files: file.reset(seek=tries) r = self.session.request(verb, url, headers=headers, data=data, files=multipart) r.encoding = 'utf-8' # Coerce empty responses to return None for hygiene purposes response = r.text or None # compatibility with aiohttp r.status = r.status_code log.debug('Webhook ID %s with %s %s has returned status code %s', _id, verb, base_url, r.status) if r.headers['Content-Type'] == 'application/json': response = json.loads(response) # check if we have rate limit header information remaining = r.headers.get('X-Ratelimit-Remaining') if remaining == '0' and r.status != 429 and self.sleep: delta = utils._parse_ratelimit_header(r) log.debug('Webhook ID %s has been pre-emptively rate limited, waiting %.2f seconds', _id, delta) time.sleep(delta) if 300 > r.status >= 200: return response # we are being rate limited if r.status == 429: if self.sleep: if not r.headers.get('Via'): # Banned by Cloudflare more than likely. raise HTTPException(r, data) retry_after = response['retry_after'] / 1000.0 log.warning('Webhook ID %s is rate limited. Retrying in %.2f seconds', _id, retry_after) time.sleep(retry_after) continue else: raise HTTPException(r, response) if self.sleep and r.status in (500, 502): time.sleep(1 + tries * 2) continue if r.status == 403: raise Forbidden(r, response) elif r.status == 404: raise NotFound(r, response) else: raise HTTPException(r, response) # no more retries if r.status >= 500: raise DiscordServerError(r, response) raise HTTPException(r, response)
async def request(self, verb, url, payload=None, multipart=None, *, files=None, reason=None): headers = {} data = None files = files or [] if payload: headers['Content-Type'] = 'application/json' data = utils.to_json(payload) if reason: headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') base_url = url.replace(self._request_url, '/') or '/' _id = self._webhook_id for tries in range(5): for file in files: file.reset(seek=tries) if multipart: data = aiohttp.FormData() for key, value in multipart.items(): if key.startswith('file'): data.add_field(key, value[1], filename=value[0], content_type=value[2]) else: data.add_field(key, value) async with self.session.request(verb, url, headers=headers, data=data) as r: log.debug('Webhook ID %s with %s %s has returned status code %s', _id, verb, base_url, r.status) # Coerce empty strings to return None for hygiene purposes response = (await r.text(encoding='utf-8')) or None if r.headers['Content-Type'] == 'application/json': response = json.loads(response) # check if we have rate limit header information remaining = r.headers.get('X-Ratelimit-Remaining') if remaining == '0' and r.status != 429: delta = utils._parse_ratelimit_header(r) log.debug('Webhook ID %s has been pre-emptively rate limited, waiting %.2f seconds', _id, delta) await asyncio.sleep(delta) if 300 > r.status >= 200: return response # we are being rate limited if r.status == 429: if not r.headers.get('Via'): # Banned by Cloudflare more than likely. raise HTTPException(r, data) retry_after = response['retry_after'] / 1000.0 log.warning('Webhook ID %s is rate limited. Retrying in %.2f seconds', _id, retry_after) await asyncio.sleep(retry_after) continue if r.status in (500, 502): await asyncio.sleep(1 + tries * 2) continue if r.status == 403: raise Forbidden(r, response) elif r.status == 404: raise NotFound(r, response) else: raise HTTPException(r, response) # no more retries if r.status >= 500: raise DiscordServerError(r, response) raise HTTPException(r, response)
def request(self: discord.http.HTTPClient, route: discord.http.Route, rate_limit_info: RateLimitInfo, *, header_bypass_delay=None, **kwargs): global global_over bucket = route.bucket method = route.method url = route.url lock = self._locks.get(bucket) if lock is None: lock = asyncio.Lock(loop=self.loop) if bucket is not None: self._locks[bucket] = lock # header creation headers = { 'User-Agent': self.user_agent, } if self.token is not None: headers[ 'Authorization'] = 'Bot ' + self.token if self.bot_token else self.token # some checking if it's a JSON request if 'json' in kwargs: headers['Content-Type'] = 'application/json' kwargs['data'] = utils.to_json(kwargs.pop('json')) kwargs['headers'] = headers if not global_over.is_set(): # wait until the global lock is complete yield from global_over.wait() yield from lock with discord.http.MaybeUnlock(lock) as maybe_lock: for tries in range(5): r = yield from self.session.request(method, url, **kwargs) log.debug( self.REQUEST_LOG.format(method=method, url=url, status=r.status, json=kwargs.get('data'))) try: # even errors have text involved in them so this is safe to call data = yield from discord.http.json_or_text(r) # check if we have rate limit header information rate_limit_info.remaining = r.headers.get( 'X-Ratelimit-Remaining') if rate_limit_info.remaining is not None: rate_limit_info.limit = r.headers['X-Ratelimit-Limit'] rate_limit_info.now = discord.http.parsedate_to_datetime( r.headers['Date']) rate_limit_info.reset = datetime.datetime.fromtimestamp( int(r.headers['X-Ratelimit-Reset']), datetime.timezone.utc) if rate_limit_info.remaining == '0' and r.status != 429: # we've depleted our current bucket if header_bypass_delay is None: now = discord.http.parsedate_to_datetime( r.headers['Date']) reset = datetime.datetime.fromtimestamp( int(r.headers['X-Ratelimit-Reset']), datetime.timezone.utc) delta = (reset - now).total_seconds() else: delta = header_bypass_delay fmt = 'A rate limit bucket has been exhausted (bucket: {bucket}, retry: {delta}).' log.info(fmt.format(bucket=bucket, delta=delta)) maybe_lock.defer() self.loop.call_later(delta, lock.release) # the request was successful so just return the text/json if 300 > r.status >= 200: log.debug( self.SUCCESS_LOG.format(method=method, url=url, text=data)) return data # we are being rate limited if r.status == 429: fmt = 'We are being rate limited. Retrying in {:.2} seconds. Handled under the bucket "{}"' # sleep a bit retry_after = data['retry_after'] / 1000.0 log.info(fmt.format(retry_after, bucket)) # check if it's a global rate limit is_global = data.get('global', False) if is_global: log.info( 'Global rate limit has been hit. Retrying in {:.2} seconds.' .format(retry_after)) global_over.clear() yield from asyncio.sleep(retry_after, loop=self.loop) log.debug('Done sleeping for the rate limit. Retrying...') # release the global lock now that the # global rate limit has passed if is_global: global_over.set() log.debug('Global rate limit is now over.') continue # we've received a 502, unconditional retry if r.status == 502 and tries <= 5: yield from asyncio.sleep(1 + tries * 2, loop=self.loop) continue # the usual error cases if r.status == 403: raise Forbidden(r, data) elif r.status == 404: raise NotFound(r, data) else: raise HTTPException(r, data) finally: # clean-up just in case yield from r.release()