github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/internal/service/schedule/service.go (about)

     1  package schedule
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/sharovik/devbot/internal/config"
     6  	"github.com/sharovik/devbot/internal/database"
     7  	"github.com/sharovik/devbot/internal/dto"
     8  	"github.com/sharovik/devbot/internal/dto/databasedto"
     9  	"github.com/sharovik/devbot/internal/dto/event"
    10  	"github.com/sharovik/devbot/internal/log"
    11  	"github.com/sharovik/devbot/internal/service/message/conversation"
    12  	_time "github.com/sharovik/devbot/internal/service/time"
    13  	"github.com/sharovik/orm/clients"
    14  	cdto "github.com/sharovik/orm/dto"
    15  	"github.com/sharovik/orm/query"
    16  	"strings"
    17  	"time"
    18  )
    19  
    20  // Service schedule service struct
    21  type Service struct {
    22  	Config        config.Config
    23  	DB            clients.BaseClientInterface
    24  	DefinedEvents map[string]event.DefinedEventInterface
    25  }
    26  
    27  const (
    28  	//VariablesDelimiter global variables
    29  	VariablesDelimiter = ";"
    30  )
    31  
    32  var (
    33  	S            Service
    34  	toBeExecuted = map[string][]Item{}
    35  )
    36  
    37  // Item the item struct for schedule object
    38  type Item struct {
    39  	ID int
    40  
    41  	// Author - who triggers the event
    42  	Author string
    43  
    44  	//Channel - target channel, where event will output the response
    45  	Channel string
    46  
    47  	//ScenarioID - id of scenario, which should be triggered
    48  	ScenarioID int64
    49  
    50  	//EventID - id of event. It should be used in combination with scenario id
    51  	EventID int64
    52  
    53  	//ReactionType - the event alias, which will be used during the event execution
    54  	ReactionType string
    55  
    56  	//Variables - the event variables, which will be used during the event execution
    57  	Variables string //; separated
    58  	Scenario  database.EventScenario
    59  
    60  	//ExecuteAt - time of event execution
    61  	ExecuteAt ExecuteAt
    62  	//IsRepeatable if it is set to true, that means we want to repeat it
    63  	IsRepeatable bool
    64  }
    65  
    66  func InitS(cfg config.Config, db clients.BaseClientInterface, definedEvents map[string]event.DefinedEventInterface) {
    67  	S = Service{
    68  		Config:        cfg,
    69  		DB:            db,
    70  		DefinedEvents: definedEvents,
    71  	}
    72  }
    73  
    74  // Run runs the schedule service in goroutine
    75  func (s *Service) Run() (err error) {
    76  	log.Logger().Debug().Msg("Start schedule service")
    77  	go func() {
    78  		lastExecutedStr := ""
    79  		for {
    80  			if lastExecutedStr == time.Now().Format("2006-01-02T15:04") {
    81  				time.Sleep(time.Second)
    82  				continue
    83  			}
    84  
    85  			s.triggerEvents()
    86  			lastExecutedStr = time.Now().Format("2006-01-02T15:04")
    87  		}
    88  	}()
    89  
    90  	log.Logger().Debug().Msg("Finished schedule service")
    91  
    92  	return err
    93  }
    94  
    95  func alreadyExists(item Item) bool {
    96  	//Check if the entry already exists. If so, false will be returned
    97  	for _, scheduledTimeSlot := range toBeExecuted {
    98  		for _, entry := range scheduledTimeSlot {
    99  			if generateItemID(entry) == generateItemID(item) {
   100  				//it's already exists
   101  				return true
   102  			}
   103  		}
   104  	}
   105  
   106  	return false
   107  }
   108  
   109  func (s *Service) triggerEvents() {
   110  	now := _time.Service.Now()
   111  	for _, item := range s.getSchedules() {
   112  		if !item.IsRepeatable && now.After(item.ExecuteAt.getDatetime()) {
   113  			if alreadyExists(item) {
   114  				continue
   115  			}
   116  
   117  			toBeExecuted[now.Format(timeFormat)] = append(toBeExecuted[now.Format(timeFormat)], item)
   118  			continue
   119  		}
   120  
   121  		if alreadyExists(item) {
   122  			continue
   123  		}
   124  
   125  		toBeExecuted[item.ExecuteAt.getDatetime().Format(timeFormat)] = append(toBeExecuted[item.ExecuteAt.getDatetime().Format(timeFormat)], item)
   126  	}
   127  
   128  	tStr := now.Format(timeFormat)
   129  	for _, item := range toBeExecuted[tStr] {
   130  		s.trigger(item)
   131  	}
   132  
   133  	//We clean up the already executed events
   134  	for timeStr := range toBeExecuted {
   135  		targ, err := time.ParseInLocation(timeFormat, timeStr, _time.Service.TimeZone)
   136  		if err != nil {
   137  			log.Logger().AddError(err).Msg("Failed to parse time")
   138  			continue
   139  		}
   140  
   141  		if now.After(targ) {
   142  			delete(toBeExecuted, timeStr)
   143  		}
   144  	}
   145  
   146  	delete(toBeExecuted, now.Format(timeFormat))
   147  }
   148  
   149  func (s *Service) trigger(item Item) {
   150  	log.Logger().Info().Interface("item", item).Msg("Trigger scheduled event")
   151  	scenario := database.EventScenario{
   152  		ID:        item.ScenarioID,
   153  		EventName: item.ReactionType,
   154  		EventID:   item.EventID,
   155  	}
   156  
   157  	for _, variable := range strings.Split(item.Variables, VariablesDelimiter) {
   158  		scenario.RequiredVariables = append(scenario.RequiredVariables, database.ScenarioVariable{
   159  			Value: variable,
   160  		})
   161  	}
   162  
   163  	if conversation.GetConversation(item.Channel).ScenarioID != 0 {
   164  		log.Logger().Debug().
   165  			Str("channel", item.Channel).
   166  			Interface("item", item).
   167  			Msg("There is open conversation for selected channel. Skipping.")
   168  		return
   169  	}
   170  
   171  	conversation.AddConversation(scenario, dto.BaseChatMessage{
   172  		Channel: item.Channel,
   173  		AsUser:  true,
   174  		Ts:      _time.Service.Now(),
   175  		DictionaryMessage: dto.DictionaryMessage{
   176  			ScenarioID:   item.ScenarioID,
   177  			EventID:      item.EventID,
   178  			ReactionType: item.ReactionType,
   179  		},
   180  		OriginalMessage: dto.BaseOriginalMessage{},
   181  	})
   182  
   183  	go func() {
   184  		if s.DefinedEvents[item.ReactionType] == nil {
   185  			log.Logger().Error().
   186  				Str("reaction_type", item.ReactionType).
   187  				Msg("Reaction type not exists")
   188  
   189  			return
   190  		}
   191  
   192  		if _, err := s.DefinedEvents[item.ReactionType].Execute(conversation.GetConversation(item.Channel).LastQuestion); err != nil {
   193  			log.Logger().AddError(err).Msg("Failed to execute event")
   194  		}
   195  
   196  		conversation.FinaliseConversation(item.Channel)
   197  	}()
   198  
   199  	if !item.IsRepeatable {
   200  		q := new(clients.Query).Delete().From(databasedto.SchedulesModel).Where(query.Where{
   201  			First:    "id",
   202  			Operator: "=",
   203  			Second: query.Bind{
   204  				Field: "id",
   205  				Value: item.ID,
   206  			},
   207  		})
   208  		if _, err := s.DB.Execute(q); err != nil {
   209  			log.Logger().
   210  				AddError(err).
   211  				Int("item_id", item.ID).
   212  				Msg("Failed to delete scheduled item from the database")
   213  		}
   214  	}
   215  
   216  	log.Logger().Info().Interface("item", item).Msg("Scheduled event has been executed")
   217  }
   218  
   219  func (s *Service) Schedule(item Item) (err error) {
   220  	model := databasedto.SchedulesModel
   221  	model.AddModelField(cdto.ModelField{
   222  		Name:  "author",
   223  		Value: item.Author,
   224  	})
   225  	model.AddModelField(cdto.ModelField{
   226  		Name:  "channel",
   227  		Value: item.Channel,
   228  	})
   229  	model.AddModelField(cdto.ModelField{
   230  		Name:  "scenario_id",
   231  		Value: item.ScenarioID,
   232  	})
   233  	model.AddModelField(cdto.ModelField{
   234  		Name:  "event_id",
   235  		Value: item.ScenarioID,
   236  	})
   237  	model.AddModelField(cdto.ModelField{
   238  		Name:  "reaction_type",
   239  		Value: item.ReactionType,
   240  	})
   241  	model.AddModelField(cdto.ModelField{
   242  		Name:  "is_repeatable",
   243  		Value: item.IsRepeatable,
   244  	})
   245  	model.AddModelField(cdto.ModelField{
   246  		Name:  "execute_at",
   247  		Value: item.ExecuteAt.toString(),
   248  	})
   249  	model.AddModelField(cdto.ModelField{
   250  		Name:  "variables",
   251  		Value: item.Variables,
   252  	})
   253  	q := new(clients.Query).
   254  		Insert(model)
   255  
   256  	_, err = s.DB.Execute(q)
   257  	if err != nil {
   258  		log.Logger().AddError(err).Msg("Failed to insert data into database")
   259  		return err
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (s *Service) getSchedules() (items []Item) {
   266  	q := new(clients.Query).
   267  		Select(databasedto.SchedulesModel.GetColumns()).
   268  		From(databasedto.SchedulesModel)
   269  
   270  	result, err := s.DB.Execute(q)
   271  	if err != nil {
   272  		log.Logger().AddError(err).Msg("Failed to retrieve schedule list")
   273  		return nil
   274  	}
   275  
   276  	for _, item := range result.Items() {
   277  		executeAt, err := new(ExecuteAt).FromString(item.GetField("execute_at").Value.(string))
   278  		if err != nil {
   279  			log.Logger().AddError(err).Msg("Failed to parse execute_at")
   280  			return nil
   281  		}
   282  
   283  		isRepeatable := false
   284  		if item.GetField("is_repeatable").Value.(int) == 1 {
   285  			isRepeatable = true
   286  			executeAt.IsRepeatable = isRepeatable
   287  		}
   288  
   289  		items = append(items, Item{
   290  			ID:           item.GetField("id").Value.(int),
   291  			Author:       item.GetField("author").Value.(string),
   292  			Channel:      item.GetField("channel").Value.(string),
   293  			ScenarioID:   int64(item.GetField("scenario_id").Value.(int)),
   294  			EventID:      int64(item.GetField("event_id").Value.(int)),
   295  			ReactionType: item.GetField("reaction_type").Value.(string),
   296  			Scenario:     database.EventScenario{},
   297  			ExecuteAt:    executeAt,
   298  			IsRepeatable: isRepeatable,
   299  			Variables:    item.GetField("variables").Value.(string),
   300  		})
   301  	}
   302  
   303  	return items
   304  }
   305  
   306  func generateItemID(item Item) string {
   307  	return fmt.Sprintf("%d-%s-%s", item.ID, item.Channel, item.ReactionType)
   308  }