diff --git a/.github/workflows/check-cogs.yml b/.github/workflows/check-cogs.yml
new file mode 100644
index 0000000..31f5fe0
--- /dev/null
+++ b/.github/workflows/check-cogs.yml
@@ -0,0 +1,171 @@
+
+# For tests with nektos/act use:
+# act -e .\.github\workflows\fixtures\push.json -W .\.github\workflows\check-cogs.yml --container-options "-v /c/privat/codex/d-cogs/.artifacts/dist:/tmp/dist:ro" --secret-file .secrets
+
+# To test with local Red-DiscordBot build artifact, mount the host folder containing the built wheels to /tmp/dist in the container.
+# docker run -it --rm -v /c/privat/codex/d-cogs/.artifacts/dist:/tmp/dist python:3.11 bash
+name: "Check Cogs"
+
+on:
+ pull_request:
+
+env:
+ BUILD_ARTIFACT_NAME: "my-build-artifact"
+ COG_PATHS: "dworld" # comma-separated list of cog folder names
+ RPC_PORT: "6133"
+
+jobs:
+ install:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # DEMO: how to install Red-DiscordBot, can install from PyPI directly!
+ - name: Build Red-DiscordBot
+ uses: nntin/d-flows/actions/build-red-discordbot@v1
+ with:
+ red_commit: "" # optional commit SHA
+ artifact_name: ${{ env.BUILD_ARTIFACT_NAME }} # optional artifact name
+
+ test-cogs:
+ runs-on: ubuntu-latest
+ needs: install
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Skip artifact_name if installing from PyPI directly
+ - name: Install Red-DiscordBot
+ uses: nntin/d-flows/actions/install-red-discordbot@v1
+ with:
+ artifact_name: ${{ env.BUILD_ARTIFACT_NAME }} # same artifact name used for build
+
+ # Configure Red-DiscordBot with instance name "tinkerer"
+ # todo: add input for skipping --dry-run
+ - name: Configure Red-DiscordBot
+ uses: nntin/d-flows/actions/setup-red-discordbot@v1
+ with:
+ token: ${{ secrets.DISCORD_BOT_TOKEN }}
+ optional_args: "--no-cogs" # Example optional argument to run without loading any cogs
+ # --dry-run fails due to bug in Red-DiscordBot https://github.com/Cog-Creators/Red-DiscordBot/issues/6572
+ continue-on-error: true
+
+ # For dworld cog d-back and wtforms are required
+ - name: Install dependencies
+ run: |
+ uv pip install d-back wtforms --system
+
+ # Actual magic: test that cogs can be loaded/unloaded via RPC
+ - name: Test cogs via RPC
+ uses: nntin/d-flows/actions/test-red-discordbot@v1
+ with:
+ token: ${{ secrets.DISCORD_BOT_TOKEN }}
+ cog_paths: ${{ env.COG_PATHS }}
+ rpc_port: ${{ env.RPC_PORT }}
+
+ ##################################################################
+ #### Build validation results for Discord notification ####
+ ##################################################################
+ build-validation-fields:
+ name: Build Validation Fields
+ runs-on: ubuntu-latest
+ needs: [install, test-cogs]
+ if: always()
+ outputs:
+ discord_fields: ${{ steps.discord_fields.outputs.fields }}
+ validation_result: ${{ steps.validation_result.outputs.result }}
+
+ steps:
+ - name: Determine validation result
+ id: validation_result
+ run: |
+ BUILD_STATUS="${{ needs.install.result }}"
+ TEST_STATUS="${{ needs['test-cogs'].result }}"
+
+ if [[ "$BUILD_STATUS" == "success" && "$TEST_STATUS" == "success" ]]; then
+ echo "result=success" >> "$GITHUB_OUTPUT"
+ else
+ echo "result=failed" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Build Discord Fields
+ id: discord_fields
+ run: |
+ BUILD_STATUS="${{ needs.install.result }}"
+ TEST_STATUS="${{ needs['test-cogs'].result }}"
+
+ BUILD_EMOJI=$([[ "$BUILD_STATUS" == "success" ]] && echo "✅" || echo "❌")
+ TEST_EMOJI=$([[ "$TEST_STATUS" == "success" ]] && echo "✅" || echo "❌")
+
+ FIELDS=$(jq -n \
+ --arg build_status "$BUILD_EMOJI $BUILD_STATUS" \
+ --arg test_status "$TEST_EMOJI $TEST_STATUS" \
+ --arg cogs "${{ env.COG_PATHS }}" \
+ --arg artifact "${{ env.BUILD_ARTIFACT_NAME }}" \
+ --arg actor "${{ github.actor }}" \
+ --arg pr_number "${{ github.event.pull_request.number }}" \
+ --arg pr_url "${{ github.event.pull_request.html_url }}" \
+ --arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
+ '[
+ {"name": "Build Red-DiscordBot", "value": $build_status, "inline": true},
+ {"name": "Cog Tests", "value": $test_status, "inline": true},
+ {"name": "Cogs Tested", "value": $cogs, "inline": true},
+ {"name": "Artifact", "value": $artifact, "inline": true},
+ {"name": "PR Details", "value": ("[#" + $pr_number + "](" + $pr_url + ") by @" + $actor), "inline": false},
+ {"name": "Run Details", "value": ("[View Full Run](" + $run_url + ")"), "inline": false}
+ ]')
+
+ delimiter="$(openssl rand -hex 8)"
+ {
+ echo "fields<<${delimiter}"
+ echo "$FIELDS"
+ echo "${delimiter}"
+ } >> "$GITHUB_OUTPUT"
+
+ ##################################################################
+ #### Display validation results via Step Summary ####
+ ##################################################################
+ display-validation-summary:
+ name: Display Validation Summary
+ runs-on: ubuntu-latest
+ needs: [install, test-cogs, build-validation-fields]
+ if: always()
+
+ steps:
+ - name: Write Step Summary
+ uses: nntin/d-flows/actions/step-summary@v1
+ with:
+ title: 'Cog Validation Results'
+ markdown: |
+ | Stage | Status |
+ |-------|--------|
+ | Build Red-DiscordBot | ${{ needs.install.result == 'success' && '✅ Passed' || '❌ Failed' }} |
+ | Cog Tests | ${{ needs['test-cogs'].result == 'success' && '✅ Passed' || '❌ Failed' }} |
+
+ **Artifact**: `${{ env.BUILD_ARTIFACT_NAME }}`
+
+ **Cogs Tested**: `${{ env.COG_PATHS }}`
+
+ **Overall Result**: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '✅ All checks passed' || '⚠️ Some checks failed or were skipped' }}
+ overwrite: false
+
+ ##################################################################
+ #### Display validation results via Discord ####
+ ##################################################################
+ notify-cog-validation:
+ name: Notify Validation Results
+ runs-on: ubuntu-latest
+ needs: [build-validation-fields, display-validation-summary]
+ if: always()
+
+ steps:
+ - name: Send Discord Notification
+ uses: nntin/d-flows/actions/discord-notify@v1
+ with:
+ webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message_type: 'embed'
+ title: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '✅ Cog Validation Completed Successfully' || '⚠️ Cog Validation Completed with Issues' }}
+ description: >-
+ ${{ needs.build-validation-fields.outputs.validation_result == 'success' && format('All cogs ({0}) passed build and RPC validation.', env.COG_PATHS) || format('One or more stages failed for cogs: {0}. Review the workflow logs.', env.COG_PATHS) }}
+ color: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '3066993' || '16776960' }}
+ fields: ${{ needs.build-validation-fields.outputs.discord_fields }}
\ No newline at end of file
diff --git a/ampremover/ampremover.py b/ampremover/ampremover.py
index 4ff289a..ee51903 100644
--- a/ampremover/ampremover.py
+++ b/ampremover/ampremover.py
@@ -5,6 +5,7 @@
import aiohttp
import asyncio
from .dashboard_integration import DashboardIntegration
+import time
class AmputatorBot(DashboardIntegration, commands.Cog):
"""Cog to convert AMP URLs to canonical forms using the AmputatorBot API.
@@ -14,8 +15,17 @@ class AmputatorBot(DashboardIntegration, commands.Cog):
def __init__(self, bot):
self.bot = bot
- self.config = Config.get_conf(self, identifier=492089091320446976) # Use a unique identifier for your cog
- self.config.register_guild(opted_in=False) # Register a guild-specific variable for opted-in status
+ self.config = Config.get_conf(self, identifier=492089091320446976)
+ # Register a guild-specific variable for opted-in status and basic stats
+ self.config.register_guild(
+ opted_in=False,
+ stats={
+ "total_conversions": 0,
+ "total_urls_detected": 0,
+ "total_canonical_returned": 0,
+ "last_conversion_ts": 0,
+ },
+ )
self.opted_in_users = set()
async def initialize_config(self, guild):
@@ -54,6 +64,9 @@ async def convert_amp(self, ctx, *, message: str):
return
canonical_links = await self.fetch_canonical_links(urls)
+ # Update stats if invoked in a guild context
+ if ctx.guild is not None:
+ await self._update_guild_stats(ctx.guild, urls_detected=len(urls), canonical_returned=len(canonical_links))
if canonical_links:
if ctx.guild: # If in a server, respond in the channel
await ctx.send(f"Canonical URL(s): {'; '.join(canonical_links)}")
@@ -91,6 +104,8 @@ async def on_message(self, message):
urls = self.extract_urls(message.content)
if urls:
canonical_links = await self.fetch_canonical_links(urls)
+ # Update stats for guild automatic conversion checks
+ await self._update_guild_stats(message.guild, urls_detected=len(urls), canonical_returned=len(canonical_links))
if canonical_links:
await message.channel.send(f"Canonical URL(s): {'; '.join(canonical_links)}")
else: # DM context
@@ -101,6 +116,17 @@ async def on_message(self, message):
if canonical_links:
await message.author.send(f"Canonical URL(s): {'; '.join(canonical_links)}")
+ async def _update_guild_stats(self, guild, *, urls_detected: int, canonical_returned: int) -> None:
+ """Update guild-level stats used by the dashboard.
+
+ Increments total conversion events, accumulates counts, and records the last conversion timestamp.
+ """
+ async with self.config.guild(guild).stats() as stats:
+ stats["total_conversions"] = int(stats.get("total_conversions", 0)) + 1
+ stats["total_urls_detected"] = int(stats.get("total_urls_detected", 0)) + int(urls_detected)
+ stats["total_canonical_returned"] = int(stats.get("total_canonical_returned", 0)) + int(canonical_returned)
+ stats["last_conversion_ts"] = int(time.time())
+
@amputator.command(name='settings')
async def show_settings(self, ctx):
"""Display the current configuration settings for the AmputatorBot in this guild."""
diff --git a/ampremover/dashboard_integration.py b/ampremover/dashboard_integration.py
index bed1af0..abb9dfc 100644
--- a/ampremover/dashboard_integration.py
+++ b/ampremover/dashboard_integration.py
@@ -197,24 +197,37 @@ def __init__(self):
@dashboard_page(name="stats", description="View AMP URL conversion statistics", methods=("GET",))
async def stats_page(self, user: discord.User, guild: discord.Guild, **kwargs) -> typing.Dict[str, typing.Any]:
"""Dashboard page for viewing conversion statistics."""
- # Get guild settings
+ # Get guild settings and stats
opted_in = await self.config.guild(guild).opted_in()
-
- # For now, we'll show basic stats. You can expand this later with actual conversion tracking
+ stats = await self.config.guild(guild).stats()
+
+ total_conversions = int(stats.get("total_conversions", 0))
+ total_urls_detected = int(stats.get("total_urls_detected", 0))
+ total_canonical_returned = int(stats.get("total_canonical_returned", 0))
+ last_ts = int(stats.get("last_conversion_ts", 0))
+
+ # Compute simple rate
+ success_rate = 0.0
+ if total_urls_detected > 0:
+ success_rate = (total_canonical_returned / total_urls_detected) * 100.0
+
+ # Format last conversion time (Dashboard templates can format raw epoch too; keep simple here)
+ last_conversion = "Never" if last_ts == 0 else f"{last_ts}"
+
stats_html = f"""
-
Statistics for {guild.name}
- -Automatic Conversion:
-
+ Automatic Conversion:
+
{'Enabled' if opted_in else 'Disabled'}
Bot Name: {self.bot.user.display_name}
-API Used: AmputatorBot API
-Commands Available:
-[p]amputator convert - Manual conversion[p]amputator optin - Enable auto-conversion[p]amputator optout - Disable auto-conversion[p]amputator settings - View settingsTo track conversion statistics, you would need to add logging functionality to the main cog. - This could include tracking the number of URLs converted, success rates, and user activity.
-