github.com/hungdoo/bot@v0.0.0-20240325145135-dd1f386f7b81/src/services/telecommands/service.go (about)

     1  package telecommands
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
    10  	"github.com/hungdoo/bot/src/common"
    11  	"github.com/hungdoo/bot/src/packages/cmdparser"
    12  	command "github.com/hungdoo/bot/src/packages/command/common"
    13  	"github.com/hungdoo/bot/src/packages/db"
    14  	"github.com/hungdoo/bot/src/packages/interfaces"
    15  	"github.com/hungdoo/bot/src/packages/log"
    16  	"github.com/hungdoo/bot/src/packages/telegram"
    17  	"github.com/hungdoo/bot/src/packages/utils"
    18  	lock "github.com/square/mongo-lock"
    19  	"github.com/urfave/cli"
    20  )
    21  
    22  // @dev external facing controller
    23  // Responsibility: listen to command from tele, executes & reports
    24  type CommandService struct {
    25  	interfaces.IService
    26  	Factory CommandFactory
    27  	Parser  *cli.App
    28  	LockCli *lock.Client
    29  }
    30  
    31  func (s *CommandService) RegisterCommands() {
    32  	s.Parser.Commands = []cli.Command{
    33  		{
    34  			Name:    "add",
    35  			Aliases: []string{"a"},
    36  			Usage:   "add a task to the list",
    37  			Subcommands: []cli.Command{
    38  				{
    39  					Name:      "call",
    40  					UsageText: "name, rpc, contract address, method (axy(address)(uint256), params(pr1;pr2), value index, margin(1%), usdcPrice;precision",
    41  					Action: func(c *cli.Context) error {
    42  						fmt.Fprintln(s.Parser.Writer, s.Factory.Add(command.ContractCall, c.Args()))
    43  						return nil
    44  					},
    45  				},
    46  				{
    47  					Name:      "tomb",
    48  					UsageText: "name, rpc, contract, up, pkIdx, k",
    49  					Action: func(c *cli.Context) error {
    50  						fmt.Fprintln(s.Parser.Writer, s.Factory.Add(command.Tomb, c.Args()))
    51  						return nil
    52  					},
    53  				},
    54  				{
    55  					Name:      "balance",
    56  					UsageText: "name, rpc, ...wallets",
    57  					Action: func(c *cli.Context) error {
    58  						fmt.Fprintln(s.Parser.Writer, s.Factory.Add(command.Balance, c.Args()))
    59  						return nil
    60  					},
    61  				},
    62  				{
    63  					Name:      "bybit",
    64  					UsageText: "name",
    65  					Action: func(c *cli.Context) error {
    66  						fmt.Fprintln(s.Parser.Writer, s.Factory.Add(command.BybitIdo, c.Args()))
    67  						return nil
    68  					},
    69  				},
    70  			},
    71  		},
    72  		{
    73  			Name:    "remove",
    74  			Aliases: []string{"rm"},
    75  			Flags: []cli.Flag{
    76  				cli.StringFlag{Name: "task,t", Required: true},
    77  			},
    78  			Usage: "rm a task",
    79  			Action: func(c *cli.Context) error {
    80  				task := c.String("task")
    81  				fmt.Fprintln(s.Parser.Writer, s.Factory.Remove(task))
    82  				return nil
    83  			},
    84  		},
    85  		{
    86  			Name:    "show",
    87  			Aliases: []string{"s"},
    88  			Flags: []cli.Flag{
    89  				cli.StringFlag{Name: "task,t", Required: true},
    90  			},
    91  			Usage: "show a task details",
    92  			Action: func(c *cli.Context) error {
    93  				task := c.String("task")
    94  				fmt.Fprintln(s.Parser.Writer, s.Factory.Show(task))
    95  				return nil
    96  			},
    97  		},
    98  		{
    99  			Name:    "execute",
   100  			Aliases: []string{"exec"},
   101  			Usage:   "exec a task",
   102  			Subcommands: []cli.Command{
   103  				{
   104  					Name: "call",
   105  					Flags: []cli.Flag{
   106  						cli.StringFlag{Name: "task,t", Required: true},
   107  					},
   108  					Action: func(c *cli.Context) error {
   109  						task := c.String("task")
   110  						res, err := s.Factory.Exec(command.ContractCall, task, "")
   111  						if err != nil {
   112  							fmt.Fprintln(s.Parser.Writer, err.Error())
   113  							return err
   114  						}
   115  						fmt.Fprintln(s.Parser.Writer, res)
   116  						return nil
   117  					},
   118  				},
   119  				{
   120  					Name: "balance",
   121  					Flags: []cli.Flag{
   122  						cli.StringFlag{Name: "task,t", Required: true},
   123  					},
   124  					Action: func(c *cli.Context) error {
   125  						task := c.String("task")
   126  						res, err := s.Factory.Exec(command.Balance, task, "")
   127  						if err != nil {
   128  							fmt.Fprintln(s.Parser.Writer, err.Error())
   129  							return err
   130  						}
   131  						fmt.Fprintln(s.Parser.Writer, res)
   132  						return nil
   133  					},
   134  				},
   135  				{
   136  					Name: "tomb",
   137  					Flags: []cli.Flag{
   138  						cli.StringFlag{Name: "task,t", Required: true},
   139  						cli.StringFlag{Name: "subcommand,sc", Usage: "stats, clear, claim, cronjob, default", Value: "stats"},
   140  					},
   141  					Action: func(c *cli.Context) error {
   142  						task := c.String("task")
   143  						subcommand := c.String("subcommand")
   144  						res, err := s.Factory.Exec(command.Tomb, task, subcommand)
   145  						if err != nil {
   146  							fmt.Fprintln(s.Parser.Writer, err.Error())
   147  							return err
   148  						}
   149  						fmt.Fprintln(s.Parser.Writer, res)
   150  						return nil
   151  					},
   152  				},
   153  				{
   154  					Name: "bybit",
   155  					Flags: []cli.Flag{
   156  						cli.StringFlag{Name: "task,t", Required: true},
   157  						cli.StringFlag{Name: "subcommand,sc", Usage: "latest, all, default", Value: "latest"},
   158  					},
   159  					Action: func(c *cli.Context) error {
   160  						task := c.String("task")
   161  						subcommand := c.String("subcommand")
   162  						res, err := s.Factory.Exec(command.BybitIdo, task, subcommand)
   163  						if err != nil {
   164  							fmt.Fprintln(s.Parser.Writer, err.Error())
   165  							return err
   166  						}
   167  						fmt.Fprintln(s.Parser.Writer, res)
   168  						return nil
   169  					},
   170  				},
   171  			},
   172  		},
   173  		{
   174  			Name:    "list",
   175  			Aliases: []string{"ls"},
   176  			Flags: []cli.Flag{
   177  				cli.BoolFlag{Name: "all,a", Usage: "show all"},
   178  			},
   179  			Usage: "list tasks",
   180  			Action: func(c *cli.Context) error {
   181  				showAll := c.Bool("all")
   182  				fmt.Fprintln(s.Parser.Writer, s.Factory.List(showAll))
   183  				return nil
   184  			},
   185  		},
   186  		{
   187  			Name:    "on",
   188  			Aliases: []string{"on"},
   189  			Usage:   "enable a task",
   190  			Flags: []cli.Flag{
   191  				cli.StringFlag{Name: "task,t", Required: true},
   192  			},
   193  			Action: func(c *cli.Context) error {
   194  				task := c.String("task")
   195  				fmt.Fprintln(s.Parser.Writer, s.Factory.On(task))
   196  				return nil
   197  			},
   198  		},
   199  		{
   200  			Name:    "off",
   201  			Aliases: []string{"off"},
   202  			Usage:   "disable a task",
   203  			Flags: []cli.Flag{
   204  				cli.StringFlag{Name: "task,t", Required: true},
   205  			},
   206  			Action: func(c *cli.Context) error {
   207  				task := c.String("task")
   208  				fmt.Fprintln(s.Parser.Writer, s.Factory.Off(task))
   209  				return nil
   210  			},
   211  		},
   212  		{
   213  			Name:    "setInterval",
   214  			Aliases: []string{"si"},
   215  			Usage:   "set new run interval for task in s",
   216  			Flags: []cli.Flag{
   217  				cli.StringFlag{Name: "task,t", Required: true},
   218  				cli.StringFlag{Name: "duration, d", Required: true, Usage: "30s, 1m, 2h"},
   219  			},
   220  			Action: func(c *cli.Context) error {
   221  				task := c.String("task")
   222  				duration, err := utils.ParseCustomDuration(c.String("duration"))
   223  				if err != nil {
   224  					fmt.Fprintln(s.Parser.Writer, err.Error())
   225  					return err
   226  				}
   227  				fmt.Fprintln(s.Parser.Writer, s.Factory.SetInterval(task, duration))
   228  				return nil
   229  			},
   230  		},
   231  	}
   232  }
   233  
   234  func (c *CommandService) process(message string) string {
   235  	// Refresh jobs
   236  	_, err := c.Factory.GetJobs()
   237  	if err != nil {
   238  		return fmt.Sprintf("GetJobs err: [%s]", err)
   239  	}
   240  
   241  	messages := strings.Split(strings.TrimSpace(message), " ")
   242  	if err = c.Parser.Run(append([]string{"tele"}, messages...)); err != nil {
   243  		return fmt.Sprintf("Parser run err: %s", err)
   244  	}
   245  	return cmdparser.GetOutput()
   246  }
   247  
   248  // Work all commands in intervals
   249  func (c *CommandService) Work() {
   250  	for {
   251  		jobs, err := c.Factory.GetJobs()
   252  		if err != nil {
   253  			log.GeneralLogger.Printf("GetJobs err: [%s]", err)
   254  		}
   255  		var results []string
   256  		for _, j := range jobs {
   257  			// Create an exclusive lock on a resource.
   258  			lockId := "mutexLock" + j.GetName()
   259  			err = c.LockCli.XLock(context.Background(), j.GetName(), lockId, lock.LockDetails{TTL: 60 * 5}) // lock expires in 5min
   260  			if err != nil {
   261  				log.GeneralLogger.Printf("Job [%s] XLock err: [%s]", j.GetName(), err.Error())
   262  				j.SetError(err.Error())
   263  				continue
   264  			}
   265  
   266  			if !j.IsIdle() {
   267  				result, execErr := j.Execute(false, "")
   268  				log.GeneralLogger.Printf("[%s] execution: [%s]", j.GetName(), result)
   269  				j.SetExecutedTime(time.Now())
   270  				if execErr != nil && execErr.Level >= common.Error {
   271  					log.GeneralLogger.Printf("Job [%s] exec failed: [%s]", j.GetName(), execErr.Error())
   272  					if execErr.Level >= common.Critical { // should report asap
   273  						off := c.Factory.Off(j.GetName())
   274  						results = append(results, fmt.Sprintf("%v failed with CRIT[%s]: off[%s]", j.GetName(), execErr.Error(), off))
   275  					} else {
   276  						j.SetError(execErr.Error()) // log for ls cmd
   277  						// results = append(results, fmt.Sprintf("%v failed with [%s:%s]", j.GetName(), execErr.Level, execErr.Error()))
   278  					}
   279  				}
   280  
   281  				// record result & info error for logging with tele.List cmd, no realtime report
   282  				if result != "" {
   283  					j.SetDisplayMsg(result)
   284  					results = append(results, fmt.Sprintf("[%s]\n%s", j.GetName(), result))
   285  				}
   286  				// exec seccessfully -> update db
   287  				UpdateCmd(j)
   288  			}
   289  
   290  			// Unlock the resource
   291  			_, err = c.LockCli.Unlock(context.Background(), lockId)
   292  			if err != nil {
   293  				log.GeneralLogger.Printf("XLock Unlock err: [%s]", err)
   294  				results = append(results, fmt.Sprintf("%v XLock Unlock failed with [%s]", j.GetName(), err))
   295  			}
   296  			log.GeneralLogger.Printf("[%s] XLock Release successfully", j.GetName())
   297  		}
   298  
   299  		if len(results) != 0 {
   300  			reportChatId, err := telegram.GetReportChatId()
   301  			if err != nil {
   302  				log.GeneralLogger.Printf("[work] get report id failed: [%s]", err)
   303  			}
   304  			msg := tgbotapi.NewMessage(reportChatId, strings.Join(results, "\n"))
   305  			msg.ParseMode = ""
   306  			telegram.GetBot().Send(msg)
   307  		}
   308  		time.Sleep(time.Second * 30)
   309  	}
   310  }
   311  
   312  func (c *CommandService) ListenToCommand() error {
   313  	telegramUpdateChan, err := telegram.GetUpdates()
   314  	if err != nil {
   315  		return fmt.Errorf("%v", err.Error())
   316  	}
   317  	for update := range telegramUpdateChan {
   318  		if update.Message == nil {
   319  			continue
   320  		}
   321  
   322  		fromUser := update.Message.From.UserName
   323  
   324  		if !telegram.IsWhitelisted(fromUser) {
   325  			telegram.ReportInvalidAccess(fromUser)
   326  			continue
   327  		}
   328  		reportChatId, err := telegram.GetReportChatId()
   329  		if err != nil {
   330  			log.ErrorLogger.Print(err)
   331  		}
   332  		log.GeneralLogger.Printf("[%s:%d] %s", fromUser, reportChatId, update.Message.Text)
   333  		msg := tgbotapi.NewMessage(reportChatId, c.process(update.Message.Text))
   334  		msg.ReplyToMessageID = update.Message.MessageID
   335  		msg.ParseMode = ""
   336  
   337  		telegram.GetBot().Send(msg)
   338  	}
   339  	return nil
   340  }
   341  
   342  func NewService() *CommandService {
   343  	parser := cmdparser.GetParser()
   344  	// keys := bson.M{
   345  	// 	"_id": 1, // index in ascending order; -1 for descending order
   346  	// }
   347  	// index := mongo.IndexModel{
   348  	// 	Keys:    keys,
   349  	// 	Options: options.Index().SetUnique(true),
   350  	// }
   351  	// _, err := db.GetDb().GetCollection("commands").Indexes().CreateOne(context.Background(), index)
   352  	// if err != nil {
   353  	// 	log.ErrorLogger.Fatal(err)
   354  	// }
   355  
   356  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
   357  	defer cancel()
   358  
   359  	collection := db.GetDb().GetCollection("commands_lock")
   360  
   361  	// Create a MongoDB lock client.
   362  	cli := lock.NewClient(collection)
   363  
   364  	// Create the required and recommended indexes.
   365  	cli.CreateIndexes(ctx)
   366  
   367  	return &CommandService{Factory: NewCommandFactory(), Parser: parser, LockCli: cli}
   368  }