|
| 1 | +import io |
1 | 2 | import time |
2 | 3 | import discord |
3 | 4 | from discord import app_commands |
|
33 | 34 | SHOP_DATA, |
34 | 35 | MODERATOR_ROLE_ID, |
35 | 36 | REDEMPTION_TICKET_CATEGORY_ID, |
| 37 | + REDEMPTION_TRANSCRIPT_CHANNEL_ID, |
36 | 38 | TRIAL_MODERATOR_ROLE_ID, |
37 | 39 | PASSIVE_REWARD_EXCLUDED_CHANNEL_IDS, |
38 | 40 | ) |
@@ -247,6 +249,81 @@ async def handle_redemption_delete_attempt(ctx: commands.Context) -> None: |
247 | 249 | ) |
248 | 250 |
|
249 | 251 |
|
| 252 | +async def _build_redemption_transcript_text( |
| 253 | + channel: discord.TextChannel, |
| 254 | + item: str, |
| 255 | + token_cost: int, |
| 256 | + balance_before: int, |
| 257 | + balance_after: int, |
| 258 | +) -> str: |
| 259 | + opener_raw = _extract_topic_value(channel.topic, "redemption-opener") |
| 260 | + lines: list[str] = [ |
| 261 | + f"Channel: {channel.name}", |
| 262 | + f"Opener ID: {opener_raw or 'Unknown'}", |
| 263 | + f"Item: {item}", |
| 264 | + f"Token Cost: {token_cost}", |
| 265 | + f"Balance Before: {balance_before}", |
| 266 | + f"Balance After: {balance_after}", |
| 267 | + "", |
| 268 | + ] |
| 269 | + async for msg in channel.history(limit=None, oldest_first=True): |
| 270 | + ts = msg.created_at.strftime("%Y-%m-%d %H:%M") |
| 271 | + author = f"{msg.author} ({msg.author.id})" |
| 272 | + content = msg.content or "" |
| 273 | + if msg.attachments: |
| 274 | + attachment_list = ", ".join(a.url for a in msg.attachments) |
| 275 | + if content: |
| 276 | + content += " " |
| 277 | + content += f"[Attachments: {attachment_list}]" |
| 278 | + lines.append(f"[{ts}] {author}: {content}") |
| 279 | + if len(lines) <= 7: |
| 280 | + lines.append("No messages in this ticket.") |
| 281 | + return "\n".join(lines) |
| 282 | + |
| 283 | + |
| 284 | +async def _save_redemption_transcript( |
| 285 | + channel: discord.TextChannel, |
| 286 | + actor: discord.Member, |
| 287 | + item: str, |
| 288 | + token_cost: int, |
| 289 | + balance_before: int, |
| 290 | + balance_after: int, |
| 291 | + outcome: str, |
| 292 | +) -> None: |
| 293 | + transcript_text = await _build_redemption_transcript_text( |
| 294 | + channel, item, token_cost, balance_before, balance_after |
| 295 | + ) |
| 296 | + transcript_bytes = transcript_text.encode("utf-8") |
| 297 | + filename = f"{channel.name}_transcript.txt" |
| 298 | + |
| 299 | + opener_raw = _extract_topic_value(channel.topic, "redemption-opener") |
| 300 | + opener_display = f"<@{opener_raw}>" if opener_raw else "unknown" |
| 301 | + item_display = SHOP_DATA.get(item, {}).get("display", item).replace("**", "") |
| 302 | + |
| 303 | + if outcome == "refunded": |
| 304 | + balance_original = balance_before + token_cost |
| 305 | + outcome_line = f"🔄 **Refunded** | **Balance:** {balance_original:,} → {balance_before:,} → {balance_after:,} R7 tokens" |
| 306 | + else: |
| 307 | + outcome_line = f"✅ **Fulfilled** | **Balance:** {balance_before:,} → {balance_after:,} R7 tokens" |
| 308 | + |
| 309 | + log_channel = ( |
| 310 | + channel.guild.get_channel(REDEMPTION_TRANSCRIPT_CHANNEL_ID) |
| 311 | + if isinstance(REDEMPTION_TRANSCRIPT_CHANNEL_ID, int) |
| 312 | + else None |
| 313 | + ) |
| 314 | + if isinstance(log_channel, discord.TextChannel): |
| 315 | + log_file = discord.File(io.BytesIO(transcript_bytes), filename=filename) |
| 316 | + await log_channel.send( |
| 317 | + content=( |
| 318 | + f"📝 Transcript for redemption ticket **#{channel.name}** " |
| 319 | + f"deleted by **{actor.name}** (opener: {opener_display}).\n" |
| 320 | + f"**Item:** {item_display} | **Cost:** {token_cost:,} R7 tokens\n" |
| 321 | + f"{outcome_line}" |
| 322 | + ), |
| 323 | + file=log_file, |
| 324 | + ) |
| 325 | + |
| 326 | + |
250 | 327 | class RedemptionClosedOptionsView(discord.ui.View): |
251 | 328 | def __init__(self): |
252 | 329 | super().__init__(timeout=None) |
@@ -314,13 +391,26 @@ async def refund_delete_button( |
314 | 391 | opener_raw = _extract_topic_value( |
315 | 392 | interaction.channel.topic, "redemption-opener" |
316 | 393 | ) |
317 | | - item = _extract_topic_value(interaction.channel.topic, "item") |
| 394 | + item = _extract_topic_value(interaction.channel.topic, "item") or "" |
| 395 | + token_cost = _token_price_for_item(item) if item else 0 |
| 396 | + balance_before = 0 |
| 397 | + balance_after = 0 |
318 | 398 | if opener_raw and opener_raw.isdigit() and item: |
319 | 399 | refund_amount = _token_price_for_item(item) |
320 | 400 | if refund_amount > 0: |
321 | | - current_balance = await get_user_balance(opener_raw) |
322 | | - await update_user_balance(opener_raw, current_balance + refund_amount) |
323 | | - |
| 401 | + balance_before = await get_user_balance(opener_raw) |
| 402 | + balance_after = balance_before + refund_amount |
| 403 | + await update_user_balance(opener_raw, balance_after) |
| 404 | + |
| 405 | + await _save_redemption_transcript( |
| 406 | + interaction.channel, |
| 407 | + interaction.user, |
| 408 | + item, |
| 409 | + token_cost, |
| 410 | + balance_before, |
| 411 | + balance_after, |
| 412 | + outcome="refunded", |
| 413 | + ) |
324 | 414 | await interaction.channel.delete( |
325 | 415 | reason=f"Redemption ticket refunded and deleted by {interaction.user}" |
326 | 416 | ) |
@@ -365,6 +455,23 @@ async def budget_delete_button( |
365 | 455 | except ValueError: |
366 | 456 | cost = _budget_cost_for_item(item) |
367 | 457 |
|
| 458 | + opener_raw = _extract_topic_value( |
| 459 | + interaction.channel.topic, "redemption-opener" |
| 460 | + ) |
| 461 | + token_cost = _token_price_for_item(item) if item else 0 |
| 462 | + current_balance = 0 |
| 463 | + if opener_raw and opener_raw.isdigit(): |
| 464 | + current_balance = await get_user_balance(opener_raw) |
| 465 | + |
| 466 | + await _save_redemption_transcript( |
| 467 | + interaction.channel, |
| 468 | + interaction.user, |
| 469 | + item, |
| 470 | + token_cost, |
| 471 | + current_balance + token_cost, |
| 472 | + current_balance, |
| 473 | + outcome="fulfilled", |
| 474 | + ) |
368 | 475 | await add_budget_spent(cost) |
369 | 476 | await interaction.channel.delete( |
370 | 477 | reason=f"Redemption fulfilled and deleted by {interaction.user}" |
|
0 commit comments