Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
3aeb9e6
Adding redis to docker-compose file.
May 16, 2020
5382dd8
Adding redis-py to the Pipfile
May 16, 2020
7a501fd
Boilerplate for the RedisCacheMixin
May 16, 2020
588521c
Refactor - no more mixins!
May 16, 2020
ee8386e
Add basic dict methods for RedisDict.
May 16, 2020
5859711
copy should dictify the .items(), not just keys.
May 16, 2020
9eeee1c
Implements .clear with hash deletion.
May 16, 2020
677a7f7
Implement .get, equality, and membership check
May 17, 2020
0843728
Add fakeredis to the Pipfile
May 17, 2020
7cf0e83
Implement .pop, .popitem and .setdefault.
May 17, 2020
bf6c113
Test suite for the redis dict.
May 17, 2020
258ad9d
Make redis host and port configurable.
May 17, 2020
8456023
namespace "general" -> "global"
May 17, 2020
f19c38c
Merge branch 'master' into redis_persistence
lemonsaurus May 17, 2020
e993566
Fix linting errors introduced by flake8 3.8
May 17, 2020
79f62db
Merge branch 'master' into redis_persistence
lemonsaurus May 17, 2020
adf50a6
Changes discord-py to discord.py in Pipfile
May 17, 2020
ecf7f24
Add the REDIS_PASSWORD environment variable
May 17, 2020
cc3591d
Add the REDIS_PASSWORD environment variable
May 22, 2020
23c2e7a
Replace redis-py with aioredis.
May 22, 2020
3f596d5
Opens a Redis connection in the Bot class.
May 22, 2020
57fe4bf
Set up async testbed
May 22, 2020
fd6f3d3
Fix assertion for `create_task` in duck pond tests
MarkKoz May 22, 2020
45e6f8d
Improve aiohttp context manager mocking in snekbox tests
MarkKoz May 22, 2020
6aed2f6
Fix unawaited coro warning when instantiating Bot for MockBot's spec
MarkKoz May 22, 2020
1ad7833
Properly mock the redis pool in MockBot
MarkKoz May 22, 2020
d8f1634
Use autospecced mocks in MockBot for the stats and aiohttp session
MarkKoz May 22, 2020
eb63fb0
Finish .set and .get, and add tests.
May 23, 2020
c8a9a77
Finish asyncifying RedisCache methods
May 23, 2020
387bf5c
Complete asyncified test suite for RedisCache
May 23, 2020
5bd8e13
Better docstring for RedisCache
May 23, 2020
db75440
Better docstring for RedisCache.contains
May 23, 2020
87c7679
Merge branch 'master' into redis_persistence
lemonsaurus May 23, 2020
54bb228
Merge branch 'redis_persistence' of github.com:python-discord/bot int…
May 23, 2020
fc1999e
Unbreak the error_handler
May 23, 2020
49492ff
Moving the Redis session creation to Bot._recreate
May 23, 2020
489f940
CI needs REDIS_PASSWORD to pass tests
May 23, 2020
1ea4718
Update exception message
lemonsaurus May 23, 2020
aa0bb02
Fix typo in test_to_dict docstring
May 23, 2020
2fb8625
Don't rely on HDEL ignoring keys for .pop
May 23, 2020
5120717
DRY approach to typestring prefix resolution
May 23, 2020
a52a130
Remove redis session mock from MockBot
MarkKoz May 23, 2020
923d03a
Show a warning when redis pool isn't closed
MarkKoz May 23, 2020
96a3ac7
Merge branch 'master' into redis_persistence
lemonsaurus May 23, 2020
3f8dce7
use __name__ for type list
lemonsaurus May 24, 2020
ed12a2f
len(prefix) instead of hardcoding 2
lemonsaurus May 24, 2020
98d8bb3
Refactor the nice prefix/type strings to constants
May 24, 2020
1d05a4d
Improves various docstrings and comments.
May 24, 2020
f05fefb
Better RuntimeErrors.
May 24, 2020
b2009d5
Make .items return ItemsView instead of AsyncIter
May 24, 2020
01bedca
Add .increment and .decrement methods.
May 24, 2020
f80ce10
Rename Bot._redis_ready to Bot.redis_ready
May 24, 2020
361b740
Add logging to the RedisCache.
May 24, 2020
c5e6e8f
MockBot needs to be aware of redis_ready
May 24, 2020
66d273f
Add an option to use fakeredis in Bot.
May 24, 2020
ad8b1fa
Improve error and error testing for increment
May 24, 2020
856cecb
Add support for Union type annotations for constants
MarkKoz May 25, 2020
9b9aa9b
Support validating collection types for constants
MarkKoz May 25, 2020
87d42ad
Improve output of section name in config validation subtests
MarkKoz May 25, 2020
8b5c1aa
Expose the redis port to the host
MarkKoz May 25, 2020
9b882c7
Turn log.exception into log.error
May 26, 2020
723c1d3
Fix edge case where pop might not delete.
May 26, 2020
b630ace
Add better docstring for RedisCache.update
May 26, 2020
1920673
Make self.increment_lock private.
May 26, 2020
46a377d
Improve some docstrings for RedisCache.
May 26, 2020
ec8205c
Swap the order for the validate_cache checks.
May 26, 2020
1ab34dd
Add a test for RuntimeErrors.
May 26, 2020
38fcc10
Merge branch 'master' into redis_persistence
lemonsaurus May 26, 2020
35a1de3
Clear cache in asyncSetUp instead of tests.
May 27, 2020
b189307
Refactor .increment and add lock test.
May 27, 2020
4db313e
Floats are no longer permitted as RedisCache keys.
May 27, 2020
b6093bf
Refactor typestring converters to partialmethods.
May 27, 2020
63b81b0
Fix ATROCIOUS comment.
May 27, 2020
44c3091
Merge branch 'master' into redis_persistence
lemonsaurus May 27, 2020
bdb7bbc
Reduce complexity on some of the typestring stuff.
May 27, 2020
11542fb
Make prefix consts private and more precise.
May 27, 2020
f66a635
Add custom exceptions for each error state.
May 27, 2020
b7c30d4
Prevent a state where a coro could wait forever.
May 28, 2020
cc45960
Move the `self.redis_closed` into session create.
May 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ verify_ssl = true
name = "pypi"

