Skip to content
This repository has been archived by the owner on Oct 10, 2024. It is now read-only.

Commit

Permalink
Finish submission list command (#485)
Browse files Browse the repository at this point in the history
* ⬆️ update dependencies

* 🚧 add first stages of submission list command

* 🚧 fix slack block formatting issue

* ✨ add new command to get submissions for a user
  • Loading branch information
itsthejoker authored May 15, 2023
1 parent 731ebaf commit f2ff873
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 17 deletions.
32 changes: 30 additions & 2 deletions blossom/api/slack/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import hmac
import logging
import time
from pprint import pprint
from typing import Dict

from django.conf import settings
Expand All @@ -12,7 +13,7 @@
from blossom.api.slack import client
from blossom.api.slack.actions.report import process_submission_report_update
from blossom.api.slack.actions.unclaim import process_unclaim_action
from blossom.api.slack.commands.list_submissions import _process_submission_list
from blossom.api.slack.commands.list_submissions import process_submission_list
from blossom.api.slack.commands.migrate_user import process_migrate_user
from blossom.api.slack.transcription_check.actions import process_check_action
from blossom.strings import translation
Expand All @@ -24,6 +25,9 @@
def process_action(data: Dict) -> None:
"""Process a Slack action, e.g. a button press."""
value: str = data["actions"][0].get("value")
if not value:
# we hit a link button. It's a block action that doesn't have a valid action.
return
if value.startswith("check"):
process_check_action(data)
elif value.startswith("unclaim"):
Expand All @@ -35,14 +39,38 @@ def process_action(data: Dict) -> None:
# buttons related to account gamma migrations
process_migrate_user(data)
elif "submission_list_" in value:
_process_submission_list(data)
process_submission_list(data)
else:
client.chat_postMessage(
channel=data["channel"]["id"],
text=i18n["slack"]["errors"]["unknown_payload"].format(value),
)


def process_modal(data: dict) -> None:
"""Process a slack modal submission with UI components.
Slack UI modal events are very different from block kit events, for reasons
that they do not make entirely clear. As such, modals are keyed off of the
*callback_id* that is listed when building the original View, as opposed to
the action_ids that are the cornerstone of the block kit.
"""
try:
value = data["view"]["callback_id"]
except AttributeError:
print("Something went wrong while processing a modal submission from Slack.")
pprint(data)
return

if value == "submission_list_modal":
process_submission_list(data)
else:
client.chat_postMessage(
channel=data["view"]["response_urls"][0]["channel_id"],
text=i18n["slack"]["errors"]["unknown_username"].format(value),
)


def is_valid_github_request(request: HttpRequest) -> bool:
"""Verify that a webhook from github sponsors is encoded using our key."""
if (github_signature := request.headers.get("x-hub-signature")) is None:
Expand Down
144 changes: 129 additions & 15 deletions blossom/api/slack/commands/list_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@
from datetime import datetime
from uuid import uuid4

from django.db.models import QuerySet
from slack.web.classes.blocks import *
from slack.web.classes.elements import *
from slack.web.classes.views import View

from blossom.api.models import Submission
from blossom.api.slack import client
from blossom.authentication.models import BlossomUser
from blossom.strings import translation

i18n = translation()


def _build_message() -> View:
"""Create the modal view that will display in Slack.
def fmt_date(date_obj: datetime.date) -> str:
"""Convert date or datetime into slack-compatible date string."""
return date_obj.strftime("%Y-%m-%d")

Action IDs:

* submission_list_username_input
* submission_list_select_start_date
* submission_list_select_end_date
"""
def _build_message() -> View:
"""Create the modal view that will display in Slack."""
return View(
type="modal",
title="Get Submissions",
callback_id="submission_list_modal",
submit=PlainTextObject(text="Submit"),
close=PlainTextObject(text="Close"),
blocks=[
Expand All @@ -40,35 +42,147 @@ def _build_message() -> View:
]
),
InputBlock(
block_id="username",
label="Username",
element=PlainTextInputElement(
placeholder="itsthejoker", action_id="submission_list_username_input"
placeholder="itsthejoker", action_id="username_input"
),
),
SectionBlock(
block_id="date_start",
text=PlainTextObject(text="Start Date:"),
accessory=DatePickerElement(
initial_date="2017-04-01",
action_id="submission_list_select_start_date",
action_id="select_start_date",
),
),
SectionBlock(
block_id="date_end",
text=PlainTextObject(text="Start Date:"),
accessory=DatePickerElement(
initial_date=datetime.now(tz=zoneinfo.ZoneInfo("UTC")).strftime("%Y-%m-%d"),
action_id="submission_list_select_end_date",
initial_date=fmt_date(datetime.now(tz=zoneinfo.ZoneInfo("UTC"))),
action_id="select_end_date",
),
),
],
)


