From e0cf42f8f60f7b6c933894d89ec7388708ef8b6c Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 11:03:12 +0900 Subject: [PATCH 01/17] baseline fedi abstraction stuff. --- .tool-versions | 1 + CLAUDE.md | 31 ++++++++ bot/config.py | 14 +++- bot/fediverse_factory.py | 31 ++++++++ bot/fediverse_service.py | 57 ++++++++++++++ bot/fediverse_types.py | 73 ++++++++++++++++++ bot/misskey_service.py | 141 +++++++++++++++++++++++++++++++++++ bot/pleroma_service.py | 155 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 9 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 .tool-versions create mode 100644 CLAUDE.md create mode 100644 bot/fediverse_factory.py create mode 100644 bot/fediverse_service.py create mode 100644 bot/fediverse_types.py create mode 100644 bot/misskey_service.py create mode 100644 bot/pleroma_service.py diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..45e4b9f --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 23.4.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2e60baf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,31 @@ +# Kemoverse - Fediverse Gacha Bot + +A Python-based gacha-style bot for the Fediverse that interfaces with either Misskey or Pleroma instances. + +## Project Overview +- **Language**: Python 3.11+ +- **Framework**: Flask for web interface +- **Database**: SQLite +- **Fediverse Support**: Configurable for Misskey or Pleroma instances +- **API Integration**: Uses Misskey.py library for Fediverse API communication + +## Key Components +- `bot/` - Core bot functionality including gacha mechanics, character management, and API interactions +- `web/` - Flask web application for user interface and card management +- `db.py` - Database utilities and schema management +- `config.ini` - Instance configuration (API keys, instance URL, admin users) + +## Configuration +The bot is configured via `config.ini` to connect to either a Misskey or Pleroma Fediverse instance. Key settings include: +- Instance URL and API credentials +- Bot user account details +- Administrator user permissions +- Database location + +## Development Commands +- Install dependencies: `pip install -r requirements.txt` +- Run bot: `python dev_runner.py` +- Start web interface: `python web/app.py` + +## Testing +Run tests with appropriate Python testing framework (check for pytest, unittest, or similar in project). \ No newline at end of file diff --git a/bot/config.py b/bot/config.py index 227f949..5b5848f 100644 --- a/bot/config.py +++ b/bot/config.py @@ -38,9 +38,21 @@ config.read(get_config_file()) USER = config['credentials']['User'].lower() # API key for the bot KEY = config['credentials']['Token'] -# Bot's Misskey instance URL +# Bot's Misskey/Pleroma instance URL INSTANCE = config['credentials']['Instance'].lower() +# Instance type validation +if 'InstanceType' not in config['application']: + raise ValueError("InstanceType must be specified in config.ini") + +instance_type = config['application']['InstanceType'].lower() +if instance_type not in ('misskey', 'pleroma'): + raise ValueError("InstanceType must be either 'misskey' or 'pleroma'") + +INSTANCE_TYPE = instance_type + +# Extra stuff for control of the bot + # TODO: move this to db # Fedi handles in the traditional 'user@domain.tld' style, allows these users # to use extra admin exclusive commands with the bot diff --git a/bot/fediverse_factory.py b/bot/fediverse_factory.py new file mode 100644 index 0000000..f2a9017 --- /dev/null +++ b/bot/fediverse_factory.py @@ -0,0 +1,31 @@ +from fediverse_service import FediverseService +from misskey_service import MisskeyService +from pleroma_service import PleromaService +import config + + +class FediverseServiceFactory: + """Factory for creating FediverseService implementations based on configuration""" + + @staticmethod + def create_service() -> FediverseService: + """ + Create a FediverseService implementation based on the configured instance type. + + Returns: + FediverseService implementation (MisskeyService or PleromaService) + + Raises: + ValueError: If the instance type is not supported + """ + if config.INSTANCE_TYPE == "misskey": + return MisskeyService() + elif config.INSTANCE_TYPE == "pleroma": + return PleromaService() + else: + raise ValueError(f"Unsupported instance type: {config.INSTANCE_TYPE}") + + +def get_fediverse_service() -> FediverseService: + """Convenience function to get a FediverseService instance""" + return FediverseServiceFactory.create_service() \ No newline at end of file diff --git a/bot/fediverse_service.py b/bot/fediverse_service.py new file mode 100644 index 0000000..2d12d89 --- /dev/null +++ b/bot/fediverse_service.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from fediverse_types import FediverseNotification, FediversePost, Visibility + + +class FediverseService(ABC): + """Abstract interface for Fediverse platform services (Misskey, Pleroma, etc.)""" + + @abstractmethod + def get_notifications(self, since_id: Optional[str] = None) -> List[FediverseNotification]: + """ + Retrieve notifications from the Fediverse instance. + + Args: + since_id: Optional ID to get notifications newer than this ID + + Returns: + List of FediverseNotification objects + """ + pass + + @abstractmethod + def create_post( + self, + text: str, + reply_to_id: Optional[str] = None, + visibility: Visibility = Visibility.HOME, + file_ids: Optional[List[str]] = None, + visible_user_ids: Optional[List[str]] = None + ) -> str: + """ + Create a new post on the Fediverse instance. + + Args: + text: The text content of the post + reply_to_id: Optional ID of post to reply to + visibility: Visibility level for the post + file_ids: Optional list of file IDs to attach + visible_user_ids: Optional list of user IDs who can see the post (for specified visibility) + + Returns: + ID of the created post + """ + pass + + @abstractmethod + def get_post_by_id(self, post_id: str) -> Optional[FediversePost]: + """ + Retrieve a specific post by its ID. + + Args: + post_id: The ID of the post to retrieve + + Returns: + FediversePost object if found, None otherwise + """ + pass \ No newline at end of file diff --git a/bot/fediverse_types.py b/bot/fediverse_types.py new file mode 100644 index 0000000..c1580b6 --- /dev/null +++ b/bot/fediverse_types.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +from typing import Optional, List, Dict, Any +from enum import Enum + + +class NotificationType(Enum): + MENTION = "mention" + REPLY = "reply" + FOLLOW = "follow" + FAVOURITE = "favourite" + REBLOG = "reblog" + POLL = "poll" + OTHER = "other" + + +class Visibility(Enum): + PUBLIC = "public" + UNLISTED = "unlisted" + HOME = "home" + FOLLOWERS = "followers" + SPECIFIED = "specified" + DIRECT = "direct" + + +@dataclass +class FediverseUser: + """Common user representation across Fediverse platforms""" + id: str + username: str + host: Optional[str] = None # None for local users + display_name: Optional[str] = None + + @property + def full_handle(self) -> str: + """Returns the full fediverse handle (@user@domain or @user for local)""" + if self.host: + return f"@{self.username}@{self.host}" + return f"@{self.username}" + + +@dataclass +class FediverseFile: + """Common file/attachment representation""" + id: str + url: str + type: Optional[str] = None + name: Optional[str] = None + + +@dataclass +class FediversePost: + """Common post representation across Fediverse platforms""" + id: str + text: Optional[str] + user: FediverseUser + visibility: Visibility + created_at: Optional[str] = None + files: List[FediverseFile] = None + reply_to_id: Optional[str] = None + + def __post_init__(self): + if self.files is None: + self.files = [] + + +@dataclass +class FediverseNotification: + """Common notification representation across Fediverse platforms""" + id: str + type: NotificationType + user: FediverseUser + post: Optional[FediversePost] = None + created_at: Optional[str] = None \ No newline at end of file diff --git a/bot/misskey_service.py b/bot/misskey_service.py new file mode 100644 index 0000000..845ed27 --- /dev/null +++ b/bot/misskey_service.py @@ -0,0 +1,141 @@ +import misskey +from typing import List, Optional, Dict, Any +from fediverse_service import FediverseService +from fediverse_types import ( + FediverseNotification, FediversePost, FediverseUser, FediverseFile, + NotificationType, Visibility +) +import config + + +class MisskeyService(FediverseService): + """Misskey implementation of FediverseService""" + + def __init__(self): + self.client = misskey.Misskey(address=config.INSTANCE, i=config.KEY) + + def _convert_misskey_user(self, user_data: Dict[str, Any]) -> FediverseUser: + """Convert Misskey user data to FediverseUser""" + return FediverseUser( + id=user_data.get("id", ""), + username=user_data.get("username", "unknown"), + host=user_data.get("host"), + display_name=user_data.get("name") + ) + + def _convert_misskey_file(self, file_data: Dict[str, Any]) -> FediverseFile: + """Convert Misskey file data to FediverseFile""" + return FediverseFile( + id=file_data.get("id", ""), + url=file_data.get("url", ""), + type=file_data.get("type"), + name=file_data.get("name") + ) + + def _convert_misskey_visibility(self, visibility: str) -> Visibility: + """Convert Misskey visibility to our enum""" + visibility_map = { + "public": Visibility.PUBLIC, + "unlisted": Visibility.UNLISTED, + "home": Visibility.HOME, + "followers": Visibility.FOLLOWERS, + "specified": Visibility.SPECIFIED + } + return visibility_map.get(visibility, Visibility.HOME) + + def _convert_to_misskey_visibility(self, visibility: Visibility) -> str: + """Convert our visibility enum to Misskey visibility""" + visibility_map = { + Visibility.PUBLIC: "public", + Visibility.UNLISTED: "unlisted", + Visibility.HOME: "home", + Visibility.FOLLOWERS: "followers", + Visibility.SPECIFIED: "specified", + Visibility.DIRECT: "specified" # Map direct to specified for Misskey + } + return visibility_map.get(visibility, "home") + + def _convert_misskey_notification_type(self, notif_type: str) -> NotificationType: + """Convert Misskey notification type to our enum""" + type_map = { + "mention": NotificationType.MENTION, + "reply": NotificationType.REPLY, + "follow": NotificationType.FOLLOW, + "favourite": NotificationType.FAVOURITE, + "reblog": NotificationType.REBLOG, + "poll": NotificationType.POLL + } + return type_map.get(notif_type, NotificationType.OTHER) + + def _convert_misskey_post(self, note_data: Dict[str, Any]) -> FediversePost: + """Convert Misskey note data to FediversePost""" + files = [] + if note_data.get("files"): + files = [self._convert_misskey_file(f) for f in note_data["files"]] + + return FediversePost( + id=note_data.get("id", ""), + text=note_data.get("text"), + user=self._convert_misskey_user(note_data.get("user", {})), + visibility=self._convert_misskey_visibility(note_data.get("visibility", "home")), + created_at=note_data.get("createdAt"), + files=files, + reply_to_id=note_data.get("replyId") + ) + + def _convert_misskey_notification(self, notification_data: Dict[str, Any]) -> FediverseNotification: + """Convert Misskey notification data to FediverseNotification""" + post = None + if notification_data.get("note"): + post = self._convert_misskey_post(notification_data["note"]) + + return FediverseNotification( + id=notification_data.get("id", ""), + type=self._convert_misskey_notification_type(notification_data.get("type", "")), + user=self._convert_misskey_user(notification_data.get("user", {})), + post=post, + created_at=notification_data.get("createdAt") + ) + + def get_notifications(self, since_id: Optional[str] = None) -> List[FediverseNotification]: + """Get notifications from Misskey instance""" + params = {} + if since_id: + params["sinceId"] = since_id + + notifications = self.client.i_notifications(**params) + return [self._convert_misskey_notification(notif) for notif in notifications] + + def create_post( + self, + text: str, + reply_to_id: Optional[str] = None, + visibility: Visibility = Visibility.HOME, + file_ids: Optional[List[str]] = None, + visible_user_ids: Optional[List[str]] = None + ) -> str: + """Create a post on Misskey instance""" + params = { + "text": text, + "visibility": self._convert_to_misskey_visibility(visibility) + } + + if reply_to_id: + params["replyId"] = reply_to_id + + if file_ids: + params["fileIds"] = file_ids + + if visible_user_ids and visibility == Visibility.SPECIFIED: + params["visibleUserIds"] = visible_user_ids + + response = self.client.notes_create(**params) + return response.get("createdNote", {}).get("id", "") + + def get_post_by_id(self, post_id: str) -> Optional[FediversePost]: + """Get a specific post by ID from Misskey instance""" + try: + note = self.client.notes_show(noteId=post_id) + return self._convert_misskey_post(note) + except Exception: + return None \ No newline at end of file diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py new file mode 100644 index 0000000..1e53cd7 --- /dev/null +++ b/bot/pleroma_service.py @@ -0,0 +1,155 @@ +from mastodon import Mastodon +from typing import List, Optional, Dict, Any +from fediverse_service import FediverseService +from fediverse_types import ( + FediverseNotification, FediversePost, FediverseUser, FediverseFile, + NotificationType, Visibility +) +import config + + +class PleromaService(FediverseService): + """Pleroma implementation of FediverseService using Mastodon.py""" + + def __init__(self): + self.client = Mastodon( + access_token=config.KEY, + api_base_url=config.INSTANCE + ) + + def _convert_mastodon_user(self, user_data: Dict[str, Any]) -> FediverseUser: + """Convert Mastodon/Pleroma user data to FediverseUser""" + acct = user_data.get("acct", "") + if "@" in acct: + username, host = acct.split("@", 1) + else: + username = acct + host = None + + return FediverseUser( + id=str(user_data.get("id", "")), + username=username, + host=host, + display_name=user_data.get("display_name") + ) + + def _convert_mastodon_file(self, file_data: Dict[str, Any]) -> FediverseFile: + """Convert Mastodon/Pleroma media attachment to FediverseFile""" + return FediverseFile( + id=str(file_data.get("id", "")), + url=file_data.get("url", ""), + type=file_data.get("type"), + name=file_data.get("description") + ) + + def _convert_mastodon_visibility(self, visibility: str) -> Visibility: + """Convert Mastodon/Pleroma visibility to our enum""" + visibility_map = { + "public": Visibility.PUBLIC, + "unlisted": Visibility.UNLISTED, + "private": Visibility.FOLLOWERS, + "direct": Visibility.DIRECT + } + return visibility_map.get(visibility, Visibility.PUBLIC) + + def _convert_to_mastodon_visibility(self, visibility: Visibility) -> str: + """Convert our visibility enum to Mastodon/Pleroma visibility""" + visibility_map = { + Visibility.PUBLIC: "public", + Visibility.UNLISTED: "unlisted", + Visibility.HOME: "unlisted", # Map home to unlisted for Pleroma + Visibility.FOLLOWERS: "private", + Visibility.SPECIFIED: "direct", # Map specified to direct for Pleroma + Visibility.DIRECT: "direct" + } + return visibility_map.get(visibility, "public") + + def _convert_mastodon_notification_type(self, notif_type: str) -> NotificationType: + """Convert Mastodon/Pleroma notification type to our enum""" + type_map = { + "mention": NotificationType.MENTION, + "follow": NotificationType.FOLLOW, + "favourite": NotificationType.FAVOURITE, + "reblog": NotificationType.REBLOG, + "poll": NotificationType.POLL + } + return type_map.get(notif_type, NotificationType.OTHER) + + def _convert_mastodon_status(self, status_data: Dict[str, Any]) -> FediversePost: + """Convert Mastodon/Pleroma status data to FediversePost""" + files = [] + if status_data.get("media_attachments"): + files = [self._convert_mastodon_file(f) for f in status_data["media_attachments"]] + + # Extract plain text from HTML content + content = status_data.get("content", "") + # Basic HTML stripping - in production you might want to use a proper HTML parser + import re + plain_text = re.sub(r'<[^>]+>', '', content) if content else None + + return FediversePost( + id=str(status_data.get("id", "")), + text=plain_text, + user=self._convert_mastodon_user(status_data.get("account", {})), + visibility=self._convert_mastodon_visibility(status_data.get("visibility", "public")), + created_at=status_data.get("created_at"), + files=files, + reply_to_id=str(status_data["in_reply_to_id"]) if status_data.get("in_reply_to_id") else None + ) + + def _convert_mastodon_notification(self, notification_data: Dict[str, Any]) -> FediverseNotification: + """Convert Mastodon/Pleroma notification data to FediverseNotification""" + post = None + if notification_data.get("status"): + post = self._convert_mastodon_status(notification_data["status"]) + + return FediverseNotification( + id=str(notification_data.get("id", "")), + type=self._convert_mastodon_notification_type(notification_data.get("type", "")), + user=self._convert_mastodon_user(notification_data.get("account", {})), + post=post, + created_at=notification_data.get("created_at") + ) + + def get_notifications(self, since_id: Optional[str] = None) -> List[FediverseNotification]: + """Get notifications from Pleroma instance""" + params = {} + if since_id: + params["since_id"] = since_id + + notifications = self.client.notifications(**params) + return [self._convert_mastodon_notification(notif) for notif in notifications] + + def create_post( + self, + text: str, + reply_to_id: Optional[str] = None, + visibility: Visibility = Visibility.HOME, + file_ids: Optional[List[str]] = None, + visible_user_ids: Optional[List[str]] = None + ) -> str: + """Create a post on Pleroma instance""" + params = { + "status": text, + "visibility": self._convert_to_mastodon_visibility(visibility) + } + + if reply_to_id: + params["in_reply_to_id"] = reply_to_id + + if file_ids: + params["media_ids"] = file_ids + + # Note: Pleroma/Mastodon doesn't have direct equivalent to visible_user_ids + # For direct messages, you typically mention users in the status text + + response = self.client.status_post(**params) + return str(response.get("id", "")) + + def get_post_by_id(self, post_id: str) -> Optional[FediversePost]: + """Get a specific post by ID from Pleroma instance""" + try: + status = self.client.status(post_id) + return self._convert_mastodon_status(status) + except Exception: + return None \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1db6f9b..3d237b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ Jinja2==3.1.6 MarkupSafe==3.0.2 Werkzeug==3.1.3 Misskey.py==4.1.0 +Mastodon.py==1.8.1 -- 2.39.2 From a47530180d3c074131a644015e2df1a6a11dafed Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 11:08:00 +0900 Subject: [PATCH 02/17] first whack at file upload --- bot/fediverse_service.py | 19 ++++++++++++++++++- bot/misskey_service.py | 16 ++++++++++++++-- bot/pleroma_service.py | 12 ++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/bot/fediverse_service.py b/bot/fediverse_service.py index 2d12d89..d4e32fe 100644 --- a/bot/fediverse_service.py +++ b/bot/fediverse_service.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Optional +from typing import List, Optional, Union, BinaryIO from fediverse_types import FediverseNotification, FediversePost, Visibility @@ -54,4 +54,21 @@ class FediverseService(ABC): Returns: FediversePost object if found, None otherwise """ + pass + + @abstractmethod + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + """ + Upload a file to the Fediverse instance. + + Args: + file_data: File data as binary stream or bytes + filename: Optional filename for the uploaded file + + Returns: + File ID that can be used in posts + + Raises: + RuntimeError: If file upload fails + """ pass \ No newline at end of file diff --git a/bot/misskey_service.py b/bot/misskey_service.py index 845ed27..e8bd396 100644 --- a/bot/misskey_service.py +++ b/bot/misskey_service.py @@ -1,5 +1,5 @@ import misskey -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Union, BinaryIO from fediverse_service import FediverseService from fediverse_types import ( FediverseNotification, FediversePost, FediverseUser, FediverseFile, @@ -138,4 +138,16 @@ class MisskeyService(FediverseService): note = self.client.notes_show(noteId=post_id) return self._convert_misskey_post(note) except Exception: - return None \ No newline at end of file + return None + + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + """Upload a file to Misskey Drive""" + try: + from misskey.exceptions import MisskeyAPIException + + media = self.client.drive_files_create(file_data) + return media["id"] + except MisskeyAPIException as e: + raise RuntimeError(f"Failed to upload file to Misskey Drive: {e}") from e + except Exception as e: + raise RuntimeError(f"Unexpected error during file upload: {e}") from e \ No newline at end of file diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py index 1e53cd7..7333df0 100644 --- a/bot/pleroma_service.py +++ b/bot/pleroma_service.py @@ -1,5 +1,5 @@ from mastodon import Mastodon -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Union, BinaryIO from fediverse_service import FediverseService from fediverse_types import ( FediverseNotification, FediversePost, FediverseUser, FediverseFile, @@ -152,4 +152,12 @@ class PleromaService(FediverseService): status = self.client.status(post_id) return self._convert_mastodon_status(status) except Exception: - return None \ No newline at end of file + return None + + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + """Upload a file to Pleroma instance""" + try: + media = self.client.media_post(file_data, mime_type=None, description=filename) + return str(media["id"]) + except Exception as e: + raise RuntimeError(f"Failed to upload file to Pleroma: {e}") from e \ No newline at end of file -- 2.39.2 From fe8e7d246f35f4a632662daea016027530188b3a Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 11:10:18 +0900 Subject: [PATCH 03/17] actually use the abstracted type not just the id --- bot/fediverse_service.py | 6 +++--- bot/misskey_service.py | 4 ++-- bot/pleroma_service.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/fediverse_service.py b/bot/fediverse_service.py index d4e32fe..1292d63 100644 --- a/bot/fediverse_service.py +++ b/bot/fediverse_service.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from typing import List, Optional, Union, BinaryIO -from fediverse_types import FediverseNotification, FediversePost, Visibility +from fediverse_types import FediverseNotification, FediversePost, FediverseFile, Visibility class FediverseService(ABC): @@ -57,7 +57,7 @@ class FediverseService(ABC): pass @abstractmethod - def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """ Upload a file to the Fediverse instance. @@ -66,7 +66,7 @@ class FediverseService(ABC): filename: Optional filename for the uploaded file Returns: - File ID that can be used in posts + FediverseFile object with ID, URL, and other metadata Raises: RuntimeError: If file upload fails diff --git a/bot/misskey_service.py b/bot/misskey_service.py index e8bd396..3a16cd4 100644 --- a/bot/misskey_service.py +++ b/bot/misskey_service.py @@ -140,13 +140,13 @@ class MisskeyService(FediverseService): except Exception: return None - def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """Upload a file to Misskey Drive""" try: from misskey.exceptions import MisskeyAPIException media = self.client.drive_files_create(file_data) - return media["id"] + return self._convert_misskey_file(media) except MisskeyAPIException as e: raise RuntimeError(f"Failed to upload file to Misskey Drive: {e}") from e except Exception as e: diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py index 7333df0..753b1dc 100644 --- a/bot/pleroma_service.py +++ b/bot/pleroma_service.py @@ -154,10 +154,10 @@ class PleromaService(FediverseService): except Exception: return None - def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> str: + def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """Upload a file to Pleroma instance""" try: media = self.client.media_post(file_data, mime_type=None, description=filename) - return str(media["id"]) + return self._convert_mastodon_file(media) except Exception as e: raise RuntimeError(f"Failed to upload file to Pleroma: {e}") from e \ No newline at end of file -- 2.39.2 From dac05a3ed81b8fe01d6ea1231b2e174d03566886 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 11:22:20 +0900 Subject: [PATCH 04/17] completely untested refactor. bug fixes to come later. --- bot/add_character.py | 74 ++++++++++++++++++++++--------------------- bot/bot_app.py | 75 ++++++++++++++++++++++++++++++++++++-------- bot/parsing.py | 68 ++++++++++++++++++++++++++++----------- bot/response.py | 5 +-- 4 files changed, 153 insertions(+), 69 deletions(-) diff --git a/bot/add_character.py b/bot/add_character.py index 18b0f98..7590c1b 100644 --- a/bot/add_character.py +++ b/bot/add_character.py @@ -1,32 +1,25 @@ import requests -from misskey.exceptions import MisskeyAPIException -from client import client_connection -from db_utils import insert_character -from custom_types import Character +from fediverse_factory import get_fediverse_service +from db_utils import get_db_connection from config import RARITY_TO_WEIGHT - -def add_character( - name: str, - rarity: int, - image_url: str) -> tuple[int, str]: - ''' - Adds a character to the database, uploading the image from a public URL to - the bot's Misskey Drive. +def add_character(name: str, rarity: int, weight: float, image_url: str) -> tuple[int, str]: + """ + Adds a character to the database, uploading the image from a public URL to the Fediverse instance. Args: name (str): Character name. rarity (int): Character rarity (e.g., 1-5). - image_url (str): Public URL of the image from the post (e.g., from - note['files'][i]['url']). + weight (float): Pull weight (e.g., 0.02). + image_url (str): Public URL of the image from the post. Returns: - tuple[int, str]: Character ID and bot's Drive file_id. + tuple[int, str]: Character ID and file_id. Raises: ValueError: If inputs are invalid. RuntimeError: If image download/upload or database operation fails. - ''' + """ stripped_name = name.strip() @@ -40,25 +33,34 @@ def add_character( if not image_url: raise ValueError('Image URL must be provided.') - # Download image - response = requests.get(image_url, stream=True, timeout=30) - if response.status_code != 200: - raise RuntimeError(f'Failed to download image from {image_url}') - - # Upload to bot's Drive - mk = client_connection() try: - media = mk.drive_files_create(response.raw) - file_id = media['id'] - except MisskeyAPIException as e: - raise RuntimeError(f'Failed to upload image to bot\'s Drive: {e}')\ - from e + # Download image + response = requests.get(image_url, stream=True, timeout=30) + if response.status_code != 200: + raise RuntimeError(f"Failed to download image from {image_url}") - # Insert into database - character_id = insert_character( - stripped_name, - rarity, - RARITY_TO_WEIGHT[rarity], - file_id - ) - return character_id, file_id + # Upload to Fediverse instance + fediverse_service = get_fediverse_service() + try: + uploaded_file = fediverse_service.upload_file(response.raw) + file_id = uploaded_file.id + except RuntimeError as e: + raise RuntimeError(f"Failed to upload image: {e}") from e + + # Insert into database + conn = get_db_connection() + cur = conn.cursor() + cur.execute( + 'INSERT INTO characters (name, rarity, weight, file_id) VALUES (?, ?, ?, ?)', + (stripped_name, rarity, float(weight), file_id) + ) + conn.commit() + character_id = cur.lastrowid + + return character_id, file_id + + except Exception as e: + raise + finally: + if 'conn' in locals(): + conn.close() diff --git a/bot/bot_app.py b/bot/bot_app.py index 825695e..fc8a66a 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -1,18 +1,67 @@ import time -import misskey as misskey -from client import client_connection -import db_utils as db +import traceback +from parsing import parse_notification +from db_utils import get_config, set_config +from fediverse_factory import get_fediverse_service +import config -from config import NOTIFICATION_POLL_INTERVAL -from notification import process_notifications - -if __name__ == '__main__': - # Initialize the Misskey client - client = client_connection() - # Connect to DB - db.connect() +def stream_notifications(): + # Initialize the Fediverse service + fediverse_service = get_fediverse_service() + + # Get the last seen notification ID from the database + last_seen_id = get_config("last_seen_notif_id") + whitelisted_instances = getattr(config, 'WHITELISTED_INSTANCES', []) print('Listening for notifications...') while True: - if not process_notifications(client): - time.sleep(NOTIFICATION_POLL_INTERVAL) + try: + # Get notifications from the fediverse service + notifications = fediverse_service.get_notifications(since_id=last_seen_id) + + if notifications: + new_last_seen_id = last_seen_id + + for notification in notifications: + notif_id = notification.id + + # Skip old or same ID notifications + if last_seen_id is not None and notif_id <= last_seen_id: + continue + + username = notification.user.username + host = notification.user.host + + instance = host if host else "local" + + if instance in whitelisted_instances or instance == "local": + note = notification.post.text if notification.post else "" + notif_type = notification.type.value + + print(f"📨 [{notif_type}] from @{username}@{instance}") + print(f"💬 {note}") + print("-" * 30) + + # 🧠 Send to the parser + parse_notification(notification, fediverse_service) + + else: + print(f"⚠️ Blocked notification from untrusted instance: {host}") + + # Update only if this notif_id is greater + if new_last_seen_id is None or notif_id > new_last_seen_id: + new_last_seen_id = notif_id + + # Save the latest seen ID + if new_last_seen_id and new_last_seen_id != last_seen_id: + set_config("last_seen_notif_id", new_last_seen_id) + last_seen_id = new_last_seen_id + + time.sleep(5) + + except Exception as e: + print(f"An exception has occured: {e}\n{traceback.format_exc()}") + time.sleep(5) + +if __name__ == "__main__": + stream_notifications() diff --git a/bot/parsing.py b/bot/parsing.py index eece077..66d6ed8 100644 --- a/bot/parsing.py +++ b/bot/parsing.py @@ -1,27 +1,38 @@ import re -from typing import Dict, Any - -import misskey - import config +from response import generate_response +from fediverse_factory import get_fediverse_service +from fediverse_types import FediverseNotification, NotificationType, Visibility from custom_types import ParsedNotification +def parse_notification(notification: FediverseNotification, fediverse_service=None): + '''Parses any notifications received by the bot and sends any commands to + generate_response()''' + + if fediverse_service is None: + fediverse_service = get_fediverse_service() -def parse_notification( - notification: Dict[str, Any], - client: misskey.Misskey) -> ParsedNotification | None: - '''Parses any notifications received by the bot''' + # We get the type of notification to filter the ones that we actually want + # to parse + if notification.type not in (NotificationType.MENTION, NotificationType.REPLY): + return # Ignore anything that isn't a mention + + # Return early if no post attached + if not notification.post: + return + + # We want the visibility to be related to the type that was received (so if + # people don't want to dump a bunch of notes on home they don't have to) + if notification.post.visibility != Visibility.SPECIFIED: + visibility = Visibility.HOME + else: + visibility = Visibility.SPECIFIED # Get the full Activitypub ID of the user - user = notification.get("user", {}) - username = user.get("username", "unknown") - host = user.get("host") - # Local users may not have a hostname attached - full_user = f"@{username}" if not host else f"@{username}@{host}" + full_user = notification.user.full_handle - note_obj = notification.get("note", {}) - note_text = note_obj.get("text") - note_id = note_obj.get("id") + note_text = notification.post.text + note_id = notification.post.id note = note_text.strip().lower() if note_text else "" @@ -44,9 +55,30 @@ def parse_notification( command = parts[0].lower() if parts else None arguments = parts[1:] if len(parts) > 1 else [] - return { + # Create ParsedNotification object for the new response system + parsed_notification: ParsedNotification = { 'author': full_user, 'command': command, 'arguments': arguments, - 'note_obj': note_obj + 'note_obj': { + 'id': note_id, + 'text': note_text, + 'files': [{'url': f.url} for f in notification.post.files] if notification.post.files else [] + } } + + # Generate response using the new system + response = generate_response(parsed_notification) + if not response: + return + + # Handle attachment URLs (convert to file IDs if needed) + file_ids = response['attachment_urls'] if response['attachment_urls'] else None + + fediverse_service.create_post( + text=response['message'], + reply_to_id=note_id, + visibility=visibility, + file_ids=file_ids + #visible_user_ids=[] #todo: write actual visible users ids so pleromers can use the bot privately + ) diff --git a/bot/response.py b/bot/response.py index 3fde3ed..5632e2e 100644 --- a/bot/response.py +++ b/bot/response.py @@ -103,10 +103,10 @@ dumbass.', 'attachment_urls': None } - if len(arguments) != 2: + if len(arguments) != 3: return { 'message': f'{author} Please specify the following attributes \ -in order: name, rarity', +in order: name, rarity, weight', 'attachment_urls': None } @@ -126,6 +126,7 @@ must be a decimal value between 0.0 and 1.0', character_id, file_id = add_character( name=arguments[0], rarity=int(arguments[1]), + weight=float(arguments[2]), image_url=image_url ) return { -- 2.39.2 From b2ca6dd59ad83f85018dfd416c0efd43845931f5 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 11:26:10 +0900 Subject: [PATCH 05/17] documentation update for configurable backends --- example_config.ini | 2 ++ readme.md | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/example_config.ini b/example_config.ini index af7e0f2..d4ba8bf 100644 --- a/example_config.ini +++ b/example_config.ini @@ -33,3 +33,5 @@ User = @bot@example.tld ; Generate one by going to Settings > API > Generate access token Token = abcdefghijklmnopqrstuvwxyz012345 +; Instance type - either "misskey" or "pleroma" +InstanceType = misskey diff --git a/readme.md b/readme.md index 437b09f..6ae910c 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Kemoverse -A gacha-style bot for the Fediverse built with Python. Users can roll for characters, trade, duel, and perhaps engage with popularity-based mechanics. Currently designed for use with Misskey. Name comes from Kemonomimi and Fediverse. +A gacha-style bot for the Fediverse built with Python. Users can roll for characters, trade, duel, and perhaps engage with popularity-based mechanics. Supports both Misskey and Pleroma instances. Name comes from Kemonomimi and Fediverse.