[packages]
discord-py = "~=1.3.2"
aio-pika = "~=6.1"
aiodns = "~=2.0"
aiohttp = "~=3.5"
sphinx = "~=2.2"
markdownify = "~=0.4"
lxml = "~=4.4"
pyyaml = "~=5.1"
aioredis = "~=1.3.1"
beautifulsoup4 = "~=4.9"
colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"}
coloredlogs = "~=14.0"
deepdiff = "~=4.0"
discord.py = "~=1.3.2"
fakeredis = "~=1.4"
feedparser = "~=5.2"
fuzzywuzzy = "~=0.17"
aio-pika = "~=6.1"
lxml = "~=4.4"
markdownify = "~=0.4"
more_itertools = "~=8.2"
python-dateutil = "~=2.8"
deepdiff = "~=4.0"
pyyaml = "~=5.1"
requests = "~=2.22"
more_itertools = "~=8.2"
sentry-sdk = "~=0.14"
coloredlogs = "~=14.0"
colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"}
sphinx = "~=2.2"
statsd = "~=3.3"
feedparser = "~=5.2"
beautifulsoup4 = "~=4.9"

[dev-packages]
coverage = "~=5.0"
Expand Down
308 changes: 192 additions & 116 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
REDDIT_CLIENT_ID: spam
REDDIT_SECRET: ham
WOLFRAM_API_KEY: baz
REDIS_PASSWORD: ''

steps:
- task: UsePythonVersion@0
Expand Down
45 changes: 44 additions & 1 deletion bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from typing import Optional

import aiohttp
import aioredis
import discord
import fakeredis.aioredis
from discord.ext import commands
from sentry_sdk import push_scope