def _process_submission_list(data: dict) -> None:
from pprint import pprint
def _build_response_blocks(
submissions: QuerySet[Submission],
user: BlossomUser,
start_date: datetime.date,
end_date: datetime.date,
) -> list[Block]:
if submissions.count() > 48:
# can have a max of 50 blocks in a message, so we need to revert to regular text if we need
# more than that
return []
response = [
SectionBlock(
text=MarkdownTextObject(
text=(
f"Transcriptions by *{user.username}* from *{fmt_date(start_date)}*"
f" to *{fmt_date(end_date)}*:"
)
)
),
DividerBlock(),
]
for submission in submissions:
if submission.tor_url:
# this is a valid transcription and not one of the very old dummy ones.
response += [
SectionBlock(
text=MarkdownTextObject(
text=(
f"*{fmt_date(submission.complete_time)}*"
f" {submission.get_subreddit_name()} | {submission.title}"
)
),
accessory=LinkButtonElement(text="Open on Reddit", url=submission.tor_url),
)
]
else:
response += [
SectionBlock(
text=MarkdownTextObject(
text=f"*{fmt_date(submission.complete_time)}* Dummy Submission"
),
)
]
return response


def _build_raw_text_message(
submissions: QuerySet[Submission],
user: BlossomUser,
start_date: datetime.date,
end_date: datetime.date,
) -> str:
resp = f"""Too many submissions returned, defaulting to text.\n\nTranscriptions by
*{user.username}* from *{fmt_date(start_date)}* to *{fmt_date(end_date)}*:\n\n
"""
for submission in submissions:
if submission.tor_url:
additional_submission = (
f"*{fmt_date(submission.complete_time)}* {submission.get_subreddit_name()}"
f" | {submission.title} | <{submission.tor_url}|ToR Post>"
)
else:
additional_submission = f"*{fmt_date(submission.complete_time)}* | {submission.title}"
resp += additional_submission
return resp


def process_submission_list(data: dict) -> None:
"""Handle the form submission."""
# This processes the modal listed above, so the structure is different from the usual
# actions that we process. See
# https://api.slack.com/reference/interaction-payloads/views#view_submission_example
# for the response body.
channel_id = data["view"]["response_urls"][0]["channel_id"]

username: str = data["view"]["state"]["values"]["username"]["username_input"]["value"]
start_date: datetime.date = datetime.strptime(
data["view"]["state"]["values"]["date_start"]["select_start_date"]["value"], "%Y-%m-%d"
).replace(tzinfo=zoneinfo.ZoneInfo("UTC"))
end_date: datetime.date = datetime.strptime(
data["view"]["state"]["values"]["date_end"]["select_end_date"]["value"], "%Y-%m-%d"
).replace(tzinfo=zoneinfo.ZoneInfo("UTC"))

try:
user = BlossomUser.objects.get(username=username)
except BlossomUser.DoesNotExist:
client.chat_postMessage(
channel=channel_id,
text=i18n["slack"]["errors"]["unknown_username"].format(username=username),
)
return

submissions = Submission.objects.filter(
completed_by=user, complete_time__gte=start_date, complete_time__lte=end_date
)

if submissions.count() == 0:
client.chat_postMessage(
channel=data["view"]["response_urls"][0]["channel_id"],
text=i18n["slack"]["submissions"]["no_submissions"].format(
username=username, start=fmt_date(start_date), end=fmt_date(end_date)
),
)
return

pprint(data)
blocks = _build_response_blocks(submissions, user, start_date, end_date)
if blocks:
client.chat_postMessage(channel=channel_id, blocks=blocks)
else:
client.chat_postMessage(
channel=channel_id,
text=_build_raw_text_message(submissions, user, start_date, end_date),
)


