Skip to content

Commit e63e341

Browse files
committed
Implement log channel
1 parent 4a7bfdf commit e63e341

21 files changed

Lines changed: 486 additions & 148 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
Please note that the database structure may change at any time, and database migrations between development versions are not available.
99
You will need to drop the database when the structure changes.
1010

11+
### Short-Term TODOs
12+
13+
- [x] Forum implementation, alternative to category/channel structure
14+
- [x] Implement log channel message
15+
- [ ] Convert certain embeds to use component v2
16+
- [ ] "Custom locale" to allow overriding specific locale strings without editing language files
17+
- [ ] Proper pytest unit tests (see how other projects do it)
18+
- [ ] pyinstaller or nuitka compiled build, also with an installer
19+
- [ ] Message attachment support (linked attachment to storage channel)
20+
1121
## Acknowledgements
1222

1323
The current release of Modmail (v5) is a complete rewrite of the original Modmail bot.

modmail/backends/common/client_base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,22 @@ async def create_ticket(self, ticket: TicketModel) -> None:
198198
ticket: The ticket object to create.
199199
200200
Raises:
201+
TicketCreationError: If a ticket with a duplicate key or channel.
201202
TicketRecipientOccupiedError: If the recipient is already in another open ticket.
202203
"""
203204

205+
@abstractmethod
206+
async def set_ticket_log_channel_message_id(self, ticket_key: str, message_id: int | None) -> None:
207+
"""Sets the log channel message ID for a ticket.
208+
209+
Args:
210+
ticket_key: The key of the ticket.
211+
message_id: The message ID to set, or None to clear it.
212+
213+
Raises:
214+
TicketNotFoundError: If the ticket is not found.
215+
"""
216+
204217
@abstractmethod
205218
async def save_message(self, ticket_message: TicketMessageModel) -> None:
206219
"""Saves a message to the database.

modmail/backends/common/models/ticket_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class TicketModel(BaseModel):
3535
status: The current status of the ticket (open, closed, etc.).
3636
closed_by: The user who closed the ticket (if applicable).
3737
closed_at: The timestamp when the ticket was closed (if applicable).
38+
log_channel_message_id: The ID of the thread info message when sent to log channel.
3839
title: An optional title for the ticket.
3940
nsfw: A boolean indicating if the ticket is NSFW (not safe for work).
4041
"""
@@ -53,6 +54,8 @@ class TicketModel(BaseModel):
5354
closed_at: datetime | None = None
5455
closed_by: TicketUserModel | None = None
5556

57+
log_channel_message_id: int | None = None
58+
5659
status: TicketStatus
5760
title: str | None = None
5861
nsfw: bool = False

modmail/backends/mongodb/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,32 @@ async def create_ticket(self, ticket: TicketModel) -> None:
465465
)
466466
logger.info("Created ticket %s for %s.", ticket.key, ticket.recipients)
467467

468+
async def set_ticket_log_channel_message_id(self, ticket_key: str, message_id: int | None) -> None:
469+
"""Sets the log channel message ID for a ticket.
470+
471+
Args:
472+
ticket_key: The key of the ticket.
473+
message_id: The message ID to set, or None to clear it.
474+
475+
Raises:
476+
TicketNotFoundError: If the ticket is not found.
477+
"""
478+
ticket_document = await MongoDBTicketDocument.find_one(
479+
MongoDBTicketDocument.bot_id == self._config.bot.bot_id and MongoDBTicketDocument.key == ticket_key
480+
)
481+
if ticket_document is None:
482+
raise TicketNotFoundError(f"Ticket with key {ticket_key} not found.")
483+
484+
ticket_document.log_channel_message_id = message_id
485+
await ticket_document.replace()
486+
487+
# Update the cache if exists
488+
cached_ticket = self.__open_tickets_cache.get(key=ticket_key)
489+
if cached_ticket is not None:
490+
cached_ticket.log_channel_message_id = message_id
491+
492+
logger.debug("Set log channel message ID for ticket %s to %s.", ticket_key, message_id)
493+
468494
async def get_ticket_by_channel(self, channel_id: int, *, only_open: bool = True) -> TicketModel | None:
469495
"""Get a ticket by channel ID.
470496

modmail/backends/mongodb/convert.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ async def ticket_model_to_document(ticket: TicketModel) -> MongoDBTicketDocument
111111
created_by=created_by,
112112
closed_at=ticket.closed_at,
113113
closed_by=closed_by,
114+
log_channel_message_id=ticket.log_channel_message_id,
114115
status=ticket.status,
115116
title=ticket.title,
116117
nsfw=ticket.nsfw,

