Skip to content
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

fix(webui): rename discord channels when changes made in web #179

Merged
merged 2 commits into from
Oct 7, 2024
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
10 changes: 9 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# https://pre-commit.com
default_install_hook_types: [commit-msg, pre-commit]
default_stages: [commit, manual]
default_stages: [pre-commit, manual]
fail_fast: true
repos:
- repo: "https://github.com/commitizen-tools/commitizen"
Expand Down Expand Up @@ -76,6 +76,14 @@ repos:
- id: djlint
args: ["--configuration", "pyproject.toml"]

# - repo: "https://github.com/pre-commit/mirrors-mypy"
# rev: v1.11.2
# hooks:
# - id: mypy
# name: "check type hints"
# args: [--ignore-missing-imports]
# additional_dependencies: [types-aiofiles]

- repo: local
hooks:
# This calls a custom pre-commit script.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ This project uses [uv](https://docs.astral.sh/uv/) to manage Python requirements
- Run `uv add {package}` from within the development environment to install a run time dependency and add it to `pyproject.toml` and `uv.lock`.
- Run `uv remove {package}` from within the development environment to uninstall a run time dependency and remove it from `pyproject.toml` and `uv.lock`.

## Common Patterns

Documentation for common patterns used in development are available here:

- [Developing the Discord bot](docs/discord.md)
- [Developing the WebUI](docs/webui.md)

## Third Party Package Documentation

Many Python packages are used in this project. Here are some of the most important ones with links to their documentation:
Expand Down
29 changes: 29 additions & 0 deletions docs/discord.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Development Notes for Discord

Code snippets and notes to help you develop the Discord bot with pycord.

## Cogs

### Confirming actions

Use this pattern in any cog to ask for user confirmation before performing an action.

- `hidden` makes the confirmation message ephemeral and viewable only by the author.
- `audit` logs the confirmation in the audit log.

```python
from valentina.discord.views import confirm_action

@something.command()
async def do_something(self, ctx: ValentinaContext) -> None:

is_confirmed, interaction, confirmation_embed = await confirm_action(
ctx, title="Do something", hidden=True, audit=True
)
if not is_confirmed:
return

# Do something ...

await interaction.edit_original_response(embed=confirmation_embed, view=None)
```
3 changes: 3 additions & 0 deletions docs/webui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Development Notes for the Web UI

Code snippets and notes to help you develop the Web UI.
26 changes: 24 additions & 2 deletions src/valentina/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class Emoji(Enum):
WARNING = "⚠️"
WEREWOLF = "🐺"
YES = "✅"
CHANNEL_PLAYER = "👤"
CHANNEL_PRIVATE = "🔒"
CHANNEL_GENERAL = "✨"
CHANNEL_PLAYER_DEAD = "💀"


class LogLevel(StrEnum):
Expand All @@ -106,6 +110,24 @@ class LogLevel(StrEnum):
CRITICAL = "CRITICAL"


class DBSyncUpdateType(str, Enum):
"""Type of update to sync between web and discord."""

CREATE = "create"
UPDATE = "update"
DELETE = "delete"


class DBSyncModelType(str, Enum):
"""Model type to sync between web and discord."""

CAMPAIGN = "campaign"
CHARACTER = "character"
GUILD = "guild"
ROLE = "role"
USER = "user"


class ChannelPermission(Enum):
"""Enum for permissions when creating a character. Default is UNRESTRICTED."""

Expand All @@ -119,8 +141,8 @@ class ChannelPermission(Enum):
class CampaignChannelName(Enum):
"""Enum for common campaign channel names."""

GENERAL = f"{Emoji.SPARKLES.value}-general"
STORYTELLER = f"{Emoji.LOCK.value}-storyteller"
GENERAL = f"{Emoji.CHANNEL_GENERAL.value}-general"
STORYTELLER = f"{Emoji.CHANNEL_PRIVATE.value}-storyteller"


class DiceType(Enum):
Expand Down
101 changes: 15 additions & 86 deletions src/valentina/discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@
import semver
from beanie import UpdateResponse
from beanie.operators import Set
from discord.ext import commands
from discord.ext import commands, tasks
from loguru import logger

