Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,7 @@ python -m unittest discover -s tests -v
```

Os testes usam transportes locais e não enviam mensagens para pessoas reais no Discord.

### Comando `/daily` (modo Hub)

Use `/daily` em qualquer canal do servidor ou no privado do bot para selecionar um projeto e preencher o daily. No servidor, a seleção e o formulário aparecem só para você. Conecte sua conta em **Minha conta → Integrações** no Hub; apenas projetos em que você participa com daily e Discord ativos em servidores aprovados aparecem. No privado, a lista reúne seus projetos habilitados. Reinicie o bot após atualizar para sincronizar o comando global.
27 changes: 24 additions & 3 deletions src/hub_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def on_submit(self, interaction: discord.Interaction) -> None:
class DailyProjectPickerView(discord.ui.View):
"""Private, paginated project choices; the API rechecks access on selection."""

def __init__(self, bridge: HubBridge, guild_id: str, discord_id: str, projects: list[dict], channel_id: str | None = None) -> None:
def __init__(self, bridge: HubBridge, guild_id: str | None, discord_id: str, projects: list[dict], channel_id: str | None = None) -> None:
super().__init__(timeout=900)
self.bridge = bridge
self.guild_id = guild_id
Expand Down Expand Up @@ -185,7 +185,9 @@ async def open_project(self, interaction: discord.Interaction, project_id: str)
await interaction.response.send_message("Selecione um dos projetos disponíveis no menu.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
payload = {"guildId": self.guild_id, "discordId": str(interaction.user.id)}
selected = next(project for project in self.projects if project["projectId"] == project_id)
guild_id = self.guild_id or selected["guildId"]
payload = {"guildId": guild_id, "discordId": str(interaction.user.id)}
if self.channel_id is not None:
payload["channelId"] = self.channel_id
try:
Expand All @@ -197,7 +199,7 @@ async def open_project(self, interaction: discord.Interaction, project_id: str)
if any(isinstance(answer.get("value"), str) and len(answer["value"]) > 4000 for answer in previous.get("answers", [])) or len(previous.get("blockerDetails") or "") > 4000:
await interaction.followup.send("Este daily contém respostas maiores que o limite do Discord. Para preservar o conteúdo, atualize este relatório pelo Hub.", ephemeral=True)
return
draft = DailyDraft(self.bridge, project_id, self.guild_id, str(interaction.user.id), form)
draft = DailyDraft(self.bridge, project_id, guild_id, str(interaction.user.id), form)
name = discord.utils.escape_markdown(form["projectName"])
await interaction.followup.send(f"**Daily · {name}**\nPreencha as perguntas do projeto. Só você vê este formulário. O envio final ficará disponível para a liderança no Hub.\n\n{draft.prompt()}", view=DailyStepView(draft), ephemeral=True, allowed_mentions=NO_MENTIONS)

Expand Down Expand Up @@ -233,3 +235,22 @@ async def open_daily(self, interaction: discord.Interaction) -> None:
return
view = DailyProjectPickerView(self.bridge, guild_id, str(interaction.user.id), result["projects"], channel_id)
await interaction.followup.send(view.prompt(), view=view, ephemeral=True, allowed_mentions=NO_MENTIONS)


async def open_daily_command(interaction: discord.Interaction, bridge: HubBridge) -> None:
"""Open private choices from any guild channel or the bot DM."""
await interaction.response.defer(ephemeral=True)
guild_id = str(interaction.guild_id) if interaction.guild_id is not None else None
payload = {"discordId": str(interaction.user.id)}
if guild_id is not None:
payload["guildId"] = guild_id
try:
result = await bridge.request("POST", "/projects", payload)
except HubError as exc:
await interaction.followup.send(str(exc), ephemeral=True, allowed_mentions=NO_MENTIONS)
return
if not result["projects"]:
await interaction.followup.send("Nenhum projeto disponível. Confira seu vínculo com o Hub e sua participação em um projeto com daily e Discord habilitados.", ephemeral=True, allowed_mentions=NO_MENTIONS)
return
view = DailyProjectPickerView(bridge, guild_id, str(interaction.user.id), result["projects"])
await interaction.followup.send(view.prompt(), view=view, ephemeral=True, allowed_mentions=NO_MENTIONS)
6 changes: 5 additions & 1 deletion src/hub_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from discord import app_commands

from src.hub_bridge import HubBridge, HubError
from src.hub_daily import HubDailyPanelView, NO_MENTIONS
from src.hub_daily import HubDailyPanelView, NO_MENTIONS, open_daily_command

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -52,6 +52,10 @@ async def link(interaction: discord.Interaction, codigo: str) -> None:
await interaction.followup.send("✅ Conta vinculada ao Hub. Agora você pode usar o botão Registrar Daily. Os lembretes por DM podem ser desativados no Hub.", ephemeral=True)

self.tree.add_command(app_commands.Command(name="hub-vincular", description="Vincular sua conta do Discord ao Kina Hub", callback=link))
async def daily(interaction: discord.Interaction) -> None:
await open_daily_command(interaction, self.bridge)

self.tree.add_command(app_commands.Command(name="daily", description="Selecionar um projeto e preencher seu daily em privado", callback=daily, allowed_contexts=app_commands.AppCommandContext(guild=True, dm_channel=True, private_channel=False), allowed_installs=app_commands.AppInstallationType(guild=True, user=False)))
await self.tree.sync()
self.poll_task = asyncio.create_task(self.poll())

Expand Down
34 changes: 33 additions & 1 deletion tests/test_hub_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aiohttp.test_utils import TestServer

from src.hub_bridge import HubBridge, HubError
from src.hub_daily import DailyDraft, DailyProjectPickerView, DailyStepView, HubDailyModal, HubDailyPanelView, field_pages, parse_boolean
from src.hub_daily import DailyDraft, DailyProjectPickerView, DailyStepView, HubDailyModal, HubDailyPanelView, field_pages, parse_boolean, open_daily_command
from src.hub_main import HubDailyBot


Expand Down Expand Up @@ -110,6 +110,38 @@ async def asyncSetUp(self):
self.bridge.request.return_value = {"projects": self.projects}
self.bridge.project.return_value = form()

async def test_daily_command_in_any_channel_does_not_require_a_panel(self):
event = interaction()
await open_daily_command(event, self.bridge)
self.bridge.request.assert_awaited_once_with("POST", "/projects", {"guildId": "111111111111111111", "discordId": "333333333333333333"})
menu = event.followup.send.call_args.kwargs["view"]
self.assertIsNone(menu.channel_id)
self.assertTrue(event.followup.send.call_args.kwargs["ephemeral"])

async def test_daily_command_in_dm_uses_selected_project_guild(self):
self.bridge.request.return_value = {"projects": [{"projectId": "alpha", "projectName": "Alfa", "guildId": "111111111111111111"}]}
event = interaction(guild_id=None)
await open_daily_command(event, self.bridge)
self.bridge.request.assert_awaited_once_with("POST", "/projects", {"discordId": "333333333333333333"})
menu = event.followup.send.call_args.kwargs["view"]
self.assertTrue(await menu.interaction_check(event))
await menu.open_project(event, "alpha")
self.bridge.project.assert_awaited_once_with("alpha", "form", {"guildId": "111111111111111111", "discordId": "333333333333333333"})
self.assertEqual(event.followup.send.call_args.kwargs["view"].draft.guild_id, "111111111111111111")

async def test_command_registration_allows_guild_and_bot_dm(self):
bot = HubDailyBot(self.bridge, "revision")
bot.tree.sync = AsyncMock()
bot.poll = AsyncMock()
try:
await bot.setup_hook()
command = bot.tree.get_command("daily")
self.assertTrue(command.allowed_contexts.guild)
self.assertTrue(command.allowed_contexts.dm_channel)
self.assertFalse(command.allowed_contexts.private_channel)
finally:
await bot.close()

async def test_persistent_panel_shows_private_choices_before_loading_any_report(self):
panel = HubDailyPanelView(None, self.bridge, {})
self.assertTrue(panel.is_persistent())
Expand Down
Loading