diff --git a/internal/handler/assignment_handler.go b/internal/handler/assignment_handler.go index 07015ec..8bdc272 100644 --- a/internal/handler/assignment_handler.go +++ b/internal/handler/assignment_handler.go @@ -14,12 +14,14 @@ import ( ) type AssignmentHandler struct { - assignmentService *service.AssignmentService + assignmentService *service.AssignmentService + notificationService *service.NotificationService } -func NewAssignmentHandler() *AssignmentHandler { +func NewAssignmentHandler(notificationService *service.NotificationService) *AssignmentHandler { return &AssignmentHandler{ - assignmentService: service.NewAssignmentService(), + assignmentService: service.NewAssignmentService(), + notificationService: notificationService, } } @@ -229,7 +231,7 @@ func (h *AssignmentHandler) Create(c *gin.Context) { return } } else { - _, err = h.assignmentService.Create(userID, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) + assignment, err := h.assignmentService.Create(userID, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) if err != nil { role, _ := c.Get(middleware.UserRoleKey) name, _ := c.Get(middleware.UserNameKey) @@ -245,6 +247,10 @@ func (h *AssignmentHandler) Create(c *gin.Context) { }) return } + + if h.notificationService != nil { + go h.notificationService.SendAssignmentCreatedNotification(userID, assignment) + } } c.Redirect(http.StatusFound, "/assignments") diff --git a/internal/handler/profile_handler.go b/internal/handler/profile_handler.go index 7b672b0..9d4df11 100644 --- a/internal/handler/profile_handler.go +++ b/internal/handler/profile_handler.go @@ -145,6 +145,7 @@ func (h *ProfileHandler) UpdateNotificationSettings(c *gin.Context) { TelegramChatID: c.PostForm("telegram_chat_id"), LineEnabled: c.PostForm("line_enabled") == "on", LineNotifyToken: c.PostForm("line_token"), + NotifyOnCreate: c.PostForm("notify_on_create") == "on", } err := h.notificationService.UpdateUserSettings(userID, settings) @@ -172,4 +173,3 @@ func (h *ProfileHandler) UpdateNotificationSettings(c *gin.Context) { "notifySettings": notifySettings, }) } - diff --git a/internal/models/notification_settings.go b/internal/models/notification_settings.go index fb90382..d9a45f0 100644 --- a/internal/models/notification_settings.go +++ b/internal/models/notification_settings.go @@ -13,6 +13,7 @@ type UserNotificationSettings struct { TelegramChatID string `json:"telegram_chat_id"` LineEnabled bool `gorm:"default:false" json:"line_enabled"` LineNotifyToken string `json:"-"` + NotifyOnCreate bool `gorm:"default:true" json:"notify_on_create"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/internal/router/router.go b/internal/router/router.go index 65abe63..cb37715 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -183,7 +183,7 @@ func Setup(cfg *config.Config) *gin.Engine { notificationService.StartReminderScheduler() authHandler := handler.NewAuthHandler() - assignmentHandler := handler.NewAssignmentHandler() + assignmentHandler := handler.NewAssignmentHandler(notificationService) adminHandler := handler.NewAdminHandler() profileHandler := handler.NewProfileHandler(notificationService) apiHandler := handler.NewAPIHandler() diff --git a/internal/service/notification_service.go b/internal/service/notification_service.go index 3744fab..4074380 100644 --- a/internal/service/notification_service.go +++ b/internal/service/notification_service.go @@ -24,7 +24,6 @@ func NewNotificationService(telegramBotToken string) *NotificationService { } } - func (s *NotificationService) GetUserSettings(userID uint) (*models.UserNotificationSettings, error) { var settings models.UserNotificationSettings result := database.GetDB().Where("user_id = ?", userID).First(&settings) @@ -41,14 +40,14 @@ func (s *NotificationService) GetUserSettings(userID uint) (*models.UserNotifica func (s *NotificationService) UpdateUserSettings(userID uint, settings *models.UserNotificationSettings) error { settings.UserID = userID - + var existing models.UserNotificationSettings result := database.GetDB().Where("user_id = ?", userID).First(&existing) - + if result.RowsAffected == 0 { return database.GetDB().Create(settings).Error } - + settings.ID = existing.ID return database.GetDB().Save(settings).Error } @@ -61,28 +60,28 @@ func (s *NotificationService) SendTelegramNotification(chatID, message string) e } apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", s.telegramBotToken) - + payload := map[string]string{ "chat_id": chatID, "text": message, "parse_mode": "HTML", } - + jsonPayload, err := json.Marshal(payload) if err != nil { return err } - + resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonPayload)) if err != nil { return err } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { return fmt.Errorf("telegram API returned status %d", resp.StatusCode) } - + return nil } @@ -92,29 +91,29 @@ func (s *NotificationService) SendLineNotification(token, message string) error } apiURL := "https://notify-api.line.me/api/notify" - + data := url.Values{} data.Set("message", message) - + req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode())) if err != nil { return err } - + req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { return fmt.Errorf("LINE Notify API returned status %d", resp.StatusCode) } - + return nil } @@ -153,6 +152,63 @@ func (s *NotificationService) SendAssignmentReminder(userID uint, assignment *mo return nil } +func (s *NotificationService) SendAssignmentCreatedNotification(userID uint, assignment *models.Assignment) error { + settings, err := s.GetUserSettings(userID) + if err != nil { + return err + } + + if !settings.NotifyOnCreate { + return nil + } + + if !settings.TelegramEnabled && !settings.LineEnabled { + return nil + } + + message := fmt.Sprintf( + "新しい課題が追加されました\n\n【%s】\n科目: %s\n優先度: %s\n期限: %s\n\n%s", + assignment.Title, + assignment.Subject, + getPriorityLabel(assignment.Priority), + assignment.DueDate.Format("2006/01/02 15:04"), + assignment.Description, + ) + + var errors []string + + if settings.TelegramEnabled && settings.TelegramChatID != "" { + if err := s.SendTelegramNotification(settings.TelegramChatID, message); err != nil { + errors = append(errors, fmt.Sprintf("Telegram: %v", err)) + } + } + + if settings.LineEnabled && settings.LineNotifyToken != "" { + if err := s.SendLineNotification(settings.LineNotifyToken, message); err != nil { + errors = append(errors, fmt.Sprintf("LINE: %v", err)) + } + } + + if len(errors) > 0 { + return fmt.Errorf("notification errors: %s", strings.Join(errors, "; ")) + } + + return nil +} + +func getPriorityLabel(priority string) string { + switch priority { + case "high": + return "大" + case "medium": + return "中" + case "low": + return "小" + default: + return priority + } +} + func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models.Assignment) error { settings, err := s.GetUserSettings(userID) if err != nil { @@ -224,24 +280,24 @@ func getUrgentReminderInterval(priority string) time.Duration { func (s *NotificationService) ProcessPendingReminders() { now := time.Now() - + var assignments []models.Assignment result := database.GetDB().Where( "reminder_enabled = ? AND reminder_sent = ? AND reminder_at <= ? AND is_completed = ?", true, false, now, false, ).Find(&assignments) - + if result.Error != nil { log.Printf("Error fetching pending reminders: %v", result.Error) return } - + for _, assignment := range assignments { if err := s.SendAssignmentReminder(assignment.UserID, &assignment); err != nil { log.Printf("Error sending reminder for assignment %d: %v", assignment.ID, err) continue } - + database.GetDB().Model(&assignment).Update("reminder_sent", true) log.Printf("Sent reminder for assignment %d to user %d", assignment.ID, assignment.UserID) } @@ -250,41 +306,41 @@ func (s *NotificationService) ProcessPendingReminders() { func (s *NotificationService) ProcessUrgentReminders() { now := time.Now() urgentStartTime := 3 * time.Hour - + var assignments []models.Assignment result := database.GetDB().Where( "urgent_reminder_enabled = ? AND is_completed = ? AND due_date > ?", true, false, now, ).Find(&assignments) - + if result.Error != nil { log.Printf("Error fetching urgent reminders: %v", result.Error) return } - + for _, assignment := range assignments { timeUntilDue := assignment.DueDate.Sub(now) - + if timeUntilDue > urgentStartTime { continue } - + interval := getUrgentReminderInterval(assignment.Priority) - + if assignment.LastUrgentReminderSent != nil { timeSinceLastReminder := now.Sub(*assignment.LastUrgentReminderSent) if timeSinceLastReminder < interval { continue } } - + if err := s.SendUrgentReminder(assignment.UserID, &assignment); err != nil { log.Printf("Error sending urgent reminder for assignment %d: %v", assignment.ID, err) continue } - + database.GetDB().Model(&assignment).Update("last_urgent_reminder_sent", now) - log.Printf("Sent urgent reminder for assignment %d (priority: %s) to user %d", + log.Printf("Sent urgent reminder for assignment %d (priority: %s) to user %d", assignment.ID, assignment.Priority, assignment.UserID) } } @@ -293,7 +349,7 @@ func (s *NotificationService) StartReminderScheduler() { go func() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() - + for range ticker.C { s.ProcessPendingReminders() s.ProcessUrgentReminders() @@ -301,4 +357,3 @@ func (s *NotificationService) StartReminderScheduler() { }() log.Println("Reminder scheduler started (one-time + urgent reminders)") } - diff --git a/web/templates/pages/profile.html b/web/templates/pages/profile.html index a528f31..f1e5336 100644 --- a/web/templates/pages/profile.html +++ b/web/templates/pages/profile.html @@ -112,6 +112,14 @@ +
+
+ + +