Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/
bot

dist/
tg-captcha-bot
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,59 @@ This bot has been tested on several supergroups (2000+ people) for a long time a

`/healthz` - check that the bot is working correctly

## Сustomization
## Configuration

You can change several bot's settings (welcome message, ban duration, socks5 proxy server) through the configuration file `config.toml`
The bot uses a TOML configuration file and environment variables.

## Alternatives / Forks
### Environment Variables

- [momai/tg-captcha-bot](https://github.com/momai/tg-captcha-bot) - fork of `tg-captcha-bot` with interesting additional features.
| Variable | Required | Description |
|----------|----------|-------------|
| `TGTOKEN` | Yes | Telegram bot token from [@BotFather](https://t.me/BotFather) |
| `CONFIG_PATH` | No | Directory path containing `config.toml`. Defaults to current directory |

### Configuration File Options

Create a `config.toml` file with the following options:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `button_text` | string | `"I'm not a robot!"` | Text displayed on the captcha button |
| `welcome_message` | string | `"Hello! This is the spam protection system..."` | Message sent to new users |
| `after_success_message` | string | `"User passed the validation."` | Message shown after successful verification |
| `after_fail_message` | string | `"User didn't pass the validation and was banned."` | Message shown after failed verification |
| `success_message_strategy` | string | `"show"` | Action after success: `"show"` (edit message) or `"del"` (delete message) |
| `fail_message_strategy` | string | `"del"` | Action after failure: `"show"` (edit message) or `"del"` (delete message) |
| `welcome_timeout` | string | `"30"` | Seconds user has to press the button |
| `ban_duration` | string | `"forever"` | Ban duration: `"forever"` or number of minutes (e.g., `"10"`) |
| `delete_join_message_on_fail` | string | `"no"` | Delete system join/leave messages for failed users: `"yes"` or `"no"` |
| `use_socks5_proxy` | string | `"no"` | Enable SOCKS5 proxy: `"yes"` or `"no"` |
| `socks5_address` | string | `"1.1.1.1"` | SOCKS5 proxy IP address |
| `socks5_port` | string | `"1080"` | SOCKS5 proxy port |
| `socks5_login` | string | `"login"` | SOCKS5 proxy username |
| `socks5_password` | string | `"password"` | SOCKS5 proxy password |

### Example Configuration

```toml
button_text = "I'm not a robot!"
welcome_message = "Hello! Please press the button within 30 seconds or you will be banned!"
after_success_message = "User passed the validation."
after_fail_message = "User didn't pass the validation and was banned."
success_message_strategy = "show"
fail_message_strategy = "del"
welcome_timeout = "30"
ban_duration = "forever"
delete_join_message_on_fail = "no"
use_socks5_proxy = "no"
```

### Default behavior

With the default configuration:

1. New user joins the group
2. Bot restricts the user and sends a welcome message with "I'm not a robot!" button
3. User has 30 seconds to press the button
4. **If user passes**: Challenge message is edited to show "User passed the validation."
5. **If user fails**: User is banned forever, challenge message is deleted
15 changes: 13 additions & 2 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ after_success_message = "User passed the validation."
# Edit welcome message with this text if user failed validation
after_fail_message = "User didn't pass the validation and was banned."

# Show success and failure messages or delete them. Pick "show" or "del"
print_success_and_fail_messages_strategy = "show"
# What to do with challenge message when user passes verification. Pick "show" or "del"
# "show" - edit message with after_success_message text
# "del" - delete the challenge message
success_message_strategy = "show"

# What to do with challenge message when user fails verification. Pick "show" or "del"
# "show" - edit message with after_fail_message text
# "del" - delete the challenge message
fail_message_strategy = "del"

# During this time in seconds, the new user have to press the captha button
welcome_timeout = "30"

# If the new user does not press the button, the bot will ban the user for this duration of time. Can be "forever" or number of minutes ("10")
ban_duration = "forever"

# Delete the "user joined the group" system message when verification fails. Can be "yes" or "no"
# This helps prevent spam advertising via usernames in join messages
delete_join_message_on_fail = "no"

# Do you want to use a socks5 proxy server? Can be "yes" or "no"
use_socks5_proxy = "no"

Expand Down
59 changes: 43 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,25 @@ import (

// Config struct for toml config file
type Config struct {
ButtonText string `mapstructure:"button_text"`
WelcomeMessage string `mapstructure:"welcome_message"`
AfterSuccessMessage string `mapstructure:"after_success_message"`
AfterFailMessage string `mapstructure:"after_fail_message"`
PrintSuccessAndFail string `mapstructure:"print_success_and_fail_messages_strategy"`
WelcomeTimeout string `mapstructure:"welcome_timeout"`
BanDurations string `mapstructure:"ban_duration"`
UseSocks5Proxy string `mapstructure:"use_socks5_proxy"`
Socks5Address string `mapstructure:"socks5_address"`
Socks5Port string `mapstructure:"socks5_port"`
Socks5Login string `mapstructure:"socks5_login"`
Socks5Password string `mapstructure:"socks5_password"`
ButtonText string `mapstructure:"button_text"`
WelcomeMessage string `mapstructure:"welcome_message"`
AfterSuccessMessage string `mapstructure:"after_success_message"`
AfterFailMessage string `mapstructure:"after_fail_message"`
SuccessMessageStrategy string `mapstructure:"success_message_strategy"`
FailMessageStrategy string `mapstructure:"fail_message_strategy"`
WelcomeTimeout string `mapstructure:"welcome_timeout"`
BanDurations string `mapstructure:"ban_duration"`
DeleteJoinMsgOnFail string `mapstructure:"delete_join_message_on_fail"`
UseSocks5Proxy string `mapstructure:"use_socks5_proxy"`
Socks5Address string `mapstructure:"socks5_address"`
Socks5Port string `mapstructure:"socks5_port"`
Socks5Login string `mapstructure:"socks5_login"`
Socks5Password string `mapstructure:"socks5_password"`
}

var config Config
var passedUsers = sync.Map{}
var bannedUsers = sync.Map{} // tracks users banned by the bot for join message cleanup
var bot *tb.Bot
var tgtoken = "TGTOKEN"
var configPath = "CONFIG_PATH"
Expand Down Expand Up @@ -76,6 +79,7 @@ func main() {

bot.Handle(tb.OnUserJoined, challengeUser)
bot.Handle(tb.OnCallback, passChallenge)
bot.Handle(tb.OnUserLeft, handleUserLeft)

bot.Handle("/healthz", func(m *tb.Message) {
msg := "I'm OK"
Expand Down Expand Up @@ -138,23 +142,30 @@ func challengeUser(m *tb.Message) {
log.Println(e)
}
chatMember := tb.ChatMember{User: m.UserJoined, RestrictedUntil: banDuration}
// Mark user as banned for cleanup of "removed" message
if config.DeleteJoinMsgOnFail == "yes" {
bannedUsers.Store(m.UserJoined.ID, struct{}{})
}
err := bot.Ban(m.Chat, &chatMember)
if err != nil {
log.Println(err)
}

switch config.PrintSuccessAndFail {
switch config.FailMessageStrategy {
case "show":
_, err := bot.Edit(challengeMsg, config.AfterFailMessage)
if err != nil {
log.Println(err)
}
case "del":
err := bot.Delete(m)
err := bot.Delete(challengeMsg)
if err != nil {
log.Println(err)
}
err = bot.Delete(challengeMsg)
}
// Delete join message if configured (prevents spam advertising via usernames)
if config.DeleteJoinMsgOnFail == "yes" {
err = bot.Delete(m)
if err != nil {
log.Println(err)
}
Expand All @@ -177,7 +188,7 @@ func passChallenge(c *tb.Callback) {
}
passedUsers.Store(c.Sender.ID, struct{}{})

switch config.PrintSuccessAndFail {
switch config.SuccessMessageStrategy {
case "show":
_, err := bot.Edit(c.Message, config.AfterSuccessMessage)
if err != nil {
Expand All @@ -202,6 +213,22 @@ func passChallenge(c *tb.Callback) {
}
}

// handleUserLeft deletes the "bot removed user" message for banned users
func handleUserLeft(m *tb.Message) {
if m.UserLeft == nil {
return
}
// Check if this user was banned by the bot
_, wasBanned := bannedUsers.Load(m.UserLeft.ID)
if wasBanned {
bannedUsers.Delete(m.UserLeft.ID)
err := bot.Delete(m)
if err != nil {
log.Println(err)
}
}
}

func readConfig() (err error) {
v := viper.New()
path, ok := os.LookupEnv(configPath)
Expand Down
Loading