Archived
1

Some work on ordering. Special user behaviour

See #10
This commit is contained in:
Vladimir Hodakov 2017-11-26 15:28:55 +04:00
parent 8368a3c60b
commit 53a99b0ff3
20 changed files with 466 additions and 16 deletions

View File

@ -10,6 +10,7 @@ import (
"lab.pztrn.name/fat0troll/i2_bot/lib/chatter"
"lab.pztrn.name/fat0troll/i2_bot/lib/forwarder"
"lab.pztrn.name/fat0troll/i2_bot/lib/migrations"
"lab.pztrn.name/fat0troll/i2_bot/lib/orders"
"lab.pztrn.name/fat0troll/i2_bot/lib/pinner"
"lab.pztrn.name/fat0troll/i2_bot/lib/pokedexer"
"lab.pztrn.name/fat0troll/i2_bot/lib/router"
@ -42,6 +43,7 @@ func main() {
squader.New(c)
users.New(c)
statistics.New(c)
orders.New(c)
c.Log.Info("=======================")
c.Log.Info("= i2_bot initialized. =")

View File

@ -13,6 +13,7 @@ import (
"lab.pztrn.name/fat0troll/i2_bot/lib/connections"
"lab.pztrn.name/fat0troll/i2_bot/lib/forwarder/forwarderinterface"
"lab.pztrn.name/fat0troll/i2_bot/lib/migrations/migrationsinterface"
"lab.pztrn.name/fat0troll/i2_bot/lib/orders/ordersinterface"
"lab.pztrn.name/fat0troll/i2_bot/lib/pinner/pinnerinterface"
"lab.pztrn.name/fat0troll/i2_bot/lib/pokedexer/pokedexerinterface"
"lab.pztrn.name/fat0troll/i2_bot/lib/router/routerinterface"
@ -46,6 +47,7 @@ type Context struct {
Squader squaderinterface.SquaderInterface
Users usersinterface.UsersInterface
Statistics statisticsinterface.StatisticsInterface
Orders ordersinterface.OrdersInterface
}
// Init is a initialization function for context
@ -153,6 +155,12 @@ func (c *Context) RegisterSquaderInterface(si squaderinterface.SquaderInterface)
c.Squader.Init()
}
// RegisterOrdersInterface registers orders interface in application
func (c *Context) RegisterOrdersInterface(oi ordersinterface.OrdersInterface) {
c.Orders = oi
c.Orders.Init()
}
// RegisterUsersInterface registers users interface in application
func (c *Context) RegisterUsersInterface(ui usersinterface.UsersInterface) {
c.Users = ui

21
lib/dbmapping/orders.go Normal file
View File

@ -0,0 +1,21 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package dbmapping
import (
"github.com/go-sql-driver/mysql"
"time"
)
// Order is a struct, which represents `orders` table item in databse.
type Order struct {
ID int `db:"id"`
Target string `db:"target"`
TargetSquads string `db:"target_squads"`
Scheduled bool `db:"scheduled"`
ScheduledAt mysql.NullTime `db:"scheduled_at"`
Status string `db:"status"`
AuthorID int `db:"author_id"`
CreatedAt time.Time `db:"created_at"`
}

View File

@ -0,0 +1,51 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package migrations
import (
"database/sql"
)
// CreateOrdersUp creates `orders` table
func CreateOrdersUp(tx *sql.Tx) error {
request := "CREATE TABLE `orders` ("
request += "`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID приказа',"
request += "`target` varchar(191) NOT NULL COMMENT 'Цель приказа',"
request += "`target_squads` varchar(191) NOT NULL COMMENT 'Отряды, для которых этот приказ действителен',"
request += "`scheduled` bool NOT NULL DEFAULT false COMMENT 'Является ли запланированным',"
request += "`scheduled_at` datetime COMMENT 'Время запланированного пина',"
request += "`reusable` bool NOT NULL DEFAULT true COMMENT 'Можно ли повторить приказ',"
request += "`status` varchar(191) NOT NULL DEFAULT 'new' COMMENT 'Статус приказа',"
request += "`author_id` int(11) NOT NULL COMMENT 'ID автора приказа',"
request += "`created_at` datetime NOT NULL COMMENT 'Добавлен в базу',"
request += "PRIMARY KEY (`id`),"
request += "UNIQUE KEY `id` (`id`),"
request += "KEY `orders_created_at` (`created_at`)"
request += ") ENGINE=InnoDB AUTO_INCREMENT=4201 DEFAULT CHARSET=utf8mb4 COMMENT='Приказы'"
_, err := tx.Exec(request)
if err != nil {
return err
}
// Fill some default templates to send
_, err = tx.Exec("INSERT INTO `orders` VALUES(NULL, 'M', 'all', false, NULL, true, 'new', 1, NOW())")
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO `orders` VALUES(NULL, 'O', 'all', false, NULL, true, 'new', 1, NOW())")
if err != nil {
return err
}
return nil
}
// CreateOrdersDown drops `chats` table
func CreateOrdersDown(tx *sql.Tx) error {
_, err := tx.Exec("DROP TABLE `orders`")
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,29 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package migrations
import (
// stdlib
"database/sql"
)
// RemoveReusableUp removes `reusable` field in `orders` table
func RemoveReusableUp(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE `orders` DROP COLUMN `reusable`")
if err != nil {
return err
}
return nil
}
// RemoveReusableDown reverts `reusable` column
func RemoveReusableDown(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE `orders` ADD COLUMN `reusable` bool NOT NULL DEFAULT true COMMENT 'Можно ли повторить приказ' AFTER `scheduled_at")
if err != nil {
return err
}
return nil
}

View File

@ -34,6 +34,8 @@ func (m *Migrations) Init() {
goose.AddNamedMigration("21_change_telegram_id_column.go", ChangeTelegramIDColumnUp, ChangeTelegramIDColumnDown)
goose.AddNamedMigration("22_add_flood_chat_id.go", AddFloodChatIDUp, AddFloodChatIDDown)
goose.AddNamedMigration("23_add_user_type.go", AddUserTypeUp, AddUserTypeDown)
goose.AddNamedMigration("24_create_orders.go", CreateOrdersUp, CreateOrdersDown)
goose.AddNamedMigration("25_remove_reusable.go", RemoveReusableUp, RemoveReusableDown)
}
// Migrate migrates database to current version

28
lib/orders/exported.go Normal file
View File

@ -0,0 +1,28 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package orders
import (
"lab.pztrn.name/fat0troll/i2_bot/lib/appcontext"
"lab.pztrn.name/fat0troll/i2_bot/lib/orders/ordersinterface"
)
var (
c *appcontext.Context
)
// Orders is a function-handling struct for package orders.
type Orders struct{}
// New is an initialization function for appcontext
func New(ac *appcontext.Context) {
c = ac
o := &Orders{}
c.RegisterOrdersInterface(ordersinterface.OrdersInterface(o))
}
// Init is a initialization function for package
func (o *Orders) Init() {
c.Log.Info("Initializing Orders...")
}

34
lib/orders/getters.go Normal file
View File

@ -0,0 +1,34 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package orders
import (
"lab.pztrn.name/fat0troll/i2_bot/lib/dbmapping"
)
// GetAllOrders returns all orders in database
func (o *Orders) GetAllOrders() ([]dbmapping.Order, bool) {
orders := []dbmapping.Order{}
err := c.Db.Select(&orders, "SELECT * FROM orders ORDER BY created_at asc")
if err != nil {
c.Log.Error(err)
return orders, false
}
return orders, true
}
// GetOrderByID returns single order by ID
func (o *Orders) GetOrderByID(orderID int) (dbmapping.Order, bool) {
order := dbmapping.Order{}
err := c.Db.Get(&order, c.Db.Rebind("SELECT * FROM orders WHERE id=?"), orderID)
if err != nil {
c.Log.Error(err.Error())
return order, false
}
return order, true
}

94
lib/orders/orders.go Normal file
View File

@ -0,0 +1,94 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package orders
import (
"github.com/go-telegram-bot-api/telegram-bot-api"
"lab.pztrn.name/fat0troll/i2_bot/lib/dbmapping"
"strconv"
"strings"
)
// Internal functions
func (o *Orders) getOrderByID(orderID int) (dbmapping.Order, bool) {
order := dbmapping.Order{}
err := c.Db.Get(&order, c.Db.Rebind("SELECT * FROM orders WHERE id=?"), orderID)
if err != nil {
c.Log.Error(err.Error())
return order, false
}
return order, true
}
func (o *Orders) sendOrder(order *dbmapping.Order) string {
targetChats := []dbmapping.Chat{}
ok := false
if order.TargetSquads == "all" {
targetChats, ok = c.Squader.GetAllSquadChats()
if !ok {
return "fail"
}
} else {
targetChats, ok = c.Squader.GetSquadChatsBySquadsIDs(order.TargetSquads)
if !ok {
return "fail"
}
}
for i := range targetChats {
message := "Поступил приказ:"
msg := tgbotapi.NewMessage(targetChats[i].TelegramID, message)
keyboard := tgbotapi.InlineKeyboardMarkup{}
var row []tgbotapi.InlineKeyboardButton
btn := tgbotapi.NewInlineKeyboardButtonSwitch("В атаку!", strconv.Itoa(order.ID))
row = append(row, btn)
keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, row)
msg.ReplyMarkup = keyboard
msg.ParseMode = "Markdown"
pinnableMessage, err := c.Bot.Send(msg)
if err != nil {
c.Log.Error(err.Error())
} else {
pinChatMessageConfig := tgbotapi.PinChatMessageConfig{
ChatID: pinnableMessage.Chat.ID,
MessageID: pinnableMessage.MessageID,
DisableNotification: true,
}
_, err = c.Bot.PinChatMessage(pinChatMessageConfig)
if err != nil {
c.Log.Error(err.Error())
}
}
}
return "ok"
}
// External functions
// SendOrder sends order to selected or all squads
func (o *Orders) SendOrder(update *tgbotapi.Update) string {
command := update.Message.Command()
orderNumber := strings.TrimPrefix(command, "send_order")
orderID, _ := strconv.Atoi(orderNumber)
if orderID == 0 {
return "fail"
}
order, ok := o.getOrderByID(orderID)
if !ok {
return "fail"
}
return o.sendOrder(&order)
}

View File

@ -0,0 +1,21 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package ordersinterface
import (
"github.com/go-telegram-bot-api/telegram-bot-api"
"lab.pztrn.name/fat0troll/i2_bot/lib/dbmapping"
)
// OrdersInterface implements Orders for importing via appcontext.
type OrdersInterface interface {
Init()
GetAllOrders() ([]dbmapping.Order, bool)
GetOrderByID(orderID int) (dbmapping.Order, bool)
ListAllOrders(update *tgbotapi.Update) string
SendOrder(update *tgbotapi.Update) string
}

44
lib/orders/responders.go Normal file
View File

@ -0,0 +1,44 @@
// i2_bot Instinct PokememBro Bot
// Copyright (c) 2017 Vladimir "fat0troll" Hodakov
package orders
import (
"github.com/go-telegram-bot-api/telegram-bot-api"
"strconv"
)
// ListAllOrders returns to user all orders in database
func (o *Orders) ListAllOrders(update *tgbotapi.Update) string {
orders, ok := o.GetAllOrders()
if !ok {
return "fail"
}
message := "*Приказы на атаку*\n"
for i := range orders {
message += "\\[" + strconv.Itoa(orders[i].ID) + "] " + orders[i].TargetSquads + " → "
if orders[i].Target == "M" {
message += "🈳 МИСТИКА "
} else {
message += "🈵 ОТВАГА "
}
if orders[i].Scheduled {
message += "запланировано на "
message += orders[i].ScheduledAt.Time.Format("02.01.2006 15:04:05")
}
if orders[i].Status == "sent" {
message += "\nПросмотреть выполнение приказа: /show\\_order" + strconv.Itoa(orders[i].ID)
} else {
message += "\nОтправить приказ прямо сейчас: /send\\_order" + strconv.Itoa(orders[i].ID)
}
message += "\n"
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, message)
msg.ParseMode = "Markdown"
c.Bot.Send(msg)
return "ok"
}

View File

@ -5,31 +5,65 @@ package router
import (
"github.com/go-telegram-bot-api/telegram-bot-api"
"strconv"
"strings"
)
// RouteInline routes inline requests to bot
func (r *Router) RouteInline(update *tgbotapi.Update) string {
availableCommands := make(map[string]string)
availableCommands["0"] = "🌲Лес"
availableCommands["1"] = "⛰Горы"
availableCommands["2"] = "🚣Озеро"
availableCommands["3"] = "🏙Город"
availableCommands["4"] = "🏛Катакомбы"
availableCommands["5"] = "⛪️Кладбище"
outputCommands := make(map[string]string)
for i, value := range availableCommands {
if strings.Contains(value, update.InlineQuery.Query) {
outputCommands[i] = value
}
playerRaw, ok := c.Users.GetOrCreatePlayer(update.InlineQuery.From.ID)
if !ok {
return "fail"
}
results := make([]interface{}, 0)
for i, value := range outputCommands {
article := tgbotapi.NewInlineQueryResultArticle(i, "Команда боту @PokememBroBot:", value)
article.Description = value
if playerRaw.LeagueID != 1 {
article := tgbotapi.NewInlineQueryResultArticle("0", "Команда боту @PokememBroBot:", "👤Герой")
article.Description = "👤Герой"
results = append(results, article)
} else {
orderNumber, _ := strconv.Atoi(update.InlineQuery.Query)
if orderNumber != 0 {
order, ok := c.Orders.GetOrderByID(orderNumber)
if !ok {
return "fail"
}
attackTarget := ""
if order.Target == "M" {
attackTarget = "⚔ 🈳 МИСТИКА"
} else {
attackTarget = "⚔ 🈵 ОТВАГА"
}
article := tgbotapi.NewInlineQueryResultArticle(strconv.Itoa(orderNumber), "Выполнить приказ отряда:", attackTarget)
article.Description = attackTarget
results = append(results, article)
} else {
availableCommands := make(map[string]string)
availableCommands["10"] = "🌲Лес"
availableCommands["11"] = "⛰Горы"
availableCommands["12"] = "🚣Озеро"
availableCommands["13"] = "🏙Город"
availableCommands["14"] = "🏛Катакомбы"
availableCommands["15"] = "⛪️Кладбище"
outputCommands := make(map[string]string)
for i, value := range availableCommands {
if strings.Contains(value, update.InlineQuery.Query) {
outputCommands[i] = value
}
}
for i, value := range outputCommands {
article := tgbotapi.NewInlineQueryResultArticle(i, "Команда боту @PokememBroBot:", value)
article.Description = value
results = append(results, article)
}
}
}
inlineConf := tgbotapi.InlineConfig{
@ -44,5 +78,5 @@ func (r *Router) RouteInline(update *tgbotapi.Update) string {
c.Log.Error(err.Error())
}
return "fail"
return "ok"
}

View File

@ -17,6 +17,7 @@ func (r *Router) routePrivateRequest(update *tgbotapi.Update, playerRaw *dbmappi
var pokememeInfoMsg = regexp.MustCompile("/pk(\\d+)")
var usersMsg = regexp.MustCompile("/users\\d?\\z")
var squadInfoMsg = regexp.MustCompile("/show_squad(\\d+)\\z")
var orderSendMsg = regexp.MustCompile("/send_order(\\d+)\\z")
if update.Message.ForwardFrom != nil {
if update.Message.ForwardFrom.ID != 360402625 {
@ -34,6 +35,11 @@ func (r *Router) routePrivateRequest(update *tgbotapi.Update, playerRaw *dbmappi
switch {
case update.Message.Command() == "start":
if playerRaw.LeagueID != 0 {
if playerRaw.Status == "special" {
c.Welcomer.PrivateWelcomeMessageSpecial(update, playerRaw)
return "ok"
}
c.Welcomer.PrivateWelcomeMessageAuthorized(update, playerRaw)
return "ok"
}
@ -108,6 +114,19 @@ func (r *Router) routePrivateRequest(update *tgbotapi.Update, playerRaw *dbmappi
return c.Talkers.AnyMessageUnauthorized(update)
case update.Message.Command() == "orders":
if c.Users.PlayerBetterThan(playerRaw, "admin") {
return c.Orders.ListAllOrders(update)
}
return c.Talkers.AnyMessageUnauthorized(update)
case orderSendMsg.MatchString(text):
if c.Users.PlayerBetterThan(playerRaw, "admin") {
return c.Orders.SendOrder(update)
}
return c.Talkers.AnyMessageUnauthorized(update)
case usersMsg.MatchString(text):
if c.Users.PlayerBetterThan(playerRaw, "admin") {
return c.Users.UsersList(update)

View File

@ -5,6 +5,8 @@ package squader
import (
"lab.pztrn.name/fat0troll/i2_bot/lib/dbmapping"
"strconv"
"strings"
)
// GetSquadByID returns squad will all support information
@ -77,6 +79,43 @@ func (s *Squader) GetAllSquadFloodChats() ([]dbmapping.Chat, bool) {
return groupChats, true
}
// GetSquadChatsBySquadsIDs returns main squad chats for given squads IDs
func (s *Squader) GetSquadChatsBySquadsIDs(squadsIDs string) ([]dbmapping.Chat, bool) {
groupChats := []dbmapping.Chat{}
squadsIDsArray := strings.Split(squadsIDs, ",")
if len(squadsIDsArray) < 1 {
return groupChats, false
}
sIDs := make([]int, 0)
for i := range squadsIDsArray {
sID, _ := strconv.Atoi(squadsIDsArray[i])
if sID != 0 {
sIDs = append(sIDs, sID)
}
}
if len(sIDs) < 1 {
return groupChats, false
}
queryLine := ""
for i := range sIDs {
queryLine += strconv.Itoa(sIDs[i])
if i < len(sIDs)-1 {
queryLine += ","
}
}
err := c.Db.Select(&groupChats, "SELECT ch.* FROM chats ch, squads s WHERE s.chat_id=ch.id AND s.id IN ("+queryLine+")")
if err != nil {
c.Log.Error(err)
return groupChats, false
}
return groupChats, true
}
// GetUserRolesInSquads lists all user roles
func (s *Squader) GetUserRolesInSquads(playerRaw *dbmapping.Player) ([]dbmapping.SquadPlayerFull, bool) {
userRoles := []dbmapping.SquadPlayerFull{}

View File

@ -16,6 +16,7 @@ type SquaderInterface interface {
GetAllSquadFloodChats() ([]dbmapping.Chat, bool)
GetAvailableSquadChatsForUser(playerRaw *dbmapping.Player) ([]dbmapping.Chat, bool)
GetSquadByID(squadID int) (dbmapping.SquadChat, bool)
GetSquadChatsBySquadsIDs(squadsID string) ([]dbmapping.Chat, bool)
GetUserRolesInSquads(playerRaw *dbmapping.Player) ([]dbmapping.SquadPlayerFull, bool)
IsChatASquadEnabled(chatRaw *dbmapping.Chat) string

View File

@ -24,6 +24,7 @@ func (t *Talkers) HelpMessage(update *tgbotapi.Update, playerRaw *dbmapping.Play
message += "+ /squads — получить список отрядов.\n"
message += "+ /pin _номера чатов_ _текст_ — отправить сообщение в чаты с номерами. Сообщение будет автоматичекси запинено. Пример: \"/pin 2,3,5 привет мир\". Внимание: между номерами чатов ставятся запятые без пробелов! Всё, что идёт после второго пробела в команде — сообщение\n"
message += "+ /pin\\_all _текст_ — отправить сообщение во все группы, где находится бот. Сообщение будет автоматически запинено.\n"
message += "+ /orders — просмотреть приказы на атаку\n"
message += "+ /users — просмотреть зарегистрированных пользователей бота\n"
}
message += "+ /help выводит данное сообщение\n"

View File

@ -64,6 +64,8 @@ func (u *Users) GetOrCreatePlayer(telegramID int) (dbmapping.Player, bool) {
func (u *Users) PlayerBetterThan(playerRaw *dbmapping.Player, powerLevel string) bool {
var isBetter = false
switch playerRaw.Status {
case "special":
isBetter = true
case "owner":
isBetter = true
case "admin":

View File

@ -87,6 +87,14 @@ func (u *Users) ParseProfile(update *tgbotapi.Update, playerRaw *dbmapping.Playe
}
}
}
if strings.HasPrefix(currentString, "id: ") {
realUserID := strings.TrimPrefix(currentString, "id: ")
c.Log.Debug("Profile user ID: " + realUserID)
realUID, _ := strconv.Atoi(realUserID)
if realUID != playerRaw.TelegramID {
return "fail"
}
}
if strings.HasPrefix(currentString, "👤Уровень:") {
levelRx := regexp.MustCompile("\\d+")
levelArray := levelRx.FindAllString(currentString, -1)

View File

@ -32,6 +32,17 @@ func (w *Welcomer) PrivateWelcomeMessageAuthorized(update *tgbotapi.Update, play
c.Bot.Send(msg)
}
// PrivateWelcomeMessageSpecial greets existing user with `special` access
func (w *Welcomer) PrivateWelcomeMessageSpecial(update *tgbotapi.Update, playerRaw *dbmapping.Player) {
message := "*Бот Инстинкта приветствует тебя. Снова.*\n\n"
message += "Привет, " + update.Message.From.FirstName + " " + update.Message.From.LastName + "!\n"
message += "\nБудь аккуратен, суперюзер!"
msg := tgbotapi.NewMessage(update.Message.Chat.ID, message)
msg.ParseMode = "Markdown"
c.Bot.Send(msg)
}
// GroupWelcomeMessage welcomes new user on group or bot itself
func (w *Welcomer) GroupWelcomeMessage(update *tgbotapi.Update) string {
newUsers := *update.Message.NewChatMembers

View File

@ -14,5 +14,6 @@ type WelcomerInterface interface {
PrivateWelcomeMessageUnauthorized(update *tgbotapi.Update)
PrivateWelcomeMessageAuthorized(update *tgbotapi.Update, playerRaw *dbmapping.Player)
PrivateWelcomeMessageSpecial(update *tgbotapi.Update, playerRaw *dbmapping.Player)
GroupWelcomeMessage(update *tgbotapi.Update) string
}