github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/events/unknownquestion/event.go (about) 1 package unknownquestion 2 3 import ( 4 "database/sql" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/sharovik/devbot/internal/database" 10 "github.com/sharovik/devbot/internal/service" 11 "github.com/sharovik/devbot/internal/service/message" 12 "github.com/sharovik/devbot/internal/service/message/conversation" 13 "github.com/sharovik/orm/clients" 14 cdto "github.com/sharovik/orm/dto" 15 cquery "github.com/sharovik/orm/query" 16 17 "github.com/sharovik/devbot/internal/log" 18 19 "github.com/sharovik/devbot/internal/container" 20 "github.com/sharovik/devbot/internal/dto" 21 ) 22 23 const ( 24 //EventName the name of the event 25 EventName = "unknownquestion" 26 27 //EventVersion the version of the event 28 EventVersion = "1.0.0" 29 30 helpMessage = "Ask me `similar questions QUESTION_STRING` and I will try to find the similar events and questions in my memory." 31 32 scenarioShouldITriggerThis = `Should I trigger this event?` 33 ) 34 35 // EventStruct the struct for the event object. It will be used for initialisation of the event in defined-events.go file. 36 type EventStruct struct { 37 } 38 39 var ( 40 // Event - object which is ready to use 41 Event = EventStruct{} 42 selectedConversations = map[string]string{} 43 ) 44 45 // Help retrieves the help message 46 func (e EventStruct) Help() string { 47 return helpMessage 48 } 49 50 // Alias retrieves the event alias 51 func (e EventStruct) Alias() string { 52 return EventName 53 } 54 55 // Execute method which is called by message processor 56 func (e EventStruct) Execute(message dto.BaseChatMessage) (dto.BaseChatMessage, error) { 57 conv := conversation.GetConversation(message.Channel) 58 if len(conv.Scenario.RequiredVariables) > 0 && selectedConversations[message.Channel] != "" { 59 selected := selectedConversations[message.Channel] 60 delete(selectedConversations, message.Channel) 61 62 if getAnswer(message) { 63 return triggerSelectedScenarioQuestion(message, selected) 64 } 65 66 message.Text = "I've got a negative answer from you. I will not trigger this scenario. Perhaps, you may use `events list` command, to see all events." 67 68 return message, nil 69 } 70 71 textStr, err := extractRequestString(message.OriginalMessage.Text) 72 if err != nil { 73 message.Text = "Failed to parse the answer" 74 75 return message, nil 76 } 77 78 words := strings.Split(textStr, " ") 79 80 var foundResults, cleanItems = findResults(words) 81 82 switch len(foundResults) { 83 case 0: 84 message.Text = "Unfortunately, I don't understand what you mean. Please, have a look on current available `events list`.\nJust type `events list` and I will show you everything what I can." 85 return message, nil 86 case 1: 87 return triggerPotentialScenarioQuestion(message, cleanItems[0]) 88 } 89 90 message.Text = "Maybe you mean:\n" 91 for _, str := range foundResults { 92 message.Text += str 93 } 94 95 //This answer will be show once the event get triggered. 96 //Leave message.Text empty, once you need to not show the message, once this event get triggered. 97 message.Text += "\n\nOR you also can write `events list` and I will show you all available events." 98 return message, nil 99 } 100 101 func getAnswer(message dto.BaseChatMessage) (result bool) { 102 conv := conversation.GetConversation(message.Channel) 103 104 //If we already have opened conversation, we will try to get the answer from the required variables 105 if conv.Scenario.ID != int64(0) { 106 for _, variable := range conv.Scenario.RequiredVariables { 107 if variable.Value != "" { 108 answer := strings.ToLower(variable.Value) 109 switch answer { 110 case "yes": 111 return true 112 default: 113 return false 114 } 115 } 116 } 117 } 118 119 return false 120 } 121 122 func triggerSelectedScenarioQuestion(msg dto.BaseChatMessage, eventAlias string) (dto.BaseChatMessage, error) { 123 eventID, err := container.C.Dictionary.FindEventByAlias(eventAlias) 124 if err != nil { 125 msg.Text = "Failed to prepare event for triggering. Try again later. Sorry." 126 127 return msg, err 128 } 129 130 //We prepare the scenario, with our event name, to make sure we execute the right at the end 131 scenario, err := service.PrepareEventScenario(eventID, eventAlias) 132 if err != nil { 133 msg.Text = "Failed to prepare event-scenario for triggering. Try again later. Sorry." 134 135 return msg, err 136 } 137 138 if len(scenario.RequiredVariables) == 0 || len(scenario.Questions) == 0 { 139 scenario.ID = 0 140 } 141 142 if err = message.TriggerScenario(msg.Channel, scenario, false); err != nil { 143 msg.Text = "Failed to trigger selected scenario. Try again later. Sorry." 144 145 return msg, err 146 } 147 148 msg.Text = "" 149 150 return msg, nil 151 } 152 153 func triggerPotentialScenarioQuestion(msg dto.BaseChatMessage, item cdto.ModelInterface) (dto.BaseChatMessage, error) { 154 scenarioID, err := getDoubleCheckEventScenarioID() 155 if err != nil { 156 msg.Text = "Failed to trigger the main questions for the schedule scenario" 157 return msg, err 158 } 159 160 //We prepare the scenario, with our event name, to make sure we execute the right at the end 161 scenario, err := service.PrepareScenario(scenarioID, EventName) 162 if err != nil { 163 msg.Text = "Failed to get the scenario" 164 165 return msg, err 166 } 167 168 scenario.RequiredVariables = []database.ScenarioVariable{ 169 { 170 Question: fmt.Sprintf("Do you want me to trigger the `%s` event (later you can ask `%s --help` for more details)?\nPlease, answer yes or no", item.GetField("alias").Value.(string), item.GetField("question").Value.(string)), 171 }, 172 } 173 174 selectedConversations[msg.Channel] = item.GetField("alias").Value.(string) 175 176 if err = message.TriggerScenario(msg.Channel, scenario, false); err != nil { 177 msg.Text = "Failed to ask scenario questions" 178 179 return msg, err 180 } 181 182 msg.Text = "" 183 184 return msg, nil 185 } 186 187 func getDoubleCheckEventScenarioID() (int64, error) { 188 res, err := container.C.Dictionary.FindAnswer("potential_event") 189 if err != nil { 190 return 0, err 191 } 192 193 return res.ScenarioID, nil 194 } 195 196 func implodeDatabaseResults(items []cdto.ModelInterface) string { 197 var result string 198 for _, item := range items { 199 result += fmt.Sprintf("`%s` to trigger event `%s`? Then ask `%s --help` for more details;\n", item.GetField("question").Value.(string), item.GetField("alias").Value.(string), item.GetField("question").Value.(string)) 200 } 201 202 return result 203 } 204 205 func findResults(words []string) (foundResults []string, cleanItems []cdto.ModelInterface) { 206 var processedEvents = map[string]string{} 207 208 for _, word := range words { 209 if word == "" { 210 continue 211 } 212 213 wordRes, err := findPotentialEvents(word) 214 if err != nil { 215 log.Logger().AddError(err). 216 Str("word", word). 217 Msg("Failed to find potential event for word") 218 continue 219 } 220 221 //We remove duplicates 222 for _, item := range wordRes { 223 if processedEvents[item.GetField("alias").Value.(string)] != "" { 224 continue 225 } 226 227 cleanItems = append(cleanItems, item) 228 processedEvents[item.GetField("alias").Value.(string)] = item.GetField("alias").Value.(string) 229 } 230 231 if len(wordRes) > 0 { 232 foundResults = append(foundResults, implodeDatabaseResults(cleanItems)) 233 } 234 } 235 236 return 237 } 238 239 // Install method for installation of event 240 func (e EventStruct) Install() error { 241 log.Logger().Debug(). 242 Str("event_name", EventName). 243 Str("event_version", EventVersion). 244 Msg("Triggered event installation") 245 246 if err := container.C.Dictionary.InstallNewEventScenario(database.EventScenario{ 247 EventName: EventName, 248 EventVersion: EventVersion, 249 Questions: []database.Question{ 250 { 251 Question: "similar questions", 252 Answer: "", 253 QuestionRegex: "(?im)similar questions", 254 QuestionGroup: "", 255 }, 256 }, 257 }); err != nil { 258 return err 259 } 260 261 return container.C.Dictionary.InstallNewEventScenario(database.EventScenario{ 262 EventName: EventName, 263 ScenarioName: "potential_event", 264 EventVersion: EventVersion, 265 Questions: []database.Question{ 266 { 267 Question: "potential_event", 268 Answer: "Ok", 269 QuestionRegex: "(?i)(potential_question)", 270 QuestionGroup: "", 271 }, 272 }, 273 RequiredVariables: []database.ScenarioVariable{ 274 { 275 Name: "answer", 276 Value: "", 277 Question: scenarioShouldITriggerThis, 278 }, 279 }, 280 }) 281 } 282 283 // Update for event update actions 284 func (e EventStruct) Update() error { 285 return nil 286 } 287 288 func extractRequestString(text string) (result string, err error) { 289 re, err := regexp.Compile(`similar questions (.+)`) 290 if err != nil { 291 return "", err 292 } 293 294 matches := re.FindStringSubmatch(text) 295 if len(matches) != 2 { 296 return "", nil 297 } 298 299 if matches[1] == "" { 300 return "", nil 301 } 302 303 return matches[1], err 304 } 305 306 func findPotentialEvents(text string) (result []cdto.ModelInterface, err error) { 307 query := new(clients.Query). 308 Select([]interface{}{ 309 "questions.answer", 310 "questions.question", 311 "events.alias", 312 }). 313 From(&cdto.BaseModel{TableName: "questions"}). 314 Join(cquery.Join{ 315 Target: cquery.Reference{ 316 Table: "scenarios", 317 Key: "id", 318 }, 319 With: cquery.Reference{ 320 Table: "questions", 321 Key: "scenario_id", 322 }, 323 Condition: "=", 324 Type: cquery.InnerJoinType, 325 }). 326 Join(cquery.Join{ 327 Target: cquery.Reference{ 328 Table: "events", 329 Key: "id", 330 }, 331 With: cquery.Reference{ 332 Table: "scenarios", 333 Key: "event_id", 334 }, 335 Condition: "=", 336 Type: cquery.LeftJoinType, 337 }).Where(cquery.Where{ 338 First: "questions.answer", 339 Operator: "LIKE", 340 Type: cquery.WhereOrType, 341 Second: `"%` + text + `%"`, 342 }).Where(cquery.Where{ 343 First: "questions.question", 344 Operator: "LIKE", 345 Type: cquery.WhereOrType, 346 Second: `"%` + text + `%"`, 347 }).Where(cquery.Where{ 348 First: "events.alias", 349 Operator: "LIKE", 350 Type: cquery.WhereOrType, 351 Second: `"%` + text + `%"`, 352 }).GroupBy("events.id") 353 354 res, err := container.C.Dictionary.GetDBClient().Execute(query) 355 if err == sql.ErrNoRows { 356 return result, nil 357 } else if err != nil { 358 return result, err 359 } 360 361 if len(res.Items()) == 0 { 362 return result, nil 363 } 364 365 return res.Items(), nil 366 }