Working captcha, need to template all the messages
This commit is contained in:
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
@ -43,3 +44,18 @@ func IsAdmin(member models.ChatMember) bool {
|
||||
func DeleteAfterSeconds(chatID int64, msgID int, seconds int) error {
|
||||
return AddMessageToDelete(MessageToDelete{MessageId: msgID, ChatId: chatID, DeleteDate: time.Now().Add(time.Duration(seconds) * time.Second).Unix(), Tries: 0})
|
||||
}
|
||||
|
||||
func generateRandomDigits(length int) string {
|
||||
digits := make([]byte, length)
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
digits[i] = byte(rand.Intn(10) + '0') // Generate a random digit
|
||||
}
|
||||
|
||||
return string(digits)
|
||||
}
|
||||
|
||||
func Mention(name string, id int64) string {
|
||||
text := fmt.Sprintf("[%s](tg://user?id=%d)", bot.EscapeMarkdown(name), id)
|
||||
return text
|
||||
}
|
||||
|
131
src/captcha.go
131
src/captcha.go
@ -1,29 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"log"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
var (
|
||||
res embed.FS
|
||||
//go:embed resources/*
|
||||
res embed.FS
|
||||
fnt font.Face
|
||||
Images []image.Image
|
||||
CorrectImage int
|
||||
)
|
||||
|
||||
func GenCaptcha() {
|
||||
dc := gg.NewContext(400, 300)
|
||||
gd := gg.NewLinearGradient(0, 0, 400, 300)
|
||||
gd.AddColorStop(0, color.RGBA{189, 24, 229, 255})
|
||||
gd.AddColorStop(1, color.RGBA{27, 21, 123, 255})
|
||||
dc.SetFillStyle(gd)
|
||||
dc.DrawRectangle(0, 0, 400, 300)
|
||||
dc.Fill()
|
||||
dc.SetRGBA(0, 0, 0, 0.05)
|
||||
dc.SetLineWidth(27)
|
||||
for i := 0.0; i < 20; i += 1 {
|
||||
dc.DrawLine(i * 70, -100, i * 70 - 500, 400)
|
||||
dc.Stroke()
|
||||
}
|
||||
dc.SavePNG("grad.png")
|
||||
type Item struct {
|
||||
img image.Image
|
||||
code string
|
||||
correct bool
|
||||
}
|
||||
|
||||
func InitResources() {
|
||||
dir := "resources"
|
||||
files, err := res.ReadDir(dir)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
name := file.Name()
|
||||
p := filepath.Join(dir, name)
|
||||
ext := filepath.Ext(name)
|
||||
|
||||
switch ext {
|
||||
case ".ttf":
|
||||
b, _ := res.ReadFile(p)
|
||||
f, _ := truetype.Parse(b)
|
||||
fnt = truetype.NewFace(f, &truetype.Options{
|
||||
Size: 25,
|
||||
})
|
||||
case ".png":
|
||||
b, _ := res.ReadFile(p)
|
||||
img, _, err := image.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
log.Fatal("Can't load image: ", err)
|
||||
}
|
||||
Images = append(Images, img)
|
||||
|
||||
if strings.HasPrefix(name, "correct") {
|
||||
CorrectImage = i
|
||||
log.Println("Correct one found: ", CorrectImage)
|
||||
}
|
||||
log.Println(name, "loaded successfully")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GenCaptcha() ([]byte, string) {
|
||||
width, height := 500.0, 500.0
|
||||
ctx := gg.NewContext(int(width), int(height))
|
||||
// Make gradient
|
||||
gradient := gg.NewLinearGradient(0, 0, width, height)
|
||||
gradient.AddColorStop(0, color.RGBA{189, 24, 229, 255})
|
||||
gradient.AddColorStop(1, color.RGBA{27, 21, 123, 255})
|
||||
|
||||
// Fill background
|
||||
ctx.SetFillStyle(gradient)
|
||||
ctx.DrawRectangle(0, 0, width, height)
|
||||
ctx.Fill()
|
||||
|
||||
// Stripes for background
|
||||
ctx.SetRGBA(0, 0, 0, 0.05)
|
||||
ctx.SetLineWidth(27)
|
||||
for i := 0.0; i < 20; i += 1 {
|
||||
ctx.DrawLine(i*70, -100, i*70-500, width)
|
||||
ctx.Stroke()
|
||||
}
|
||||
|
||||
items := []Item{}
|
||||
for i, image := range Images {
|
||||
items = append(items, Item{
|
||||
img: image,
|
||||
code: generateRandomDigits(4),
|
||||
correct: i == CorrectImage,
|
||||
})
|
||||
}
|
||||
rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] })
|
||||
|
||||
correctAnswer := ""
|
||||
minX, maxX := 60, int(width-60)
|
||||
xrange := maxX - minX
|
||||
step := xrange / (len(items) - 1)
|
||||
ctx.SetFontFace(fnt)
|
||||
ctx.SetRGBA(1, 1, 1, 0.5)
|
||||
for i, img := range items {
|
||||
x := minX + i*step
|
||||
y := 100 + rand.Intn(int(height)-200)
|
||||
if img.correct {
|
||||
correctAnswer = img.code
|
||||
}
|
||||
ctx.DrawImageAnchored(img.img, x, y, 0.5, 0.5)
|
||||
offset := 70
|
||||
if rand.Float32() < 0.5 {
|
||||
offset = -70
|
||||
}
|
||||
ctx.DrawStringAnchored(img.code, float64(x), float64(y+offset), 0.5, 0.5)
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
err := png.Encode(buff, ctx.Image())
|
||||
if err != nil {
|
||||
log.Fatal("can't encode png: ", err)
|
||||
}
|
||||
return buff.Bytes(), correctAnswer
|
||||
}
|
||||
|
20
src/db.go
20
src/db.go
@ -55,7 +55,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
chat_id INTEGER,
|
||||
message_id INTEGER,
|
||||
correct_answer INTEGER DEFAULT 0,
|
||||
correct_answer TEXT DEFAULT '',
|
||||
blocked_until INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (chat_id) REFERENCES chats (id) ON DELETE CASCADE
|
||||
@ -66,7 +66,7 @@ type ChatSchema struct {
|
||||
Id int64 `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Topic int64 `json:"topic" db:"topic"`
|
||||
Topic int `json:"topic" db:"topic"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
}
|
||||
|
||||
@ -85,12 +85,12 @@ type User struct {
|
||||
}
|
||||
|
||||
type Captcha struct {
|
||||
Id int64 `db:"id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
ChatID int64 `db:"chat_id"`
|
||||
MessageID int `db:"message_id"`
|
||||
CorrectAnswer int `db:"correct_answer"`
|
||||
BlockedUntil int64 `db:"blocked_until"`
|
||||
Id int64 `db:"id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
ChatID int64 `db:"chat_id"`
|
||||
MessageID int `db:"message_id"`
|
||||
CorrectAnswer string `db:"correct_answer"`
|
||||
BlockedUntil int64 `db:"blocked_until"`
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
@ -149,8 +149,8 @@ func UseActivation(code string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func ActivateChat(id int64) error {
|
||||
_, err := db.Exec(`update chats set active = 1 where id = $1`, id)
|
||||
func ActivateChat(id int64, thread int) error {
|
||||
_, err := db.Exec(`update chats set active = 1, topic = $2 where id = $1`, id, thread)
|
||||
return err
|
||||
}
|
||||
|
||||
|
137
src/handlers.go
137
src/handlers.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
@ -43,7 +44,7 @@ func registerChat(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||
log.Println("register: wrong code: ", args[1])
|
||||
return
|
||||
}
|
||||
if err := ActivateChat(msg.Chat.ID); err != nil {
|
||||
if err := ActivateChat(msg.Chat.ID, msg.MessageThreadID); err != nil {
|
||||
log.Println("Error activating chat: ", err)
|
||||
return
|
||||
}
|
||||
@ -66,12 +67,56 @@ func registerChat(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||
log.Println("register: error", err)
|
||||
}
|
||||
}
|
||||
func unregisterChat(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||
msg := update.Message
|
||||
log.Println("unregistering", msg.Chat.ID)
|
||||
m, err := FetchMemberFromChat(ctx, b, msg.Chat.ID, msg.From.ID)
|
||||
if err == nil {
|
||||
if !IsAdmin(*m) {
|
||||
log.Println("register: user is not admin")
|
||||
return
|
||||
}
|
||||
b.DeleteMessage(ctx, &bot.DeleteMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
MessageID: msg.ID,
|
||||
})
|
||||
db.Exec("update chats set active = 0 where id = $1", msg.Chat.ID)
|
||||
sent, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: msg.Chat.ID,
|
||||
Text: "Чат удалён",
|
||||
MessageThreadID: msg.MessageThreadID,
|
||||
})
|
||||
if err == nil {
|
||||
err := DeleteAfterSeconds(msg.Chat.ID, sent.ID, 60)
|
||||
if err != nil {
|
||||
log.Println("register: failed to add to delete", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Println("register: error", err)
|
||||
}
|
||||
}
|
||||
|
||||
var NewUserTemplate = `
|
||||
Приветствую тебя, *%s*\!
|
||||
Ты не можешь писать ничего в данном чате, пока не пройдешь капчу, которую я тебе пришлю в личку\.
|
||||
|
||||
Нужно только нажать на кнопку ниже\.
|
||||
`
|
||||
|
||||
func handleNewJoined(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
var chat ChatSchema
|
||||
err := db.Get(&chat, "select * from chats where id = $1 and active = 1", u.Message.Chat.ID)
|
||||
if err != nil {
|
||||
log.Println("can't get chat for new joined: ", err)
|
||||
}
|
||||
for _, user := range u.Message.NewChatMembers {
|
||||
msg, _ := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: u.Message.Chat.ID,
|
||||
Text: "Check the capthca!",
|
||||
text := fmt.Sprintf(NewUserTemplate, Mention(user.FirstName, user.ID))
|
||||
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chat.Id,
|
||||
MessageThreadID: int(chat.Topic),
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeMarkdown,
|
||||
ReplyMarkup: models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
@ -80,7 +125,10 @@ func handleNewJoined(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err := db.Exec(`INSERT INTO captchas (user_id, chat_id, message_id) values ($1, $2, $3)`, user.ID, u.Message.Chat.ID, msg.ID)
|
||||
if err != nil {
|
||||
log.Println("Can't send message: ", err, "\n", text)
|
||||
}
|
||||
_, err = db.Exec(`INSERT INTO captchas (user_id, chat_id, message_id) values ($1, $2, $3)`, user.ID, u.Message.Chat.ID, msg.ID)
|
||||
if err != nil {
|
||||
log.Println("newusers: can't add to db: ", err)
|
||||
return
|
||||
@ -115,6 +163,13 @@ func banUser(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
|
||||
}
|
||||
|
||||
var NewCaptchaTemplate = `
|
||||
*%s*, тебе необходимо пройти капчу для чата *%s*\.
|
||||
|
||||
Для этого посмотри на картинку, найди логотип движка, который относится к вышеуказанному чату, а потом введи сюда код, который расположен над или под ним\.
|
||||
Время у тебя неограничено, я буду ждать\!
|
||||
`
|
||||
|
||||
func handlePrivateStartCaptcha(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
args := strings.Split(u.Message.Text, " ")
|
||||
userID := u.Message.From.ID
|
||||
@ -133,37 +188,59 @@ func handlePrivateStartCaptcha(ctx context.Context, b *bot.Bot, u *models.Update
|
||||
err = db.Get(&captcha, `select * from captchas where user_id = $1 and chat_id = $2`, userID, chatID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
Text: " There's no captchas for that chat you came from.",
|
||||
Text: "В чате, откуда ты пришёл, у тебя нет активных капч. Приходи в другой раз.",
|
||||
ChatID: u.Message.Chat.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = db.Get(&captcha, `select * from captchas where user_id = $1 and correct_answer != 0`, userID)
|
||||
err = db.Get(&captcha, `select * from captchas where user_id = $1 and correct_answer != ''`, userID)
|
||||
if err != nil {
|
||||
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
Text: " There's no captchas for that chat you came from.",
|
||||
Text: "У тебя нет активных капч ни в одном чате. Приходи в другой раз.",
|
||||
ChatID: u.Message.Chat.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if captcha.CorrectAnswer == 0 {
|
||||
captcha.CorrectAnswer = 42
|
||||
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
Text: "Get me the answer!",
|
||||
ChatID: u.Message.Chat.ID,
|
||||
})
|
||||
if err == nil {
|
||||
captcha.MessageID = msg.ID
|
||||
|
||||
type UserChatCaptcha struct {
|
||||
Id int64 `db:"id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
UserName string `db:"user_name"`
|
||||
ChatID int64 `db:"chat_id"`
|
||||
ChatName string `db:"chat_name"`
|
||||
}
|
||||
|
||||
var userchat UserChatCaptcha
|
||||
err = db.Get(&userchat, `
|
||||
SELECT U.id AS user_id, C.id as chat_id, U.name as user_name, C.name as chat_name
|
||||
FROM captchas
|
||||
JOIN users AS U ON U.id = captchas.user_id
|
||||
JOIN chats AS C ON C.id = captchas.chat_id
|
||||
WHERE captchas.id = $1`, captcha.Id)
|
||||
if err != nil {
|
||||
log.Println("Can't get user and chat names: ", err)
|
||||
}
|
||||
|
||||
if captcha.CorrectAnswer == "" {
|
||||
img, answer := GenCaptcha()
|
||||
captcha.CorrectAnswer = answer
|
||||
if _, err := b.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||
Caption: fmt.Sprintf(NewCaptchaTemplate, userchat.UserName, userchat.ChatName),
|
||||
Photo: &models.InputFileUpload{Filename: "captcha.png", Data: bytes.NewReader(img)},
|
||||
ChatID: u.Message.Chat.ID,
|
||||
ParseMode: models.ParseModeMarkdown,
|
||||
}); err != nil {
|
||||
log.Println("can't send private captcha: ", err)
|
||||
}
|
||||
_, err = db.NamedExec("update captchas set correct_answer = :correct_answer, message_id = :message_id where id = :id", captcha)
|
||||
_, err = db.NamedExec("update captchas set correct_answer = :correct_answer where id = :id", captcha)
|
||||
if err != nil {
|
||||
log.Println("Can't update captcha:", err)
|
||||
}
|
||||
} else {
|
||||
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
Text: fmt.Sprintf("You already have captcha for chat %d", captcha.ChatID),
|
||||
Text: fmt.Sprintf("Я тебе уже выдавал капчу для %d", captcha.ChatID),
|
||||
ChatID: u.Message.Chat.ID,
|
||||
})
|
||||
}
|
||||
@ -186,13 +263,10 @@ func handlePrivateCaptcha(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
}
|
||||
|
||||
ban_minutes := 0
|
||||
num, err := strconv.Atoi(msg.Text)
|
||||
text := "That's not a number. Try again in 30 minutes."
|
||||
if err != nil {
|
||||
ban_minutes = 30
|
||||
} else if num != captcha.CorrectAnswer {
|
||||
text = "That's the wrong answer. Try again in 5 hours."
|
||||
ban_minutes = 300
|
||||
text := ""
|
||||
if msg.Text != captcha.CorrectAnswer {
|
||||
text = "That's the wrong answer. Try again in 1 hour."
|
||||
ban_minutes = 60
|
||||
}
|
||||
|
||||
if ban_minutes > 0 {
|
||||
@ -223,7 +297,16 @@ func handlePrivateCaptcha(ctx context.Context, b *bot.Bot, u *models.Update) {
|
||||
if err != nil {
|
||||
log.Println("Can't unrestrict user: ", err)
|
||||
}
|
||||
log.Println("Deleting message: ", captcha.ChatID, captcha.MessageID)
|
||||
result, err := b.DeleteMessage(ctx, &bot.DeleteMessageParams{
|
||||
ChatID: captcha.ChatID,
|
||||
MessageID: captcha.MessageID,
|
||||
})
|
||||
log.Println("Deleting message:", result, err)
|
||||
db.Exec("delete from captchas where id = $1", captcha.Id)
|
||||
|
||||
b.SendMessage(ctx, &bot.SendMessageParams{Text: "Captcha solved! Congrats!", ChatID: msg.From.ID})
|
||||
|
||||
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
Text: "Капча решена! Поздравляю! Теперь можешь вернуться в чат, я вернул тебе возможность отправлять там сообщения.\n\nСоветую ознакомиться с местными правилами, прежде чем что-либо писать!",
|
||||
ChatID: msg.From.ID,
|
||||
})
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
GenCaptcha()
|
||||
log.SetFlags(log.Lshortfile + log.Ltime + log.Ldate)
|
||||
InitResources()
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
@ -36,6 +37,7 @@ func main() {
|
||||
}
|
||||
|
||||
b.RegisterHandler(bot.HandlerTypeMessageText, "/register", bot.MatchTypePrefix, registerChat)
|
||||
b.RegisterHandler(bot.HandlerTypeMessageText, "/unregister", bot.MatchTypePrefix, unregisterChat)
|
||||
b.RegisterHandler(bot.HandlerTypeMessageText, "/ban", bot.MatchTypePrefix, banUser)
|
||||
b.RegisterHandlerMatchFunc(func(update *models.Update) bool {
|
||||
return update.Message != nil && len(update.Message.NewChatMembers) > 0
|
||||
|
BIN
src/resources/correct_godot.png
Normal file
BIN
src/resources/correct_godot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/resources/font.ttf
Normal file
BIN
src/resources/font.ttf
Normal file
Binary file not shown.
BIN
src/resources/gamemaker.png
Normal file
BIN
src/resources/gamemaker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
src/resources/unity.png
Normal file
BIN
src/resources/unity.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/resources/unreal.png
Normal file
BIN
src/resources/unreal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user