課題作成時にもTelegram/LINEに通知する機能を追加

This commit is contained in:
2026-01-08 13:00:17 +09:00
parent 920928746e
commit f0712bddfe
6 changed files with 106 additions and 36 deletions

View File

@@ -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")

View File

@@ -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,
})
}

View File

@@ -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:"-"`

View File

@@ -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()

View File

@@ -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)")
}

View File

@@ -112,6 +112,14 @@
</div>
</div>
</div>
<hr class="my-3">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="notify_on_create" name="notify_on_create"
{{if .notifySettings.NotifyOnCreate}}checked{{end}}>
<label class="form-check-label" for="notify_on_create">
<i class="bi bi-plus-circle me-1"></i>課題追加時に通知する
</label>
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>通知設定を保存</button>
</form>
</div>