2020-12-18 23:21:17 +00:00
// Vikunja is a to-do list application to facilitate your life.
2023-09-01 06:32:28 +00:00
// Copyright 2018-present Vikunja and contributors. All rights reserved.
2020-12-18 23:21:17 +00:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 15:41:52 +00:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-12-18 23:21:17 +00: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 15:41:52 +00:00
// GNU Affero General Public Licensee for more details.
2020-12-18 23:21:17 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-12-18 23:21:17 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"time"
2021-04-11 15:08:43 +00:00
"code.vikunja.io/api/pkg/utils"
"xorm.io/builder"
2021-02-07 21:05:09 +00:00
"code.vikunja.io/api/pkg/notifications"
2020-12-23 15:32:28 +00:00
"code.vikunja.io/api/pkg/db"
"xorm.io/xorm"
2020-12-18 23:21:17 +00: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"
)
2023-03-27 20:07:06 +00:00
// ReminderRelation represents the date attribute of the task which a period based reminder relates to
type ReminderRelation string
// All valid ReminderRelations
const (
ReminderRelationDueDate ReminderRelation = ` due_date `
ReminderRelationStartDate ReminderRelation = ` start_date `
ReminderRelationEndDate ReminderRelation = ` end_date `
)
// TaskReminder holds a reminder on a task.
// If RelativeTo and the assciated date field are defined, then the attribute Reminder will be computed.
// If RelativeTo is missing, than Reminder must be given.
2020-12-18 23:21:17 +00:00
type TaskReminder struct {
2023-03-27 20:07:06 +00:00
ID int64 ` xorm:"bigint autoincr not null unique pk" json:"-" `
TaskID int64 ` xorm:"bigint not null INDEX" json:"-" `
// The absolute time when the user wants to be reminded of the task.
Reminder time . Time ` xorm:"DATETIME not null INDEX 'reminder'" json:"reminder" `
Created time . Time ` xorm:"created not null" json:"-" `
// A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.
RelativePeriod int64 ` xorm:"bigint null" json:"relative_period" `
// The name of the date field to which the relative period refers to.
RelativeTo ReminderRelation ` xorm:"varchar(50) null" json:"relative_to" `
2020-12-18 23:21:17 +00:00
}
// 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 15:08:43 +00: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 17:40:39 +00:00
if len ( taskIDs ) == 0 {
return
}
2020-12-18 23:21:17 +00:00
// Get all creators of tasks
creators := make ( map [ int64 ] * user . User , len ( taskIDs ) )
2020-12-23 15:32:28 +00:00
err = s .
2022-06-16 14:20:26 +00:00
Select ( "users.*" ) .
2020-12-18 23:21:17 +00:00
Join ( "LEFT" , "tasks" , "tasks.created_by_id = users.id" ) .
In ( "tasks.id" , taskIDs ) .
2021-04-11 15:08:43 +00:00
Where ( cond ) .
2022-01-16 11:05:56 +00:00
GroupBy ( "tasks.id, users.id, users.username, users.email, users.name, users.timezone" ) .
2020-12-18 23:21:17 +00:00
Find ( & creators )
if err != nil {
return
}
taskMap := make ( map [ int64 ] * Task , len ( taskIDs ) )
2020-12-23 15:32:28 +00:00
err = s . In ( "id" , taskIDs ) . Find ( & taskMap )
2020-12-18 23:21:17 +00:00
if err != nil {
return
}
2022-01-16 11:05:56 +00:00
for _ , task := range taskMap {
u , exists := creators [ task . CreatedByID ]
2021-02-07 21:05:09 +00:00
if ! exists {
continue
}
2020-12-18 23:21:17 +00:00
taskUsers = append ( taskUsers , & taskUser {
2022-01-16 11:05:56 +00:00
Task : taskMap [ task . ID ] ,
2021-02-07 21:05:09 +00:00
User : u ,
2020-12-18 23:21:17 +00:00
} )
}
2021-04-11 15:08:43 +00: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 21:05:09 +00:00
if err != nil {
return
}
2020-12-18 23:21:17 +00:00
for _ , assignee := range assignees {
taskUsers = append ( taskUsers , & taskUser {
Task : taskMap [ assignee . TaskID ] ,
User : & assignee . User ,
} )
}
return
}
2022-01-16 11:05:56 +00:00
func getTasksWithRemindersDueAndTheirUsers ( s * xorm . Session , now time . Time ) ( reminderNotifications [ ] * ReminderDueNotification , err error ) {
2021-04-11 15:08:43 +00:00
now = utils . GetTimeWithoutNanoSeconds ( now )
2022-01-16 11:05:56 +00:00
reminderNotifications = [ ] * ReminderDueNotification { }
2021-01-09 13:59:54 +00: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 11:54:15 +00:00
Join ( "INNER" , "tasks" , "tasks.id = task_reminders.task_id" ) .
2022-01-16 11:05:56 +00: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 11:54:15 +00:00
And ( "tasks.done = false" ) .
2021-01-09 13:59:54 +00: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 11:05:56 +00:00
var taskIDs [ ] int64
2021-01-09 13:59:54 +00:00
for _ , r := range reminders {
taskIDs = append ( taskIDs , r . TaskID )
}
2022-01-16 11:05:56 +00: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 )
}
2023-08-24 08:47:17 +00:00
seen := make ( map [ int64 ] map [ int64 ] bool )
2022-01-16 11:05:56 +00:00
// 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 ] {
2023-08-24 08:47:17 +00:00
// This ensures we send each reminder only once to each user
if seen [ r . TaskID ] == nil {
2023-08-24 09:16:07 +00:00
seen [ r . TaskID ] = make ( map [ int64 ] bool )
2023-08-24 08:47:17 +00:00
}
if _ , exists := seen [ r . TaskID ] [ u . User . ID ] ; exists {
continue
}
seen [ r . TaskID ] [ u . User . ID ] = true
2022-01-16 11:05:56 +00:00
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 13:59:54 +00:00
return
}
2020-12-18 23:21:17 +00: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 15:08:43 +00:00
s := db . NewSession ( )
defer s . Close ( )
2020-12-18 23:21:17 +00:00
2021-01-09 13:59:54 +00:00
now := time . Now ( )
2022-01-16 11:05:56 +00:00
reminders , err := getTasksWithRemindersDueAndTheirUsers ( s , now )
2020-12-18 23:21:17 +00:00
if err != nil {
2021-01-09 13:59:54 +00:00
log . Errorf ( "[Task Reminder Cron] Could not get tasks with reminders in the next minute: %s" , err )
2020-12-18 23:21:17 +00:00
return
}
2022-01-16 11:05:56 +00:00
if len ( reminders ) == 0 {
2021-02-07 13:46:47 +00:00
return
}
2022-01-16 11:05:56 +00:00
log . Debugf ( "[Task Reminder Cron] Sending %d reminders" , len ( reminders ) )
2021-02-07 21:05:09 +00:00
2022-01-16 11:05:56 +00:00
for _ , n := range reminders {
err = notifications . Notify ( n . User , n )
2021-02-07 21:05:09 +00:00
if err != nil {
2022-01-16 11:05:56 +00:00
log . Errorf ( "[Task Reminder Cron] Could not notify user %d: %s" , n . User . ID , err )
2021-02-07 21:05:09 +00:00
return
2020-12-18 23:21:17 +00:00
}
2022-01-16 11:05:56 +00:00
log . Debugf ( "[Task Reminder Cron] Sent reminder email for task %d to user %d" , n . Task . ID , n . User . ID )
2020-12-18 23:21:17 +00:00
}
} )
if err != nil {
log . Fatalf ( "Could not register reminder cron: %s" , err )
}
}