120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/kristianwind/verdande/internal/ai"
|
||
|
|
"github.com/kristianwind/verdande/internal/quickadd"
|
||
|
|
"github.com/kristianwind/verdande/internal/store"
|
||
|
|
)
|
||
|
|
|
||
|
|
// En mail, der bliver til en brugbar opgave.
|
||
|
|
//
|
||
|
|
// Uden det her hedder opgaven, hvad mailen hed: "Anders Jensen: SV: SV: Vedr.
|
||
|
|
// levering uge 12". Det er nok til at genkende den og ikke nok til at gøre noget
|
||
|
|
// ved den — og en indbakke af den slags linjer er præcis den bunke, man havde
|
||
|
|
// mail for at slippe for.
|
||
|
|
//
|
||
|
|
// Ét kald pr. gennemløb frem for ét pr. mail. Femogtyve mails er femogtyve gange
|
||
|
|
// ventetid og femogtyve gange betaling for det samme spørgsmål, og et gennemløb,
|
||
|
|
// der tager fem minutter, er et, der overlapper med det næste.
|
||
|
|
//
|
||
|
|
// Falder det ud, er der ikke sket noget: opgaven hedder afsender og emne, som den
|
||
|
|
// altid har gjort. En model, der er nede, må ikke kunne stoppe posten.
|
||
|
|
|
||
|
|
// mailForAI er én mail, som modellen får den at se.
|
||
|
|
type mailForAI struct {
|
||
|
|
From string
|
||
|
|
Subject string
|
||
|
|
Snippet string
|
||
|
|
}
|
||
|
|
|
||
|
|
// mailTaskLines beder modellen om én linje pr. mail, i quick-add-syntaks.
|
||
|
|
//
|
||
|
|
// Returnerer en tabel fra mailens plads i listen til linjen. Ikke en liste, fordi
|
||
|
|
// en model, der springer en mail over, ellers ville flytte alle de følgende
|
||
|
|
// linjer én plads — og så ville en opgave hedde noget, der stod i en anden mail.
|
||
|
|
// Det er den samme forsigtighed som ved oprydningen i indbakken, og den samme
|
||
|
|
// grund: en forskydning på én er den slags fejl, ingen opdager.
|
||
|
|
func (s *Server) mailTaskLines(ctx context.Context, user *store.User, mails []mailForAI) map[int]string {
|
||
|
|
if len(mails) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
cfg, err := s.aiConfigFor(ctx, user)
|
||
|
|
if err != nil || !cfg.Configured() {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
lines := make([]string, 0, len(mails))
|
||
|
|
for i, m := range mails {
|
||
|
|
line := strconv.Itoa(i+1) + ". " + m.From + " — " + m.Subject
|
||
|
|
if m.Snippet != "" {
|
||
|
|
line += "\n " + firstWords(m.Snippet, 40)
|
||
|
|
}
|
||
|
|
lines = append(lines, line)
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
today := time.Now().In(userLocation(user.Timezone)).Format("2006-01-02")
|
||
|
|
out, err := ai.New(cfg).TasksFromMail(ctx, lines, today, user.Locale)
|
||
|
|
if err != nil {
|
||
|
|
s.log.Info("mail tasks without the model", "err", err, "user", user.ID)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
byIndex := map[int]string{}
|
||
|
|
for i, sug := range out {
|
||
|
|
n, line := numberedLine(sug.Line)
|
||
|
|
if n == 0 {
|
||
|
|
n = i + 1
|
||
|
|
}
|
||
|
|
if n < 1 || n > len(mails) || strings.TrimSpace(line) == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
byIndex[n-1] = line
|
||
|
|
}
|
||
|
|
return byIndex
|
||
|
|
}
|
||
|
|
|
||
|
|
// applyMailLine lægger modellens linje ind i opgaven, hvis der er en.
|
||
|
|
//
|
||
|
|
// Linjen går gennem quick-add-parseren som alt andet, så en mail, hvor der står
|
||
|
|
// "inden fredag", får en dato — og "#Firma" i en linje, modellen fandt på, flytter
|
||
|
|
// ingenting, for projektet slås op blandt dem, der findes.
|
||
|
|
//
|
||
|
|
// Emnet bliver stående i beskrivelsen. Modellen har skrevet en titel ud fra det,
|
||
|
|
// og hvis den har misforstået mailen, skal det oprindelige stadig kunne læses ved
|
||
|
|
// siden af — ellers er den eneste vej tilbage at finde mailen frem igen.
|
||
|
|
func (s *Server) applyMailLine(ctx context.Context, user *store.User, task *store.Task, line, subject string) {
|
||
|
|
if strings.TrimSpace(line) == "" {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
parsed := quickadd.Parse(line, time.Now().In(userLocation(user.Timezone)), user.Locale)
|
||
|
|
if strings.TrimSpace(parsed.Content) == "" {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
task.Content = parsed.Content
|
||
|
|
if parsed.Priority != 0 {
|
||
|
|
task.Priority = parsed.Priority
|
||
|
|
}
|
||
|
|
if parsed.DueDate != "" {
|
||
|
|
task.DueDate = parsed.DueDate
|
||
|
|
}
|
||
|
|
if parsed.Project != "" {
|
||
|
|
if id, err := s.db.ProjectByName(ctx, user.ID, parsed.Project); err == nil {
|
||
|
|
if _, err := store.RequireProjectRole(ctx, s.db, id, user.ID, store.RoleEditor); err == nil {
|
||
|
|
task.ProjectID = id
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if subject != "" {
|
||
|
|
task.Description = strings.TrimSpace(subject + "\n\n" + task.Description)
|
||
|
|
}
|
||
|
|
}
|