diff --git a/.gitmodules b/.gitmodules index 7530951..071b750 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "telegram-approval-join"] - path = telegram-approval-join +[submodule "telegram-join-approval-bot"] + path = telegram-join-approval-bot url = ssh://git@git.zio.sh:2222/astra/telegram-join-approval-bot.git diff --git a/Dockerfile b/Dockerfile index d55b48a..33ffec3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,14 +23,14 @@ RUN apk add --no-cache ca-certificates WORKDIR /opt # Copy binary into a standard location -COPY --from=builder /bin/telegram-join-approval-nuzzles /usr/local/bin/telegram-approval-join +COPY --from=builder /bin/telegram-join-approval-nuzzles /usr/local/bin/telegram-join-approval-bot # Create a non-root user and group with specific UID:GID and set ownership RUN addgroup -g 65532 app && \ adduser -D -H -u 65532 -G app -s /sbin/nologin app -h /opt && \ - chown -R app:app /opt /usr/local/bin/telegram-approval-join + chown -R app:app /opt /usr/local/bin/telegram-join-approval-bot # Run as the created non-root user USER app:app -ENTRYPOINT ["/usr/local/bin/telegram-approval-join"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/telegram-join-approval-bot"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b77a21 --- /dev/null +++ b/README.md @@ -0,0 +1,301 @@ +# Telegram Join Approval Bot + +A Telegram bot for managing and approving join requests to private groups with approval workflows, canned responses, and admin controls. + +## Overview + +This bot automates the process of approving new members joining a private Telegram group. When a user requests to join, the bot: +1. Sends them a prompt to explain why they want to join +2. Forwards their request to the admin chat with approve/decline/ban buttons +3. Allows admins to manage requests with customizable decline reasons +4. Notifies users of the approval decision + +## Features + +- **Automated Join Requests**: Intercepts and manages join requests to your target group +- **Admin Approval Workflow**: Approve, decline, or ban users with a single click +- **Join Reason Collection**: Users must provide a reason before their request reaches admins +- **Canned Responses**: Pre-configured decline messages for common rejection reasons +- **Flexible Admin Interface**: + - Approve, decline, or ban users + - Add custom decline reasons + - Configure bot messages and settings + - Organize requests in topic threads +- **Approval Notifications**: Optional message sent to approved users +- **Configurable Messages**: Customize entry and approval messages +- **User Banning**: Ban users for 24 hours with a single action + +## How It Works + +### Join Request Flow + +``` +User requests to join group + ↓ +Bot sends entry prompt to user + ↓ +Bot notifies admins with join request + (showing user info and join reason placeholder) + ↓ +User sends their join reason + (message to bot in private chat) + ↓ +Bot updates admin message with user's reason + ↓ +Admin clicks Approve/Decline/Ban + ↓ +User notified of decision (if configured) + ↓ +User added/rejected/banned from group +``` + +### Architecture + +The bot consists of several key components: + +#### Main Entry Point (`main.go`) +- Initializes the bot with config from `config.yaml` +- Starts the long polling update loop +- Routes updates to appropriate handlers + +#### Configuration (`config/config.go`) +- Loads settings from `config.yaml` +- Auto-generates template config if missing +- Supports hot-reloading configuration changes + +#### Handlers +- **`join.go`**: Manages join request flow and user responses +- **`callbacks.go`**: Handles inline button actions (approve/decline/ban) +- **`admin.go`**: Processes admin commands for configuration +- **`handlers.go`**: Shared types and utilities + +#### Key Data Structures + +```go +// ExtendedChatJoinRequest tracks a user's approval request +type ExtendedChatJoinRequest struct { + *ChatJoinRequest // Telegram join request + JoinReason string // User's explanation + JoinRequestMessageID int // Admin notification message ID +} + +// Bot maintains runtime state +type Bot struct { + API BotAPI // Telegram API client + Config Config // Configuration + WaitingForApproval map // In-memory user request tracking +} +``` + +## Configuration + +The bot reads from `config.yaml` in its working directory. On first run, it creates a template: + +```yaml +bot_token: "YOUR_BOT_TOKEN_HERE" # @BotFather token +admin_chat_id: 0 # Chat ID where admins review requests +admin_chat_topic_id: 0 # Optional: Topic ID in admin chat (0 = main) +target_chat_id: 0 # Private group to protect +entry_message: "Please explain why..." # Prompt shown to joining users +approval_message: "" # Message sent after approval (optional) +send_approval_message: false # Whether to send approval message +delete_request_after_decision: false # Auto-delete admin messages after decision +canned_decline_responses: # Pre-configured decline reasons + - "Your profile doesn't meet our criteria" + - "We're at capacity" +``` + +### Getting Required IDs + +**Bot Token**: Create a bot with [@BotFather](https://t.me/botfather) + +**Chat IDs**: +```bash +# Forward a message from the chat to @getidsbot +# It will show you the chat ID +``` + +## Admin Commands + +Admins can use these commands in the admin chat: + +### `/setentrymessage ` +Change the message shown when users request to join. + +``` +/setentrymessage Tell us about yourself and why you want to join! +``` + +### `/setapprovalmessage ` +Set a message sent to approved users. Must be set before enabling approval notifications. + +``` +/setapprovalmessage Welcome to our group! We're excited to have you. +``` + +### `/togglesendapproval` +Enable/disable sending the approval message to approved users. + +### `/setadmintopic ` +Direct admin messages to a specific topic thread (0 = main chat). + +``` +/setadmintopic 42 +``` + +### `/info` +Display current bot configuration. + +### `/edit ` +Reply to a message and use this to edit it. Useful for correcting admin notifications. + +``` +/edit User was banned incorrectly +``` + +## Approval Workflow + +### Admin Actions + +When a user requests to join, an admin sees: + +``` +New join #request from @username [123456789] + +Join reason: "I'm interested in this community" +``` + +With inline buttons: +- **Approve** - Add user to group, optionally send approval message +- **Decline** - Reject request, show decline reason button panel +- **Ban** - Ban user for 24 hours (shows confirmation) + +### Decline Responses + +When declining with canned responses configured: + +1. Admin clicks **Decline** +2. Message shows available canned responses as buttons +3. Admin clicks a canned response +4. User receives the selected reason +5. Admin message updates with the reason + +Or, admin can reply to a decline message with custom text (with or without `/` prefix): + +``` +[Reply to decline message] +/We can only accept members with 2+ years experience +``` + +## Deployment + +### Docker + +Build and run the container: + +```bash +docker build -t telegram-approval-bot . +docker run -it \ + --mount type=bind,source=$PWD/config.yaml,target=/opt/config.yaml \ + telegram-approval-bot +``` + +The Dockerfile: +- Builds a static binary with minimal dependencies +- Uses Alpine for small image size +- Runs as unprivileged user (UID 65532) +- Mounts config as a volume for persistence + +### Compose Example + +```yaml +version: '3' +services: + bot: + build: . + volumes: + - ./config.yaml:/opt/config.yaml + restart: unless-stopped +``` + +## Implementation Details + +### State Management + +Join requests are tracked in memory using a mutex-protected map: + +```go +WaitingForApproval map[int64]*ExtendedChatJoinRequest +``` + +When a user provides their join reason, the bot: +1. Updates the request with their reason +2. Edits the admin message to show the reason +3. Keeps the message indexed by message ID for button handling + +### Update Processing + +The bot receives Telegram updates via long polling: + +1. **ChatJoinRequest**: User requests to join → calls `HandleJoinRequest` +2. **CallbackQuery**: Admin clicks button → calls `HandleCallbackQuery` +3. **Message**: User sends reason or admin command → routes accordingly + +### Message Formatting + +Admin notifications use HTML formatting with: +- User display name with link +- Numeric user ID +- Join reason (HTML-escaped for safety) +- Admin action history +- Timestamp information + +### Error Handling + +- Failed API requests restore the original message +- Missing user state after restart shows helpful error +- Invalid callback data is logged without crashing + +## Development + +### Dependencies + +- `github.com/OvyFlash/telegram-bot-api` - Telegram Bot API client +- `go.yaml.in/yaml/v3` - YAML configuration parsing + +### Go Version + +Requires Go 1.25.3+ + +### Building Locally + +```bash +go build -o telegram-approval-bot ./... +./telegram-approval-bot +``` + +## Troubleshooting + +### Bot doesn't respond to join requests +- Verify `target_chat_id` is correct +- Check bot has admin rights in the target group +- Ensure bot is configured to receive join request updates + +### Admin messages don't appear +- Verify `admin_chat_id` is correct +- Check bot has message permission in admin chat +- If using topics, verify `admin_chat_topic_id` is set + +### Approve/Decline buttons don't work +- Bot needs admin rights in target group +- Join request may have expired (Telegram timeout) +- Check logs for API errors + +### Config changes don't apply +- Some changes require bot restart +- Use `/info` to verify current settings +- Check config.yaml for syntax errors + +## License + +[Specify your license here] diff --git a/scripts/sync.sh b/scripts/sync.sh index 058fe58..550827a 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# sync.sh — Copy telegram-approval-join submodule into internal/, then apply patches. +# sync.sh — Copy telegram-join-approval-bot submodule into internal/, then apply patches. # Usage: ./scripts/sync.sh set -euo pipefail @@ -7,7 +7,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -SUBMODULE_DIR="$ROOT_DIR/telegram-approval-join" +SUBMODULE_DIR="$ROOT_DIR/telegram-join-approval-bot" PATCH_FILE="$ROOT_DIR/patches/0001-nuzzles.patch" # ── 1. Ensure submodule is initialised ──────────────────────────────────────── @@ -22,19 +22,13 @@ git -C "$ROOT_DIR" submodule update --init --recursive --remote # Capture the submodule commit after update NEW_COMMIT=$(git -C "$SUBMODULE_DIR" rev-parse HEAD 2>/dev/null || echo "") -# Check if there were any changes -# if [[ "$OLD_COMMIT" == "$NEW_COMMIT" ]] && [[ -n "$OLD_COMMIT" ]]; then -# echo " Submodule is already up to date. Nothing to do." -# exit 0 -# fi - # ── 2. Wipe and re-copy the submodule source ────────────────────────────────── -echo "→ Copying telegram-approval-join source to root..." +echo "→ Copying telegram-join-approval-bot source to root..." # Clean up directories that will be replaced rm -rf "$ROOT_DIR/cmd" "$ROOT_DIR/internal" "$ROOT_DIR/go.mod" "$ROOT_DIR/go.sum" "$ROOT_DIR/config.yaml.example" # Copy source files, excluding .git and keeping patches/ and scripts/ -rsync -a --stats --exclude='.git' --exclude='internal/telegram-approval-join' \ +rsync -a --stats --exclude='.git' --exclude='internal/telegram-join-approval-bot' \ "$SUBMODULE_DIR/" "$ROOT_DIR/" \ --exclude='patches' --exclude='scripts' --exclude='README.md' --exclude='Dockerfile' @@ -42,13 +36,13 @@ rsync -a --stats --exclude='.git' --exclude='internal/telegram-approval-join' \ sed -i '/^scripts\/$/d' "$ROOT_DIR/.gitignore" # ── 3. Rewrite the Go module path inside the copied source ──────────────────── -# Change the module from telegram-approval-join to telegram-join-approval-nuzzles +# Change the module from telegram-join-approval-bot to telegram-join-approval-nuzzles echo "→ Rewriting module path in go.mod ..." -sed -i "s|^module git\.zio\.sh/astra/telegram-approval-join|module git.zio.sh/astra/telegram-join-approval-nuzzles|" "$ROOT_DIR/go.mod" +sed -i "s|^module git\.zio\.sh/astra/telegram-join-approval-bot|module git.zio.sh/astra/telegram-join-approval-nuzzles|" "$ROOT_DIR/go.mod" # Fix all import references in the copied source echo "→ Rewriting import paths in .go files ..." -find "$ROOT_DIR" -name '*.go' -not -path "*/telegram-approval-join/*" | xargs sed -i 's|git\.zio\.sh/astra/telegram-approval-join|git.zio.sh/astra/telegram-join-approval-nuzzles|g' +find "$ROOT_DIR" -name '*.go' -not -path "*/telegram-join-approval-bot/*" | xargs sed -i 's|git\.zio\.sh/astra/telegram-join-approval-bot|git.zio.sh/astra/telegram-join-approval-nuzzles|g' # ── 4. Apply string patch ──────────────────────────────────────────── if [[ -f "$PATCH_FILE" ]]; then @@ -59,7 +53,7 @@ if [[ -f "$PATCH_FILE" ]]; then echo " Patch applied successfully." else echo "" - echo "⚠️ Patch did not apply cleanly — telegram-approval-join may have changed." + echo "⚠️ Patch did not apply cleanly — telegram-join-approval-bot may have changed." echo " Run the following to see conflicts:" echo " patch --dry-run -p1 -d $ROOT_DIR < $PATCH_FILE" echo "" @@ -77,7 +71,7 @@ go build ./... 2>&1 && echo " Build OK." || { echo "❌ Build failed."; exit 1 # ── 6. Commit changes ───────────────────────────────────────────────────────── echo "→ Committing changes..." git -C "$ROOT_DIR" add -A -git -C "$ROOT_DIR" commit -m "Update telegram-approval-join submodule and apply patches" || true +git -C "$ROOT_DIR" commit -m "Update telegram-join-approval-bot submodule and apply patches" || true echo "" -echo "✅ Sync complete. Root directory is up to date with telegram-approval-join (patched)." \ No newline at end of file +echo "✅ Sync complete. Root directory is up to date with telegram-join-approval-bot (patched)." \ No newline at end of file diff --git a/telegram-approval-join b/telegram-join-approval-bot similarity index 100% rename from telegram-approval-join rename to telegram-join-approval-bot