17 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This is a microservices-based Telegram bot system for text monitoring and notifications. The system consists of 4 main services that communicate via RabbitMQ:
- telegram-listener: Scans open Telegram channels and chats for messages
- telegram-client: Bot interface for user interactions
- text-matcher: Matches incoming messages against user subscriptions
- users: Manages users and their preferences
Important: This repository is the parent project containing all services as git submodules. Each service (telegram-listener, telegram-client, text-matcher, users) is a separate repository added as a submodule. Git tags are created at the parent repository level, which triggers the Drone CI/CD pipeline to automatically build and publish NuGet packages with versions based on the git tag.
Architecture
The services follow a message bus pattern using RabbitMQ for async communication:
- TelegramListener publishes:
- MessageReceived events (for new messages)
- MessageEdited events (for edited messages)
- TextMatcher subscribes to MessageReceived and MessageEdited, stores match history in database, and publishes:
- TextSubscriptionMatched events (for first-time matches)
- TextSubscriptionUpdated events (for updates to previously matched messages)
- TelegramClient subscribes to both TextSubscriptionMatched and TextSubscriptionUpdated events and notifies users
Note: WTelegram sends both UpdateNewChannelMessage and UpdateEditChannelMessage for the same message. TelegramListener publishes separate events to avoid duplicate notifications downstream.
Each service follows Clean Architecture with separate projects for:
- Host (API/entry point) - ASP.NET Core application, controllers, startup configuration
- AppServices (business logic) - Use cases, business logic, command/query handlers
- Core (shared utilities) - Domain entities, interfaces, shared logic
- Api.Contracts (REST API contracts) - DTOs, request/response models for REST endpoints (published as NuGet)
- Async.Api.Contracts (event contracts) - Event DTOs for RabbitMQ messaging (published as NuGet)
- Persistence (database layer) - EF Core DbContext, repositories, data access (text-matcher, users only)
- Migrator (database migrations) - EF Core migrations, migration scripts (text-matcher, users only)
Project Structure Examples:
- telegram-listener: Host, AppServices, Core, Async.Api.Contracts (no database)
- telegram-client: Host, AppServices, Core (no database, no published contracts)
- text-matcher: Host, AppServices, Core, Api.Contracts, Async.Api.Contracts, Persistence, Migrator
- users: Host, AppServices, Core, Api.Contracts, Persistence, Migrator
Development Commands
Running the System
# IMPORTANT: Before building with Docker Compose, prepare the build environment
./prepare-build.sh
# Start all services with Docker Compose
docker-compose up
# Start individual services for development (no preparation needed)
cd telegram-client && dotnet run --project src/Nocr.TelegramClient.Host
cd telegram-listener && dotnet run --project src/Nocr.TelegramListener.Host
cd text-matcher && dotnet run --project src/Nocr.TextMatcher.Host
cd users && dotnet run --project src/Nocr.Users.Host
Note: The prepare-build.sh script copies nuget.config to all submodule roots, which is required for Docker builds. This step is automatic in CI/CD but must be run manually for local Docker Compose builds.
Building
# Build individual services
cd <service-name> && dotnet build
# Build specific projects
dotnet build src/Nocr.<ServiceName>.Host
Testing
# Run all tests in text-matcher
cd text-matcher && dotnet test
# Run tests for a specific project
cd text-matcher && dotnet test tests/Nocr.TextMatcher.AppServices.UnitTests/Nocr.TextMatcher.AppServices.UnitTests.csproj
# Run tests with verbose output
cd text-matcher && dotnet test --verbosity detailed
# Run tests with code coverage (if configured)
cd text-matcher && dotnet test --collect:"XPlat Code Coverage"
Database Migrations
For text-matcher and users services:
# Add new migration (from service root directory)
cd text-matcher && ./src/Nocr.TextMatcher.Migrator/AddMigration.sh MyMigrationName
cd users && ./src/Nocr.Users.Migrator/AddMigration.sh MyMigrationName
# Apply migrations (handled automatically by migrator containers in docker-compose)
Working with Git Submodules
# Initialize submodules after cloning the repository
git submodule update --init --recursive
# Update all submodules to their latest commits on main
./update-submodules.sh
# Commit and push changes to all submodules at once
./commit-all.sh "Your commit message"
# Update a single submodule
cd telegram-listener
git pull origin main
cd ..
git add telegram-listener
git commit -m "Update telegram-listener submodule"
# Check status of all submodules
git submodule status
Configuration
📖 See CONFIGURATION.md for detailed configuration guide.
Quick Start
The system uses ASP.NET Core's layered configuration with Environment Variables having highest priority.
Configuration priority (lowest → highest):
appsettings.json(base settings, committed)appsettings.{Environment}.json(environment-specific, some committed via.examplefiles)- User Secrets (Development only)
- Environment Variables (ALWAYS WINS)
Three Deployment Modes
-
VS Code (Local Development)
- Copy
appsettings.Development.json.example→appsettings.Development.jsonin each service - Or use environment variables in
.vscode/launch.json - Start infrastructure:
docker-compose up nocr-rabbitmq nocr-text-matcher-db nocr-users-db -d
- Copy
-
Docker Compose (Local Full Stack)
- Copy
.nocr.env.example→.nocr.envin project root - Fill in your Telegram API credentials and Bot token
- Run:
docker-compose up
- Copy
-
Kubernetes (Production)
- Create K8s Secrets (do NOT use
.nocr.env) - Reference secrets in deployment manifests via
envFrom
- Create K8s Secrets (do NOT use
Debug Mode
Enable configuration debug logging on startup:
export NOCR_DEBUG_MODE=true
This prints masked configuration values to help troubleshoot issues.
Important Notes
- ❌ Never commit secrets - use
.examplefiles as templates - ✅ Environment variables override all appsettings files
- ✅ All services use
.examplefiles for documentation - ✅ Docker Compose uses
.nocr.envfile (gitignored)
Service Ports
When running with docker-compose:
- telegram-client: 5050 (http://localhost:5050/health)
- telegram-listener: 5040 (http://localhost:5040/health)
- text-matcher: 5041 (http://localhost:5041/health)
- users: 5042 (http://localhost:5042/health)
- RabbitMQ: 5672 (AMQP), 15672 (Management UI - http://localhost:15672, admin/admin)
- MariaDB: 3316 (text-matcher), 3326 (users)
Key Technologies
- .NET 8 - All services built on .NET 8
- ASP.NET Core Web APIs - REST endpoints and hosting
- Entity Framework Core with MariaDB - ORM for text-matcher and users databases
- WTelegramClient - MTProto API client for telegram-listener
- Telegram.Bot - Bot API client for telegram-client
- Rebus - Message bus abstraction over RabbitMQ
- RabbitMQ - Message broker for async event-driven communication
- Docker & Docker Compose - Local development and containerization
- Kaniko - Container image building in CI/CD
- Drone CI - CI/CD pipeline on Kubernetes
NuGet Package Management
The project uses Central Package Management (CPM) with Package Source Mapping to manage NuGet dependencies:
Package Sources
- nuget.org: All public packages (Microsoft., Serilog., etc.)
- musk (private): Internal
Nocr.*contract packages
Configuration
The nuget.config file in the project root defines package source mapping:
<packageSourceMapping>
<packageSource key="musk">
<package pattern="Nocr.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
How It Works
- Local Development: Copy
nuget.configto each submodule root when needed - CI/CD: Drone automatically copies
nuget.configto/root/.nuget/NuGet/NuGet.Config - Docker Builds: Kaniko copies
nuget.configto submodule root before building
This eliminates NuGet warning NU1507 and ensures consistent package resolution across all environments.
CI/CD Pipeline
📖 See _deploy/README.md for full CI/CD documentation.
The project uses Drone CI with 5 specialized pipelines:
1. Feature Validation (feature/*, fix/* branches)
Purpose: Test feature branches before merging to main
How to trigger:
# Create feature branch and push
git checkout -b feature/my-new-feature
# ... make changes ...
git add .
git commit -m "Add new feature"
git push origin feature/my-new-feature
What happens:
- Runs build + tests with Testcontainers support
- Provides fast feedback before merge
- Duration: ~3-5 minutes
2. Main Validation (main branch)
Purpose: Ensure main branch stays healthy after merge
How to trigger:
# Merge feature to main
git checkout main
git merge feature/my-new-feature
git push origin main
What happens:
- Same checks as feature validation
- Ensures main branch builds and tests pass
- Duration: ~3-5 minutes
3. Contracts-Only Publish
Purpose: Publish NuGet contracts without building Docker images (fast iteration)
How to trigger:
# Update contracts in a submodule
cd telegram-listener
# ... update Async.Api.Contracts ...
git add . && git commit -m "Add MessageEdited event"
git push
# From parent repo, tag with contracts_only marker
cd ..
git add telegram-listener
git commit -m "contracts_only:telegram_listener - Add MessageEdited event"
git tag 0.7.41
git push && git push --tags
What happens:
- Publishes only the specified service's NuGet contracts
- Skips Docker image builds
- Duration: ~2 minutes
Services: telegram_listener, text_matcher, users
4. Full Release
Purpose: Complete release - contracts + images + deployment
How to trigger:
# Make your changes, commit to main
git add .
git commit -m "Implement new feature"
git push
# Tag for full release (no special markers in commit message)
git tag v1.3.0
git push --tags
What happens:
- Publishes all NuGet contracts (parallel)
- Builds all Docker images with Kaniko (parallel, after contracts)
- Deploys to Kubernetes (automatic for
v*tags)
- Duration: ~8-10 minutes
5. Deploy-Only
Purpose: Deploy existing images without rebuilding (rollbacks, hotfixes)
How to trigger:
# Deploy already-built images (e.g., rollback to previous version)
git commit --allow-empty -m "deploy_only: Rollback to v1.2.9"
git tag deploy-v1.2.9
git push && git push --tags
# Or deploy latest images
git commit --allow-empty -m "deploy_only: Deploy latest"
git tag deploy-latest
git push && git push --tags
What happens:
- Skips building entirely
- Deploys specified tag's images (or
latest) - Duration: ~1 minute
Development Workflows
Making Changes to a Single Service
When working on a specific service (e.g., telegram-listener):
# 1. Navigate to the submodule
cd telegram-listener
# 2. Create a feature branch in the submodule
git checkout -b feature/new-feature
# 3. Make your changes to the code
# ... edit files ...
# 4. Test locally (if tests exist)
dotnet test
# 5. Commit and push in the submodule
git add .
git commit -m "Add new feature"
git push origin feature/new-feature
# 6. Return to parent repo and update submodule reference
cd ..
git add telegram-listener
git commit -m "Update telegram-listener: Add new feature"
git push origin main
Updating Contract Packages
When you change event contracts (Async.Api.Contracts) or REST contracts (Api.Contracts):
# 1. Make changes to contracts in the submodule
cd text-matcher
# ... edit Nocr.TextMatcher.Async.Api.Contracts ...
git add . && git commit -m "Add new event contract"
git push origin main
# 2. Return to parent repo and publish contracts only
cd ..
git add text-matcher
git commit -m "contracts_only:text_matcher - Add new event contract"
git tag 0.8.6
git push && git push --tags
# 3. Other services can now reference the new contract version
# Update their .csproj or Directory.Packages.props to use version 0.8.6
Working Across Multiple Services
When implementing a feature that spans multiple services:
# 1. Update each submodule in sequence
cd telegram-listener
git checkout -b feature/cross-service-feature
# ... make changes ...
git commit -m "Part 1: Update listener"
git push origin feature/cross-service-feature
cd ../text-matcher
git checkout -b feature/cross-service-feature
# ... make changes ...
git commit -m "Part 2: Update matcher"
git push origin feature/cross-service-feature
# 2. Update parent repo to reference all changes
cd ..
git add telegram-listener text-matcher
git commit -m "Implement cross-service feature"
git push origin main
# 3. Tag for full release
git tag v1.5.0
git push --tags
Common CI/CD Workflows
Test a Feature Branch
git checkout -b feature/add-logging
# ... make changes ...
git add . && git commit -m "Add structured logging"
git push origin feature/add-logging
# ✅ Drone runs feature-validation pipeline
Publish Contracts After API Changes
cd text-matcher
# Update Nocr.TextMatcher.Async.Api.Contracts
git add . && git commit -m "Add UserDeleted event"
git push
cd ..
git add text-matcher
git commit -m "contracts_only:text_matcher - Add UserDeleted event"
git tag 0.8.5
git push && git push --tags
# ✅ Drone publishes text-matcher contracts to NuGet
Full Release Workflow
# Work on main branch
git add .
git commit -m "Implement payment processing"
git push
# ✅ Drone runs main-validation
# Tag for release
git tag v2.0.0
git push --tags
# ✅ Drone runs full-release pipeline:
# 1. Publishes all contracts
# 2. Builds all Docker images
# 3. Deploys to Kubernetes
Emergency Rollback
# Rollback to last known good version
git commit --allow-empty -m "deploy_only: Emergency rollback to v1.9.5"
git tag rollback-v1.9.5
git push && git push --tags
# ✅ Drone deploys v1.9.5 images immediately (~1 minute)
Deployment Scripts
Located in _deploy/scripts/:
- deploy.sh - Deploy services to Kubernetes with health checks
- rollback.sh - Rollback deployments to previous version
- health-check.sh - Check health of all services
Key Optimizations
- Shared NuGet cache across pipeline steps (~60% faster builds)
- Parallel execution for independent operations
- Proper dependency order: Contracts → Images → Deploy
- Kaniko layer caching for faster Docker builds
- Docker-in-Docker support for Testcontainers in tests
Troubleshooting
Submodule Issues
Problem: Submodule directories are empty after cloning
# Solution: Initialize submodules
git submodule update --init --recursive
Problem: Submodule is in detached HEAD state
# Solution: Check out the main branch in the submodule
cd <submodule-name>
git checkout main
git pull origin main
cd ..
Problem: Changes in submodule not reflected in parent repo
# Solution: Update submodule reference in parent
cd .. # Return to parent repo
git add <submodule-name>
git commit -m "Update <submodule-name> reference"
Build Issues
Problem: NuGet package restore fails with NU1507 warning
# Solution: Copy nuget.config to submodule root
cp nuget.config telegram-listener/
cd telegram-listener && dotnet restore
Problem: Cannot find Nocr.* contract packages
# Solution: Ensure you have access to the private NuGet feed
# Check nuget.config has the correct source configuration
# Verify credentials for the "musk" package source
Docker Compose Issues
Problem: Service cannot connect to RabbitMQ
# Solution: Use the Docker network hostname, not localhost
# In docker-compose environment: nocr-rabbitmq:5672
# In local development: localhost:5672
Problem: Database connection fails
# Solution: Wait for database health checks to pass
# Check docker-compose logs for database container
docker-compose logs nocr-text-matcher-db
# Database takes 10-30 seconds to be ready on first start
CI/CD Issues
Problem: Pipeline doesn't trigger on tag
# Solution: Ensure tag matches expected patterns
# Feature validation: feature/*, fix/*, issues/*
# Main validation: main branch
# Contracts: Tag with commit message containing "contracts_only:<service>"
# Full release: Any tag without special markers
# Deploy-only: Tag with commit message containing "deploy_only:"
Problem: Docker image build fails in Kaniko
# Solution: Check if nuget.config is correctly copied
# Verify _deploy/deploy.sh contains nuget.config copy step
# Check Drone logs for nuget.config presence in build context