Skip to content
Open
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
34 changes: 32 additions & 2 deletions cogs/developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import subprocess
import sys
import logging
from importlib import metadata
import psutil
from textwrap import shorten
Expand All @@ -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 []


Expand All @@ -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 = []
Expand All @@ -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",
Expand Down
25 changes: 5 additions & 20 deletions cogs/link_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -155,20 +140,20 @@ 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)
await asyncio.gather(*(safe_send_coro(m.delete(), not_found=True, forbidden=True) for m in to_delete))
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)
Expand Down
2 changes: 2 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 6 additions & 5 deletions privacy-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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.

---

Expand Down
8 changes: 4 additions & 4 deletions src/websites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading