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  }