From 663fc3c7a569eb8c027ce5123a209a80a686eb83 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 17 Nov 2021 21:02:22 +0700 Subject: [PATCH 01/13] feat(wip): implement analytics --- analytics/incr.go | 93 ++++++++++++++++++++++++++++++++++++++++++ analytics/join.go | 63 ++++++++++++++++++++++++++++ analytics/migration.go | 59 +++++++++++++++++++++++++++ analytics/msg.go | 54 ++++++++++++++++++++++++ analytics/parser.go | 27 ++++++++++++ go.mod | 10 +++-- go.sum | 31 ++++++++------ logic/captcha_join.go | 22 +--------- logic/captcha_leave.go | 3 +- logic/captcha_non.go | 3 +- logic/captcha_wait.go | 3 +- logic/welcome.go | 3 +- main.go | 1 + utils/tele.go | 21 ++++++++++ 14 files changed, 353 insertions(+), 40 deletions(-) create mode 100644 analytics/incr.go create mode 100644 analytics/join.go create mode 100644 analytics/migration.go create mode 100644 analytics/msg.go create mode 100644 analytics/parser.go create mode 100644 utils/tele.go diff --git a/analytics/incr.go b/analytics/incr.go new file mode 100644 index 0000000..b7993a8 --- /dev/null +++ b/analytics/incr.go @@ -0,0 +1,93 @@ +package analytics + +import ( + "context" + "database/sql" + "strconv" + "time" + + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" +) + +func IncrementUsrDB(db *sqlx.DB, ctx context.Context, users []UserMap) error { + c, err := db.Connx(ctx) + if err != nil { + return err + } + defer c.Close() + + t, err := c.BeginTxx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + + for _, user := range users { + r, err := t.QueryxContext( + ctx, + `SELECT counter FROM analytics WHERE user_id = $1`, + user.UserID, + ) + if err != nil { + t.Rollback() + return err + } + defer r.Close() + + var counter int + if r.Next() { + err = r.Scan(&counter) + if err != nil { + t.Rollback() + return err + } + } + + now := time.Now() + + _, err = t.ExecContext( + ctx, + `UPDATE analytics + SET counter = $1, + updated_at = $2, + username = $3, + display_name = $4 + WHERE user_id = $5`, + counter+user.Counter, + now, + now, + user.Username, + user.DisplayName, + user.UserID, + ) + if err != nil { + t.Rollback() + return err + } + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} + +func IncrementUsrRedis(cache *redis.Client, ctx context.Context, user UserMap) error { + p := cache.TxPipeline() + defer p.Close() + + err := p.Incr(ctx, "analytics:"+strconv.FormatInt(user.UserID, 10)).Err() + if err != nil { + return err + } + + err = p.Do(ctx).Err() + if err != nil { + return err + } + + return nil +} diff --git a/analytics/join.go b/analytics/join.go new file mode 100644 index 0000000..0eefedc --- /dev/null +++ b/analytics/join.go @@ -0,0 +1,63 @@ +package analytics + +import ( + "context" + "database/sql" + "teknologi-umum-bot/utils" + "time" + + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" + tb "gopkg.in/tucnak/telebot.v2" +) + +func NewUser(db *sqlx.DB, redis *redis.Client, user *tb.User) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + c, err := db.Connx(ctx) + if err != nil { + return err + } + defer c.Close() + + t, err := c.BeginTxx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + + now := time.Now() + + _, err = t.ExecContext( + ctx, + `INSERT INTO analytics + (user_id, username, display_name, counter, created_at, joined_at, updated_at) + VALUES + ($1, $2, $3, $4, $5, $6, $7) + ON DUPLICATE KEY + UPDATE + SET joined_at = $8, + updated_at = $9`, + user.ID, + user.Username, + user.FirstName+utils.ShouldAddSpace(user)+user.LastName, + 0, + now, + now, + now, + now, + now, + ) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/analytics/migration.go b/analytics/migration.go new file mode 100644 index 0000000..a19b601 --- /dev/null +++ b/analytics/migration.go @@ -0,0 +1,59 @@ +package analytics + +import ( + "context" + "database/sql" + "time" + + "github.com/jmoiron/sqlx" +) + +func Migrate(db *sqlx.DB) error { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) + defer cancel() + + c, err := db.Connx(ctx) + if err != nil { + return err + } + defer c.Close() + + t, err := c.BeginTxx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + + _, err = t.ExecContext( + ctx, + `CREATE TABLE analytics ( + user_id INTEGER PRIMARY KEY, + username VARCHAR(255), + display_name VARCHAR(255), + counter INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + joined_at TIMESTAMP, + updated_at TIMESTAMP + )`, + ) + if err != nil { + t.Rollback() + return err + } + + _, err = t.ExecContext( + ctx, + `CREATE INDEX ON analytics (counter)`, + ) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/analytics/msg.go b/analytics/msg.go new file mode 100644 index 0000000..d833981 --- /dev/null +++ b/analytics/msg.go @@ -0,0 +1,54 @@ +package analytics + +import ( + "context" + "strconv" + "time" + + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" + tb "gopkg.in/tucnak/telebot.v2" +) + +func NewMsg(db *sqlx.DB, redis *redis.Client, user *tb.User) error { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) + defer cancel() + + // Check latest hour + p := redis.TxPipeline() + defer p.Close() + + hour, err := p.Get(ctx, "analytics:hour").Result() + if err != nil { + return err + } + + now := time.Now().Hour() + + // Create new hour + if hour == "" && now > time.Now().Hour() { + counter, err := p.Get(ctx, "analytics:counter").Result() + if err != nil { + return err + } + + // Insert a new counter to Redis, do nothing on the DB + if counter == "" { + + } + + err = p.Set(ctx, "analytics:hour", strconv.Itoa(now), 0).Err() + if err != nil { + return err + } + + } + + c, err := db.Connx(ctx) + if err != nil { + return err + } + defer c.Close() + + return nil +} diff --git a/analytics/parser.go b/analytics/parser.go new file mode 100644 index 0000000..ef6b293 --- /dev/null +++ b/analytics/parser.go @@ -0,0 +1,27 @@ +package analytics + +import ( + "teknologi-umum-bot/utils" + "time" + + tb "gopkg.in/tucnak/telebot.v2" +) + +type UserMap struct { + UserID int64 `db:"user_id"` + Username string `db:"username"` + DisplayName string `db:"display_name"` + Counter int `db:"counter"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + JoinedAt time.Time `db:"joined_at"` +} + +func ParseToUser(user *tb.User) UserMap { + return UserMap{ + UserID: int64(user.ID), + DisplayName: user.FirstName + utils.ShouldAddSpace(user) + user.LastName, + Username: user.Username, + } +} + \ No newline at end of file diff --git a/go.mod b/go.mod index aa4969e..e31602a 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,16 @@ require ( github.com/aldy505/decrr v0.0.1 github.com/allegro/bigcache/v3 v3.0.1 github.com/getsentry/sentry-go v0.11.0 - github.com/go-redis/redis/v8 v8.11.3 - github.com/joho/godotenv v1.3.0 + github.com/go-redis/redis/v8 v8.11.4 + github.com/jmoiron/sqlx v1.3.4 + github.com/joho/godotenv v1.4.0 + github.com/lib/pq v1.10.4 gopkg.in/tucnak/telebot.v2 v2.4.1 ) require ( - github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/stretchr/testify v1.6.1 // indirect + github.com/stretchr/testify v1.7.0 // indirect ) diff --git a/go.sum b/go.sum index 1cd81f0..d7faf6f 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/allegro/bigcache/v3 v3.0.1 h1:Q4Xl3chywXuJNOw7NV+MeySd3zGQDj4KCpkCg0t github.com/allegro/bigcache/v3 v3.0.1/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 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= @@ -45,8 +45,10 @@ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclK github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8= -github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= @@ -82,8 +84,10 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -101,11 +105,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= @@ -130,8 +139,8 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= @@ -158,8 +167,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -251,8 +260,6 @@ gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/tucnak/telebot.v2 v2.4.0 h1:nOeqOWnOAD3dzbKW+NRumd8zjj5vrWwSa0WRTxvgfag= -gopkg.in/tucnak/telebot.v2 v2.4.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= gopkg.in/tucnak/telebot.v2 v2.4.1 h1:bUOFHtHhuhPekjHGe1Q1BmITvtBLdQI4yjSMC405KcU= gopkg.in/tucnak/telebot.v2 v2.4.1/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/logic/captcha_join.go b/logic/captcha_join.go index 9c46983..46b3bfc 100644 --- a/logic/captcha_join.go +++ b/logic/captcha_join.go @@ -62,7 +62,7 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { return } - if m.Sender.IsBot || m.Private() || isAdmin(admins, m.Sender) { + if m.Sender.IsBot || m.Private() || utils.IsAdmin(admins, m.Sender) { return } @@ -76,7 +76,7 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { strings.Replace(CaptchaQuestion, "{captcha}", captcha, 1), "{user}", ""+ - sanitizeInput(m.Sender.FirstName)+shouldAddSpace(m)+sanitizeInput(m.Sender.LastName)+ + sanitizeInput(m.Sender.FirstName)+utils.ShouldAddSpace(m.Sender)+sanitizeInput(m.Sender.LastName)+ "", 1, ) @@ -130,27 +130,9 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { go waitOrDelete(d.Cache, d.Logger, d.Bot, m, msgQuestion, cond, &done) } -// Check whether or not a user is in the admin list -func isAdmin(admins []tb.ChatMember, user *tb.User) bool { - for _, v := range admins { - if v.User.ID == user.ID { - return true - } - } - return false -} - func sanitizeInput(inp string) string { var str string str = strings.ReplaceAll(inp, ">", ">") str = strings.ReplaceAll(str, "<", "<") return str } - -func shouldAddSpace(m *tb.Message) string { - if m.Sender.LastName != "" { - return " " - } - - return "" -} diff --git a/logic/captcha_leave.go b/logic/captcha_leave.go index 7bcf7c7..c092b0a 100644 --- a/logic/captcha_leave.go +++ b/logic/captcha_leave.go @@ -3,6 +3,7 @@ package logic import ( "encoding/json" "strconv" + "teknologi-umum-bot/utils" tb "gopkg.in/tucnak/telebot.v2" ) @@ -17,7 +18,7 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { return } - if m.Sender.IsBot || m.Private() || isAdmin(admins, m.Sender) { + if m.Sender.IsBot || m.Private() || utils.IsAdmin(admins, m.Sender) { return } diff --git a/logic/captcha_non.go b/logic/captcha_non.go index 85d2fc6..bd2f9ce 100644 --- a/logic/captcha_non.go +++ b/logic/captcha_non.go @@ -3,6 +3,7 @@ package logic import ( "encoding/json" "strconv" + "teknologi-umum-bot/utils" "time" tb "gopkg.in/tucnak/telebot.v2" @@ -50,7 +51,7 @@ func (d *Dependencies) NonTextListener(m *tb.Message) { m.Chat, "Hai, "+ sanitizeInput(m.Sender.FirstName)+ - shouldAddSpace(m)+ + utils.ShouldAddSpace(m.Sender)+ sanitizeInput(m.Sender.LastName)+ ". "+ "Selesain captchanya dulu yuk, baru kirim yang aneh-aneh. Kamu punya "+ diff --git a/logic/captcha_wait.go b/logic/captcha_wait.go index 91de57a..990f685 100644 --- a/logic/captcha_wait.go +++ b/logic/captcha_wait.go @@ -4,6 +4,7 @@ import ( "encoding/json" "strconv" "sync" + "teknologi-umum-bot/utils" "time" "github.com/allegro/bigcache/v3" @@ -47,7 +48,7 @@ func waitOrDelete(cache *bigcache.BigCache, logger *sentry.Client, bot *tb.Bot, kickMsg, err := bot.Send(msgUser.Chat, ""+ sanitizeInput(msgUser.Sender.FirstName)+ - shouldAddSpace(msgUser)+ + utils.ShouldAddSpace(msgUser.Sender)+ sanitizeInput(msgUser.Sender.LastName)+ " nggak nyelesain captcha, mari kita kick!", &tb.SendOptions{ diff --git a/logic/welcome.go b/logic/welcome.go index f6028a0..31ac06d 100644 --- a/logic/welcome.go +++ b/logic/welcome.go @@ -4,6 +4,7 @@ import ( "math/rand" "strconv" "strings" + "teknologi-umum-bot/utils" "time" "github.com/getsentry/sentry-go" @@ -59,7 +60,7 @@ func sendWelcomeMessage(bot *tb.Bot, m *tb.Message, logger *sentry.Client) error currentWelcomeMessages[randomNum()], "{user}", ""+ - sanitizeInput(m.Sender.FirstName)+shouldAddSpace(m)+sanitizeInput(m.Sender.LastName)+ + sanitizeInput(m.Sender.FirstName)+utils.ShouldAddSpace(m.Sender)+sanitizeInput(m.Sender.LastName)+ "", 1, ), diff --git a/main.go b/main.go index c4ee70d..dcea2c9 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/allegro/bigcache/v3" sentry "github.com/getsentry/sentry-go" _ "github.com/joho/godotenv/autoload" + _ "github.com/lib/pq" tb "gopkg.in/tucnak/telebot.v2" ) diff --git a/utils/tele.go b/utils/tele.go new file mode 100644 index 0000000..304dd57 --- /dev/null +++ b/utils/tele.go @@ -0,0 +1,21 @@ +package utils + +import tb "gopkg.in/tucnak/telebot.v2" + +func ShouldAddSpace(m *tb.User) string { + if m.LastName != "" { + return " " + } + + return "" +} + +// Check whether or not a user is in the admin list +func IsAdmin(admins []tb.ChatMember, user *tb.User) bool { + for _, v := range admins { + if v.User.ID == user.ID { + return true + } + } + return false +} From 9bbc5961ce8b6a3332212b4bb148a4ddf57dd51d Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 23 Nov 2021 16:45:24 +0700 Subject: [PATCH 02/13] refactor: cleaner code with domain-esque approach --- .github/workflows/pr.yml | 17 ++++ analytics/analytics.go | 22 +++++ analytics/incr.go | 13 ++- analytics/join.go | 6 +- analytics/migration.go | 12 ++- analytics/msg.go | 29 +++--- analytics/users.go | 45 +++++++++ {logic => ascii}/ascii.go | 13 ++- .../additional.go | 6 +- logic/captcha_answer.go => captcha/answer.go | 37 ++++---- logic/dependencies.go => captcha/captcha.go | 13 +-- logic/captcha_exists.go => captcha/exists.go | 2 +- logic/captcha_join.go => captcha/join.go | 17 ++-- logic/captcha_leave.go => captcha/leave.go | 23 ++--- logic/captcha_non.go => captcha/non.go | 19 ++-- logic/captcha_wait.go => captcha/wait.go | 23 ++--- {logic => captcha}/welcome.go | 2 +- cmd/cmd.go | 92 +++++++++++++++++++ go.mod | 1 + go.sum | 29 ++++++ main.go | 74 ++++++++------- {logic => shared}/error.go | 4 +- 22 files changed, 365 insertions(+), 134 deletions(-) create mode 100644 analytics/analytics.go create mode 100644 analytics/users.go rename {logic => ascii}/ascii.go (73%) rename logic/captcha_additional.go => captcha/additional.go (90%) rename logic/captcha_answer.go => captcha/answer.go (81%) rename logic/dependencies.go => captcha/captcha.go (55%) rename logic/captcha_exists.go => captcha/exists.go (98%) rename logic/captcha_join.go => captcha/join.go (89%) rename logic/captcha_leave.go => captcha/leave.go (74%) rename logic/captcha_non.go => captcha/non.go (78%) rename logic/captcha_wait.go => captcha/wait.go (79%) rename {logic => captcha}/welcome.go (99%) create mode 100644 cmd/cmd.go rename {logic => shared}/error.go (92%) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3c198c6..88e64f1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,6 +10,20 @@ jobs: name: CI runs-on: ubuntu-latest timeout-minutes: 10 + container: golang:1.17.3 + services: + db: + image: postgres:14-alpine + ports: + - 5432:5432 + emv: + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_DB: captcha + cache: + image: redis:6-alpine + ports: + - 6379:6379 steps: - name: Checkout code uses: actions/checkout@v2 @@ -29,6 +43,9 @@ jobs: run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENVIRONMENT: development + DB_URL: postgres://postgres:password@db:5432/captcha + REDIS_URL: redis://@cache:6379/ + TZ: UTC - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/analytics/analytics.go b/analytics/analytics.go new file mode 100644 index 0000000..a20d1d4 --- /dev/null +++ b/analytics/analytics.go @@ -0,0 +1,22 @@ +package analytics + +// On this package we have 2 main keys on redis: +// analytics:hour and analytics:counter + +import ( + "github.com/allegro/bigcache/v3" + "github.com/bsm/redislock" + "github.com/getsentry/sentry-go" + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" + tb "gopkg.in/tucnak/telebot.v2" +) + +type Dependency struct { + Memory *bigcache.BigCache + Redis *redis.Client + Locker *redislock.Client + Bot *tb.Bot + Logger *sentry.Client + DB *sqlx.DB +} diff --git a/analytics/incr.go b/analytics/incr.go index b7993a8..4d21044 100644 --- a/analytics/incr.go +++ b/analytics/incr.go @@ -5,13 +5,10 @@ import ( "database/sql" "strconv" "time" - - "github.com/go-redis/redis/v8" - "github.com/jmoiron/sqlx" ) -func IncrementUsrDB(db *sqlx.DB, ctx context.Context, users []UserMap) error { - c, err := db.Connx(ctx) +func (d *Dependency) IncrementUsrDB(ctx context.Context, users []UserMap) error { + c, err := d.DB.Connx(ctx) if err != nil { return err } @@ -75,10 +72,12 @@ func IncrementUsrDB(db *sqlx.DB, ctx context.Context, users []UserMap) error { return nil } -func IncrementUsrRedis(cache *redis.Client, ctx context.Context, user UserMap) error { - p := cache.TxPipeline() +func (d *Dependency) IncrementUsrRedis(ctx context.Context, user UserMap) error { + p := d.Redis.TxPipeline() defer p.Close() + // Per Redis' documentation, INCR will create a new key + // if the named key does not exists in the first place. err := p.Incr(ctx, "analytics:"+strconv.FormatInt(user.UserID, 10)).Err() if err != nil { return err diff --git a/analytics/join.go b/analytics/join.go index 0eefedc..b1f90ba 100644 --- a/analytics/join.go +++ b/analytics/join.go @@ -6,16 +6,14 @@ import ( "teknologi-umum-bot/utils" "time" - "github.com/go-redis/redis/v8" - "github.com/jmoiron/sqlx" tb "gopkg.in/tucnak/telebot.v2" ) -func NewUser(db *sqlx.DB, redis *redis.Client, user *tb.User) error { +func (d *Dependency) NewUser(user *tb.User) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - c, err := db.Connx(ctx) + c, err := d.DB.Connx(ctx) if err != nil { return err } diff --git a/analytics/migration.go b/analytics/migration.go index a19b601..5712875 100644 --- a/analytics/migration.go +++ b/analytics/migration.go @@ -8,11 +8,19 @@ import ( "github.com/jmoiron/sqlx" ) -func Migrate(db *sqlx.DB) error { +func MustMigrate(db *sqlx.DB) error { + d := &Dependency{ + DB: db, + } + + return d.Migrate() +} + +func (d *Dependency) Migrate() error { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) defer cancel() - c, err := db.Connx(ctx) + c, err := d.DB.Connx(ctx) if err != nil { return err } diff --git a/analytics/msg.go b/analytics/msg.go index d833981..ec982ed 100644 --- a/analytics/msg.go +++ b/analytics/msg.go @@ -5,20 +5,17 @@ import ( "strconv" "time" - "github.com/go-redis/redis/v8" - "github.com/jmoiron/sqlx" tb "gopkg.in/tucnak/telebot.v2" ) -func NewMsg(db *sqlx.DB, redis *redis.Client, user *tb.User) error { +func (d *Dependency) NewMsg(user *tb.User) error { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) defer cancel() - // Check latest hour - p := redis.TxPipeline() - defer p.Close() + usr := ParseToUser(user) - hour, err := p.Get(ctx, "analytics:hour").Result() + // Check latest hour + hour, err := d.Redis.Get(ctx, "analytics:hour").Result() if err != nil { return err } @@ -26,29 +23,33 @@ func NewMsg(db *sqlx.DB, redis *redis.Client, user *tb.User) error { now := time.Now().Hour() // Create new hour - if hour == "" && now > time.Now().Hour() { - counter, err := p.Get(ctx, "analytics:counter").Result() + if hour == "" || now > time.Now().Hour() { + counter, err := d.Redis.Get(ctx, "analytics:counter").Result() if err != nil { return err } // Insert a new counter to Redis, do nothing on the DB if counter == "" { - + err = d.IncrementUsrRedis(ctx, usr) + if err != nil { + return err + } + return nil } - err = p.Set(ctx, "analytics:hour", strconv.Itoa(now), 0).Err() + err = d.Redis.Set(ctx, "analytics:hour", strconv.Itoa(now), 0).Err() if err != nil { return err } + return nil } - c, err := db.Connx(ctx) + // If current hour = hour on redis + err = d.IncrementUsrRedis(ctx, usr) if err != nil { return err } - defer c.Close() - return nil } diff --git a/analytics/users.go b/analytics/users.go new file mode 100644 index 0000000..e10bc3a --- /dev/null +++ b/analytics/users.go @@ -0,0 +1,45 @@ +package analytics + +import ( + "context" + "strings" + + "github.com/go-redis/redis/v8" +) + +func (d *Dependency) GetAllUserID(ctx context.Context) ([]string, error) { + r, err := d.Redis.Get(ctx, "analytics:users").Result() + if err != nil { + return []string{}, err + } + + return strings.Split(r, " "), nil +} + +func (d *Dependency) FlushAllUserID(ctx context.Context) error { + ids, err := d.GetAllUserID(ctx) + if err != nil { + return err + } + + tx := d.Redis.TxPipeline() + defer tx.Close() + + for _, v := range ids { + err = tx.Del(ctx, "analytics"+v).Err() + if err != nil { + return err + } + } + + err = tx.Set(ctx, "analytics:user", "", redis.KeepTTL).Err() + if err != nil { + return err + } + + err = tx.Do(ctx).Err() + if err != nil { + return err + } + return nil +} diff --git a/logic/ascii.go b/ascii/ascii.go similarity index 73% rename from logic/ascii.go rename to ascii/ascii.go index b76771e..1ce6973 100644 --- a/logic/ascii.go +++ b/ascii/ascii.go @@ -1,12 +1,19 @@ -package logic +package ascii import ( "errors" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" + "github.com/getsentry/sentry-go" tb "gopkg.in/tucnak/telebot.v2" ) +type Dependencies struct { + Bot *tb.Bot + Logger *sentry.Client +} + // Send ASCII art message for fun. func (d *Dependencies) Ascii(m *tb.Message) { if m.Payload == "" { @@ -28,11 +35,11 @@ func (d *Dependencies) Ascii(m *tb.Message) { }, ) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } else { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } diff --git a/logic/captcha_additional.go b/captcha/additional.go similarity index 90% rename from logic/captcha_additional.go rename to captcha/additional.go index eebddbc..7dc51b3 100644 --- a/logic/captcha_additional.go +++ b/captcha/additional.go @@ -1,4 +1,4 @@ -package logic +package captcha import ( "encoding/json" @@ -24,7 +24,7 @@ func (d *Dependencies) collectAdditionalAndCache(captcha *Captcha, m *tb.Message return err } - err = d.Cache.Set(strconv.Itoa(m.Sender.ID), data) + err = d.Memory.Set(strconv.Itoa(m.Sender.ID), data) if err != nil { return err } @@ -42,7 +42,7 @@ func (d *Dependencies) collectUserMsgAndCache(captcha *Captcha, m *tb.Message) e return err } - err = d.Cache.Set(strconv.Itoa(m.Sender.ID), data) + err = d.Memory.Set(strconv.Itoa(m.Sender.ID), data) if err != nil { return err } diff --git a/logic/captcha_answer.go b/captcha/answer.go similarity index 81% rename from logic/captcha_answer.go rename to captcha/answer.go index 436a1f9..4ca8f2f 100644 --- a/logic/captcha_answer.go +++ b/captcha/answer.go @@ -1,10 +1,11 @@ -package logic +package captcha import ( "encoding/json" "errors" "strconv" "strings" + "teknologi-umum-bot/shared" "time" tb "gopkg.in/tucnak/telebot.v2" @@ -16,9 +17,9 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { // Check if the message author is in the captcha:users list or not // If not, return // If yes, check if the answer is correct or not - exists, err := userExists(d.Cache, strconv.Itoa(m.Sender.ID)) + exists, err := userExists(d.Memory, strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -32,22 +33,22 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { // // Get the answer and all of the data surrounding captcha from // this specific user ID from the cache. - data, err := d.Cache.Get(strconv.Itoa(m.Sender.ID)) + data, err := d.Memory.Get(strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } var captcha Captcha err = json.Unmarshal(data, &captcha) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.collectUserMsgAndCache(&captcha, m) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -71,13 +72,13 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { }, ) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.collectAdditionalAndCache(&captcha, m, wrongMsg) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -99,13 +100,13 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { }, ) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.collectAdditionalAndCache(&captcha, m, wrongMsg) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -114,7 +115,7 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { err = d.removeUserFromCache(strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -128,7 +129,7 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { MessageID: captcha.QuestionID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -142,7 +143,7 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { MessageID: msgID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } @@ -157,7 +158,7 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { MessageID: msgID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } @@ -165,18 +166,18 @@ func (d *Dependencies) WaitForAnswer(m *tb.Message) { // It... remove the user from cache. What else do you expect? func (d *Dependencies) removeUserFromCache(key string) error { - users, err := d.Cache.Get("captcha:users") + users, err := d.Memory.Get("captcha:users") if err != nil { return err } str := strings.Replace(string(users), ";"+key, "", 1) - err = d.Cache.Set("captcha:users", []byte(str)) + err = d.Memory.Set("captcha:users", []byte(str)) if err != nil { return err } - err = d.Cache.Delete(key) + err = d.Memory.Delete(key) if err != nil { return err } diff --git a/logic/dependencies.go b/captcha/captcha.go similarity index 55% rename from logic/dependencies.go rename to captcha/captcha.go index bdb65de..2ad22f8 100644 --- a/logic/dependencies.go +++ b/captcha/captcha.go @@ -1,8 +1,6 @@ -package logic +package captcha import ( - "context" - "github.com/allegro/bigcache/v3" "github.com/getsentry/sentry-go" "github.com/go-redis/redis/v8" @@ -10,9 +8,8 @@ import ( ) type Dependencies struct { - Cache *bigcache.BigCache - Redis *redis.Client - Bot *tb.Bot - Context context.Context - Logger *sentry.Client + Memory *bigcache.BigCache + Redis *redis.Client + Bot *tb.Bot + Logger *sentry.Client } diff --git a/logic/captcha_exists.go b/captcha/exists.go similarity index 98% rename from logic/captcha_exists.go rename to captcha/exists.go index 770bc03..40ed678 100644 --- a/logic/captcha_exists.go +++ b/captcha/exists.go @@ -1,4 +1,4 @@ -package logic +package captcha import ( "errors" diff --git a/logic/captcha_join.go b/captcha/join.go similarity index 89% rename from logic/captcha_join.go rename to captcha/join.go index 6670144..167e4af 100644 --- a/logic/captcha_join.go +++ b/captcha/join.go @@ -1,10 +1,11 @@ -package logic +package captcha import ( "encoding/json" "strconv" "strings" "sync" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" "time" @@ -59,7 +60,7 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { // If they're not, continue execute the captcha. admins, err := d.Bot.AdminsOf(m.Chat) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -97,7 +98,7 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { }, ) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -113,20 +114,20 @@ func (d *Dependencies) CaptchaUserJoin(m *tb.Message) { QuestionID: strconv.Itoa(msgQuestion.ID), }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } // Yes, the cache key is their User ID in string format. - err = d.Cache.Set(strconv.Itoa(m.Sender.ID), captchaData) + err = d.Memory.Set(strconv.Itoa(m.Sender.ID), captchaData) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } - err = d.Cache.Append("captcha:users", []byte(";"+strconv.Itoa(m.Sender.ID))) + err = d.Memory.Append("captcha:users", []byte(";"+strconv.Itoa(m.Sender.ID))) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } diff --git a/logic/captcha_leave.go b/captcha/leave.go similarity index 74% rename from logic/captcha_leave.go rename to captcha/leave.go index 59a554b..b3c1f61 100644 --- a/logic/captcha_leave.go +++ b/captcha/leave.go @@ -1,8 +1,9 @@ -package logic +package captcha import ( "encoding/json" "strconv" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" tb "gopkg.in/tucnak/telebot.v2" @@ -14,7 +15,7 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { // If they're not, continue execute the captcha. admins, err := d.Bot.AdminsOf(m.Chat) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -24,9 +25,9 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { // We need to check if the user is in the captcha:users cache // or not. - check, err := userExists(d.Cache, strconv.Itoa(m.Sender.ID)) + check, err := userExists(d.Memory, strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -36,22 +37,22 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { // OK, they exists in the cache. Now we've got to delete // all the message that we've sent before. - data, err := d.Cache.Get(strconv.Itoa(m.Sender.ID)) + data, err := d.Memory.Get(strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } var captcha Captcha err = json.Unmarshal(data, &captcha) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.removeUserFromCache(strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -61,7 +62,7 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { MessageID: captcha.QuestionID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -75,7 +76,7 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { MessageID: msgID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } @@ -90,7 +91,7 @@ func (d *Dependencies) CaptchaUserLeave(m *tb.Message) { MessageID: msgID, }) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } diff --git a/logic/captcha_non.go b/captcha/non.go similarity index 78% rename from logic/captcha_non.go rename to captcha/non.go index 21b1d64..af27f83 100644 --- a/logic/captcha_non.go +++ b/captcha/non.go @@ -1,8 +1,9 @@ -package logic +package captcha import ( "encoding/json" "strconv" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" "time" @@ -16,9 +17,9 @@ func (d *Dependencies) NonTextListener(m *tb.Message) { // Check if the message author is in the captcha:users list or not // If not, return // If yes, check if the answer is correct or not - exists, err := userExists(d.Cache, strconv.Itoa(m.Sender.ID)) + exists, err := userExists(d.Memory, strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -32,16 +33,16 @@ func (d *Dependencies) NonTextListener(m *tb.Message) { // // Get the answer and all of the data surrounding captcha from // this specific user ID from the cache. - data, err := d.Cache.Get(strconv.Itoa(m.Sender.ID)) + data, err := d.Memory.Get(strconv.Itoa(m.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } var captcha Captcha err = json.Unmarshal(data, &captcha) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } @@ -63,19 +64,19 @@ func (d *Dependencies) NonTextListener(m *tb.Message) { }, ) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.Bot.Delete(m) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } err = d.collectAdditionalAndCache(&captcha, m, wrongMsg) if err != nil { - handleError(err, d.Logger, d.Bot, m) + shared.HandleError(err, d.Logger, d.Bot, m) return } } diff --git a/logic/captcha_wait.go b/captcha/wait.go similarity index 79% rename from logic/captcha_wait.go rename to captcha/wait.go index 03501cd..f54d21e 100644 --- a/logic/captcha_wait.go +++ b/captcha/wait.go @@ -1,9 +1,10 @@ -package logic +package captcha import ( "encoding/json" "strconv" "sync" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" "time" @@ -24,20 +25,20 @@ func (d *Dependencies) waitOrDelete(msgUser *tb.Message, msgQst *tb.Message, con // // If they're still in the cache, we will say goodbye and // kick them from the group. - check := cacheExists(d.Cache, strconv.Itoa(msgUser.Sender.ID)) + check := cacheExists(d.Memory, strconv.Itoa(msgUser.Sender.ID)) if check { // Fetch the captcha data first var captcha Captcha - user, err := d.Cache.Get(strconv.Itoa(msgUser.Sender.ID)) + user, err := d.Memory.Get(strconv.Itoa(msgUser.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } err = json.Unmarshal(user, &captcha) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } @@ -52,7 +53,7 @@ func (d *Dependencies) waitOrDelete(msgUser *tb.Message, msgQst *tb.Message, con ParseMode: tb.ModeHTML, }) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } @@ -64,7 +65,7 @@ func (d *Dependencies) waitOrDelete(msgUser *tb.Message, msgQst *tb.Message, con User: msgUser.Sender, }, true) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } @@ -75,7 +76,7 @@ func (d *Dependencies) waitOrDelete(msgUser *tb.Message, msgQst *tb.Message, con } err = d.Bot.Delete(&msgToBeDeleted) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } @@ -86,16 +87,16 @@ func (d *Dependencies) waitOrDelete(msgUser *tb.Message, msgQst *tb.Message, con } err = d.Bot.Delete(&msgToBeDeleted) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } } go deleteMessage(d.Bot, tb.StoredMessage{MessageID: strconv.Itoa(kickMsg.ID), ChatID: kickMsg.Chat.ID}) - err = d.Cache.Delete(strconv.Itoa(msgUser.Sender.ID)) + err = d.Memory.Delete(strconv.Itoa(msgUser.Sender.ID)) if err != nil { - handleError(err, d.Logger, d.Bot, msgUser) + shared.HandleError(err, d.Logger, d.Bot, msgUser) return } diff --git a/logic/welcome.go b/captcha/welcome.go similarity index 99% rename from logic/welcome.go rename to captcha/welcome.go index 31ac06d..23904d5 100644 --- a/logic/welcome.go +++ b/captcha/welcome.go @@ -1,4 +1,4 @@ -package logic +package captcha import ( "math/rand" diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..e3a525b --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "teknologi-umum-bot/analytics" + "teknologi-umum-bot/ascii" + "teknologi-umum-bot/captcha" + "teknologi-umum-bot/shared" + + "github.com/allegro/bigcache/v3" + "github.com/bsm/redislock" + "github.com/getsentry/sentry-go" + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" + tb "gopkg.in/tucnak/telebot.v2" +) + +type Dependency struct { + Memory *bigcache.BigCache + Redis *redis.Client + Locker *redislock.Client + Bot *tb.Bot + Logger *sentry.Client + DB *sqlx.DB + captcha *captcha.Dependencies + ascii *ascii.Dependencies + analytics *analytics.Dependency +} + +func New(deps Dependency) *Dependency { + return &Dependency{ + captcha: &captcha.Dependencies{ + Memory: deps.Memory, + Redis: deps.Redis, + Bot: deps.Bot, + Logger: deps.Logger, + }, + ascii: &ascii.Dependencies{ + Bot: deps.Bot, + }, + analytics: &analytics.Dependency{ + Memory: deps.Memory, + Redis: deps.Redis, + Locker: deps.Locker, + Bot: deps.Bot, + Logger: deps.Logger, + DB: deps.DB, + }, + } +} + +func (d *Dependency) OnTextHandler(m *tb.Message) { + err := d.analytics.NewMsg(m.Sender) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + return + } + + d.captcha.WaitForAnswer(m) +} + +func (d *Dependency) OnUserJoinHandler(m *tb.Message) { + var tempSender *tb.User + if m.UserJoined.ID != 0 { + tempSender = m.UserJoined + } else { + tempSender = m.Sender + } + + err := d.analytics.NewUser(tempSender) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + return + } + + d.captcha.CaptchaUserJoin(m) +} + +func (d *Dependency) OnNonTextHandler(m *tb.Message) { + err := d.analytics.NewMsg(m.Sender) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + return + } + + d.captcha.NonTextListener(m) +} +func (d *Dependency) OnUserLeftHandler(m *tb.Message) { + d.captcha.CaptchaUserLeave(m) +} +func (d *Dependency) AsciiCmdHandler(m *tb.Message) { + d.ascii.Ascii(m) +} diff --git a/go.mod b/go.mod index e31602a..b142041 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aldy505/asciitxt v0.0.2 github.com/aldy505/decrr v0.0.1 github.com/allegro/bigcache/v3 v3.0.1 + github.com/bsm/redislock v0.7.1 github.com/getsentry/sentry-go v0.11.0 github.com/go-redis/redis/v8 v8.11.4 github.com/jmoiron/sqlx v1.3.4 diff --git a/go.sum b/go.sum index d7faf6f..b025db6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= @@ -13,6 +15,13 @@ github.com/allegro/bigcache/v3 v3.0.1 h1:Q4Xl3chywXuJNOw7NV+MeySd3zGQDj4KCpkCg0t github.com/allegro/bigcache/v3 v3.0.1/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/bsm/ginkgo v1.16.4 h1:pkHpo2VJRvI0NGlxCYi8qovww76L7+g82MgM+UBvH4A= +github.com/bsm/ginkgo v1.16.4/go.mod h1:RabIZLzOCPghgHJKUqHZpqrQETA5AnF4aCSIYy5C1bk= +github.com/bsm/gomega v1.13.0 h1:fzOh8E2Wu/x407rP+v3mEb9yGJaMVguiJBtmFkuOmlc= +github.com/bsm/gomega v1.13.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= +github.com/bsm/redislock v0.7.1 h1:nBMm91MRuGOOSlHZNEF0+HpiaH1i8QpSALrF/q7b/Es= +github.com/bsm/redislock v0.7.1/go.mod h1:TSF3xUotaocycoHjVAp535/bET+ZmvrtcyNrXc0Whm8= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= @@ -44,7 +53,9 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-redis/redis/v8 v8.1.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -67,6 +78,7 @@ github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85q github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -135,10 +147,12 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -167,6 +181,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -187,13 +202,21 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -215,13 +238,17 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -238,6 +265,7 @@ golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -253,6 +281,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= diff --git a/main.go b/main.go index 463fc83..bc32002 100644 --- a/main.go +++ b/main.go @@ -15,17 +15,20 @@ package main import ( - "context" "log" "os" "os/signal" "syscall" - "teknologi-umum-bot/logic" + "teknologi-umum-bot/cmd" + "time" "github.com/aldy505/decrr" "github.com/allegro/bigcache/v3" + "github.com/bsm/redislock" sentry "github.com/getsentry/sentry-go" + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" _ "github.com/joho/godotenv/autoload" _ "github.com/lib/pq" tb "gopkg.in/tucnak/telebot.v2" @@ -51,6 +54,23 @@ func init() { } func main() { + db, err := sqlx.Open("postgres", os.Getenv("DB_URL")) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // Setup Redis + parsedRedisURL, err := redis.ParseURL(os.Getenv("REDIS_URL")) + if err != nil { + log.Fatal(err) + } + rds := redis.NewClient(parsedRedisURL) + defer rds.Close() + + // Setup Redis locker + redisLocker := redislock.New(rds) + // Setup in memory cache cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Hour * 12)) if err != nil { @@ -58,18 +78,6 @@ func main() { } defer cache.Close() - // This Redis line below is commented out because it's not needed - // for now. Yet, while I'm a shaman, I'm not sure if I'll need it - // or not. So, I'll just leave it here. - // - // Setup redis - // parsedRedisURL, err := redis.ParseURL(os.Getenv("REDIS_URL")) - // if err != nil { - // log.Fatal(err) - // } - // rds := redis.NewClient(parsedRedisURL) - // defer rds.Close() - // Setup Sentry for error handling. logger, err := sentry.NewClient(sentry.ClientOptions{ Dsn: os.Getenv("SENTRY_DSN"), @@ -109,12 +117,14 @@ func main() { } }() - deps := &logic.Dependencies{ - Cache: cache, - Bot: b, - Context: context.Background(), - Logger: logger, - } + deps := cmd.New(cmd.Dependency{ + Memory: cache, + Redis: rds, + Locker: redisLocker, + Bot: b, + Logger: logger, + DB: db, + }) // This is basically just for health check. b.Handle("/start", func(m *tb.Message) { @@ -124,18 +134,18 @@ func main() { }) // Captcha handlers - b.Handle(tb.OnUserJoined, deps.CaptchaUserJoin) - b.Handle(tb.OnText, deps.WaitForAnswer) - b.Handle(tb.OnPhoto, deps.NonTextListener) - b.Handle(tb.OnAnimation, deps.NonTextListener) - b.Handle(tb.OnVideo, deps.NonTextListener) - b.Handle(tb.OnDocument, deps.NonTextListener) - b.Handle(tb.OnSticker, deps.NonTextListener) - b.Handle(tb.OnVoice, deps.NonTextListener) - b.Handle(tb.OnVideoNote, deps.NonTextListener) - b.Handle(tb.OnUserLeft, deps.CaptchaUserLeave) - - b.Handle("/ascii", deps.Ascii) + b.Handle(tb.OnUserJoined, deps.OnUserJoinHandler) + b.Handle(tb.OnText, deps.OnTextHandler) + b.Handle(tb.OnPhoto, deps.OnNonTextHandler) + b.Handle(tb.OnAnimation, deps.OnNonTextHandler) + b.Handle(tb.OnVideo, deps.OnNonTextHandler) + b.Handle(tb.OnDocument, deps.OnNonTextHandler) + b.Handle(tb.OnSticker, deps.OnNonTextHandler) + b.Handle(tb.OnVoice, deps.OnNonTextHandler) + b.Handle(tb.OnVideoNote, deps.OnNonTextHandler) + b.Handle(tb.OnUserLeft, deps.OnUserLeftHandler) + + b.Handle("/ascii", deps.AsciiCmdHandler) log.Println("Bot started!") go func() { diff --git a/logic/error.go b/shared/error.go similarity index 92% rename from logic/error.go rename to shared/error.go index 2cf8eaf..45d55bd 100644 --- a/logic/error.go +++ b/shared/error.go @@ -1,4 +1,4 @@ -package logic +package shared import ( "log" @@ -11,7 +11,7 @@ import ( ) // We handle error by apologizing to the user and then sending the error to Sentry. -func handleError(e error, logger *sentry.Client, bot *tb.Bot, m *tb.Message) { +func HandleError(e error, logger *sentry.Client, bot *tb.Bot, m *tb.Message) { if os.Getenv("ENVIRONMENT") == "development" { log.Println(e) } From 9ca488cb98fc656f1f11a052d2df95189534de50 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 24 Nov 2021 14:26:36 +0700 Subject: [PATCH 03/13] Update pr.yml --- .github/workflows/pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 88e64f1..cee728a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,4 +1,4 @@ -name: Deploy +name: Check on: pull_request: @@ -16,7 +16,7 @@ jobs: image: postgres:14-alpine ports: - 5432:5432 - emv: + env: POSTGRES_PASSWORD: password POSTGRES_USER: postgres POSTGRES_DB: captcha From 38e93ad5e334db7e7d866d596ba13099c004126e Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 09:43:57 +0700 Subject: [PATCH 04/13] docs: create documentation for cmd package --- .env.example | 3 +++ cmd/cmd.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.env.example b/.env.example index 439ba8d..0473aae 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ ENVIRONMENT= BOT_TOKEN= SENTRY_DSN= +REDIS_URL= +DATABASE_URL= +TZ= diff --git a/cmd/cmd.go b/cmd/cmd.go index e3a525b..a608c81 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -26,6 +26,9 @@ type Dependency struct { analytics *analytics.Dependency } +// New returns a pointer struct of Dependency +// which map the incoming dependencies provided +// into what's needed by each domain. func New(deps Dependency) *Dependency { return &Dependency{ captcha: &captcha.Dependencies{ @@ -48,6 +51,7 @@ func New(deps Dependency) *Dependency { } } +// OnTextHandler handle any incoming text from the group func (d *Dependency) OnTextHandler(m *tb.Message) { err := d.analytics.NewMsg(m.Sender) if err != nil { @@ -58,6 +62,10 @@ func (d *Dependency) OnTextHandler(m *tb.Message) { d.captcha.WaitForAnswer(m) } +// OnUserJoinHandler handle any incoming user join, +// whether they was invited by someone (meaning they are +// added by someone else into the group), or they join +// the group all by themself. func (d *Dependency) OnUserJoinHandler(m *tb.Message) { var tempSender *tb.User if m.UserJoined.ID != 0 { @@ -75,6 +83,8 @@ func (d *Dependency) OnUserJoinHandler(m *tb.Message) { d.captcha.CaptchaUserJoin(m) } +// OnNonTextHandler meant to handle anything else +// than an incoming text message. func (d *Dependency) OnNonTextHandler(m *tb.Message) { err := d.analytics.NewMsg(m.Sender) if err != nil { @@ -84,9 +94,14 @@ func (d *Dependency) OnNonTextHandler(m *tb.Message) { d.captcha.NonTextListener(m) } + +// OnUserLeftHandle handles during an event in which +// a user left the group. func (d *Dependency) OnUserLeftHandler(m *tb.Message) { d.captcha.CaptchaUserLeave(m) } + +// AsciiCmdHandler handle the /ascii command. func (d *Dependency) AsciiCmdHandler(m *tb.Message) { d.ascii.Ascii(m) } From 6cf846a419bd2b8ee974286580fa08540db0070a Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 12:41:10 +0700 Subject: [PATCH 05/13] feat: working analytics --- analytics/incr.go | 83 +++++++++++++++++++++++------------------- analytics/join.go | 17 ++++----- analytics/migration.go | 20 +++++----- analytics/msg.go | 74 ++++++++++++++++++++++++++----------- analytics/parser.go | 6 +-- analytics/users.go | 46 +++++++++++++++++------ cmd/cmd.go | 10 ++--- main.go | 17 ++++++++- 8 files changed, 172 insertions(+), 101 deletions(-) diff --git a/analytics/incr.go b/analytics/incr.go index 4d21044..5d2ca6e 100644 --- a/analytics/incr.go +++ b/analytics/incr.go @@ -3,8 +3,12 @@ package analytics import ( "context" "database/sql" + "errors" "strconv" "time" + + "github.com/aldy505/decrr" + "github.com/go-redis/redis/v8" ) func (d *Dependency) IncrementUsrDB(ctx context.Context, users []UserMap) error { @@ -20,42 +24,27 @@ func (d *Dependency) IncrementUsrDB(ctx context.Context, users []UserMap) error } for _, user := range users { - r, err := t.QueryxContext( - ctx, - `SELECT counter FROM analytics WHERE user_id = $1`, - user.UserID, - ) - if err != nil { - t.Rollback() - return err - } - defer r.Close() - - var counter int - if r.Next() { - err = r.Scan(&counter) - if err != nil { - t.Rollback() - return err - } - } - now := time.Now() _, err = t.ExecContext( ctx, - `UPDATE analytics - SET counter = $1, - updated_at = $2, - username = $3, - display_name = $4 - WHERE user_id = $5`, - counter+user.Counter, - now, - now, + `INSERT INTO analytics + (user_id, username, display_name, counter, created_at, joined_at, updated_at) + VALUES + ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (user_id) + DO UPDATE + SET counter = (SELECT counter FROM analytics WHERE user_id = $1)+$4, + username = $2, + display_name = $3, + updated_at = $7`, + user.UserID, user.Username, user.DisplayName, - user.UserID, + user.Counter, + now, + now, + now, ) if err != nil { t.Rollback() @@ -66,7 +55,7 @@ func (d *Dependency) IncrementUsrDB(ctx context.Context, users []UserMap) error err = t.Commit() if err != nil { t.Rollback() - return err + return decrr.Wrap(err) } return nil @@ -76,16 +65,36 @@ func (d *Dependency) IncrementUsrRedis(ctx context.Context, user UserMap) error p := d.Redis.TxPipeline() defer p.Close() + usrID := strconv.FormatInt(user.UserID, 10) + + exists, err := d.Redis.HExists(ctx, "analytics:"+usrID, "count").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return decrr.Wrap(err) + } + + if !exists { + p.HSet( + ctx, + "analytics:"+usrID, + "counter", + 0, + "username", + user.Username, + "display_name", + user.DisplayName, + ) + } + // Per Redis' documentation, INCR will create a new key // if the named key does not exists in the first place. - err := p.Incr(ctx, "analytics:"+strconv.FormatInt(user.UserID, 10)).Err() - if err != nil { - return err - } + p.HIncrBy(ctx, "analytics:"+usrID, "counter", 1) - err = p.Do(ctx).Err() + // Add the user ID into the Sets of users + p.SAdd(ctx, "analytics:users", usrID) + + _, err = p.Exec(ctx) if err != nil { - return err + return decrr.Wrap(err) } return nil diff --git a/analytics/join.go b/analytics/join.go index b1f90ba..f6a5031 100644 --- a/analytics/join.go +++ b/analytics/join.go @@ -3,25 +3,26 @@ package analytics import ( "context" "database/sql" + "teknologi-umum-bot/shared" "teknologi-umum-bot/utils" "time" tb "gopkg.in/tucnak/telebot.v2" ) -func (d *Dependency) NewUser(user *tb.User) error { +func (d *Dependency) NewUser(m *tb.Message, user *tb.User) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() c, err := d.DB.Connx(ctx) if err != nil { - return err + shared.HandleError(err, d.Logger, d.Bot, m) } defer c.Close() t, err := c.BeginTxx(ctx, &sql.TxOptions{}) if err != nil { - return err + shared.HandleError(err, d.Logger, d.Bot, m) } now := time.Now() @@ -32,8 +33,8 @@ func (d *Dependency) NewUser(user *tb.User) error { (user_id, username, display_name, counter, created_at, joined_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) - ON DUPLICATE KEY - UPDATE + ON CONFLICT (user_id) + DO UPDATE SET joined_at = $8, updated_at = $9`, user.ID, @@ -48,14 +49,12 @@ func (d *Dependency) NewUser(user *tb.User) error { ) if err != nil { t.Rollback() - return err + shared.HandleError(err, d.Logger, d.Bot, m) } err = t.Commit() if err != nil { t.Rollback() - return err + shared.HandleError(err, d.Logger, d.Bot, m) } - - return nil } diff --git a/analytics/migration.go b/analytics/migration.go index 5712875..a18ee83 100644 --- a/analytics/migration.go +++ b/analytics/migration.go @@ -17,7 +17,7 @@ func MustMigrate(db *sqlx.DB) error { } func (d *Dependency) Migrate() error { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() c, err := d.DB.Connx(ctx) @@ -33,14 +33,14 @@ func (d *Dependency) Migrate() error { _, err = t.ExecContext( ctx, - `CREATE TABLE analytics ( - user_id INTEGER PRIMARY KEY, - username VARCHAR(255), - display_name VARCHAR(255), - counter INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - joined_at TIMESTAMP, - updated_at TIMESTAMP + `CREATE TABLE IF NOT EXISTS analytics ( + user_id INTEGER PRIMARY KEY, + username VARCHAR(255), + display_name VARCHAR(255), + counter INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + joined_at TIMESTAMP, + updated_at TIMESTAMP )`, ) if err != nil { @@ -50,7 +50,7 @@ func (d *Dependency) Migrate() error { _, err = t.ExecContext( ctx, - `CREATE INDEX ON analytics (counter)`, + `CREATE INDEX IF NOT EXISTS idx_counter ON analytics (counter)`, ) if err != nil { t.Rollback() diff --git a/analytics/msg.go b/analytics/msg.go index ec982ed..0df7a9a 100644 --- a/analytics/msg.go +++ b/analytics/msg.go @@ -2,54 +2,84 @@ package analytics import ( "context" + "errors" "strconv" + "teknologi-umum-bot/shared" "time" + "github.com/aldy505/decrr" + "github.com/go-redis/redis/v8" tb "gopkg.in/tucnak/telebot.v2" ) -func (d *Dependency) NewMsg(user *tb.User) error { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) +func (d *Dependency) NewMsg(m *tb.Message) error { + user := m.Sender + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() usr := ParseToUser(user) - // Check latest hour - hour, err := d.Redis.Get(ctx, "analytics:hour").Result() + // Whatever we do, we must always increment + // the user's counter. + err := d.IncrementUsrRedis(ctx, usr) if err != nil { return err } + // Check latest hour + hour, err := d.Redis.Get(ctx, "analytics:hour").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return decrr.Wrap(err) + } + now := time.Now().Hour() // Create new hour - if hour == "" || now > time.Now().Hour() { - counter, err := d.Redis.Get(ctx, "analytics:counter").Result() - if err != nil { - return err - } - - // Insert a new counter to Redis, do nothing on the DB - if counter == "" { - err = d.IncrementUsrRedis(ctx, usr) - if err != nil { - return err - } - return nil - } - + if hour == "" { err = d.Redis.Set(ctx, "analytics:hour", strconv.Itoa(now), 0).Err() if err != nil { - return err + return decrr.Wrap(err) } return nil } - // If current hour = hour on redis - err = d.IncrementUsrRedis(ctx, usr) + hourInt, err := strconv.Atoi(hour) if err != nil { return err } + + // If hourInt < now, insert the data to the database + if hourInt < now { + go func() { + // Create new context + ctx, cancel = context.WithTimeout(context.Background(), time.Minute*1) + defer cancel() + + userMaps, err := d.GetAllUserMap(ctx) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + } + + err = d.IncrementUsrDB(ctx, userMaps) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + } + + err = d.FlushAllUserID(ctx) + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + } + + err = d.Redis.Set(ctx, "analytics:hour", strconv.Itoa(now), 0).Err() + if err != nil { + shared.HandleError(err, d.Logger, d.Bot, m) + } + }() + + return nil + } + return nil } diff --git a/analytics/parser.go b/analytics/parser.go index 4f40153..fa7ba70 100644 --- a/analytics/parser.go +++ b/analytics/parser.go @@ -9,9 +9,9 @@ import ( type UserMap struct { UserID int64 `db:"user_id"` - Username string `db:"username"` - DisplayName string `db:"display_name"` - Counter int `db:"counter"` + Username string `db:"username" redis:"username"` + DisplayName string `db:"display_name" redis:"display_name"` + Counter int `db:"counter" redis:"counter"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` JoinedAt time.Time `db:"joined_at"` diff --git a/analytics/users.go b/analytics/users.go index e10bc3a..069ecac 100644 --- a/analytics/users.go +++ b/analytics/users.go @@ -2,42 +2,66 @@ package analytics import ( "context" - "strings" "github.com/go-redis/redis/v8" ) func (d *Dependency) GetAllUserID(ctx context.Context) ([]string, error) { - r, err := d.Redis.Get(ctx, "analytics:users").Result() + r, err := d.Redis.SMembers(ctx, "analytics:users").Result() if err != nil { return []string{}, err } - return strings.Split(r, " "), nil + return r, nil } -func (d *Dependency) FlushAllUserID(ctx context.Context) error { +func (d *Dependency) GetAllUserMap(ctx context.Context) ([]UserMap, error) { ids, err := d.GetAllUserID(ctx) if err != nil { - return err + return []UserMap{}, err } + var users []UserMap + tx := d.Redis.TxPipeline() defer tx.Close() + var userCmd = make(map[string]*redis.StringStringMapCmd, len(ids)) + for _, v := range ids { - err = tx.Del(ctx, "analytics"+v).Err() - if err != nil { - return err - } + userCmd["analytics:"+v] = tx.HGetAll(ctx, "analytics:"+v) + } + + _, err = tx.Exec(ctx) + if err != nil { + return []UserMap{}, err } - err = tx.Set(ctx, "analytics:user", "", redis.KeepTTL).Err() + for _, v := range userCmd { + var user UserMap + err = v.Scan(&user) + users = append(users, user) + } + + return users, err +} + +func (d *Dependency) FlushAllUserID(ctx context.Context) error { + ids, err := d.GetAllUserID(ctx) if err != nil { return err } - err = tx.Do(ctx).Err() + tx := d.Redis.TxPipeline() + defer tx.Close() + + for _, v := range ids { + tx.Del(ctx, "analytics:"+v) + } + + tx.Del(ctx, "analytics:users") + + _, err = tx.Exec(ctx) if err != nil { return err } diff --git a/cmd/cmd.go b/cmd/cmd.go index a608c81..f3ba446 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -53,7 +53,7 @@ func New(deps Dependency) *Dependency { // OnTextHandler handle any incoming text from the group func (d *Dependency) OnTextHandler(m *tb.Message) { - err := d.analytics.NewMsg(m.Sender) + err := d.analytics.NewMsg(m) if err != nil { shared.HandleError(err, d.Logger, d.Bot, m) return @@ -74,11 +74,7 @@ func (d *Dependency) OnUserJoinHandler(m *tb.Message) { tempSender = m.Sender } - err := d.analytics.NewUser(tempSender) - if err != nil { - shared.HandleError(err, d.Logger, d.Bot, m) - return - } + go d.analytics.NewUser(m, tempSender) d.captcha.CaptchaUserJoin(m) } @@ -86,7 +82,7 @@ func (d *Dependency) OnUserJoinHandler(m *tb.Message) { // OnNonTextHandler meant to handle anything else // than an incoming text message. func (d *Dependency) OnNonTextHandler(m *tb.Message) { - err := d.analytics.NewMsg(m.Sender) + err := d.analytics.NewMsg(m) if err != nil { shared.HandleError(err, d.Logger, d.Bot, m) return diff --git a/main.go b/main.go index bc32002..0b84ef9 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "os" "os/signal" "syscall" + "teknologi-umum-bot/analytics" "teknologi-umum-bot/cmd" "time" @@ -30,7 +31,7 @@ import ( "github.com/go-redis/redis/v8" "github.com/jmoiron/sqlx" _ "github.com/joho/godotenv/autoload" - _ "github.com/lib/pq" + "github.com/lib/pq" tb "gopkg.in/tucnak/telebot.v2" ) @@ -54,7 +55,13 @@ func init() { } func main() { - db, err := sqlx.Open("postgres", os.Getenv("DB_URL")) + // Setup PostgreSQL + dbURL, err := pq.ParseURL(os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal(err) + } + + db, err := sqlx.Open("postgres", dbURL) if err != nil { log.Fatal(err) } @@ -90,6 +97,12 @@ func main() { } defer logger.Flush(5 * time.Second) + // Running migration on database first. + err = analytics.MustMigrate(db) + if err != nil { + log.Fatal(decrr.Wrap(err)) + } + // Setup Telegram Bot b, err := tb.NewBot(tb.Settings{ Token: os.Getenv("BOT_TOKEN"), From 19e662f4ca67e23aca9f65b538a3951ba9ac0d25 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 12:46:48 +0700 Subject: [PATCH 06/13] test: added test for utils --- .github/workflows/deploy.yml | 22 +++++++++++++++++----- .github/workflows/pr.yml | 7 +------ utils/tele_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 utils/tele_test.go diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fe20524..f5d8dc8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,15 +10,24 @@ jobs: name: CI runs-on: ubuntu-latest timeout-minutes: 10 + container: golang:1.17.3 + services: + db: + image: postgres:14-alpine + ports: + - 5432:5432 + emv: + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_DB: captcha + cache: + image: redis:6-alpine + ports: + - 6379:6379 steps: - name: Checkout code uses: actions/checkout@v2 - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: 1.17.x - - name: Installling dependencies run: go mod download @@ -29,6 +38,9 @@ jobs: run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENVIRONMENT: development + DATABASE_URL: postgres://postgres:password@db:5432/captcha + REDIS_URL: redis://@cache:6379/ + TZ: UTC - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cee728a..1c088bc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -28,11 +28,6 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: 1.17.x - - name: Installling dependencies run: go mod download @@ -43,7 +38,7 @@ jobs: run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENVIRONMENT: development - DB_URL: postgres://postgres:password@db:5432/captcha + DATABASE_URL: postgres://postgres:password@db:5432/captcha REDIS_URL: redis://@cache:6379/ TZ: UTC diff --git a/utils/tele_test.go b/utils/tele_test.go new file mode 100644 index 0000000..2e71e4b --- /dev/null +++ b/utils/tele_test.go @@ -0,0 +1,35 @@ +package utils_test + +import ( + "teknologi-umum-bot/utils" + "testing" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func TestShouldAddSpace(t *testing.T) { + s := utils.ShouldAddSpace(&tb.User{LastName: ""}) + if s != "" { + t.Error("ShouldAddSpace should return empty string") + } + + s = utils.ShouldAddSpace(&tb.User{LastName: "Reinaldy"}) + if s != " " { + t.Error("ShouldAddSpace should return a space") + } +} + +func TestIsAdmin(t *testing.T) { + var admins []tb.ChatMember + admins = append(admins, tb.ChatMember{User: &tb.User{ID: 1}}) + admins = append(admins, tb.ChatMember{User: &tb.User{ID: 2}}) + admins = append(admins, tb.ChatMember{User: &tb.User{ID: 3}}) + + if utils.IsAdmin(admins, &tb.User{ID: 1}) == false { + t.Error("IsAdmin should return true") + } + + if utils.IsAdmin(admins, &tb.User{ID: 4}) == true { + t.Error("IsAdmin should return false") + } +} From 9a0b0234d5e2bdd684dc13b4658e519a418f396f Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 13:07:03 +0700 Subject: [PATCH 07/13] test: add analytics pkg tests --- .github/workflows/deploy.yml | 2 +- .github/workflows/pr.yml | 2 +- analytics/analytics_test.go | 43 +++++++++++++++++++++++++ analytics/parser_test.go | 28 +++++++++++++++++ analytics/users_test.go | 61 ++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 analytics/analytics_test.go create mode 100644 analytics/parser_test.go create mode 100644 analytics/users_test.go diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5d8dc8..6eaeac7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,7 @@ jobs: run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENVIRONMENT: development - DATABASE_URL: postgres://postgres:password@db:5432/captcha + DATABASE_URL: postgres://postgres:password@db:5432/captcha?sslmode=disable REDIS_URL: redis://@cache:6379/ TZ: UTC diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1c088bc..7d8b9a7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -38,7 +38,7 @@ jobs: run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENVIRONMENT: development - DATABASE_URL: postgres://postgres:password@db:5432/captcha + DATABASE_URL: postgres://postgres:password@db:5432/captcha?sslmode=disable REDIS_URL: redis://@cache:6379/ TZ: UTC diff --git a/analytics/analytics_test.go b/analytics/analytics_test.go new file mode 100644 index 0000000..138f3c5 --- /dev/null +++ b/analytics/analytics_test.go @@ -0,0 +1,43 @@ +package analytics_test + +import ( + "log" + "os" + "teknologi-umum-bot/analytics" + "testing" + + "github.com/go-redis/redis/v8" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +var DB *sqlx.DB +var Redis *redis.Client + +func TestMain(m *testing.M) { + dbURL, err := pq.ParseURL(os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal(err) + } + + DB, err := sqlx.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + defer DB.Close() + + redisURL, err := redis.ParseURL(os.Getenv("REDIS_URL")) + if err != nil { + log.Fatal(err) + } + + Redis = redis.NewClient(redisURL) + defer Redis.Close() + + err = analytics.MustMigrate(DB) + if err != nil { + log.Fatal(err) + } + + os.Exit(m.Run()) +} diff --git a/analytics/parser_test.go b/analytics/parser_test.go new file mode 100644 index 0000000..692a5e1 --- /dev/null +++ b/analytics/parser_test.go @@ -0,0 +1,28 @@ +package analytics_test + +import ( + "teknologi-umum-bot/analytics" + "testing" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func TestParseToUser(t *testing.T) { + user := &tb.User{ + ID: 1, + FirstName: "Reinaldy", + LastName: "Reinaldy", + Username: "reinaldy", + } + + userMap := analytics.ParseToUser(user) + if userMap.UserID != 1 { + t.Errorf("UserID should be 1, got: %d", userMap.UserID) + } + if userMap.DisplayName != "Reinaldy Reinaldy" { + t.Errorf("DisplayName should be Reinaldy Reinaldy, got: %s", userMap.DisplayName) + } + if userMap.Username != "reinaldy" { + t.Errorf("Username should be reinaldy, got: %s", userMap.Username) + } +} diff --git a/analytics/users_test.go b/analytics/users_test.go new file mode 100644 index 0000000..ce26e51 --- /dev/null +++ b/analytics/users_test.go @@ -0,0 +1,61 @@ +package analytics_test + +import ( + "context" + "teknologi-umum-bot/analytics" + "testing" + "time" +) + +func TestGetAllUserID(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + err := Redis.SAdd(ctx, "analytics:users", "Adam", "Bobby", "Clifford").Err() + if err != nil { + t.Error(err) + } + + deps := &analytics.Dependency{ + Redis: Redis, + } + + users, err := deps.GetAllUserID(ctx) + if err != nil { + t.Error(err) + } + + if len(users) != 3 { + t.Error("Expected 3 users, got ", len(users)) + } +} + +func TestGetAllUserMap(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + tx := Redis.TxPipeline() + defer tx.Close() + tx.SAdd(ctx, "analytics:users", "1", "2", "3") + tx.HSet(ctx, "analytics:1", "username", "adam", "display_name", "Adam", "counter", 1) + tx.HSet(ctx, "analytics:2", "username", "bobby45", "display_name", "Bobby", "counter", 5) + tx.HSet(ctx, "analytics:3", "username", "clifford77", "display_name", "Clifford", "counter", 3) + + _, err := tx.Exec(ctx) + if err != nil { + t.Error(err) + } + + deps := &analytics.Dependency{ + Redis: Redis, + } + + users, err := deps.GetAllUserMap(ctx) + if err != nil { + t.Error(err) + } + + if len(users) != 3 { + t.Error("Expected 3 users, got ", len(users)) + } +} From c526e51069eb92d013ab083b292dbfdfda05152e Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 13:12:18 +0700 Subject: [PATCH 08/13] fix: removing length for userCmd map --- analytics/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics/users.go b/analytics/users.go index 069ecac..4acef03 100644 --- a/analytics/users.go +++ b/analytics/users.go @@ -26,7 +26,7 @@ func (d *Dependency) GetAllUserMap(ctx context.Context) ([]UserMap, error) { tx := d.Redis.TxPipeline() defer tx.Close() - var userCmd = make(map[string]*redis.StringStringMapCmd, len(ids)) + var userCmd = make(map[string]*redis.StringStringMapCmd) for _, v := range ids { userCmd["analytics:"+v] = tx.HGetAll(ctx, "analytics:"+v) From 6f391a791e53a82ff9ea3df639f39ef8938eff19 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 27 Nov 2021 18:21:07 +0700 Subject: [PATCH 09/13] test: add complete analytics pkg tests --- analytics/analytics_test.go | 23 +++++++++++ analytics/incr_test.go | 80 +++++++++++++++++++++++++++++++++++++ analytics/join_test.go | 32 +++++++++++++++ analytics/msg_test.go | 34 ++++++++++++++++ analytics/users.go | 2 +- analytics/users_test.go | 8 +++- 6 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 analytics/incr_test.go create mode 100644 analytics/join_test.go create mode 100644 analytics/msg_test.go diff --git a/analytics/analytics_test.go b/analytics/analytics_test.go index 138f3c5..71b521b 100644 --- a/analytics/analytics_test.go +++ b/analytics/analytics_test.go @@ -1,10 +1,12 @@ package analytics_test import ( + "context" "log" "os" "teknologi-umum-bot/analytics" "testing" + "time" "github.com/go-redis/redis/v8" "github.com/jmoiron/sqlx" @@ -41,3 +43,24 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } + +func Cleanup(db *sqlx.DB, redis *redis.Client) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + c, err := db.Connx(ctx) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + _, err = c.ExecContext(ctx, "TRUNCATE TABLE analytics") + if err != nil { + log.Fatal(err) + } + + err = redis.FlushAll(ctx).Err() + if err != nil { + log.Fatal(err) + } +} diff --git a/analytics/incr_test.go b/analytics/incr_test.go new file mode 100644 index 0000000..bd24840 --- /dev/null +++ b/analytics/incr_test.go @@ -0,0 +1,80 @@ +package analytics_test + +import ( + "context" + "teknologi-umum-bot/analytics" + "testing" + "time" +) + +func TestIncrementUsrDB(t *testing.T) { + defer Cleanup(DB, Redis) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + users := []analytics.UserMap{ + { + UserID: 1, + Username: "reinaldy", + DisplayName: "Reinaldy", + Counter: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + JoinedAt: time.Now(), + }, + { + UserID: 2, + Username: "elianiva", + DisplayName: "Dicha", + Counter: 20, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + JoinedAt: time.Now(), + }, + { + UserID: 3, + Username: "farhan443", + DisplayName: "Farhan", + Counter: 15, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + JoinedAt: time.Now(), + }, + } + + d := &analytics.Dependency{ + DB: DB, + } + + err := d.IncrementUsrDB(ctx, users) + if err != nil { + t.Error(err) + } +} + +func TestIncrementUsrRedis(t *testing.T) { + defer Cleanup(DB, Redis) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + users := analytics.UserMap{ + UserID: 2, + Username: "elianiva", + DisplayName: "Dicha", + Counter: 20, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + JoinedAt: time.Now(), + } + + d := &analytics.Dependency{ + DB: DB, + } + + err := d.IncrementUsrRedis(ctx, users) + if err != nil { + t.Error(err) + } +} diff --git a/analytics/join_test.go b/analytics/join_test.go new file mode 100644 index 0000000..fbdbbe6 --- /dev/null +++ b/analytics/join_test.go @@ -0,0 +1,32 @@ +package analytics_test + +import ( + "teknologi-umum-bot/analytics" + "testing" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func TestNewUser(t *testing.T) { + defer Cleanup(DB, Redis) + + user := &tb.User{ + ID: 1, + Username: "reinaldy", + FirstName: "Reinaldy", + LastName: "Reinaldy", + } + + d := &analytics.Dependency{ + DB: DB, + Redis: Redis, + } + + defer func() { + if r := recover(); r != nil { + t.Errorf("panic: %v", r) + } + }() + + d.NewUser(&tb.Message{}, user) +} diff --git a/analytics/msg_test.go b/analytics/msg_test.go new file mode 100644 index 0000000..faed945 --- /dev/null +++ b/analytics/msg_test.go @@ -0,0 +1,34 @@ +package analytics_test + +import ( + "teknologi-umum-bot/analytics" + "testing" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func TestNewMsg(t *testing.T) { + defer Cleanup(DB, Redis) + + m := &tb.Message{ + Sender: &tb.User{ + ID: 123456789, + FirstName: "Reinaldy", + LastName: "Reinaldy", + Username: "reinaldy", + }, + } + + d := &analytics.Dependency{ + DB: DB, + Redis: Redis, + } + + defer func() { + if r := recover(); r != nil { + t.Errorf("panic: %v", r) + } + }() + + d.NewMsg(m) +} diff --git a/analytics/users.go b/analytics/users.go index 4acef03..069ecac 100644 --- a/analytics/users.go +++ b/analytics/users.go @@ -26,7 +26,7 @@ func (d *Dependency) GetAllUserMap(ctx context.Context) ([]UserMap, error) { tx := d.Redis.TxPipeline() defer tx.Close() - var userCmd = make(map[string]*redis.StringStringMapCmd) + var userCmd = make(map[string]*redis.StringStringMapCmd, len(ids)) for _, v := range ids { userCmd["analytics:"+v] = tx.HGetAll(ctx, "analytics:"+v) diff --git a/analytics/users_test.go b/analytics/users_test.go index ce26e51..e83f86b 100644 --- a/analytics/users_test.go +++ b/analytics/users_test.go @@ -8,7 +8,9 @@ import ( ) func TestGetAllUserID(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer Cleanup(DB, Redis) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() err := Redis.SAdd(ctx, "analytics:users", "Adam", "Bobby", "Clifford").Err() @@ -31,7 +33,9 @@ func TestGetAllUserID(t *testing.T) { } func TestGetAllUserMap(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer Cleanup(DB, Redis) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() tx := Redis.TxPipeline() From faf572bc7e7cf88a43daec955c371441a12b3757 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 30 Nov 2021 04:57:21 +0000 Subject: [PATCH 10/13] test: fix test cases --- analytics/analytics_test.go | 69 ++++++++++++++++++++++++++++--------- analytics/incr_test.go | 12 ++++--- analytics/join_test.go | 7 ++-- analytics/msg_test.go | 7 ++-- analytics/users_test.go | 16 +++++---- 5 files changed, 78 insertions(+), 33 deletions(-) diff --git a/analytics/analytics_test.go b/analytics/analytics_test.go index 71b521b..78d208a 100644 --- a/analytics/analytics_test.go +++ b/analytics/analytics_test.go @@ -2,65 +2,100 @@ package analytics_test import ( "context" + "database/sql" "log" "os" "teknologi-umum-bot/analytics" "testing" "time" + "github.com/allegro/bigcache/v3" "github.com/go-redis/redis/v8" "github.com/jmoiron/sqlx" "github.com/lib/pq" ) -var DB *sqlx.DB -var Redis *redis.Client +var db *sqlx.DB +var cache *redis.Client +var memory *bigcache.BigCache func TestMain(m *testing.M) { - dbURL, err := pq.ParseURL(os.Getenv("DATABASE_URL")) + Setup() + + defer Teardown() + + os.Exit(m.Run()) +} + +func Cleanup() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + c, err := db.Connx(ctx) if err != nil { log.Fatal(err) } + defer c.Close() - DB, err := sqlx.Open("postgres", dbURL) + tx, err := c.BeginTxx(ctx, &sql.TxOptions{}) if err != nil { log.Fatal(err) } - defer DB.Close() - redisURL, err := redis.ParseURL(os.Getenv("REDIS_URL")) + _, err = tx.ExecContext(ctx, "TRUNCATE TABLE analytics") if err != nil { + tx.Rollback() log.Fatal(err) } - Redis = redis.NewClient(redisURL) - defer Redis.Close() + err = tx.Commit() + if err != nil { + tx.Rollback() + log.Fatal(err) + } - err = analytics.MustMigrate(DB) + err = cache.FlushAll(ctx).Err() if err != nil { log.Fatal(err) } - os.Exit(m.Run()) + err = memory.Reset() + if err != nil { + log.Fatal(err) + } } -func Cleanup(db *sqlx.DB, redis *redis.Client) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() +func Setup() { + dbURL, err := pq.ParseURL(os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal(err) + } - c, err := db.Connx(ctx) + db, err = sqlx.Open("postgres", dbURL) if err != nil { log.Fatal(err) } - defer c.Close() - _, err = c.ExecContext(ctx, "TRUNCATE TABLE analytics") + redisURL, err := redis.ParseURL(os.Getenv("REDIS_URL")) + if err != nil { + log.Fatal(err) + } + + cache = redis.NewClient(redisURL) + + memory, err = bigcache.NewBigCache(bigcache.DefaultConfig(time.Hour * 1)) if err != nil { log.Fatal(err) } - err = redis.FlushAll(ctx).Err() + err = analytics.MustMigrate(db) if err != nil { log.Fatal(err) } } + +func Teardown() { + memory.Close() + cache.Close() + db.Close() +} diff --git a/analytics/incr_test.go b/analytics/incr_test.go index bd24840..ca712b9 100644 --- a/analytics/incr_test.go +++ b/analytics/incr_test.go @@ -8,7 +8,7 @@ import ( ) func TestIncrementUsrDB(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() @@ -44,7 +44,9 @@ func TestIncrementUsrDB(t *testing.T) { } d := &analytics.Dependency{ - DB: DB, + DB: db, + Redis: cache, + Memory: memory, } err := d.IncrementUsrDB(ctx, users) @@ -54,7 +56,7 @@ func TestIncrementUsrDB(t *testing.T) { } func TestIncrementUsrRedis(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() @@ -70,7 +72,9 @@ func TestIncrementUsrRedis(t *testing.T) { } d := &analytics.Dependency{ - DB: DB, + DB: db, + Redis: cache, + Memory: memory, } err := d.IncrementUsrRedis(ctx, users) diff --git a/analytics/join_test.go b/analytics/join_test.go index fbdbbe6..b0b37c0 100644 --- a/analytics/join_test.go +++ b/analytics/join_test.go @@ -8,7 +8,7 @@ import ( ) func TestNewUser(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() user := &tb.User{ ID: 1, @@ -18,8 +18,9 @@ func TestNewUser(t *testing.T) { } d := &analytics.Dependency{ - DB: DB, - Redis: Redis, + DB: db, + Redis: cache, + Memory: memory, } defer func() { diff --git a/analytics/msg_test.go b/analytics/msg_test.go index faed945..d48a544 100644 --- a/analytics/msg_test.go +++ b/analytics/msg_test.go @@ -8,7 +8,7 @@ import ( ) func TestNewMsg(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() m := &tb.Message{ Sender: &tb.User{ @@ -20,8 +20,9 @@ func TestNewMsg(t *testing.T) { } d := &analytics.Dependency{ - DB: DB, - Redis: Redis, + DB: db, + Redis: cache, + Memory: memory, } defer func() { diff --git a/analytics/users_test.go b/analytics/users_test.go index e83f86b..ff0fd8a 100644 --- a/analytics/users_test.go +++ b/analytics/users_test.go @@ -8,18 +8,20 @@ import ( ) func TestGetAllUserID(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() - err := Redis.SAdd(ctx, "analytics:users", "Adam", "Bobby", "Clifford").Err() + err := cache.SAdd(ctx, "analytics:users", "Adam", "Bobby", "Clifford").Err() if err != nil { t.Error(err) } deps := &analytics.Dependency{ - Redis: Redis, + DB: db, + Redis: cache, + Memory: memory, } users, err := deps.GetAllUserID(ctx) @@ -33,12 +35,12 @@ func TestGetAllUserID(t *testing.T) { } func TestGetAllUserMap(t *testing.T) { - defer Cleanup(DB, Redis) + defer Cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() - tx := Redis.TxPipeline() + tx := cache.TxPipeline() defer tx.Close() tx.SAdd(ctx, "analytics:users", "1", "2", "3") tx.HSet(ctx, "analytics:1", "username", "adam", "display_name", "Adam", "counter", 1) @@ -51,7 +53,9 @@ func TestGetAllUserMap(t *testing.T) { } deps := &analytics.Dependency{ - Redis: Redis, + DB: db, + Redis: cache, + Memory: memory, } users, err := deps.GetAllUserMap(ctx) From 3182ca06f0e726c7e989de79e681b157846f7961 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 30 Nov 2021 05:05:33 +0000 Subject: [PATCH 11/13] chore: securing required environment variables --- main.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/main.go b/main.go index 0b84ef9..0d74998 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "log" "os" "os/signal" + "strings" "syscall" "teknologi-umum-bot/analytics" "teknologi-umum-bot/cmd" @@ -52,6 +53,18 @@ func init() { if env == "production" && sentry == "" { log.Fatal("Please provide the SENTRY_DSN value on the .env file") } + + if dbURL := os.Getenv("DATABASE_URL"); dbURL == "" || !strings.HasPrefix(dbURL, "postgres://") { + log.Fatal("Please provide the correct DATABASE_URL value on the .env file") + } + + if redisURL := os.Getenv("REDIS_URL"); redisURL == "" || !strings.HasPrefix(redisURL, "redis://") { + log.Fatal("Please provide the correct REDIS_URL value on the .env file") + } + + if tz := os.Getenv("TZ"); tz == "" { + log.Println("You are encouraged to provide the TZ value to UTC, but eh..") + } } func main() { From 88a3b52c7026213a54fd0f50b1aec3ac1036b4a3 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 30 Nov 2021 05:19:20 +0000 Subject: [PATCH 12/13] test: missing flush all users id --- analytics/users_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/analytics/users_test.go b/analytics/users_test.go index ff0fd8a..13ae0fe 100644 --- a/analytics/users_test.go +++ b/analytics/users_test.go @@ -67,3 +67,33 @@ func TestGetAllUserMap(t *testing.T) { t.Error("Expected 3 users, got ", len(users)) } } + +func TestFlushAllUserID(t *testing.T) { + defer Cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + tx := cache.TxPipeline() + defer tx.Close() + tx.SAdd(ctx, "analytics:users", "1", "2", "3") + tx.HSet(ctx, "analytics:1", "username", "adam", "display_name", "Adam", "counter", 1) + tx.HSet(ctx, "analytics:2", "username", "bobby45", "display_name", "Bobby", "counter", 5) + tx.HSet(ctx, "analytics:3", "username", "clifford77", "display_name", "Clifford", "counter", 3) + + _, err := tx.Exec(ctx) + if err != nil { + t.Error(err) + } + + deps := &analytics.Dependency{ + DB: db, + Redis: cache, + Memory: memory, + } + + err = deps.FlushAllUserID(ctx) + if err != nil { + t.Error(err) + } +} From b0249a0a867d4cf52487692488558b957beb6ee0 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 30 Nov 2021 05:35:52 +0000 Subject: [PATCH 13/13] chore: cert url for database --- .github/workflows/deploy.yml | 1 + Dockerfile | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6eaeac7..3db0eb9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -64,6 +64,7 @@ jobs: - uses: superfly/flyctl-actions@master env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + CERT_URL: ${{ secrets.CERT_URL }} with: args: 'deploy' diff --git a/Dockerfile b/Dockerfile index b1d122b..70bd1df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM golang:1.17.1-buster +RUN curl --create-dirs -o $HOME/.postgresql/root.crt -O ${CERT_URL} + WORKDIR /usr/app COPY . . @@ -8,4 +10,4 @@ RUN go mod download RUN go build main.go -CMD [ "./main" ] \ No newline at end of file +CMD [ "./main" ]