modmail/backends/mongodb/models/ticket_model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class MongoDBTicketDocument(Document):
3636
status: The current status of the ticket (open, closed, etc.).
3737
closed_by: The user who closed the ticket (if applicable).
3838
closed_at: The timestamp when the ticket was closed (if applicable).
39+
log_channel_message_id: The ID of the thread info message when sent to log channel.
3940
title: An optional title for the ticket.
4041
nsfw: A boolean indicating if the ticket is NSFW (not safe for work).
4142
"""
@@ -52,6 +53,8 @@ class MongoDBTicketDocument(Document):
5253
closed_at: datetime | None = None
5354
closed_by: Link[MongoDBTicketUserDocument] | None = None
5455

56+
log_channel_message_id: int | None = None
57+
5558
status: TicketStatus
5659
title: str | None = None
5760
nsfw: bool = False
@@ -98,6 +101,7 @@ async def get_model(self) -> TicketModel:
98101
status=self.status,
99102
closed_by=closed_by,
100103
closed_at=self.closed_at,
104+
log_channel_message_id=self.log_channel_message_id,
101105
title=self.title,
102106
nsfw=self.nsfw,
103107
)

modmail/backends/sql/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ async def create_ticket(self, ticket: TicketModel) -> None:
537537
created_by_id=ticket.created_by.user_id,
538538
closed_at=ticket.closed_at,
539539
closed_by_id=ticket.closed_by.user_id if ticket.closed_by else None,
540+
log_channel_message_id=ticket.log_channel_message_id,
540541
status=ticket.status,
541542
title=ticket.title,
542543
nsfw=ticket.nsfw,
@@ -563,6 +564,39 @@ async def create_ticket(self, ticket: TicketModel) -> None:
563564
self.__open_tickets_cache.add(ticket_row, key=ticket_row.key, channel_id=ticket_row.channel_id)
564565
logger.info("Created ticket %s for %s.", ticket.key, ticket.recipients)
565566

567+
async def set_ticket_log_channel_message_id(self, ticket_key: str, message_id: int | None) -> None:
568+
"""Sets the log channel message ID for a ticket.
569+
570+
Args:
571+
ticket_key: The key of the ticket.
572+
message_id: The message ID to set, or None to clear it.
573+
574+
Raises:
575+
TicketNotFoundError: If the ticket is not found.
576+
"""
577+
assert self._async_session is not None, "Session is not initialized."
578+
async with self._async_session() as session:
579+
query = select(SQLTicketTable).where(
580+
and_(
581+
SQLTicketTable.bot_id == self._config.bot.bot_id,
582+
SQLTicketTable.key == ticket_key,
583+
)
584+
)
585+
result = await session.execute(query)
586+
ticket_row = result.scalar_one_or_none()
587+
if ticket_row is None:
588+
raise TicketNotFoundError(f"Ticket with key {ticket_key} not found.")
589+
590+
ticket_row.log_channel_message_id = message_id
591+
await session.commit()
592+
593+
# Update the cache if exists
594+
cached_ticket = self.__open_tickets_cache.get(key=ticket_key)
595+
if cached_ticket is not None:
596+
cached_ticket.log_channel_message_id = message_id
597+
598+
logger.debug("Set log channel message ID for ticket %s to %s.", ticket_key, message_id)
599+
566600
async def get_ticket_by_channel(self, channel_id: int, *, only_open: bool = True) -> TicketModel | None:
567601
"""Get a ticket by channel ID.
568602

modmail/backends/sql/migrations/versions/ec2d46746dcd_init.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def upgrade() -> None:
9494
sa.Enum("open", "closed_by_command", "closed_by_deletion", name="ticketstatus"),
9595
nullable=False,
9696
),
97+
sa.Column("log_channel_message_id", sa.Integer(), nullable=True),
9798
sa.Column("title", sa.String(), nullable=True),
9899
sa.Column("nsfw", sa.Boolean(), nullable=False),
99100
sa.ForeignKeyConstraint(

modmail/backends/sql/models/ticket_model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class SQLTicketTable(SQLBase):
3838
status: The current status of the ticket (open, closed, etc.).
3939
closed_by: The user who closed the ticket (if applicable).
4040
closed_at: The timestamp when the ticket was closed (if applicable).
41+
log_channel_message_id: The ID of the thread info message when sent to log channel.
4142
title: An optional title for the ticket.
4243
nsfw: A boolean indicating if the ticket is NSFW (not safe for work).
4344
"""
@@ -64,6 +65,8 @@ class SQLTicketTable(SQLBase):
6465
)
6566
closed_by: Mapped[SQLTicketUserTable | None] = relationship(foreign_keys=[closed_by_id], lazy="joined")
6667

68+
log_channel_message_id: Mapped[int | None]
69+
6770
status: Mapped[TicketStatus]
6871
title: Mapped[str | None]
6972
nsfw: Mapped[bool] = mapped_column(default=False)
@@ -90,6 +93,7 @@ async def get_model(self) -> TicketModel:
9093
status=self.status,
9194
closed_by=closed_by,
9295
closed_at=self.closed_at,
96+
log_channel_message_id=self.log_channel_message_id,
9397
title=self.title,
9498
nsfw=self.nsfw,
9599
)

modmail/cogs/modmail/commands/close.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,6 @@ async def close_command(
9090
logger.info("Failed to delete the message after closing in %s: %s", ctx.channel, e)
9191
finally:
9292
# Close the ticket
93-
await ticket.close(ctx.author, TicketStatus.closed_by_command)
94-
# TODO: delete channel, create auto delete after close config
93+
await cog.bot.staff_guild.close_ticket(
94+
ticket.model, closer=ctx.author, close_status=TicketStatus.closed_by_command
95+
)

0 commit comments

Comments
 (0)