Working captcha, need to template all the messages
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								example.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								example.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 59 KiB  | 
							
								
								
									
										7
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.mod
									
									
									
									
									
								
							@ -2,16 +2,21 @@ module nefrace.ru/kickbot.ng
 | 
			
		||||
 | 
			
		||||
go 1.22.5
 | 
			
		||||
 | 
			
		||||
require github.com/fogleman/gg v1.3.0
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/anthonynsimon/bild v0.14.0 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.1 // indirect
 | 
			
		||||
	github.com/fogleman/gg v1.3.0 
 | 
			
		||||
	github.com/glebarez/go-sqlite v1.22.0 // indirect
 | 
			
		||||
	github.com/go-telegram/bot v1.5.0 // indirect
 | 
			
		||||
	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 | 
			
		||||
	github.com/google/uuid v1.5.0 // indirect
 | 
			
		||||
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 | 
			
		||||
	github.com/jmoiron/sqlx v1.4.0 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 | 
			
		||||
	github.com/spf13/cobra v0.0.5 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.3 // indirect
 | 
			
		||||
	go.etcd.io/bbolt v1.3.10 // indirect
 | 
			
		||||
	golang.org/x/image v0.18.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.15.0 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								go.sum
									
									
									
									
									
								
							@ -1,8 +1,18 @@
 | 
			
		||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 | 
			
		||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
			
		||||
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
 | 
			
		||||
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
 | 
			
		||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 | 
			
		||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 | 
			
		||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 | 
			
		||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 | 
			
		||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
			
		||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
 | 
			
		||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 | 
			
		||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
 | 
			
		||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 | 
			
		||||
@ -12,23 +22,48 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
 | 
			
		||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 | 
			
		||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
 | 
			
		||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
			
		||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
 | 
			
		||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
 | 
			
		||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 | 
			
		||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
			
		||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 | 
			
		||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 | 
			
		||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 | 
			
		||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 | 
			
		||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 | 
			
		||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 | 
			
		||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
 | 
			
		||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 | 
			
		||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 | 
			
		||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 | 
			
		||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 | 
			
		||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 | 
			
		||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 | 
			
		||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
 | 
			
		||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
 | 
			
		||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
 | 
			
		||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
 | 
			
		||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
 | 
			
		||||
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
 | 
			
		||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
 | 
			
		||||
 | 
			
		||||
@ -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