github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/events/scheduleevent/event.go (about) 1 package scheduleevent 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/sharovik/devbot/internal/container" 8 "github.com/sharovik/devbot/internal/database" 9 "github.com/sharovik/devbot/internal/dto" 10 "github.com/sharovik/devbot/internal/helper" 11 "github.com/sharovik/devbot/internal/log" 12 "github.com/sharovik/devbot/internal/service" 13 "github.com/sharovik/devbot/internal/service/message" 14 "github.com/sharovik/devbot/internal/service/message/conversation" 15 "github.com/sharovik/devbot/internal/service/schedule" 16 ) 17 18 const ( 19 //EventName the name of the event 20 EventName = "scheduleevent" 21 22 //EventVersion the version of the event 23 EventVersion = "1.0.0" 24 25 supportedTimeFormats = "`YYYY-mm-dd HH:ii`; `DD days`; `HH hours`; `ii minutes`" 26 27 helpMessage = "Ask me `schedule event {event_name} {time_string}` and I will schedule the event. I may ask you the requested event questions. \nThe use-case scenario: you triggered the event, and you want to repeat it, but in a few hours. In that case, ask me: `schedule event {event_name} in 2 hours`. The event will be executed in 2 hours from current time" + 28 "\nSupported time formats: " + supportedTimeFormats + ".\nMake sure target event is configured OR does have the scenario questions." 29 30 questionTime = "When we need to trigger this event? Supported formats: " + supportedTimeFormats 31 questionEventAlias = "Which event I need to execute?\nPlease, provide the event alias(use events list command to get it)." 32 ) 33 34 // EventStruct the struct for the event object. It will be used for initialisation of the event in defined-events.go file. 35 type EventStruct struct { 36 } 37 38 type requestedScenario struct { 39 Scenario database.EventScenario 40 ExecuteAt schedule.ExecuteAt 41 } 42 43 // Event - object which is ready to use 44 var ( 45 Event = EventStruct{} 46 requestedScenarios = map[string]requestedScenario{} 47 ) 48 49 // Help retrieves the help message 50 func (e EventStruct) Help() string { 51 return helpMessage 52 } 53 54 // Alias retrieves the event alias 55 func (e EventStruct) Alias() string { 56 return EventName 57 } 58 59 // Execute method which is called by message processor 60 func (e EventStruct) Execute(message dto.BaseChatMessage) (dto.BaseChatMessage, error) { 61 62 //We schedule the scenario 63 if requestedScenarios[message.Channel].Scenario.ID != 0 { 64 rScenario := requestedScenarios[message.Channel] 65 executeAt := rScenario.ExecuteAt 66 if executeAt.IsEmpty() { 67 message.Text = "Failed to schedule scenario" 68 message.Text += "\nReason: No time specified. Don't know when to trigger." 69 70 return message, nil 71 } 72 73 delete(requestedScenarios, message.Channel) 74 75 if err := scheduleRequestedScenario(rScenario, message, executeAt); err != nil { 76 message.Text = "Failed to schedule scenario" 77 message.Text += fmt.Sprintf("\nReason: %s", err.Error()) 78 79 return message, err 80 } 81 82 message.Text = fmt.Sprintf("Scenario `%s` scheduled.", rScenario.Scenario.EventName) 83 84 return message, nil 85 } 86 87 if !isAllVariablesDefined(message) { 88 scenarioID, err := getMainScenarioID() 89 if err != nil { 90 message.Text = "Failed to trigger the main questions for the schedule scenario" 91 return message, err 92 } 93 94 if err = askScenarioQuestions(scenarioID, message.Channel); err != nil { 95 message.Text = "Failed to trigger the main questions for the schedule scenario" 96 return message, err 97 } 98 99 message.Text = "" 100 101 return message, nil 102 } 103 104 eventType := getReactionType(message) 105 scheduleTime := getScheduleTime(message) 106 //if event type is defined, we ask questions from that event to collect the answers and use them during schedule. 107 if eventType != "" { 108 rScenario, forceSchedule, err := askEventQuestions(eventType, message.Channel, scheduleTime) 109 if err != nil { 110 message.Text = "I cannot ask you the questions from that event. Please, try again." 111 return message, err 112 } 113 114 if forceSchedule { 115 if err = scheduleRequestedScenario(rScenario, message, scheduleTime); err != nil { 116 message.Text = "Failed to schedule scenario" 117 message.Text += fmt.Sprintf("\nReason: %s", err.Error()) 118 119 return message, err 120 } 121 122 message.Text = fmt.Sprintf("Scenario `%s` scheduled.", rScenario.Scenario.EventName) 123 124 return message, nil 125 } 126 } 127 128 message.Text = "" 129 130 return message, nil 131 } 132 133 func isAllVariablesDefined(message dto.BaseChatMessage) bool { 134 reactionType := getReactionType(message) 135 scheduleTime := getScheduleTime(message) 136 if reactionType == "" || scheduleTime.IsEmpty() { 137 return false 138 } 139 140 return true 141 } 142 143 func scheduleRequestedScenario(rScenario requestedScenario, message dto.BaseChatMessage, scheduleTime schedule.ExecuteAt) error { 144 var variables []string 145 146 for _, value := range conversation.GetConversation(message.Channel).Scenario.RequiredVariables { 147 variables = append(variables, value.Value) 148 } 149 150 item := schedule.Item{ 151 Author: message.OriginalMessage.User, 152 Channel: message.Channel, 153 ScenarioID: rScenario.Scenario.ID, 154 EventID: rScenario.Scenario.EventID, 155 ReactionType: rScenario.Scenario.EventName, 156 Variables: strings.Join(variables, schedule.VariablesDelimiter), 157 Scenario: rScenario.Scenario, 158 ExecuteAt: scheduleTime, 159 IsRepeatable: rScenario.ExecuteAt.IsRepeatable, 160 } 161 162 return schedule.S.Schedule(item) 163 } 164 165 func askEventQuestions(eventType string, channel string, scheduleTime schedule.ExecuteAt) (rScenario requestedScenario, forceSchedule bool, err error) { 166 eventID, err := container.C.Dictionary.FindEventByAlias(eventType) 167 if err != nil { 168 return 169 } 170 171 if eventID == 0 { 172 err = fmt.Errorf("failed to find the event `%s` ", eventType) 173 return 174 } 175 176 //We prepare the scenario, with our event name, to make sure we execute the right at the end. 177 //By doing this, we are rewriting the scenario event alias to make sure, after all questions were asked, we point to our schedule event back 178 scenario, err := service.PrepareEventScenario(eventID, EventName) 179 if err != nil { 180 return 181 } 182 183 if len(scenario.RequiredVariables) > 0 { 184 if err = message.TriggerScenario(channel, scenario, false); err != nil { 185 return 186 } 187 188 //We change back the event type of scenario to the original one, to make sure we schedule the right event 189 scenario.EventName = eventType 190 191 requestedScenarios[channel] = requestedScenario{ 192 Scenario: scenario, 193 ExecuteAt: scheduleTime, 194 } 195 196 return 197 } 198 199 //We change back the event type of scenario to the original one, to make sure we schedule the right event 200 scenario.EventName = eventType 201 202 //We schedule this event right away, because it does not have any questions/required variables to ask 203 return requestedScenario{ 204 Scenario: scenario, 205 ExecuteAt: scheduleTime, 206 }, true, nil 207 } 208 209 func askScenarioQuestions(scenarioID int64, channel string) error { 210 //We prepare the scenario, with our event name, to make sure we execute the right at the end 211 scenario, err := service.PrepareScenario(scenarioID, EventName) 212 if err != nil { 213 return err 214 } 215 216 if err = message.TriggerScenario(channel, scenario, false); err != nil { 217 return err 218 } 219 220 return nil 221 } 222 223 func getReactionType(message dto.BaseChatMessage) (eventType string) { 224 conv := conversation.GetConversation(message.Channel) 225 226 //If we already have opened conversation, we will try to get the answer from the required variables 227 if conv.Scenario.ID != int64(0) { 228 for _, variable := range conv.Scenario.RequiredVariables { 229 if questionEventAlias == variable.Question { 230 return variable.Value 231 } 232 } 233 } 234 235 res := helper.FindMatches("(?im)event ([a-zA-Z_]+)", message.OriginalMessage.Text) 236 237 return res["1"] 238 } 239 240 func getScheduleTime(message dto.BaseChatMessage) schedule.ExecuteAt { 241 conv := conversation.GetConversation(message.Channel) 242 243 text := message.OriginalMessage.Text 244 //If we already have opened conversation, we will try to get the answer from the required variables 245 if conv.Scenario.ID != int64(0) { 246 for _, variable := range conv.Scenario.RequiredVariables { 247 if questionTime == variable.Question { 248 text = variable.Value 249 break 250 } 251 } 252 } 253 254 r, err := new(schedule.ExecuteAt).FromString(text) 255 if err != nil { 256 log.Logger().AddError(err).Msg("Failed to parse time string") 257 return schedule.ExecuteAt{} 258 } 259 260 return r 261 } 262 263 // Install method for installation of event 264 func (e EventStruct) Install() error { 265 log.Logger().Debug(). 266 Str("event_name", EventName). 267 Str("event_version", EventVersion). 268 Msg("Triggered event installation") 269 270 if err := container.C.Dictionary.InstallNewEventScenario(database.EventScenario{ 271 EventName: EventName, 272 EventVersion: EventVersion, 273 Questions: []database.Question{ 274 { 275 Question: "schedule event", 276 Answer: "One sec", 277 QuestionRegex: "(?i)(schedule event)", 278 QuestionGroup: "", 279 }, 280 }, 281 }); err != nil { 282 return err 283 } 284 285 return container.C.Dictionary.InstallNewEventScenario(database.EventScenario{ 286 EventName: EventName, 287 ScenarioName: "schedule_event_scenario", 288 EventVersion: EventVersion, 289 Questions: []database.Question{ 290 { 291 Question: "schedule_event_scenario", 292 Answer: "Ok", 293 QuestionRegex: "(?i)(schedule_event_scenario)", 294 QuestionGroup: "", 295 }, 296 }, 297 RequiredVariables: []database.ScenarioVariable{ 298 { 299 Name: "time", 300 Value: "", 301 Question: questionTime, 302 }, 303 { 304 Name: "event", 305 Value: "", 306 Question: questionEventAlias, 307 }, 308 }, 309 }) 310 } 311 312 // Update for event update actions 313 func (e EventStruct) Update() error { 314 return nil 315 } 316 317 func getMainScenarioID() (int64, error) { 318 res, err := container.C.Dictionary.FindAnswer("schedule_event_scenario") 319 if err != nil { 320 return 0, err 321 } 322 323 return res.ScenarioID, nil 324 }