-
Notifications
You must be signed in to change notification settings - Fork 0
inbox support v1 #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
inbox support v1 #28
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
import contextlib | ||
import json | ||
from unittest import mock | ||
|
||
import pytest | ||
from django.conf import settings | ||
from django.db import connections | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
|
@@ -17,3 +20,66 @@ def github_data(): | |
open(base_path / "query_result.json"), | ||
)["data"]["node"], | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just moved to a global conftest from a different file - since now we have more than one file with async tests. |
||
|
||
# NOTE(artcz) | ||
# The fixture below (fix_async_db) is copied from this issue | ||
# https://github.com/pytest-dev/pytest-asyncio/issues/226 | ||
# it seems to fix the issue and also speed up the test from ~6s down to 1s. | ||
# Thanks to (@gbdlin) for help with debugging. | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def fix_async_db(request): | ||
""" | ||
|
||
If you don't use this fixture for async tests that use the ORM/database | ||
you won't get proper teardown of the database. | ||
This is a bug somehwere in pytest-django, pytest-asyncio or django itself. | ||
|
||
Nobody knows how to solve it, or who should solve it. | ||
Workaround here: https://github.com/django/channels/issues/1091#issuecomment-701361358 | ||
More info: | ||
https://github.com/pytest-dev/pytest-django/issues/580 | ||
https://code.djangoproject.com/ticket/32409 | ||
https://github.com/pytest-dev/pytest-asyncio/issues/226 | ||
|
||
|
||
The actual implementation of this workaround constists on ensuring | ||
Django always returns the same database connection independently of the thread | ||
the code that requests a db connection is in. | ||
|
||
We were unable to use better patching methods (the target is asgiref/local.py), | ||
so we resorted to mocking the _lock_storage context manager so that it returns a Mock. | ||
That mock contains the default connection of the main thread (instead of the connection | ||
of the running thread). | ||
|
||
This only works because our tests only ever use the default connection, which is the only thing our mock returns. | ||
|
||
We apologize in advance for the shitty implementation. | ||
""" | ||
if ( | ||
request.node.get_closest_marker("asyncio") is None | ||
or request.node.get_closest_marker("django_db") is None | ||
): | ||
# Only run for async tests that use the database | ||
yield | ||
return | ||
|
||
main_thread_local = connections._connections | ||
for conn in connections.all(): | ||
conn.inc_thread_sharing() | ||
|
||
main_thread_default_conn = main_thread_local._storage.default | ||
main_thread_storage = main_thread_local._lock_storage | ||
|
||
@contextlib.contextmanager | ||
def _lock_storage(): | ||
yield mock.Mock(default=main_thread_default_conn) | ||
|
||
try: | ||
with mock.patch.object(main_thread_default_conn, "close"): | ||
object.__setattr__(main_thread_local, "_lock_storage", _lock_storage) | ||
yield | ||
finally: | ||
object.__setattr__(main_thread_local, "_lock_storage", main_thread_storage) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import discord | ||
from core.models import DiscordMessage | ||
from core.models import DiscordMessage, InboxItem | ||
from discord.ext import commands, tasks | ||
from django.conf import settings | ||
from django.utils import timezone | ||
|
@@ -10,13 +10,79 @@ | |
|
||
bot = commands.Bot(command_prefix="!", intents=intents) | ||
|
||
# Inbox emoji used for adding messages to user's inbox | ||
INBOX_EMOJI = "📥" | ||
|
||
|
||
@bot.event | ||
async def on_ready(): | ||
print(f"Bot is ready. Logged in as {bot.user}") | ||
poll_database.start() # Start polling the database | ||
|
||
|
||
@bot.event | ||
async def on_raw_reaction_add(payload): | ||
"""Handle adding messages to inbox when users react with the inbox emoji""" | ||
if payload.emoji.name == INBOX_EMOJI: | ||
# Get the channel and message details | ||
channel = bot.get_channel(payload.channel_id) | ||
message = await channel.fetch_message(payload.message_id) | ||
|
||
# Create a new inbox item using async | ||
await InboxItem.objects.acreate( | ||
message_id=str(message.id), | ||
channel_id=str(payload.channel_id), | ||
channel_name=f"#{channel.name}", | ||
server_id=str(payload.guild_id), | ||
user_id=str(payload.user_id), | ||
author=str(message.author.name), | ||
content=message.content, | ||
) | ||
|
||
|
||
@bot.event | ||
async def on_raw_reaction_remove(payload): | ||
"""Handle removing messages from inbox when users remove the inbox emoji""" | ||
if payload.emoji.name == INBOX_EMOJI: | ||
# Remove the inbox item | ||
items = InboxItem.objects.filter( | ||
message_id=str(payload.message_id), | ||
user_id=str(payload.user_id), | ||
) | ||
await items.adelete() | ||
clytaemnestra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
@bot.command() | ||
async def inbox(ctx): | ||
""" | ||
Displays the content of the inbox for the user that calls the command. | ||
|
||
Each message is saved with user_id (which is a discord id), and here we can | ||
filter out all those messages depending on who called the command. | ||
|
||
It retuns all tracked messages, starting from the one most recently saved | ||
(a message that was most recently tagged with inbox emoji, not the message | ||
that was most recently sent). | ||
""" | ||
user_id = str(ctx.message.author.id) | ||
inbox_items = InboxItem.objects.filter(user_id=user_id).order_by("-created_at") | ||
|
||
# Use async query | ||
if not await inbox_items.aexists(): | ||
await ctx.send("Your inbox is empty.") | ||
return | ||
|
||
msg = "Currently tracking the following messages:\n" | ||
|
||
async for item in inbox_items: | ||
msg += "* " + item.summary() + "\n" | ||
|
||
# Create an embed to display the inbox | ||
embed = discord.Embed() | ||
embed.description = msg | ||
await ctx.send(embed=embed) | ||
|
||
|
||
@bot.command() | ||
async def ping(ctx): | ||
await ctx.send("Pong!") | ||
|
@@ -38,19 +104,22 @@ async def wiki(ctx): | |
suppress_embeds=True, | ||
) | ||
|
||
|
||
@bot.command() | ||
async def close(ctx): | ||
channel = ctx.channel | ||
author = ctx.message.author | ||
|
||
# Check if it's a public or private post (thread) | ||
if channel.type in (discord.ChannelType.public_thread, discord.ChannelType.private_thread): | ||
if channel.type in ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. collateral formatting change. Unrelated to the PR 😅 |
||
discord.ChannelType.public_thread, | ||
discord.ChannelType.private_thread, | ||
): | ||
parent = channel.parent | ||
|
||
# Check if the post (thread) was sent in a forum, | ||
# so we can add a tag | ||
if parent.type == discord.ChannelType.forum: | ||
|
||
# Get tag from forum | ||
tag = None | ||
for _tag in parent.available_tags: | ||
|
@@ -65,18 +134,21 @@ async def close(ctx): | |
await ctx.message.delete() | ||
|
||
# Send notification to the thread | ||
await channel.send(f"# This was marked as done by {author.mention}", suppress_embeds=True) | ||
await channel.send( | ||
f"# This was marked as done by {author.mention}", suppress_embeds=True | ||
) | ||
|
||
# We need to archive after adding tags in case it was a forum. | ||
await channel.edit(archived=True) | ||
else: | ||
# Remove command message | ||
await ctx.message.delete() | ||
|
||
await channel.send("The !close command is intended to be used inside a thread/post", | ||
suppress_embeds=True, | ||
delete_after=5) | ||
|
||
await channel.send( | ||
"The !close command is intended to be used inside a thread/post", | ||
suppress_embeds=True, | ||
delete_after=5, | ||
) | ||
|
||
|
||
@bot.command() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Generated by Django 5.1.4 on 2025-03-22 20:28 | ||
|
||
import uuid | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("core", "0003_added_extra_field_to_webhook"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="InboxItem", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
("uuid", models.UUIDField(default=uuid.uuid4)), | ||
("message_id", models.CharField(max_length=255)), | ||
("channel_id", models.CharField(max_length=255)), | ||
("channel_name", models.CharField(max_length=255)), | ||
("server_id", models.CharField(max_length=255)), | ||
("author", models.CharField(max_length=255)), | ||
("user_id", models.CharField(max_length=255)), | ||
("content", models.TextField()), | ||
("created_at", models.DateTimeField(auto_now_add=True)), | ||
], | ||
), | ||
] |
Uh oh!
There was an error while loading. Please reload this page.