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 }