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  }