from valentina.constants import (
COGS_PATH,
ChannelPermission,
EmbedColor,
LogLevel,
PermissionManageCampaign,
PermissionsGrantXP,
PermissionsKillCharacter,
PermissionsManageTraits,
)
from valentina.discord.models import SyncDiscordFromWebManager
from valentina.models import (
Campaign,
ChangelogPoster,
Expand All @@ -35,8 +35,6 @@
from valentina.utils import ValentinaConfig, errors
from valentina.utils.database import init_database

from valentina.discord.utils.discord_utils import set_channel_perms # isort:skip


# Subclass discord.ApplicationContext to create custom application context
class ValentinaContext(discord.ApplicationContext):
Expand Down Expand Up @@ -367,88 +365,6 @@ async def can_manage_campaign(self) -> bool:

return True

async def channel_update_or_add(
self,
permissions: tuple[ChannelPermission, ChannelPermission, ChannelPermission],
channel: discord.TextChannel | None = None,
name: str | None = None,
topic: str | None = None,
category: discord.CategoryChannel | None = None,
permissions_user_post: discord.User | None = None,
) -> discord.TextChannel: # pragma: no cover
"""Create or update a channel in the guild with specified permissions and attributes.

Create a new text channel or update an existing one based on the provided name. Set permissions for default role, player role, and storyteller role. Automatically grant manage permissions to bot members. If specified, set posting permissions for a specific user.

Args:
permissions (tuple[ChannelPermission, ChannelPermission, ChannelPermission]): Permissions for default role, player role, and storyteller role respectively.
channel (discord.TextChannel, optional): Existing channel to update. Defaults to None.
name (str, optional): Name for the channel. Defaults to None.
topic (str, optional): Topic description for the channel. Defaults to None.
category (discord.CategoryChannel, optional): Category to place the channel in. Defaults to None.
permissions_user_post (discord.User, optional): User to grant posting permissions. Defaults to None.

Returns:
discord.TextChannel: The newly created or updated text channel.
"""
# Fetch roles
player_role = discord.utils.get(self.guild.roles, name="Player")
storyteller_role = discord.utils.get(self.guild.roles, name="Storyteller")

# Initialize permission overwrites
overwrites = {
self.guild.default_role: set_channel_perms(permissions[0]),
player_role: set_channel_perms(permissions[1]),
storyteller_role: set_channel_perms(permissions[2]),
**{
user: set_channel_perms(ChannelPermission.MANAGE)
for user in self.guild.members
if user.bot
},
}

if permissions_user_post:
overwrites[permissions_user_post] = set_channel_perms(ChannelPermission.POST)

formatted_name = name.lower().strip().replace(" ", "-") if name else None

if name and not channel:
for existing_channel in self.guild.text_channels:
# If channel already exists in a specified category, edit it
if (
category
and existing_channel.category == category
and existing_channel.name == formatted_name
) or (not category and existing_channel.name == formatted_name):
logger.debug(f"GUILD: Update channel '{channel.name}' on '{self.guild.name}'")
await existing_channel.edit(
name=formatted_name or channel.name,
overwrites=overwrites,
topic=topic or channel.topic,
category=category or channel.category,
)
return existing_channel

# Create the channel if it doesn't exist
logger.debug(f"GUILD: Create channel '{name}' on '{self.guild.name}'")
return await self.guild.create_text_channel(
name=formatted_name,
overwrites=overwrites,
topic=topic,
category=category,
)

# Update existing channel
logger.debug(f"GUILD: Update channel '{channel.name}' on '{self.guild.name}'")
await channel.edit(
name=name or channel.name,
overwrites=overwrites,
topic=topic or channel.topic,
category=category or channel.category,
)

return channel


class Valentina(commands.Bot):
"""Extend the discord.Bot class to create a custom bot implementation.
Expand All @@ -465,6 +381,7 @@ def __init__(self, version: str, *args: Any, **kwargs: Any):
self.welcomed = False
self.version = version
self.owner_channels = [int(x) for x in ValentinaConfig().owner_channels.split(",")]
self.sync_from_web.start()

# Load Cogs
# #######################
Expand Down Expand Up @@ -689,3 +606,15 @@ async def get_application_context( # type: ignore
ValentinaContext: A custom application context for Valentina bot interactions.
"""
return await super().get_application_context(interaction, cls=cls)

@tasks.loop(minutes=5)
async def sync_from_web(self) -> None:
"""Sync objects from the webui to Discord."""
logger.debug("SYNC: Running sync_from_web task")
sync_discord = SyncDiscordFromWebManager(self)
await sync_discord.run()

@sync_from_web.before_loop
async def before_sync_from_web(self) -> None:
"""Wait for the bot to be ready before starting the sync_from_web task."""
await self.wait_until_ready()
8 changes: 6 additions & 2 deletions src/valentina/discord/characters/add_from_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from valentina.constants import MAX_BUTTONS_PER_ROW, EmbedColor, TraitCategory
from valentina.discord.bot import ValentinaContext
from valentina.discord.models import ChannelManager
from valentina.models import Campaign, Character, CharacterTrait, User
from valentina.utils.helpers import get_max_trait_value

Expand Down Expand Up @@ -200,8 +201,11 @@ async def __finalize_character(

# Create channel
if self.campaign:
await self.character.confirm_channel(self.ctx, self.campaign)
await self.campaign.sort_channels(self.ctx)
channel_manager = ChannelManager(guild=self.ctx.guild, user=self.ctx.author)
await channel_manager.confirm_character_channel(
character=self.character, campaign=self.campaign
)
await channel_manager.sort_campaign_channels(self.campaign)

# Respond to the user
embed = discord.Embed(
Expand Down
9 changes: 7 additions & 2 deletions src/valentina/discord/characters/chargen.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
VampireClan,
)
from valentina.discord.bot import Valentina, ValentinaContext
from valentina.discord.models import ChannelManager
from valentina.discord.views import ChangeNameModal, sheet_embed
from valentina.models import Campaign, Character, CharacterSheetSection, CharacterTrait, User
from valentina.utils import random_num
Expand Down Expand Up @@ -1402,8 +1403,12 @@ async def finalize_character_selection(self, character: Character) -> None:
if self.campaign:
character.campaign = str(self.campaign.id)
await character.save()
await character.confirm_channel(self.ctx, self.campaign)
await self.campaign.sort_channels(self.ctx)

channel_manager = ChannelManager(guild=self.ctx.guild, user=self.ctx.author)
await channel_manager.confirm_character_channel(
character=character, campaign=self.campaign
)
await channel_manager.sort_campaign_channels(self.campaign)

async def spend_freebie_points(self, character: Character) -> Character:
"""Spend freebie points on a character.
Expand Down
23 changes: 17 additions & 6 deletions src/valentina/discord/cogs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from valentina.constants import VALID_IMAGE_EXTENSIONS, RollResultType
from valentina.discord.bot import Valentina, ValentinaContext
from valentina.discord.models import ChannelManager
from valentina.discord.utils.autocomplete import select_any_player_character, select_campaign
from valentina.discord.utils.converters import ValidCampaign, ValidCharacterObject, ValidImageURL
from valentina.discord.utils.discord_utils import assert_permissions
Expand Down Expand Up @@ -75,9 +76,11 @@ async def rebuild_campaign_channels(
return

guild = await Guild.get(ctx.guild.id, fetch_links=True)

channel_manager = ChannelManager(guild=ctx.guild, user=ctx.author)
for campaign in guild.campaigns:
await campaign.delete_channels(ctx)
await campaign.create_channels(ctx)
await channel_manager.delete_campaign_channels(campaign)
await channel_manager.confirm_campaign_channels(campaign)

await msg.edit_original_response(embed=confirmation_embed, view=None)

Expand Down Expand Up @@ -123,7 +126,12 @@ async def associate_campaign(
if not is_confirmed:
return

await character.associate_with_campaign(ctx, campaign)
await character.associate_with_campaign(campaign)

channel_manager = ChannelManager(guild=ctx.guild, user=ctx.author)
await channel_manager.delete_character_channel(character)
await channel_manager.confirm_character_channel(character=character, campaign=campaign)
await channel_manager.sort_campaign_channels(campaign)

await interaction.edit_original_response(embed=confirmation_embed, view=None)

Expand Down Expand Up @@ -151,8 +159,9 @@ async def delete_champaign_channels(
if not is_confirmed:
return

for c in guild.campaigns:
await c.delete_channels(ctx)
channel_manager = ChannelManager(guild=ctx.guild, user=ctx.author)
for campaign in guild.campaigns:
await channel_manager.delete_campaign_channels(campaign)

await interaction.edit_original_response(embed=confirmation_embed, view=None)

Expand Down Expand Up @@ -187,7 +196,9 @@ async def character_delete(
await user.remove_character(character)
await user.save()

await character.delete_channel(ctx)
channel_manager = ChannelManager(guild=ctx.guild, user=ctx.author)
await channel_manager.delete_character_channel(character)

await character.delete(link_rule=DeleteRules.DELETE_LINKS)

await interaction.edit_original_response(embed=confirmation_embed, view=None)
Expand Down
Loading