Compare commits
No commits in common. "f1efc1d9374cf266a56e6212c798dd8b998ac678" and "be5dee5daf4f544a29f34d69b9dac293617470fb" have entirely different histories.
f1efc1d937
...
be5dee5daf
5 changed files with 21 additions and 316 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "telegram-join-approval-bot"]
|
[submodule "telegram-approval-join"]
|
||||||
path = telegram-join-approval-bot
|
path = telegram-approval-join
|
||||||
url = ssh://git@git.zio.sh:2222/astra/telegram-join-approval-bot.git
|
url = ssh://git@git.zio.sh:2222/astra/telegram-join-approval-bot.git
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,14 @@ RUN apk add --no-cache ca-certificates
|
||||||
WORKDIR /opt
|
WORKDIR /opt
|
||||||
|
|
||||||
# Copy binary into a standard location
|
# Copy binary into a standard location
|
||||||
COPY --from=builder /bin/telegram-join-approval-nuzzles /usr/local/bin/telegram-join-approval-bot
|
COPY --from=builder /bin/telegram-join-approval-nuzzles /usr/local/bin/telegram-approval-join
|
||||||
|
|
||||||
# Create a non-root user and group with specific UID:GID and set ownership
|
# Create a non-root user and group with specific UID:GID and set ownership
|
||||||
RUN addgroup -g 65532 app && \
|
RUN addgroup -g 65532 app && \
|
||||||
adduser -D -H -u 65532 -G app -s /sbin/nologin app -h /opt && \
|
adduser -D -H -u 65532 -G app -s /sbin/nologin app -h /opt && \
|
||||||
chown -R app:app /opt /usr/local/bin/telegram-join-approval-bot
|
chown -R app:app /opt /usr/local/bin/telegram-approval-join
|
||||||
|
|
||||||
# Run as the created non-root user
|
# Run as the created non-root user
|
||||||
USER app:app
|
USER app:app
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/telegram-join-approval-bot"]
|
ENTRYPOINT ["/usr/local/bin/telegram-approval-join"]
|
||||||
301
README.md
301
README.md
|
|
@ -1,301 +0,0 @@
|
||||||
# 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 <message>`
|
|
||||||
Change the message shown when users request to join.
|
|
||||||
|
|
||||||
```
|
|
||||||
/setentrymessage Tell us about yourself and why you want to join!
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/setapprovalmessage <message>`
|
|
||||||
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 <topic_id>`
|
|
||||||
Direct admin messages to a specific topic thread (0 = main chat).
|
|
||||||
|
|
||||||
```
|
|
||||||
/setadmintopic 42
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/info`
|
|
||||||
Display current bot configuration.
|
|
||||||
|
|
||||||
### `/edit <new_text>`
|
|
||||||
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]
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# sync.sh — Copy telegram-join-approval-bot submodule into internal/, then apply patches.
|
# sync.sh — Copy telegram-approval-join submodule into internal/, then apply patches.
|
||||||
# Usage: ./scripts/sync.sh
|
# Usage: ./scripts/sync.sh
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -7,7 +7,7 @@ set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
SUBMODULE_DIR="$ROOT_DIR/telegram-join-approval-bot"
|
SUBMODULE_DIR="$ROOT_DIR/telegram-approval-join"
|
||||||
PATCH_FILE="$ROOT_DIR/patches/0001-nuzzles.patch"
|
PATCH_FILE="$ROOT_DIR/patches/0001-nuzzles.patch"
|
||||||
|
|
||||||
# ── 1. Ensure submodule is initialised ────────────────────────────────────────
|
# ── 1. Ensure submodule is initialised ────────────────────────────────────────
|
||||||
|
|
@ -22,13 +22,19 @@ git -C "$ROOT_DIR" submodule update --init --recursive --remote
|
||||||
# Capture the submodule commit after update
|
# Capture the submodule commit after update
|
||||||
NEW_COMMIT=$(git -C "$SUBMODULE_DIR" rev-parse HEAD 2>/dev/null || echo "")
|
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 ──────────────────────────────────
|
# ── 2. Wipe and re-copy the submodule source ──────────────────────────────────
|
||||||
echo "→ Copying telegram-join-approval-bot source to root..."
|
echo "→ Copying telegram-approval-join source to root..."
|
||||||
# Clean up directories that will be replaced
|
# 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"
|
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/
|
# Copy source files, excluding .git and keeping patches/ and scripts/
|
||||||
rsync -a --stats --exclude='.git' --exclude='internal/telegram-join-approval-bot' \
|
rsync -a --stats --exclude='.git' --exclude='internal/telegram-approval-join' \
|
||||||
"$SUBMODULE_DIR/" "$ROOT_DIR/" \
|
"$SUBMODULE_DIR/" "$ROOT_DIR/" \
|
||||||
--exclude='patches' --exclude='scripts' --exclude='README.md' --exclude='Dockerfile'
|
--exclude='patches' --exclude='scripts' --exclude='README.md' --exclude='Dockerfile'
|
||||||
|
|
||||||
|
|
@ -36,13 +42,13 @@ rsync -a --stats --exclude='.git' --exclude='internal/telegram-join-approval-bot
|
||||||
sed -i '/^scripts\/$/d' "$ROOT_DIR/.gitignore"
|
sed -i '/^scripts\/$/d' "$ROOT_DIR/.gitignore"
|
||||||
|
|
||||||
# ── 3. Rewrite the Go module path inside the copied source ────────────────────
|
# ── 3. Rewrite the Go module path inside the copied source ────────────────────
|
||||||
# Change the module from telegram-join-approval-bot to telegram-join-approval-nuzzles
|
# Change the module from telegram-approval-join to telegram-join-approval-nuzzles
|
||||||
echo "→ Rewriting module path in go.mod ..."
|
echo "→ Rewriting module path in 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"
|
sed -i "s|^module git\.zio\.sh/astra/telegram-approval-join|module git.zio.sh/astra/telegram-join-approval-nuzzles|" "$ROOT_DIR/go.mod"
|
||||||
|
|
||||||
# Fix all import references in the copied source
|
# Fix all import references in the copied source
|
||||||
echo "→ Rewriting import paths in .go files ..."
|
echo "→ Rewriting import paths in .go files ..."
|
||||||
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'
|
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'
|
||||||
|
|
||||||
# ── 4. Apply string patch ────────────────────────────────────────────
|
# ── 4. Apply string patch ────────────────────────────────────────────
|
||||||
if [[ -f "$PATCH_FILE" ]]; then
|
if [[ -f "$PATCH_FILE" ]]; then
|
||||||
|
|
@ -53,7 +59,7 @@ if [[ -f "$PATCH_FILE" ]]; then
|
||||||
echo " Patch applied successfully."
|
echo " Patch applied successfully."
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ Patch did not apply cleanly — telegram-join-approval-bot may have changed."
|
echo "⚠️ Patch did not apply cleanly — telegram-approval-join may have changed."
|
||||||
echo " Run the following to see conflicts:"
|
echo " Run the following to see conflicts:"
|
||||||
echo " patch --dry-run -p1 -d $ROOT_DIR < $PATCH_FILE"
|
echo " patch --dry-run -p1 -d $ROOT_DIR < $PATCH_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -71,7 +77,7 @@ go build ./... 2>&1 && echo " Build OK." || { echo "❌ Build failed."; exit 1
|
||||||
# ── 6. Commit changes ─────────────────────────────────────────────────────────
|
# ── 6. Commit changes ─────────────────────────────────────────────────────────
|
||||||
echo "→ Committing changes..."
|
echo "→ Committing changes..."
|
||||||
git -C "$ROOT_DIR" add -A
|
git -C "$ROOT_DIR" add -A
|
||||||
git -C "$ROOT_DIR" commit -m "Update telegram-join-approval-bot submodule and apply patches" || true
|
git -C "$ROOT_DIR" commit -m "Update telegram-approval-join submodule and apply patches" || true
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Sync complete. Root directory is up to date with telegram-join-approval-bot (patched)."
|
echo "✅ Sync complete. Root directory is up to date with telegram-approval-join (patched)."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue