+4
.gitignore
+4
.gitignore
+97
bot-config/README.md
+97
bot-config/README.md
···
···+Contains Zulip API credentials for the bot. This file should **never** be committed to version control.+Contains bot behavior configuration and defaults. This file can be committed to version control as it contains no secrets.
+28
bot-config/botrc
+28
bot-config/botrc
···
···
+34
bot-config/botrc.template
+34
bot-config/botrc.template
···
···
-30
bot-config/run-bot.sh
-30
bot-config/run-bot.sh
···
···
+16
bot-config/zuliprc.template
+16
bot-config/zuliprc.template
···
···
+1
-1
src/thicket/bots/__init__.py
+1
-1
src/thicket/bots/__init__.py
+57
-58
src/thicket/bots/test_bot.py
+57
-58
src/thicket/bots/test_bot.py
············
············
+755
-252
src/thicket/bots/thicket_bot.py
+755
-252
src/thicket/bots/thicket_bot.py
······-bot_handler.send_reply(message, f"Unknown command: {command}. Type `@mention help` for usage.")···-content = re.sub(rf'@(?:\*\*)?{escaped_name}(?:\*\*)?', '', content, flags=re.IGNORECASE).strip()-def _send_status(self, message: Dict[str, Any], bot_handler: BotHandler, sender: str) -> None:-f"โฑ๏ธ **Sync Interval:** {self.sync_interval}s ({self.sync_interval // 60}m {self.sync_interval % 60}s)",-f"๐ **Catchup Mode:** {'Active (first run)' if len(self.posted_entries) == 0 else 'Inactive'}",-def _handle_force_sync(self, message: Dict[str, Any], bot_handler: BotHandler, sender: str) -> None:-def _handle_reset_command(self, message: Dict[str, Any], bot_handler: BotHandler, sender: str) -> None:-f"โ Posting history reset! Recent entries will be posted on next sync. (requested by {sender})"······-raise ValueError(f"User '{self.debug_user}' has no Zulip association for server '{server_url}'")-self.logger.info(f"Debug mode enabled: Will send DMs to {self.debug_user} (email: {zulip_user_id}, user_id: {actual_user_id}) on {server_url}")-self.logger.info(f"Debug mode enabled: Will send DMs to {self.debug_user} ({zulip_user_id}) on {server_url} (will resolve user ID when sending)")-self.logger.error(f"No user found with identifier '{email_or_id}'. Searched {len(users_result['members'])} users.")-def _lookup_zulip_user_info(self, bot_handler: BotHandler, email_or_id: str) -> Tuple[Optional[str], Optional[str]]:···-"โ Stream and topic must be configured first. Use `@mention config stream <name>` and `@mention config topic <name>`"···-def _post_entry_to_zulip(self, entry: AtomEntry, bot_handler: BotHandler, username: str) -> None:···debug_message = f"๐ **DEBUG:** New article from thicket user `{username}`:\n\n{message_content}"-self.logger.error(f"Failed to send DM to {self.debug_user} (tried both int and string): {e2}")···-convert=['a', 'b', 'strong', 'i', 'em', 'code', 'pre', 'p', 'br', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']-content = re.sub(r'<h[1-6](?:\s[^>]*)?>([^<]*)</h[1-6]>', r'**\1.** ', content, flags=re.IGNORECASE)-content = re.sub(r'<(?:strong|b)(?:\s[^>]*)?>([^<]*)</(?:strong|b)>', r'**\1**', content, flags=re.IGNORECASE)-content = re.sub(r'<(?:em|i)(?:\s[^>]*)?>([^<]*)</(?:em|i)>', r'*\1*', content, flags=re.IGNORECASE)-content = re.sub(r'<a(?:\s[^>]*?)?\s*href=["\']([^"\']*)["\'](?:\s[^>]*)?>([^<]*)</a>', r'[\2](\1)', content, flags=re.IGNORECASE)
·········+f"โฑ๏ธ **Sync Interval:** {self.sync_interval}s ({self.sync_interval // 60}m {self.sync_interval % 60}s)",+f"๐ **Catchup Mode:** {'Active (first run)' if len(self.posted_entries) == 0 else 'Inactive'}",+f"โ Posting history reset! Recent entries will be posted on next sync. (requested by {sender})",+f"โ Username `{username}` not found in thicket. Available users: {', '.join(self.git_store.list_users())}",+f"โ Failed to claim username `{username}`. This shouldn't happen - please contact an administrator.",······+f"Debug mode enabled: Will send DMs to {self.debug_user} (email: {zulip_user_id}, user_id: {actual_user_id}) on {server_url}"+f"Debug mode enabled: Will send DMs to {self.debug_user} ({zulip_user_id}) on {server_url} (will resolve user ID when sending)"+f"No user found with identifier '{email_or_id}'. Searched {len(users_result['members'])} users."···+"โ Stream and topic must be configured first. Use `@mention config stream <name>` and `@mention config topic <name>`",······debug_message = f"๐ **DEBUG:** New article from thicket user `{username}`:\n\n{message_content}"···
+24
-2
src/thicket/cli/commands/__init__.py
+24
-2
src/thicket/cli/commands/__init__.py
···-__all__ = ["add", "bot", "duplicates", "info_cmd", "init", "list_cmd", "search", "sync", "upload", "zulip"]
···
+63
-45
src/thicket/cli/commands/bot.py
+63
-45
src/thicket/cli/commands/bot.py
·········-def _run_bot(config_file: Path, thicket_config: Path, daemon: bool, debug_user: str = None) -> None:-print_info(f"๐ DEBUG MODE: Will send DMs to thicket user '{debug_user}' instead of posting to streams")············
·····················
+1
-1
src/thicket/cli/commands/info_cmd.py
+1
-1
src/thicket/cli/commands/info_cmd.py
+58
-55
src/thicket/cli/commands/search.py
+58
-55
src/thicket/cli/commands/search.py
···············
···············
+44
-20
src/thicket/cli/commands/upload.py
+44
-20
src/thicket/cli/commands/upload.py
············-def _dry_run_upload(git_store: GitStore, config: ThicketConfig, typesense_config: TypesenseConfig) -> None:······-console.print(f" โข Typesense server: {typesense_config.protocol}://{typesense_config.host}:{typesense_config.port}")···-def _perform_upload(git_store: GitStore, config: ThicketConfig, typesense_config: TypesenseConfig) -> None:······-console.print(f"[green]โ Upload completed: {success_count}/{total_count} documents uploaded successfully[/green]")-console.print(f" โข Server: {typesense_config.protocol}://{typesense_config.host}:{typesense_config.port}")-console.print("\n[dim]You can now search your entries using the Typesense API or dashboard.[/dim]")
··················+f" โข Typesense server: {typesense_config.protocol}://{typesense_config.host}:{typesense_config.port}"·········+f"[green]โ Upload completed: {success_count}/{total_count} documents uploaded successfully[/green]"
+55
-45
src/thicket/cli/commands/zulip.py
+55
-45
src/thicket/cli/commands/zulip.py
·····················
·····················
+9
-1
src/thicket/cli/main.py
+9
-1
src/thicket/cli/main.py
+13
-11
src/thicket/core/git_store.py
+13
-11
src/thicket/core/git_store.py
······
······
+114
-58
src/thicket/core/typesense_client.py
+114
-58
src/thicket/core/typesense_client.py
···-def from_url(cls, url: str, api_key: str, collection_name: str = "thicket_entries") -> "TypesenseConfig":············-def upload_from_git_store(self, git_store: GitStore, config: ThicketConfig) -> dict[str, Any]:·········
························
+8
-6
src/thicket/models/user.py
+8
-6
src/thicket/models/user.py
······-zulip_associations: list[ZulipAssociation] = Field(default_factory=list) # Zulip server/user pairs·········
···············
+55
-55
tests/test_bot.py
+55
-55
tests/test_bot.py
···-from thicket.bots.test_bot import BotTester, MockBotHandler, create_test_message, create_test_entry··········································
·············································
+2
-1
tests/test_feed_parser.py
+2
-1
tests/test_feed_parser.py
······
······
+7
-2
tests/test_git_store.py
+7
-2
tests/test_git_store.py
······
······
+31
-34
tests/test_models.py
+31
-34
tests/test_models.py
·····················
·····················