def submissions_cmd(channel: str, _message: str) -> None:
"""Get information from a date range about submissions of a user."""
client.views_open(trigger_id=uuid4(), view=_build_message())
client.views_open(trigger_id=str(uuid4()), view=_build_message())
124 changes: 124 additions & 0 deletions blossom/api/tests/slack/commands/test_submissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import zoneinfo
from datetime import date, datetime
from unittest.mock import patch

from django.test import Client

from blossom.api.models import Submission
from blossom.api.slack.commands.list_submissions import (
_build_message,
_build_raw_text_message,
_build_response_blocks,
fmt_date,
process_submission_list,
submissions_cmd,
)
from blossom.strings import translation
from blossom.utils.test_helpers import (
create_submission,
create_transcription,
create_user,
setup_user_client,
)

i18n = translation()


def test_fmt_date() -> None:
test_date = date(year=2023, month=5, day=14)
assert fmt_date(test_date) == "2023-05-14"

test_datetime = datetime(year=2023, month=5, day=14, hour=8, minute=10)
assert fmt_date(test_datetime) == "2023-05-14"


def test_build_message() -> None:
"""Just verify that the message builds correctly."""
resp = _build_message()
resp.validate_json()
resp.to_dict()


def test_build_response_blocks_fallback() -> None:
test_date = datetime(
year=2023, month=5, day=14, hour=8, minute=10, tzinfo=zoneinfo.ZoneInfo("UTC")
)
user = create_user(username="BonzyTh3Clown")
create_submission(completed_by=user, complete_time=test_date)

resp = _build_response_blocks(Submission.objects.all(), user, test_date, test_date)
assert len(resp) == 3
assert (
resp[0].text.text == "Transcriptions by *BonzyTh3Clown* from *2023-05-14* to *2023-05-14*:"
)
assert resp[2].text.text == "*2023-05-14* Dummy Submission"


def test_build_response_blocks() -> None:
test_date = datetime(
year=2023, month=5, day=14, hour=8, minute=10, tzinfo=zoneinfo.ZoneInfo("UTC")
)
user = create_user()
create_submission(
completed_by=user, complete_time=test_date, tor_url="https://grafeas.org", title="WAAA"
)

resp = _build_response_blocks(Submission.objects.all(), user, test_date, test_date)
assert len(resp) == 3
assert resp[2].text.text == "*2023-05-14* unit_tests | WAAA"


def test_too_many_blocks() -> None:
for _ in range(50):
create_submission(title=_)

user = create_user()
test_date = datetime(year=2023, month=5, day=14, tzinfo=zoneinfo.ZoneInfo("UTC"))
assert _build_response_blocks(Submission.objects.all(), user, test_date, test_date) == []


def test_raw_text_message() -> None:
test_date = datetime(year=2023, month=5, day=14, tzinfo=zoneinfo.ZoneInfo("UTC"))
user = create_user()
create_submission(
completed_by=user, complete_time=test_date, tor_url="https://grafeas.org", title="WAAA"
)

resp = _build_raw_text_message(Submission.objects.all(), user, test_date, test_date)
assert "Too many submissions" in resp
assert "*2023-05-14* unit_tests | WAAA | <https://grafeas.org|ToR Post>" in resp


TEST_DATA = {
"view": {
"state": {
"values": {
"username": {"username_input": {"value": "BonzyTh3Clown"}},
"date_start": {"select_start_date": {"value": "2022-01-01"}},
"date_end": {"select_end_date": {"value": "2023-01-01"}},
}
},
"response_urls": [{"channel_id": "0000"}],
}
}


def test_process_submission_list_invalid_user() -> None:
with patch("blossom.api.slack.commands.block.client.chat_postMessage") as mock:
process_submission_list(TEST_DATA)

assert mock.call_count == 1
assert mock.call_args[1]["text"] == i18n["slack"]["errors"]["unknown_username"].format(
username="BonzyTh3Clown"
)


def test_process_submission_list_no_submissions() -> None:
create_user(username="BonzyTh3Clown")

with patch("blossom.api.slack.commands.block.client.chat_postMessage") as mock:
process_submission_list(TEST_DATA)
assert mock.call_count == 1
assert mock.call_args[1]["text"] == i18n["slack"]["submissions"]["no_submissions"].format(
username="BonzyTh3Clown", start="2022-01-01", end="2023-01-01"
)
Loading

0 comments on commit f2ff873

Please sign in to comment.