2020-12-19 00:21:17 +01:00
|
|
|
// Vikunja is a to-do list application to facilitate your life.
|
2021-02-02 20:19:13 +01:00
|
|
|
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
2020-12-19 00:21:17 +01:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
2020-12-23 16:41:52 +01:00
|
|
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
2020-12-19 00:21:17 +01:00
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2020-12-23 16:41:52 +01:00
|
|
|
// GNU Affero General Public Licensee for more details.
|
2020-12-19 00:21:17 +01:00
|
|
|
//
|
2020-12-23 16:41:52 +01:00
|
|
|
// You should have received a copy of the GNU Affero General Public Licensee
|
2020-12-19 00:21:17 +01:00
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
|
|
|
"time"
|
|
|
|
|
2021-04-11 17:08:43 +02:00
|
|
|
"code.vikunja.io/api/pkg/utils"
|
|
|
|
"xorm.io/builder"
|
|
|
|
|
2021-02-07 22:05:09 +01:00
|
|
|
"code.vikunja.io/api/pkg/notifications"
|
|
|
|
|
2020-12-23 16:32:28 +01:00
|
|
|
"code.vikunja.io/api/pkg/db"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
|
2020-12-19 00:21:17 +01:00
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
|
|
"code.vikunja.io/api/pkg/cron"
|
|
|
|
"code.vikunja.io/api/pkg/log"
|
|
|
|
"code.vikunja.io/api/pkg/user"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TaskReminder holds a reminder on a task
|
|
|
|
type TaskReminder struct {
|
|
|
|
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
|
|
|
TaskID int64 `xorm:"bigint not null INDEX"`
|
|
|
|
Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"`
|
|
|
|
Created time.Time `xorm:"created not null"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// TableName returns a pretty table name
|
|
|
|
func (TaskReminder) TableName() string {
|
|
|
|
return "task_reminders"
|
|
|
|
}
|
|
|
|
|
|
|
|
type taskUser struct {
|
|
|
|
Task *Task `xorm:"extends"`
|
|
|
|
User *user.User `xorm:"extends"`
|
|
|
|
}
|
|
|
|
|
2021-04-11 17:08:43 +02:00
|
|
|
const dbTimeFormat = `2006-01-02 15:04:05`
|
|
|
|
|
|
|
|
func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (taskUsers []*taskUser, err error) {
|
2021-03-02 18:40:39 +01:00
|
|
|
if len(taskIDs) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-19 00:21:17 +01:00
|
|
|
// Get all creators of tasks
|
|
|
|
creators := make(map[int64]*user.User, len(taskIDs))
|
2020-12-23 16:32:28 +01:00
|
|
|
err = s.
|
2022-01-16 12:05:56 +01:00
|
|
|
Select("users.id, users.username, users.email, users.name, users.timezone").
|
2020-12-19 00:21:17 +01:00
|
|
|
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
|
|
|
|
In("tasks.id", taskIDs).
|
2021-04-11 17:08:43 +02:00
|
|
|
Where(cond).
|
2022-01-16 12:05:56 +01:00
|
|
|
GroupBy("tasks.id, users.id, users.username, users.email, users.name, users.timezone").
|
2020-12-19 00:21:17 +01:00
|
|
|
Find(&creators)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
taskMap := make(map[int64]*Task, len(taskIDs))
|
2020-12-23 16:32:28 +01:00
|
|
|
err = s.In("id", taskIDs).Find(&taskMap)
|
2020-12-19 00:21:17 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
for _, task := range taskMap {
|
|
|
|
u, exists := creators[task.CreatedByID]
|
2021-02-07 22:05:09 +01:00
|
|
|
if !exists {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-12-19 00:21:17 +01:00
|
|
|
taskUsers = append(taskUsers, &taskUser{
|
2022-01-16 12:05:56 +01:00
|
|
|
Task: taskMap[task.ID],
|
2021-02-07 22:05:09 +01:00
|
|
|
User: u,
|
2020-12-19 00:21:17 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-04-11 17:08:43 +02:00
|
|
|
var assignees []*TaskAssigneeWithUser
|
|
|
|
err = s.Table("task_assignees").
|
|
|
|
Select("task_id, users.*").
|
|
|
|
In("task_id", taskIDs).
|
|
|
|
Join("INNER", "users", "task_assignees.user_id = users.id").
|
|
|
|
Where(cond).
|
|
|
|
Find(&assignees)
|
2021-02-07 22:05:09 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-19 00:21:17 +01:00
|
|
|
for _, assignee := range assignees {
|
|
|
|
taskUsers = append(taskUsers, &taskUser{
|
|
|
|
Task: taskMap[assignee.TaskID],
|
|
|
|
User: &assignee.User,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (reminderNotifications []*ReminderDueNotification, err error) {
|
2021-04-11 17:08:43 +02:00
|
|
|
now = utils.GetTimeWithoutNanoSeconds(now)
|
2022-01-16 12:05:56 +01:00
|
|
|
reminderNotifications = []*ReminderDueNotification{}
|
2021-01-09 14:59:54 +01:00
|
|
|
|
|
|
|
nextMinute := now.Add(1 * time.Minute)
|
|
|
|
|
|
|
|
log.Debugf("[Task Reminder Cron] Looking for reminders between %s and %s to send...", now, nextMinute)
|
|
|
|
|
|
|
|
reminders := []*TaskReminder{}
|
|
|
|
err = s.
|
2021-01-31 12:54:15 +01:00
|
|
|
Join("INNER", "tasks", "tasks.id = task_reminders.task_id").
|
2022-01-16 12:05:56 +01:00
|
|
|
// All reminders from -12h to +14h to include all time zones
|
|
|
|
Where("reminder >= ? and reminder < ?", now.Add(time.Hour*-12).Format(dbTimeFormat), nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
2021-01-31 12:54:15 +01:00
|
|
|
And("tasks.done = false").
|
2021-01-09 14:59:54 +01:00
|
|
|
Find(&reminders)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("[Task Reminder Cron] Found %d reminders", len(reminders))
|
|
|
|
|
|
|
|
if len(reminders) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
var taskIDs []int64
|
2021-01-09 14:59:54 +01:00
|
|
|
for _, r := range reminders {
|
|
|
|
taskIDs = append(taskIDs, r.TaskID)
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
if len(taskIDs) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
usersWithReminders, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true})
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
usersPerTask := make(map[int64][]*taskUser, len(usersWithReminders))
|
|
|
|
for _, ur := range usersWithReminders {
|
|
|
|
usersPerTask[ur.Task.ID] = append(usersPerTask[ur.Task.ID], ur)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Time zone cache per time zone string to avoid parsing the same time zone over and over again
|
|
|
|
tzs := make(map[string]*time.Location)
|
|
|
|
// Figure out which reminders are actually due in the time zone of the users
|
|
|
|
for _, r := range reminders {
|
|
|
|
|
|
|
|
for _, u := range usersPerTask[r.TaskID] {
|
|
|
|
|
|
|
|
if u.User.Timezone == "" {
|
|
|
|
u.User.Timezone = config.GetTimeZone().String()
|
|
|
|
}
|
|
|
|
|
|
|
|
// I think this will break once there's more reminders than what we can handle in one minute
|
|
|
|
tz, exists := tzs[u.User.Timezone]
|
|
|
|
if !exists {
|
|
|
|
tz, err = time.LoadLocation(u.User.Timezone)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tzs[u.User.Timezone] = tz
|
|
|
|
}
|
|
|
|
|
|
|
|
actualReminder := r.Reminder.In(tz)
|
|
|
|
if (actualReminder.After(now) && actualReminder.Before(now.Add(time.Minute))) || actualReminder.Equal(now) {
|
|
|
|
reminderNotifications = append(reminderNotifications, &ReminderDueNotification{
|
|
|
|
User: u.User,
|
|
|
|
Task: u.Task,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-09 14:59:54 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-19 00:21:17 +01:00
|
|
|
// RegisterReminderCron registers a cron function which runs every minute to check if any reminders are due the
|
|
|
|
// next minute to send emails.
|
|
|
|
func RegisterReminderCron() {
|
|
|
|
if !config.ServiceEnableEmailReminders.GetBool() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !config.MailerEnabled.GetBool() {
|
|
|
|
log.Info("Mailer is disabled, not sending reminders per mail")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tz := config.GetTimeZone()
|
|
|
|
|
|
|
|
log.Debugf("[Task Reminder Cron] Timezone is %s", tz)
|
|
|
|
|
|
|
|
err := cron.Schedule("* * * * *", func() {
|
2021-04-11 17:08:43 +02:00
|
|
|
s := db.NewSession()
|
|
|
|
defer s.Close()
|
2020-12-19 00:21:17 +01:00
|
|
|
|
2021-01-09 14:59:54 +01:00
|
|
|
now := time.Now()
|
2022-01-16 12:05:56 +01:00
|
|
|
reminders, err := getTasksWithRemindersDueAndTheirUsers(s, now)
|
2020-12-19 00:21:17 +01:00
|
|
|
if err != nil {
|
2021-01-09 14:59:54 +01:00
|
|
|
log.Errorf("[Task Reminder Cron] Could not get tasks with reminders in the next minute: %s", err)
|
2020-12-19 00:21:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
if len(reminders) == 0 {
|
2021-02-07 14:46:47 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
log.Debugf("[Task Reminder Cron] Sending %d reminders", len(reminders))
|
2021-02-07 22:05:09 +01:00
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
for _, n := range reminders {
|
|
|
|
err = notifications.Notify(n.User, n)
|
2021-02-07 22:05:09 +01:00
|
|
|
if err != nil {
|
2022-01-16 12:05:56 +01:00
|
|
|
log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", n.User.ID, err)
|
2021-02-07 22:05:09 +01:00
|
|
|
return
|
2020-12-19 00:21:17 +01:00
|
|
|
}
|
|
|
|
|
2022-01-16 12:05:56 +01:00
|
|
|
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", n.Task.ID, n.User.ID)
|
2020-12-19 00:21:17 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Could not register reminder cron: %s", err)
|
|
|
|
}
|
|
|
|
}
|