diff --git a/go.mod b/go.mod index 6f3094d..2afe3d3 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200723144633-ed34468996e6 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.6.1 + github.com/undefinedlabs/go-mpatch v1.0.6 ) diff --git a/go.sum b/go.sum index d16723d..2cc5657 100644 --- a/go.sum +++ b/go.sum @@ -541,6 +541,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/undefinedlabs/go-mpatch v1.0.6 h1:h8q5ORH/GaOE1Se1DMhrOyljXZEhRcROO7agMqWXCOY= +github.com/undefinedlabs/go-mpatch v1.0.6/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= diff --git a/server/command.go b/server/command.go index be9daaa..f975afb 100644 --- a/server/command.go +++ b/server/command.go @@ -57,6 +57,9 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo case "queue": return p.executeCommandQueue(args), nil + case "requeue": + return p.executeCommandReQueue(args), nil + case "setting": return p.executeCommandSetting(args), nil @@ -77,7 +80,7 @@ func (p *Plugin) executeCommandList(args *model.CommandArgs) *model.CommandRespo weekday = int(parsedWeekday) } - hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday) + hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday, false, time.Now().Weekday()) if err != nil { return responsef("Error calculating hashtags") } @@ -160,7 +163,7 @@ func (p *Plugin) executeCommandQueue(args *model.CommandArgs) *model.CommandResp message = strings.Join(split[3:], " ") } - hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday) + hashtag, err := p.GenerateHashtag(args.ChannelId, nextWeek, weekday, false, time.Now().Weekday()) if err != nil { return responsef("Error calculating hashtags. Check the meeting settings for this channel.") } @@ -183,6 +186,99 @@ func (p *Plugin) executeCommandQueue(args *model.CommandArgs) *model.CommandResp return &model.CommandResponse{} } +func calculateQueItemNumber(args *model.CommandArgs, p *Plugin, hashtag string) (*model.CommandResponse, int) { + searchResults, appErr := p.API.SearchPostsInTeamForUser(args.TeamId, args.UserId, model.SearchParameter{Terms: &hashtag}) + if appErr != nil { + return responsef("Error calculating list number"), 0 + } + postList := *searchResults.PostList + numQueueItems := len(postList.Posts) + return nil, numQueueItems + 1 +} + +func (p *Plugin) executeCommandReQueue(args *model.CommandArgs) *model.CommandResponse { + split := strings.Fields(args.Command) + + if len(split) <= 2 { + return responsef("Missing parameters for requeue command") + } + + meeting, err := p.GetMeeting(args.ChannelId) + if err != nil { + return responsef("There was no meeting found for this channel.") + } + + oldPostID := split[2] + postToBeReQueued, appErr := p.API.GetPost(oldPostID) + if appErr != nil { + p.API.LogWarn("Error fetching post: %s", "error", appErr.Message) + return responsef("Error fetching post.") + } + var ( + prefix string + hashtagDateFormat string + ) + matchGroups := meetingDateFormatRegex.FindStringSubmatch(meeting.HashtagFormat) + if len(matchGroups) != 3 { + responsef("Error parsing hashtag format.") + } + prefix = matchGroups[1] + hashtagDateFormat = strings.TrimSpace(matchGroups[2]) + + var ( + messageRegexFormat, er = regexp.Compile(fmt.Sprintf(`(?m)^#### #%s(?P.*) [0-9]+\) (?P.*)?$`, prefix)) + ) + if er != nil { + return responsef(er.Error()) + } + + if matchGroups := messageRegexFormat.FindStringSubmatch(postToBeReQueued.Message); len(matchGroups) == 3 { + originalPostDate := p.replaceUnderscoreWithSpace(strings.TrimSpace(matchGroups[1])) // reverse what we do to make it a valid hashtag + originalPostMessage := strings.TrimSpace(matchGroups[2]) + + today := time.Now() + local, _ := time.LoadLocation("Local") + formattedDate, _ := time.ParseInLocation(hashtagDateFormat, originalPostDate, local) + if formattedDate.Year() == 0 { + thisYear := today.Year() + formattedDate = formattedDate.AddDate(thisYear, 0, 0) + } + + if today.Year() <= formattedDate.Year() && today.YearDay() < formattedDate.YearDay() { + return responsef("Re-queuing future items is not supported.") + } + + hashtag, err := p.GenerateHashtag(args.ChannelId, false, -1, true, formattedDate.Weekday()) + if err != nil { + p.API.LogWarn("Error calculating hashtags. Check the meeting settings for this channel.", "error", err.Error()) + return responsef("Error calculating hashtags. Check the meeting settings for this channel.") + } + + commandResponse, numQueueItems := calculateQueItemNumber(args, p, hashtag) + if commandResponse != nil { + return commandResponse + } + + _, appErr := p.API.UpdatePost(&model.Post{ + Id: oldPostID, + UserId: args.UserId, + ChannelId: args.ChannelId, + RootId: args.RootId, + Message: fmt.Sprintf("#### %v %v) %v", hashtag, numQueueItems, originalPostMessage), + }) + if appErr != nil { + p.API.LogWarn("Error creating post: %s", "error", appErr.Message) + return responsef("Error creating post: %s", appErr.Message) + } + return responsef(fmt.Sprintf("Item has been Re-queued to %v", hashtag)) + } + return responsef("Make sure, message is in required format.") +} + +func (p *Plugin) replaceUnderscoreWithSpace(hashtag string) string { + return strings.ReplaceAll(hashtag, "_", " ") +} + func parseMeetingPost(meeting *Meeting, post *model.Post) (string, ParsedMeetingMessage, error) { var ( prefix string diff --git a/server/meeting.go b/server/meeting.go index 241d51b..f9af614 100644 --- a/server/meeting.go +++ b/server/meeting.go @@ -113,7 +113,7 @@ func (p *Plugin) calculateQueueItemNumberAndUpdateOldItems(meeting *Meeting, arg } // GenerateHashtag returns a meeting hashtag -func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int) (string, error) { +func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int, requeue bool, assignedDay time.Weekday) (string, error) { meeting, err := p.GetMeeting(channelID) if err != nil { return "", err @@ -126,9 +126,15 @@ func (p *Plugin) GenerateHashtag(channelID string, nextWeek bool, weekday int) ( return "", err } } else { - // Get date for the list of days of the week - if meetingDate, err = nextWeekdayDateInWeek(meeting.Schedule, nextWeek); err != nil { - return "", err + // user didn't specify any specific date/day in command, Get date for the list of days of the week + if !requeue { + if meetingDate, err = nextWeekdayDateInWeek(meeting.Schedule, nextWeek); err != nil { + return "", err + } + } else { + if meetingDate, err = nextWeekdayDateInWeekSkippingDay(meeting.Schedule, nextWeek, assignedDay); err != nil { + return "", err + } } } diff --git a/server/meeting_test.go b/server/meeting_test.go index 4e24e25..6f291e2 100644 --- a/server/meeting_test.go +++ b/server/meeting_test.go @@ -138,7 +138,10 @@ func TestPlugin_GenerateHashtag(t *testing.T) { jsonMeeting, err := json.Marshal(tt.args.meeting) tAssert.Nil(err) api.On("KVGet", tt.args.meeting.ChannelID).Return(jsonMeeting, nil) - got, err := mPlugin.GenerateHashtag(tt.args.meeting.ChannelID, tt.args.nextWeek, -1) + weekday := -1 + requeue := false + assignedDay := time.Weekday(1) + got, err := mPlugin.GenerateHashtag(tt.args.meeting.ChannelID, tt.args.nextWeek, weekday, requeue, assignedDay) if (err != nil) != tt.wantErr { t.Errorf("GenerateHashtag() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/server/utils.go b/server/utils.go index 799a076..3a1c3e3 100644 --- a/server/utils.go +++ b/server/utils.go @@ -72,6 +72,25 @@ func nextWeekdayDateInWeek(meetingDays []time.Weekday, nextWeek bool) (*time.Tim return nextWeekdayDate(meetingDay, nextWeek) } +func nextWeekdayDateInWeekSkippingDay(meetingDays []time.Weekday, nextWeek bool, dayToSkip time.Weekday) (*time.Time, error) { + if len(meetingDays) == 0 { + return nil, errors.New("missing weekdays to calculate date") + } + + todayWeekday := time.Now().Weekday() + + // Find which meeting weekday to calculate the date for + meetingDay := meetingDays[0] + for _, day := range meetingDays { + if todayWeekday <= day && day != dayToSkip { + meetingDay = day + break + } + } + + return nextWeekdayDate(meetingDay, nextWeek) +} + // nextWeekdayDate calculates the date of the next given weekday // from today's date. // If nextWeek is true, it will be based on the next calendar week. diff --git a/server/utils_test.go b/server/utils_test.go index 553dd6c..98f2f90 100644 --- a/server/utils_test.go +++ b/server/utils_test.go @@ -1,8 +1,11 @@ package main import ( + "reflect" "testing" "time" + + "github.com/undefinedlabs/go-mpatch" ) func Test_parseSchedule(t *testing.T) { @@ -41,3 +44,42 @@ func Test_parseSchedule(t *testing.T) { }) } } + +func Test_nextWeekdayDateInWeekSkippingDay(t *testing.T) { + var patch, err = mpatch.PatchMethod(time.Now, func() time.Time { + return time.Date(2021, 11, 15, 00, 00, 00, 0, time.UTC) + }) + if patch == nil || err != nil { + t.Errorf("error creating patch") + } + type args struct { + meetingDays []time.Weekday + nextWeek bool + dayToSkip time.Weekday + } + tests := []struct { + name string + args args + want time.Time + wantErr bool + }{ + + {name: "test skip tuesday in week, today", args: args{[]time.Weekday{1, 2, 3, 4, 5, 6, 7}, false, time.Weekday(2)}, want: time.Now().AddDate(0, 0, 0), wantErr: false}, + {name: "test skip tuesday in few days", args: args{[]time.Weekday{2, 3, 4, 5, 6, 7}, false, time.Weekday(2)}, want: time.Now().AddDate(0, 0, 2), wantErr: false}, + {name: "test skip monday with nextWeek true", args: args{[]time.Weekday{1, 2, 3, 4}, true, time.Weekday(1)}, want: time.Now().AddDate(0, 0, 8), wantErr: false}, + {name: "test only meeting day is skipped", args: args{[]time.Weekday{3}, false, time.Weekday(3)}, want: time.Now().AddDate(0, 0, 2), wantErr: false}, + {name: "test only meeting day is skipped with nextWeek true", args: args{[]time.Weekday{3}, true, time.Weekday(3)}, want: time.Now().AddDate(0, 0, 9), wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := nextWeekdayDateInWeekSkippingDay(tt.args.meetingDays, tt.args.nextWeek, tt.args.dayToSkip) + if (err != nil) != tt.wantErr { + t.Errorf("nextWeekdayDateInWeekSkippingDay() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, &tt.want) { + t.Errorf("nextWeekdayDateInWeekSkippingDay() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.js index fa540f7..89f3f74 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.js @@ -1,7 +1,8 @@ import {searchPostsWithParams} from 'mattermost-redux/actions/search'; - +import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {Client4} from 'mattermost-redux/client'; import Client from '../client'; @@ -80,4 +81,28 @@ export function performSearch(terms) { return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: false, include_deleted_channels: viewArchivedChannels, page: 0, per_page: 20}, true)); }; -} \ No newline at end of file +} + +export function requeueItem(postId) { + return async (dispatch, getState) => { + const command = `/agenda requeue post ${postId}`; + await clientExecuteCommand(dispatch, getState, command); + return {data: true}; + }; +} + +export async function clientExecuteCommand(dispatch, getState, command) { + const state = getState(); + const currentChannel = getCurrentChannel(state); + const currentTeamId = getCurrentTeamId(state); + const args = { + channel_id: currentChannel?.id, + team_id: currentTeamId, + }; + + try { + return Client4.executeCommand(command, args); + } catch (error) { + return error; + } +} diff --git a/webapp/src/index.js b/webapp/src/index.js index 3a14357..3020ca9 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -1,5 +1,5 @@ -import {updateSearchTerms, updateSearchResultsTerms, updateRhsState, performSearch, openMeetingSettingsModal} from './actions'; +import {updateSearchTerms, updateSearchResultsTerms, updateRhsState, performSearch, openMeetingSettingsModal, requeueItem} from './actions'; import reducer from './reducer'; @@ -19,6 +19,10 @@ export default class Plugin { (channelId) => { store.dispatch(openMeetingSettingsModal(channelId)); }); + + registry.registerPostDropdownMenuAction('Re-queue', (postId) => { + store.dispatch(requeueItem(postId)); + }); } }