Fediverse Gacha Bot Logo

@@ -47,13 +47,13 @@ A gacha-style bot for the Fediverse built with Python. Users can roll for charac - 🌐 Web app to generate cards from images ### 🌍 Fediverse Support -✅ Anyone from the fediverse can play, but the server only works using a Misskey instance. Want to rewrite the program in Elixir for Pleroma? Let us know! +✅ Anyone from the fediverse can play! The bot supports both Misskey and Pleroma instances through configurable backends. ## 🗃️ Tech Stack - Python (3.12+) - SQLite -- Fediverse API integration (via Misskey endpoints) +- Fediverse API integration (Misskey and Pleroma support) - Flask - Modular DB design for extensibility @@ -65,12 +65,12 @@ The bot is meant to feel *light, fun, and competitive*. Mixing social, gacha and flowchart TD subgraph Player Interaction - A1[Misskey bot] + A1[Fediverse bot] A2[Web] end - subgraph Misskey - B1[Misskey instance] + subgraph Fediverse + B1[Fediverse instance] end subgraph Bot @@ -78,7 +78,7 @@ flowchart TD C2[Notification parser] C3[Gacha roll logic] C4[Database interface] - C5[Misskey API poster] + C5[Fediverse API poster] end subgraph Website -- 2.39.2 From 91376c0ebaad1338d4ffaee37915549e9331d7e5 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 12:43:42 +0900 Subject: [PATCH 06/17] works now but may need to revert file upload change --- bot/config.py | 3 ++ bot/pleroma_service.py | 66 ++++++++++++++++++++++++++++++++++++++++-- example_config.ini | 3 ++ web/app.py | 10 +++++-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/bot/config.py b/bot/config.py index 5b5848f..2f9f4a3 100644 --- a/bot/config.py +++ b/bot/config.py @@ -51,6 +51,9 @@ if instance_type not in ('misskey', 'pleroma'): INSTANCE_TYPE = instance_type +# Web server port +WEB_PORT = config['application'].getint('WebPort', 5000) + # Extra stuff for control of the bot # TODO: move this to db diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py index 753b1dc..4ec3496 100644 --- a/bot/pleroma_service.py +++ b/bot/pleroma_service.py @@ -1,5 +1,7 @@ from mastodon import Mastodon from typing import List, Optional, Dict, Any, Union, BinaryIO +import mimetypes +import io from fediverse_service import FediverseService from fediverse_types import ( FediverseNotification, FediversePost, FediverseUser, FediverseFile, @@ -157,7 +159,67 @@ class PleromaService(FediverseService): def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """Upload a file to Pleroma instance""" try: - media = self.client.media_post(file_data, mime_type=None, description=filename) - return self._convert_mastodon_file(media) + import requests + + # Convert file_data to bytes if it's a stream + if hasattr(file_data, 'read'): + file_bytes = file_data.read() + else: + file_bytes = file_data + + # Determine mime type from file header + mime_type = 'image/jpeg' # default + if file_bytes.startswith(b'\xff\xd8\xff'): + mime_type = 'image/jpeg' + ext = '.jpg' + elif file_bytes.startswith(b'\x89PNG\r\n\x1a\n'): + mime_type = 'image/png' + ext = '.png' + elif file_bytes.startswith(b'GIF8'): + mime_type = 'image/gif' + ext = '.gif' + elif file_bytes.startswith(b'RIFF') and b'WEBP' in file_bytes[:12]: + mime_type = 'image/webp' + ext = '.webp' + else: + ext = '.jpg' + + # Create a filename if none provided + if not filename: + filename = f"upload{ext}" + + # Direct HTTP POST to /api/v1/media with headers matching browser behavior + headers = { + 'Authorization': f'Bearer {self.client.access_token}', + 'Accept': '*/*', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'User-Agent': 'Kemoverse-Bot/1.0' + } + + # Use files parameter to let requests handle multipart/form-data and content-type + files = { + 'file': (filename, file_bytes, mime_type) + } + + url = f"{self.client.api_base_url}/api/v1/media" + + # Debug logging + print(f"DEBUG: Making POST request to: {url}") + print(f"DEBUG: Headers: {headers}") + print(f"DEBUG: Files keys: {list(files.keys())}") + + response = requests.post(url, headers=headers, files=files) + + print(f"DEBUG: Response status: {response.status_code}") + print(f"DEBUG: Response headers: {dict(response.headers)}") + print(f"DEBUG: Request method was: {response.request.method}") + + if response.status_code in (200, 201): + media = response.json() + return self._convert_mastodon_file(media) + else: + raise Exception(f"Upload failed with {response.status_code}: {response.text}") + except Exception as e: raise RuntimeError(f"Failed to upload file to Pleroma: {e}") from e \ No newline at end of file diff --git a/example_config.ini b/example_config.ini index d4ba8bf..ec57058 100644 --- a/example_config.ini +++ b/example_config.ini @@ -35,3 +35,6 @@ Token = abcdefghijklmnopqrstuvwxyz012345 ; Instance type - either "misskey" or "pleroma" InstanceType = misskey + +; Web server port (default: 5000) +WebPort = 5000 diff --git a/web/app.py b/web/app.py index 61ed38f..9837741 100644 --- a/web/app.py +++ b/web/app.py @@ -1,10 +1,16 @@ import sqlite3 +import sys +import os + +# Add bot directory to path to import config +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'bot')) +import config from flask import Flask, render_template, abort from werkzeug.exceptions import HTTPException app = Flask(__name__) -DB_PATH = "./gacha_game.db" # Adjust path if needed +DB_PATH = config.DB_PATH def get_db_connection(): conn = sqlite3.connect(DB_PATH) @@ -68,4 +74,4 @@ def submit_character(): if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) + app.run(host='0.0.0.0', port=config.WEB_PORT, debug=True) -- 2.39.2 From 89ae8a7290b1bdb82a1a27a1def489a50c4d2f70 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 12:47:51 +0900 Subject: [PATCH 07/17] back to using mastodon.py --- bot/pleroma_service.py | 72 ++++++++++++------------------------------ 1 file changed, 20 insertions(+), 52 deletions(-) diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py index 4ec3496..d7c52b6 100644 --- a/bot/pleroma_service.py +++ b/bot/pleroma_service.py @@ -159,67 +159,35 @@ class PleromaService(FediverseService): def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """Upload a file to Pleroma instance""" try: - import requests - - # Convert file_data to bytes if it's a stream + # Convert file_data to bytes if it's a stream for MIME detection if hasattr(file_data, 'read'): - file_bytes = file_data.read() + # Check if we can seek back + try: + current_pos = file_data.tell() + header = file_data.read(8) + file_data.seek(current_pos) + except (io.UnsupportedOperation, OSError): + # Non-seekable stream, read all data + remaining_data = file_data.read() + file_bytes = header + remaining_data + file_data = io.BytesIO(file_bytes) + header = file_bytes[:8] else: - file_bytes = file_data + header = file_data[:8] if len(file_data) >= 8 else file_data # Determine mime type from file header - mime_type = 'image/jpeg' # default - if file_bytes.startswith(b'\xff\xd8\xff'): + if header.startswith(b'\xff\xd8\xff'): mime_type = 'image/jpeg' - ext = '.jpg' - elif file_bytes.startswith(b'\x89PNG\r\n\x1a\n'): + elif header.startswith(b'\x89PNG\r\n\x1a\n'): mime_type = 'image/png' - ext = '.png' - elif file_bytes.startswith(b'GIF8'): + elif header.startswith(b'GIF8'): mime_type = 'image/gif' - ext = '.gif' - elif file_bytes.startswith(b'RIFF') and b'WEBP' in file_bytes[:12]: + elif header.startswith(b'RIFF') and len(header) >= 8 and b'WEBP' in header: mime_type = 'image/webp' - ext = '.webp' else: - ext = '.jpg' + mime_type = 'image/jpeg' # Default fallback - # Create a filename if none provided - if not filename: - filename = f"upload{ext}" - - # Direct HTTP POST to /api/v1/media with headers matching browser behavior - headers = { - 'Authorization': f'Bearer {self.client.access_token}', - 'Accept': '*/*', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'User-Agent': 'Kemoverse-Bot/1.0' - } - - # Use files parameter to let requests handle multipart/form-data and content-type - files = { - 'file': (filename, file_bytes, mime_type) - } - - url = f"{self.client.api_base_url}/api/v1/media" - - # Debug logging - print(f"DEBUG: Making POST request to: {url}") - print(f"DEBUG: Headers: {headers}") - print(f"DEBUG: Files keys: {list(files.keys())}") - - response = requests.post(url, headers=headers, files=files) - - print(f"DEBUG: Response status: {response.status_code}") - print(f"DEBUG: Response headers: {dict(response.headers)}") - print(f"DEBUG: Request method was: {response.request.method}") - - if response.status_code in (200, 201): - media = response.json() - return self._convert_mastodon_file(media) - else: - raise Exception(f"Upload failed with {response.status_code}: {response.text}") - + media = self.client.media_post(file_data, mime_type=mime_type, description=filename) + return self._convert_mastodon_file(media) except Exception as e: raise RuntimeError(f"Failed to upload file to Pleroma: {e}") from e \ No newline at end of file -- 2.39.2 From 67b4d949fdee156a0529ae717d8d105b869e99b2 Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 12:53:37 +0900 Subject: [PATCH 08/17] use a library for content type detection instead of handrolled --- bot/pleroma_service.py | 33 ++++++++++++++------------------- requirements.txt | 1 + 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/bot/pleroma_service.py b/bot/pleroma_service.py index d7c52b6..21320d1 100644 --- a/bot/pleroma_service.py +++ b/bot/pleroma_service.py @@ -1,7 +1,7 @@ from mastodon import Mastodon from typing import List, Optional, Dict, Any, Union, BinaryIO -import mimetypes import io +import filetype from fediverse_service import FediverseService from fediverse_types import ( FediverseNotification, FediversePost, FediverseUser, FediverseFile, @@ -159,33 +159,28 @@ class PleromaService(FediverseService): def upload_file(self, file_data: Union[BinaryIO, bytes], filename: Optional[str] = None) -> FediverseFile: """Upload a file to Pleroma instance""" try: - # Convert file_data to bytes if it's a stream for MIME detection + # Convert file_data to bytes for MIME detection if hasattr(file_data, 'read'): # Check if we can seek back try: current_pos = file_data.tell() - header = file_data.read(8) + file_bytes = file_data.read() file_data.seek(current_pos) - except (io.UnsupportedOperation, OSError): - # Non-seekable stream, read all data - remaining_data = file_data.read() - file_bytes = header + remaining_data file_data = io.BytesIO(file_bytes) - header = file_bytes[:8] + except (io.UnsupportedOperation, OSError): + # Non-seekable stream, already read all data + file_data = io.BytesIO(file_bytes) else: - header = file_data[:8] if len(file_data) >= 8 else file_data + file_bytes = file_data + file_data = io.BytesIO(file_bytes) - # Determine mime type from file header - if header.startswith(b'\xff\xd8\xff'): - mime_type = 'image/jpeg' - elif header.startswith(b'\x89PNG\r\n\x1a\n'): - mime_type = 'image/png' - elif header.startswith(b'GIF8'): - mime_type = 'image/gif' - elif header.startswith(b'RIFF') and len(header) >= 8 and b'WEBP' in header: - mime_type = 'image/webp' + # Use filetype library for robust MIME detection + kind = filetype.guess(file_bytes) + if kind is not None: + mime_type = kind.mime else: - mime_type = 'image/jpeg' # Default fallback + # Fallback to image/jpeg if detection fails + mime_type = 'image/jpeg' media = self.client.media_post(file_data, mime_type=mime_type, description=filename) return self._convert_mastodon_file(media) diff --git a/requirements.txt b/requirements.txt index 3d237b4..4a749b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ MarkupSafe==3.0.2 Werkzeug==3.1.3 Misskey.py==4.1.0 Mastodon.py==1.8.1 +filetype==1.2.0 -- 2.39.2 From 8918b5205da5b6bc4c5d257950db5279c161d69f Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 13:25:04 +0900 Subject: [PATCH 09/17] named some damn properties wrong --- .gitignore | 2 +- bot/bot_app.py | 2 +- bot/config.py | 4 ++++ bot/misskey_service.py | 13 ++++++++----- example_config.ini | 4 ++++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index e5543ec..a05fd62 100644 --- a/.gitignore +++ b/.gitignore @@ -186,4 +186,4 @@ gacha_game*.db gacha_game*.db.* config*.ini -.idea \ No newline at end of file +.idea diff --git a/bot/bot_app.py b/bot/bot_app.py index fc8a66a..f4bc1e0 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -11,7 +11,7 @@ def stream_notifications(): # Get the last seen notification ID from the database last_seen_id = get_config("last_seen_notif_id") - whitelisted_instances = getattr(config, 'WHITELISTED_INSTANCES', []) + whitelisted_instances = getattr(config, 'TRUSTED_INSTANCES', []) print('Listening for notifications...') while True: diff --git a/bot/config.py b/bot/config.py index 2f9f4a3..29a23a2 100644 --- a/bot/config.py +++ b/bot/config.py @@ -54,6 +54,10 @@ INSTANCE_TYPE = instance_type # Web server port WEB_PORT = config['application'].getint('WebPort', 5000) +# Trusted instances +trusted_instances_str = config['application'].get('TrustedInstances', '') +TRUSTED_INSTANCES = [instance.strip() for instance in trusted_instances_str.split(',') if instance.strip()] + # Extra stuff for control of the bot # TODO: move this to db diff --git a/bot/misskey_service.py b/bot/misskey_service.py index 3a16cd4..510b32b 100644 --- a/bot/misskey_service.py +++ b/bot/misskey_service.py @@ -99,9 +99,12 @@ class MisskeyService(FediverseService): def get_notifications(self, since_id: Optional[str] = None) -> List[FediverseNotification]: """Get notifications from Misskey instance""" - params = {} + params = { + 'include_types': ['mention', 'reply'], + 'limit': 50 + } if since_id: - params["sinceId"] = since_id + params["since_id"] = since_id notifications = self.client.i_notifications(**params) return [self._convert_misskey_notification(notif) for notif in notifications] @@ -121,13 +124,13 @@ class MisskeyService(FediverseService): } if reply_to_id: - params["replyId"] = reply_to_id + params["reply_id"] = reply_to_id if file_ids: - params["fileIds"] = file_ids + params["file_ids"] = file_ids if visible_user_ids and visibility == Visibility.SPECIFIED: - params["visibleUserIds"] = visible_user_ids + params["visible_user_ids"] = visible_user_ids response = self.client.notes_create(**params) return response.get("createdNote", {}).get("id", "") diff --git a/example_config.ini b/example_config.ini index ec57058..58cf483 100644 --- a/example_config.ini +++ b/example_config.ini @@ -38,3 +38,7 @@ InstanceType = misskey ; Web server port (default: 5000) WebPort = 5000 + +; Comma-separated list of trusted fediverse instances (leave empty to allow only local users) +; Example: TrustedInstances = mastodon.social,misskey.io,pleroma.example.com +TrustedInstances = -- 2.39.2 From 298d7fda72773ec7735c4bb3473584673bd1c2da Mon Sep 17 00:00:00 2001 From: Moon Date: Thu, 12 Jun 2025 13:29:12 +0900 Subject: [PATCH 10/17] rm needless file --- CLAUDE.md | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 2e60baf..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,31 +0,0 @@ -# Kemoverse - Fediverse Gacha Bot - -A Python-based gacha-style bot for the Fediverse that interfaces with either Misskey or Pleroma instances. - -## Project Overview -- **Language**: Python 3.11+ -- **Framework**: Flask for web interface -- **Database**: SQLite -- **Fediverse Support**: Configurable for Misskey or Pleroma instances -- **API Integration**: Uses Misskey.py library for Fediverse API communication - -## Key Components -- `bot/` - Core bot functionality including gacha mechanics, character management, and API interactions -- `web/` - Flask web application for user interface and card management -- `db.py` - Database utilities and schema management -- `config.ini` - Instance configuration (API keys, instance URL, admin users) - -## Configuration -The bot is configured via `config.ini` to connect to either a Misskey or Pleroma Fediverse instance. Key settings include: -- Instance URL and API credentials -- Bot user account details -- Administrator user permissions -- Database location - -## Development Commands -- Install dependencies: `pip install -r requirements.txt` -- Run bot: `python dev_runner.py` -- Start web interface: `python web/app.py` - -## Testing -Run tests with appropriate Python testing framework (check for pytest, unittest, or similar in project). \ No newline at end of file -- 2.39.2 From f70b2147cda9cbe0022bb7571e097e95af654f7d Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 13 Jun 2025 10:57:33 +0900 Subject: [PATCH 11/17] Remove legacy files that bypass FediverseService abstraction - Remove bot/client.py (replaced by fediverse factory) - Remove bot/notification.py (replaced by bot_app.py) - All functionality now properly uses FediverseService abstraction --- bot/client.py | 6 --- bot/notification.py | 122 -------------------------------------------- 2 files changed, 128 deletions(-) delete mode 100644 bot/client.py delete mode 100644 bot/notification.py diff --git a/bot/client.py b/bot/client.py deleted file mode 100644 index 57f9f4e..0000000 --- a/bot/client.py +++ /dev/null @@ -1,6 +0,0 @@ -import misskey -import config - - -def client_connection() -> misskey.Misskey: - return misskey.Misskey(address=config.INSTANCE, i=config.KEY) diff --git a/bot/notification.py b/bot/notification.py deleted file mode 100644 index 9427dbf..0000000 --- a/bot/notification.py +++ /dev/null @@ -1,122 +0,0 @@ -import traceback -from typing import Dict, Any - -import misskey -from misskey.exceptions import MisskeyAPIException - -from config import NOTIFICATION_BATCH_SIZE -from parsing import parse_notification -from db_utils import get_config, set_config -from response import generate_response -from custom_types import BotResponse - -# Define your whitelist -# TODO: move to config -WHITELISTED_INSTANCES: list[str] = [] - - -def process_notification( - client: misskey.Misskey, - notification: Dict[str, Any]) -> None: - '''Processes an individual notification''' - user = notification.get('user', {}) - username = user.get('username', 'unknown') - host = user.get('host') # None if local user - instance = host if host else 'local' - - if not (instance in WHITELISTED_INSTANCES or instance == 'local'): - print(f'⚠️ Blocked notification from untrusted instance: {instance}') - return - - # Copy visibility of the post that was received when replying (so if people - # don't want to dump a bunch of notes on home they don't have to) - visibility = notification['note']['visibility'] - if visibility != 'specified': - visibility = 'home' - - notif_type = notification.get('type', 'unknown') - notif_id = notification.get('id') - print(f'📨 <{notif_id}> [{notif_type}] from @{username}@{instance}') - - # 🧠 Send to the parser - parsed_notification = parse_notification(notification, client) - - if not parsed_notification: - return - - # Get the note Id to reply to - note_id = notification.get('note', {}).get('id') - - # Get the response - response: BotResponse | None = generate_response(parsed_notification) - - if not response: - return - - client.notes_create( - text=response['message'], - reply_id=note_id, - visibility=visibility, - file_ids=response['attachment_urls'] - # TODO: write actual visible users ids so pleromers can use the bot - # privately - # visible_user_ids=[] - ) - - -def process_notifications(client: misskey.Misskey) -> bool: - '''Processes a batch of unread notifications. Returns False if there are - no more notifications to process.''' - - last_seen_id = get_config('last_seen_notif_id') - # process_notification writes to last_seen_id, so make a copy - new_last_seen_id = last_seen_id - - try: - notifications = client.i_notifications( - # Fetch notifications we haven't seen yet. This option is a bit - # tempermental, sometimes it'll include since_id, sometimes it - # won't. We need to keep track of what notifications we've - # already processed. - since_id=last_seen_id, - # Let misskey handle the filtering - include_types=['mention', 'reply'], - # And handle the batch size while we're at it - limit=NOTIFICATION_BATCH_SIZE - ) - - # No notifications. Wait the poll period. - if not notifications: - return False - - # Iterate oldest to newest - for notification in notifications: - try: - # Skip if we've processed already - notif_id = notification.get('id', '') - if notif_id <= last_seen_id: - continue - - # Update new_last_seen_id and process - new_last_seen_id = notif_id - process_notification(client, notification) - - except Exception as e: - print(f'An exception has occured while processing a \ -notification: {e}') - print(traceback.format_exc()) - - # If we got as many notifications as we requested, there are probably - # more in the queue - return len(notifications) == NOTIFICATION_BATCH_SIZE - - except MisskeyAPIException as e: - print(f'An exception has occured while reading notifications: {e}\n') - print(traceback.format_exc()) - finally: - # Quality jank right here, but finally lets us update the last_seen_id - # even if we hit an exception or return early - if new_last_seen_id > last_seen_id: - set_config('last_seen_notif_id', new_last_seen_id) - - return False -- 2.39.2 From 7b32ee7fcf42c2e5d6c9048664df7b26dee3c1bc Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 13 Jun 2025 11:23:34 +0900 Subject: [PATCH 12/17] had to change some things to get cursor for db working. --- bot/add_character.py | 14 ++++---------- bot/bot_app.py | 5 ++++- example_config.ini | 17 +++++++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/bot/add_character.py b/bot/add_character.py index 7590c1b..55089fe 100644 --- a/bot/add_character.py +++ b/bot/add_character.py @@ -1,6 +1,6 @@ import requests from fediverse_factory import get_fediverse_service -from db_utils import get_db_connection +import db_utils from config import RARITY_TO_WEIGHT def add_character(name: str, rarity: int, weight: float, image_url: str) -> tuple[int, str]: @@ -47,20 +47,14 @@ def add_character(name: str, rarity: int, weight: float, image_url: str) -> tupl except RuntimeError as e: raise RuntimeError(f"Failed to upload image: {e}") from e - # Insert into database - conn = get_db_connection() - cur = conn.cursor() - cur.execute( + # Insert into database using the global connection pattern + db_utils.CURSOR.execute( 'INSERT INTO characters (name, rarity, weight, file_id) VALUES (?, ?, ?, ?)', (stripped_name, rarity, float(weight), file_id) ) - conn.commit() - character_id = cur.lastrowid + character_id = db_utils.CURSOR.lastrowid return character_id, file_id except Exception as e: raise - finally: - if 'conn' in locals(): - conn.close() diff --git a/bot/bot_app.py b/bot/bot_app.py index f4bc1e0..b4a95c8 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -1,11 +1,14 @@ import time import traceback from parsing import parse_notification -from db_utils import get_config, set_config +from db_utils import get_config, set_config, connect from fediverse_factory import get_fediverse_service import config def stream_notifications(): + # Initialize database connection + connect() + # Initialize the Fediverse service fediverse_service = get_fediverse_service() diff --git a/example_config.ini b/example_config.ini index 58cf483..cb9aa89 100644 --- a/example_config.ini +++ b/example_config.ini @@ -5,6 +5,13 @@ DefaultAdmins = ['admin@example.tld'] ; SQLite Database location DatabaseLocation = ./gacha_game.db +; Instance type - either "misskey" or "pleroma" +InstanceType = misskey +; Web server port (default: 5000) +WebPort = 5000 +; Comma-separated list of trusted fediverse instances (leave empty to allow only local users) +; Example: TrustedInstances = mastodon.social,misskey.io,pleroma.example.com +TrustedInstances = [gacha] ; Number of seconds players have to wait between rolls @@ -32,13 +39,3 @@ User = @bot@example.tld ; API key for the bot ; Generate one by going to Settings > API > Generate access token Token = abcdefghijklmnopqrstuvwxyz012345 - -; Instance type - either "misskey" or "pleroma" -InstanceType = misskey - -; Web server port (default: 5000) -WebPort = 5000 - -; Comma-separated list of trusted fediverse instances (leave empty to allow only local users) -; Example: TrustedInstances = mastodon.social,misskey.io,pleroma.example.com -TrustedInstances = -- 2.39.2 From 7161712f154ec37d4e7015691481b1bb91debd80 Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 13 Jun 2025 13:26:01 +0900 Subject: [PATCH 13/17] rm unused function that bypassed server abstraction --- bot/notification.py | 65 ++------------------------------------------- 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/bot/notification.py b/bot/notification.py index b69a496..ea544e2 100644 --- a/bot/notification.py +++ b/bot/notification.py @@ -1,9 +1,6 @@ -import traceback -from typing import Dict, Any - -from config import NOTIFICATION_BATCH_SIZE, USE_WHITELIST +from config import USE_WHITELIST from parsing import parse_notification -from db_utils import get_config, set_config, is_whitelisted, is_player_banned +from db_utils import is_whitelisted, is_player_banned from response import generate_response from custom_types import BotResponse from fediverse_factory import get_fediverse_service @@ -71,61 +68,3 @@ def process_fediverse_notification(notification: FediverseNotification, fedivers file_ids=file_ids # visible_user_ids=[] # TODO: write actual visible users ids so pleromers can use the bot privately ) - - -def process_notifications(client: misskey.Misskey) -> bool: - '''Processes a batch of unread notifications. Returns False if there are - no more notifications to process.''' - - last_seen_id = get_config('last_seen_notif_id') - # process_notification writes to last_seen_id, so make a copy - new_last_seen_id = last_seen_id - - try: - notifications = client.i_notifications( - # Fetch notifications we haven't seen yet. This option is a bit - # tempermental, sometimes it'll include since_id, sometimes it - # won't. We need to keep track of what notifications we've - # already processed. - since_id=last_seen_id, - # Let misskey handle the filtering - include_types=['mention', 'reply'], - # And handle the batch size while we're at it - limit=NOTIFICATION_BATCH_SIZE - ) - - # No notifications. Wait the poll period. - if not notifications: - return False - - # Iterate oldest to newest - for notification in notifications: - try: - # Skip if we've processed already - notif_id = notification.get('id', '') - if notif_id <= last_seen_id: - continue - - # Update new_last_seen_id and process - new_last_seen_id = notif_id - process_notification(client, notification) - - except Exception as e: - print(f'An exception has occured while processing a \ -notification: {e}') - print(traceback.format_exc()) - - # If we got as many notifications as we requested, there are probably - # more in the queue - return len(notifications) == NOTIFICATION_BATCH_SIZE - - except MisskeyAPIException as e: - print(f'An exception has occured while reading notifications: {e}\n') - print(traceback.format_exc()) - finally: - # Quality jank right here, but finally lets us update the last_seen_id - # even if we hit an exception or return early - if new_last_seen_id > last_seen_id: - set_config('last_seen_notif_id', new_last_seen_id) - - return False -- 2.39.2 From a516a9b55a014fe41faaba2eaae85c053ccfec63 Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 13 Jun 2025 13:36:38 +0900 Subject: [PATCH 14/17] minor change to make testing easier. --- bot/fediverse_factory.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/bot/fediverse_factory.py b/bot/fediverse_factory.py index f2a9017..bee5f73 100644 --- a/bot/fediverse_factory.py +++ b/bot/fediverse_factory.py @@ -8,9 +8,13 @@ class FediverseServiceFactory: """Factory for creating FediverseService implementations based on configuration""" @staticmethod - def create_service() -> FediverseService: + def create_service(instance_type: str = None) -> FediverseService: """ - Create a FediverseService implementation based on the configured instance type. + Create a FediverseService implementation based on the instance type. + + Args: + instance_type: The type of instance ("misskey" or "pleroma"). + If None, reads from config.INSTANCE_TYPE Returns: FediverseService implementation (MisskeyService or PleromaService) @@ -18,14 +22,24 @@ class FediverseServiceFactory: Raises: ValueError: If the instance type is not supported """ - if config.INSTANCE_TYPE == "misskey": + if instance_type is None: + instance_type = config.INSTANCE_TYPE + + instance_type = instance_type.lower() + + if instance_type == "misskey": return MisskeyService() - elif config.INSTANCE_TYPE == "pleroma": + elif instance_type == "pleroma": return PleromaService() else: - raise ValueError(f"Unsupported instance type: {config.INSTANCE_TYPE}") + raise ValueError(f"Unsupported instance type: {instance_type}") -def get_fediverse_service() -> FediverseService: - """Convenience function to get a FediverseService instance""" - return FediverseServiceFactory.create_service() \ No newline at end of file +def get_fediverse_service(instance_type: str = None) -> FediverseService: + """ + Convenience function to get a FediverseService instance + + Args: + instance_type: Optional instance type override for testing + """ + return FediverseServiceFactory.create_service(instance_type) \ No newline at end of file -- 2.39.2 From 40f018a83b3d7ab9224c0623c4f7f78ade677d59 Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 13 Jun 2025 13:53:41 +0900 Subject: [PATCH 15/17] make instantiation explicit so easier to test. --- bot/add_card.py | 6 +-- bot/bot_app.py | 3 +- bot/fediverse_factory.py | 15 +++---- bot/mock_fediverse_service.py | 74 +++++++++++++++++++++++++++++++++++ bot/notification.py | 6 +-- bot/parsing.py | 2 +- 6 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 bot/mock_fediverse_service.py diff --git a/bot/add_card.py b/bot/add_card.py index ffa1120..2c91618 100644 --- a/bot/add_card.py +++ b/bot/add_card.py @@ -1,7 +1,7 @@ import requests +import config from fediverse_factory import get_fediverse_service import db_utils -from config import RARITY_TO_WEIGHT def add_card(name: str, rarity: int, weight: float, image_url: str) -> tuple[int, str]: """ @@ -28,7 +28,7 @@ def add_card(name: str, rarity: int, weight: float, image_url: str) -> tuple[int raise ValueError('Card name cannot be empty.') if rarity < 1: raise ValueError('Rarity must be a positive integer.') - if rarity not in RARITY_TO_WEIGHT.keys(): + if rarity not in config.RARITY_TO_WEIGHT.keys(): raise ValueError(f'Invalid rarity: {rarity}') if not image_url: raise ValueError('Image URL must be provided.') @@ -40,7 +40,7 @@ def add_card(name: str, rarity: int, weight: float, image_url: str) -> tuple[int raise RuntimeError(f"Failed to download image from {image_url}") # Upload to Fediverse instance - fediverse_service = get_fediverse_service() + fediverse_service = get_fediverse_service(config.INSTANCE_TYPE) try: uploaded_file = fediverse_service.upload_file(response.raw) file_id = uploaded_file.id diff --git a/bot/bot_app.py b/bot/bot_app.py index dabc219..2609a1b 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -1,5 +1,6 @@ import time import traceback +import config from notification import process_fediverse_notification from db_utils import get_config, set_config, connect, setup_administrators from fediverse_factory import get_fediverse_service @@ -12,7 +13,7 @@ def stream_notifications(): setup_administrators() # Initialize the Fediverse service - fediverse_service = get_fediverse_service() + fediverse_service = get_fediverse_service(config.INSTANCE_TYPE) # Get the last seen notification ID from the database last_seen_id = get_config("last_seen_notif_id") diff --git a/bot/fediverse_factory.py b/bot/fediverse_factory.py index bee5f73..fe501db 100644 --- a/bot/fediverse_factory.py +++ b/bot/fediverse_factory.py @@ -1,20 +1,18 @@ from fediverse_service import FediverseService from misskey_service import MisskeyService from pleroma_service import PleromaService -import config class FediverseServiceFactory: - """Factory for creating FediverseService implementations based on configuration""" + """Factory for creating FediverseService implementations""" @staticmethod - def create_service(instance_type: str = None) -> FediverseService: + def create_service(instance_type: str) -> FediverseService: """ Create a FediverseService implementation based on the instance type. Args: - instance_type: The type of instance ("misskey" or "pleroma"). - If None, reads from config.INSTANCE_TYPE + instance_type: The type of instance ("misskey" or "pleroma") Returns: FediverseService implementation (MisskeyService or PleromaService) @@ -22,9 +20,6 @@ class FediverseServiceFactory: Raises: ValueError: If the instance type is not supported """ - if instance_type is None: - instance_type = config.INSTANCE_TYPE - instance_type = instance_type.lower() if instance_type == "misskey": @@ -35,11 +30,11 @@ class FediverseServiceFactory: raise ValueError(f"Unsupported instance type: {instance_type}") -def get_fediverse_service(instance_type: str = None) -> FediverseService: +def get_fediverse_service(instance_type: str) -> FediverseService: """ Convenience function to get a FediverseService instance Args: - instance_type: Optional instance type override for testing + instance_type: The instance type ("misskey" or "pleroma") """ return FediverseServiceFactory.create_service(instance_type) \ No newline at end of file diff --git a/bot/mock_fediverse_service.py b/bot/mock_fediverse_service.py new file mode 100644 index 0000000..34a1c0c --- /dev/null +++ b/bot/mock_fediverse_service.py @@ -0,0 +1,74 @@ +"""Mock FediverseService for testing purposes""" + +from typing import List, Optional, Union, BinaryIO +from fediverse_service import FediverseService +from fediverse_types import FediverseNotification, FediversePost, FediverseFile, Visibility + + +class MockFediverseService(FediverseService): + """Mock implementation of FediverseService for testing""" + + def __init__(self): + self.notifications = [] + self.created_posts = [] + self.uploaded_files = [] + + def get_notifications(self, since_id: Optional[str] = None) -> List[FediverseNotification]: + """Return mock notifications, optionally filtered by since_id""" + if since_id is None: + return self.notifications + + # Filter notifications newer than since_id + filtered = [] + for notif in self.notifications: + if notif.id > since_id: + filtered.append(notif) + return filtered + + def create_post( + self, + text: str, + reply_to_id: Optional[str] = None, + visibility: Visibility = Visibility.HOME, + file_ids: Optional[List[str]] = None, + visible_user_ids: Optional[List[str]] = None + ) -> str: + """Mock post creation, returns fake post ID""" + post_id = f"mock_post_{len(self.created_posts)}" + + # Store the post for assertions + self.created_posts.append({ + 'id': post_id, + 'text': text, + 'reply_to_id': reply_to_id, + 'visibility': visibility, + 'file_ids': file_ids, + 'visible_user_ids': visible_user_ids + }) + + return post_id + + def upload_file(self, file_data: Union[BinaryIO, bytes]) -> FediverseFile: + """Mock file upload, returns fake file""" + file_id = f"mock_file_{len(self.uploaded_files)}" + + mock_file = FediverseFile( + id=file_id, + url=f"https://example.com/files/{file_id}", + type="image/png", + name="test_file.png" + ) + + self.uploaded_files.append(mock_file) + return mock_file + + # Helper methods for testing + def add_mock_notification(self, notification: FediverseNotification): + """Add a mock notification for testing""" + self.notifications.append(notification) + + def clear_all(self): + """Clear all mock data""" + self.notifications.clear() + self.created_posts.clear() + self.uploaded_files.clear() \ No newline at end of file diff --git a/bot/notification.py b/bot/notification.py index ea544e2..f264718 100644 --- a/bot/notification.py +++ b/bot/notification.py @@ -1,4 +1,4 @@ -from config import USE_WHITELIST +import config from parsing import parse_notification from db_utils import is_whitelisted, is_player_banned from response import generate_response @@ -10,7 +10,7 @@ from fediverse_types import FediverseNotification, NotificationType, Visibility def process_fediverse_notification(notification: FediverseNotification, fediverse_service=None) -> None: '''Processes an individual fediverse notification using the abstraction''' if fediverse_service is None: - fediverse_service = get_fediverse_service() + fediverse_service = get_fediverse_service(config.INSTANCE_TYPE) # Get user and instance info username = notification.user.username @@ -18,7 +18,7 @@ def process_fediverse_notification(notification: FediverseNotification, fedivers instance = host if host else 'local' # Check whitelist - if USE_WHITELIST and not is_whitelisted(instance): + if config.USE_WHITELIST and not is_whitelisted(instance): print(f'⚠️ Blocked notification from untrusted instance: {instance}') return diff --git a/bot/parsing.py b/bot/parsing.py index 476e774..d625980 100644 --- a/bot/parsing.py +++ b/bot/parsing.py @@ -10,7 +10,7 @@ def parse_notification(notification: FediverseNotification, fediverse_service=No generate_response()''' if fediverse_service is None: - fediverse_service = get_fediverse_service() + fediverse_service = get_fediverse_service(config.INSTANCE_TYPE) # We get the type of notification to filter the ones that we actually want # to parse -- 2.39.2 From 3ad4edbc45e64689d9263f6108fa9f9b537a17d8 Mon Sep 17 00:00:00 2001 From: Moon Date: Sat, 14 Jun 2025 04:10:16 +0900 Subject: [PATCH 16/17] ignore custom startup script --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a05fd62..2df4c91 100644 --- a/.gitignore +++ b/.gitignore @@ -185,5 +185,6 @@ cython_debug/ gacha_game*.db gacha_game*.db.* config*.ini +run.sh .idea -- 2.39.2 From 337a9896715f6f3f11c9354463e6e2af3a6e20f6 Mon Sep 17 00:00:00 2001 From: Moon Date: Sat, 14 Jun 2025 04:34:20 +0900 Subject: [PATCH 17/17] rm vestigial trusted instances config. --- bot/config.py | 4 ---- example_config.ini | 3 --- 2 files changed, 7 deletions(-) diff --git a/bot/config.py b/bot/config.py index 11541cd..4f1f907 100644 --- a/bot/config.py +++ b/bot/config.py @@ -57,10 +57,6 @@ INSTANCE_TYPE = instance_type # Web server port WEB_PORT = config['application'].getint('WebPort', 5000) -# Trusted instances -trusted_instances_str = config['application'].get('TrustedInstances', '') -TRUSTED_INSTANCES = [instance.strip() for instance in trusted_instances_str.split(',') if instance.strip()] - # Fedi handles in the traditional 'user@domain.tld' style, allows these users # to use extra admin exclusive commands with the bot ADMINS = json.loads(config['application']['DefaultAdmins']) diff --git a/example_config.ini b/example_config.ini index 516d552..ab866fe 100644 --- a/example_config.ini +++ b/example_config.ini @@ -9,9 +9,6 @@ DatabaseLocation = ./gacha_game.db InstanceType = misskey ; Web server port (default: 5000) WebPort = 5000 -; Comma-separated list of trusted fediverse instances (leave empty to allow only local users) -; Example: TrustedInstances = mastodon.social,misskey.io,pleroma.example.com -TrustedInstances = ; Whether to limit access to the bot via an instance whitelist ; The whitelist can be adjusted via the application UseWhitelist = False -- 2.39.2