Expand All @@ -28,6 +30,9 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.http_session: Optional[aiohttp.ClientSession] = None
self.redis_session: Optional[aioredis.Redis] = None
self.redis_ready = asyncio.Event()
self.redis_closed = False
self.api_client = api.APIClient(loop=self.loop)

self._connector = None
Expand All @@ -44,6 +49,30 @@ def __init__(self, *args, **kwargs):

self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot")

async def _create_redis_session(self) -> None:
"""
Create the Redis connection pool, and then open the redis event gate.

If constants.Redis.use_fakeredis is True, we'll set up a fake redis pool instead
of attempting to communicate with a real Redis server. This is useful because it
means contributors don't necessarily need to get Redis running locally just
to run the bot.

The fakeredis cache won't have persistence across restarts, but that
usually won't matter for local bot testing.
"""
if constants.Redis.use_fakeredis:
log.info("Using fakeredis instead of communicating with a real Redis server.")
self.redis_session = await fakeredis.aioredis.create_redis_pool()
else:
self.redis_session = await aioredis.create_redis_pool(
address=(constants.Redis.host, constants.Redis.port),
password=constants.Redis.password,
)

self.redis_closed = False
self.redis_ready.set()

def add_cog(self, cog: commands.Cog) -> None:
"""Adds a "cog" to the bot and logs the operation."""
super().add_cog(cog)
Expand Down Expand Up @@ -78,14 +107,20 @@ async def close(self) -> None:
if self.stats._transport:
self.stats._transport.close()

if self.redis_session:
self.redis_closed = True
self.redis_session.close()
self.redis_ready.clear()
await self.redis_session.wait_closed()

async def login(self, *args, **kwargs) -> None:
"""Re-create the connector and set up sessions before logging into Discord."""
self._recreate()
await self.stats.create_socket()
await super().login(*args, **kwargs)

def _recreate(self) -> None:
"""Re-create the connector, aiohttp session, and the APIClient."""
"""Re-create the connector, aiohttp session, the APIClient and the Redis session."""
# Use asyncio for DNS resolution instead of threads so threads aren't spammed.
# Doesn't seem to have any state with regards to being closed, so no need to worry?
self._resolver = aiohttp.AsyncResolver()
Expand All @@ -96,6 +131,14 @@ def _recreate(self) -> None:
"The previous connector was not closed; it will remain open and be overwritten"
)

if self.redis_session and not self.redis_session.closed:
log.warning(
"The previous redis pool was not closed; it will remain open and be overwritten"
)

# Create the redis session
self.loop.create_task(self._create_redis_session())
Comment thread
lemonsaurus marked this conversation as resolved.

# Use AF_INET as its socket family to prevent HTTPS related problems both locally
# and in production.
self._connector = aiohttp.TCPConnector(
Expand Down
4 changes: 2 additions & 2 deletions bot/cogs/antispam.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async def upload_messages(self, actor_id: int, modlog: ModLog) -> None:
await modlog.send_log_message(
icon_url=Icons.filtering,
colour=Colour(Colours.soft_red),
title=f"Spam detected!",
title="Spam detected!",
text=mod_alert_message,
thumbnail=last_message.author.avatar_url_as(static_format="png"),
channel_id=Channels.mod_alerts,
Expand Down Expand Up @@ -130,7 +130,7 @@ async def alert_on_validation_error(self) -> None:
body += "\n\n**The cog has been unloaded.**"

await self.mod_log.send_log_message(
title=f"Error: AntiSpam configuration validation failed!",
title="Error: AntiSpam configuration validation failed!",
text=body,
ping_everyone=True,
icon_url=Icons.token_removed,
Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/defcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async def sync_settings(self) -> None:
else:
self.enabled = False
self.days = timedelta(days=0)
log.info(f"DEFCON disabled")
log.info("DEFCON disabled")

await self.update_channel_topic()

Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/duck_pond.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ async def relay_message(self, message: Message) -> None:
avatar_url=message.author.avatar_url
)
except discord.HTTPException:
log.exception(f"Failed to send an attachment to the webhook")
log.exception("Failed to send an attachment to the webhook")

await message.add_reaction("✅")

Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/help_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ async def init_categories(self) -> None:
self.in_use_category = await self.try_get_channel(constants.Categories.help_in_use)
self.dormant_category = await self.try_get_channel(constants.Categories.help_dormant)
except discord.HTTPException:
log.exception(f"Failed to get a category; cog will be removed")
log.exception("Failed to get a category; cog will be removed")
self.bot.remove_cog(self.qualified_name)

async def init_cog(self) -> None:
Expand Down
10 changes: 5 additions & 5 deletions bot/cogs/moderation/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def apply_infraction(
log.trace(f"Applying {infr_type} infraction #{id_} to {user}.")

# Default values for the confirmation message and mod log.
confirm_msg = f":ok_hand: applied"
confirm_msg = ":ok_hand: applied"

# Specifying an expiry for a note or warning makes no sense.
if infr_type in ("note", "warning"):
Expand Down Expand Up @@ -154,7 +154,7 @@ async def apply_infraction(
self.schedule_task(infraction["id"], infraction)
except discord.HTTPException as e:
# Accordingly display that applying the infraction failed.
confirm_msg = f":x: failed to apply"
confirm_msg = ":x: failed to apply"
expiry_msg = ""
log_content = ctx.author.mention
log_title = "failed to apply"
Expand Down Expand Up @@ -281,7 +281,7 @@ async def pardon_infraction(

log.warning(f"Failed to pardon {infr_type} infraction #{id_} for {user}.")
else:
confirm_msg = f":ok_hand: pardoned"
confirm_msg = ":ok_hand: pardoned"
log_title = "pardoned"

log.info(f"Pardoned {infr_type} infraction #{id_} for {user}.")
Expand Down Expand Up @@ -353,7 +353,7 @@ async def deactivate_infraction(
)
except discord.Forbidden:
log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.")
log_text["Failure"] = f"The bot lacks permissions to do this (role hierarchy?)"
log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)"
log_content = mod_role.mention
except discord.HTTPException as e:
log.exception(f"Failed to deactivate infraction #{id_} ({type_})")
Expand Down Expand Up @@ -402,7 +402,7 @@ async def deactivate_infraction(

# Send a log message to the mod log.
if send_log:
log_title = f"expiration failed" if "Failure" in log_text else "expired"
log_title = "expiration failed" if "Failure" in log_text else "expired"

user = self.bot.get_user(user_id)
avatar = user.avatar_url_as(static_format="png") if user else None
Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/moderation/silence.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def silence(self, ctx: Context, duration: HushDurationConverter = 10) -> N

await ctx.send(f"{Emojis.check_mark} silenced current channel for {duration} minute(s).")
await asyncio.sleep(duration*60)
log.info(f"Unsilencing channel after set delay.")
log.info("Unsilencing channel after set delay.")
await ctx.invoke(self.unsilence)

@commands.command(aliases=("unhush",))
Expand Down
4 changes: 2 additions & 2 deletions bot/cogs/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ async def on_member_join(self, member: Member) -> None:
if member.guild.id != Guild.id:
return

self.bot.stats.gauge(f"guild.total_members", len(member.guild.members))
self.bot.stats.gauge("guild.total_members", len(member.guild.members))

@Cog.listener()
async def on_member_leave(self, member: Member) -> None:
"""Update member count stat on member leave."""
if member.guild.id != Guild.id:
return

self.bot.stats.gauge(f"guild.total_members", len(member.guild.members))
self.bot.stats.gauge("guild.total_members", len(member.guild.members))

@Cog.listener()
async def on_member_update(self, _before: Member, after: Member) -> None:
Expand Down
4 changes: 2 additions & 2 deletions bot/cogs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ async def vote(self, ctx: Context, title: str, *options: str) -> None:
async def send_pep_zero(self, ctx: Context) -> None:
"""Send information about PEP 0."""
pep_embed = Embed(
title=f"**PEP 0 - Index of Python Enhancement Proposals (PEPs)**",
description=f"[Link](https://www.python.org/dev/peps/)"
title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**",
description="[Link](https://www.python.org/dev/peps/)"
)
pep_embed.set_thumbnail(url=ICON_URL)
pep_embed.add_field(name="Status", value="Active")
Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/watchchannels/talentpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str)
return

if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles):
await ctx.send(f":x: Nominating staff members, eh? Here's a cookie :cookie:")
await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:")
return

if not await self.fetch_user_cache():
Expand Down
14 changes: 7 additions & 7 deletions bot/cogs/watchchannels/watchchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def consuming_messages(self) -> bool:
exc = self._consume_task.exception()
if exc:
self.log.exception(
f"The message queue consume task has failed with:",
"The message queue consume task has failed with:",
exc_info=exc
)
return False
Expand Down Expand Up @@ -146,7 +146,7 @@ async def fetch_user_cache(self) -> bool:
try:
data = await self.bot.api_client.get(self.api_endpoint, params=self.api_default_params)
except ResponseCodeError as err:
self.log.exception(f"Failed to fetch the watched users from the API", exc_info=err)
self.log.exception("Failed to fetch the watched users from the API", exc_info=err)
return False

self.watched_users = defaultdict(dict)
Expand All @@ -173,7 +173,7 @@ async def consume_messages(self, delay_consumption: bool = True) -> None:
self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue")
await asyncio.sleep(BigBrotherConfig.log_delay)

self.log.trace(f"Started consuming the message queue")
self.log.trace("Started consuming the message queue")

# If the previous consumption Task failed, first consume the existing comsumption_queue
if not self.consumption_queue:
Expand Down Expand Up @@ -208,7 +208,7 @@ async def webhook_send(
await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed)
except discord.HTTPException as exc:
self.log.exception(
f"Failed to send a message to the webhook",
"Failed to send a message to the webhook",
exc_info=exc
)

Expand Down Expand Up @@ -254,7 +254,7 @@ async def relay_message(self, msg: Message) -> None:
)
except discord.HTTPException as exc:
self.log.exception(
f"Failed to send an attachment to the webhook",
"Failed to send an attachment to the webhook",
exc_info=exc
)

Expand Down Expand Up @@ -326,13 +326,13 @@ def _remove_user(self, user_id: int) -> None:

def cog_unload(self) -> None:
"""Takes care of unloading the cog and canceling the consumption task."""
self.log.trace(f"Unloading the cog")
self.log.trace("Unloading the cog")
if self._consume_task and not self._consume_task.done():
self._consume_task.cancel()
try:
self._consume_task.result()
except asyncio.CancelledError as e:
self.log.exception(
f"The consume task was canceled. Messages may be lost.",
"The consume task was canceled. Messages may be lost.",
exc_info=e
)
23 changes: 17 additions & 6 deletions bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from collections.abc import Mapping
from enum import Enum
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Optional

import yaml

Expand Down Expand Up @@ -198,7 +198,18 @@ class Bot(metaclass=YAMLGetter):

prefix: str
token: str
sentry_dsn: str
sentry_dsn: Optional[str]


class Redis(metaclass=YAMLGetter):
section = "bot"
subsection = "redis"

host: str
port: int
password: Optional[str]
use_fakeredis: bool # If this is True, Bot will use fakeredis.aioredis


class Filter(metaclass=YAMLGetter):
section = "filter"
Expand Down Expand Up @@ -450,7 +461,7 @@ class Guild(metaclass=YAMLGetter):
class Keys(metaclass=YAMLGetter):
section = "keys"

site_api: str
site_api: Optional[str]


class URLs(metaclass=YAMLGetter):
Expand Down Expand Up @@ -493,16 +504,16 @@ class Reddit(metaclass=YAMLGetter):
section = "reddit"

subreddits: list
client_id: str
secret: str
client_id: Optional[str]
secret: Optional[str]


class Wolfram(metaclass=YAMLGetter):
section = "wolfram"

user_limit_day: int
guild_limit_day: int
key: str
key: Optional[str]


class AntiSpam(metaclass=YAMLGetter):
Expand Down
Loading