From 9598477324b9dcc856acdb35af650eced055b0bf Mon Sep 17 00:00:00 2001 From: apple050620312 Date: Thu, 10 Sep 2026 13:52:37 +0800 Subject: [PATCH] security: lock down developer commands and telemetry --- cogs/developer.py | 34 ++++++++++++++++++++++++++++++++-- cogs/link_fix.py | 25 +++++-------------------- config.yml | 2 ++ privacy-policy.md | 11 ++++++----- src/websites.py | 8 ++++---- 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/cogs/developer.py b/cogs/developer.py index c9216d7..915d412 100644 --- a/cogs/developer.py +++ b/cogs/developer.py @@ -3,6 +3,7 @@ import json import subprocess import sys +import logging from importlib import metadata import psutil from textwrap import shorten @@ -16,6 +17,8 @@ p = psutil.Process() p.cpu_percent() +_logger = logging.getLogger(__name__) + dev_guilds = [discore.config.dev_guild] if discore.config.dev_guild else [] @@ -28,9 +31,12 @@ def execute_command(command: str, timeout: int = 30) -> str: """ try: - output = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate(timeout=timeout) + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + output = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: + process.kill() + process.communicate() return "Command expired" try: res = [] @@ -49,6 +55,30 @@ class Developer(discore.Cog, name="developer", description="The bot commands"): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self._access_is_configured(): + self.__cog_app_commands__.clear() + _logger.warning( + "Developer commands are disabled. Set developer_commands_enabled and " + "developer_user_ids to register them." + ) + + @staticmethod + def _access_is_configured() -> bool: + return bool( + getattr(discore.config, 'developer_commands_enabled', False) + and getattr(discore.config, 'dev_guild', None) + and getattr(discore.config, 'developer_user_ids', None) + ) + + async def interaction_check(self, interaction: discore.Interaction) -> bool: + allowed_user_ids = { + int(user_id) + for user_id in getattr(discore.config, 'developer_user_ids', []) + } + return self._access_is_configured() and interaction.user.id in allowed_user_ids + @discore.app_commands.command( name="update", description="Update the bot", diff --git a/cogs/link_fix.py b/cogs/link_fix.py index d5bf6de..bd019a4 100644 --- a/cogs/link_fix.py +++ b/cogs/link_fix.py @@ -74,33 +74,18 @@ def get_embeddable_urls(nodes: List[dmap.Node], spoiler: bool = False) -> List[t links += get_embeddable_urls(node.children, spoiler=spoiler) return links -async def _format_link_data(link: WebsiteLink, original_message: discore.Message, message: discore.Message | str | None = None, include_sensitive: bool = False) -> dict: +def _format_link_data(link: WebsiteLink, original_message: discore.Message) -> dict: """ Format the data of a link for logging or analytics purposes. :param link: the WebsiteLink object to format :param original_message: the original message associated with the context - :param message: the message associated with the fixed link, if any (can be a string or a discore.Message) - :param include_sensitive: whether to include sensitive data such as the message content or the fixed link URL :return: the formatted data as a dict """ - data: dict = { + return { 'link': {'id': link.id}, 'bot': original_message.author.bot } - if not include_sensitive: - return data - - if message is not None: - data['message'] = {} - if isinstance(message, discore.Message): - data['message']['repr'] = repr(message) - message = message.content - data['message']['content'] = message - - data['link']['fixed_link'] = (await link.get_fixed_url())[0] - data['link']['original_url'] = link.url - return data async def fix_embeds( @@ -155,7 +140,7 @@ async def render_and_send() -> tuple[list[tuple[str, list[WebsiteLink]]], dict[d if to_delete: err_data = [ - {'name': 'fixed_link_no_embed', 'data': await _format_link_data(link, original_message, msg, include_sensitive=True)} + {'name': 'fixed_link_no_embed', 'data': _format_link_data(link, original_message)} for msg in to_delete for link in messages.get(msg, [])] _logger.warning("Message(s) has no embed after waiting: %s", repr(err_data)) await Event.buff_cr(*err_data) @@ -163,12 +148,12 @@ async def render_and_send() -> tuple[list[tuple[str, list[WebsiteLink]]], dict[d for msg, msg_links in messages.items(): if msg not in to_delete: await Event.buff_cr(*[ - {'name': 'fixed_link', 'data': await _format_link_data(link, original_message)} + {'name': 'fixed_link', 'data': _format_link_data(link, original_message)} for link in msg_links]) if not_sent: err_data = [ - {'name': 'fixed_link_not_sent', 'data': await _format_link_data(link, original_message, msg_content, include_sensitive=True)} + {'name': 'fixed_link_not_sent', 'data': _format_link_data(link, original_message)} for msg_content, links in not_sent for link in links] _logger.warning("Message(s) failed to send: %s", repr(err_data)) await Event.buff_cr(*err_data) diff --git a/config.yml b/config.yml index cbea4fd..db422ee 100644 --- a/config.yml +++ b/config.yml @@ -2,6 +2,8 @@ description: "This bot automatically repost x.com and twitter.com posts as fxtwi version: "3.3.8" color: 0x1d9bf0 hot_reload: false +developer_commands_enabled: false +developer_user_ids: [] about_command: true analytic: true diff --git a/privacy-policy.md b/privacy-policy.md index 31278fc..0311ec6 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -17,10 +17,10 @@ FixTweetBot collects the following categories of data: - **User-specific Discord data:** User IDs, role IDs, server (guild) IDs, and text channel IDs. - **Service configuration data:** Custom settings saved by users for link fixing behavior. - **Operational logs (limited):** - - Logs of errors including the user ID, guild ID, and any information that could help resolve the error, depending on the context. + - Logs of errors and operational event types. Message content and link URLs are not included in these records. - Logs of successful usage events, including event type and timestamp. -**Note:** The bot does *not* collect or store message content or metadata beyond the above logging. +**Note:** The bot does *not* store message content or link URLs in its operational analytics. --- @@ -45,14 +45,15 @@ No profiling, advertising, or automated decision-making is performed using the d - **Data Hosting:** All data is stored on a secure, privately managed server. - **Security Measures:** We implement appropriate technical and organizational measures to protect your data against unauthorized access, alteration, disclosure, or destruction. Access to the server and database is strictly limited to authorized personnel. -- **Data Retention:** Operational logs are periodically purged, typically during software updates. Configuration data is retained until a user or server administrator requests its deletion. +- **Data Retention:** Operational event records are retained until they are deleted during maintenance. Configuration data is retained until a user or server administrator requests its deletion. --- ## 6. Data Sharing and Third Parties -- No user data is shared with third parties. -- The Bot does not rely on or transmit data to any external APIs or third-party services that would receive user information. +- Discord receives messages and links that the Bot sends as part of its normal operation. +- For websites handled by EmbedEZ, the link URL is sent to the EmbedEZ API to generate an embeddable replacement. EmbedEZ is not sent Discord user IDs, guild IDs, channel IDs, or the surrounding message content by the Bot. +- The Bot does not sell user data or share stored configuration and analytics data with third parties. --- diff --git a/src/websites.py b/src/websites.py index f710ee6..416e7c6 100644 --- a/src/websites.py +++ b/src/websites.py @@ -355,14 +355,14 @@ async def get_fixed_url(self) -> tuple[str | None, str | None]: try: async with utils.session.get("https://embedez.com/api/v1/providers/combined", params={'q': prepared_url}) as response: if response.status != 200: - _logger.warning("EmbedEZ request error for link: %s (status code: %d, body: %s)", prepared_url, response.status, await response.text()) - await Event.buff_cr({'name': 'embedez_fixer_error', 'data': {'link': prepared_url, 'status_code': response.status, 'response_body': await response.text()}}) + _logger.warning("EmbedEZ request failed with status code %d", response.status) + await Event.buff_cr({'name': 'embedez_fixer_error', 'data': {'status_code': response.status}}) return None, None search_hash = (await response.json())['data']['key'] return f"https://embedez.com/embed/{search_hash}", self.fixer_name except asyncio.TimeoutError: - _logger.warning("EmbedEZ request timeout for link: %s", prepared_url) - await Event.buff_cr({'name': 'embedez_fixer_timeout', 'data': {'link': prepared_url}}) + _logger.warning("EmbedEZ request timed out") + await Event.buff_cr({'name': 'embedez_fixer_timeout'}) return None, None