package notifications import ( "fmt" "io" "log/slog" "net/http" neturl "net/url" "os" "path/filepath" "time" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/ningen/v3" ) func Notify(state *ningen.State, message *gateway.MessageCreateEvent, cfg *config.Config) error { if !cfg.Notifications.Enabled || cfg.Status == discord.DoNotDisturbStatus { return nil } mentions := state.MessageMentions(&message.Message) if mentions == 0 { return nil } // Handle sent files content := message.Content if message.Content == "" && len(message.Attachments) > 0 { content = "Uploaded " + message.Attachments[0].Filename } if content == "" { return nil } title := message.Author.DisplayOrUsername() channel, err := state.Cabinet.Channel(message.ChannelID) if err != nil { return fmt.Errorf("failed to get channel from state: %w", err) } if channel.GuildID.IsValid() { guild, err := state.Cabinet.Guild(channel.GuildID) if err != nil { return fmt.Errorf("failed to get guild from state: %w", err) } if member := message.Member; member != nil && member.Nick != "" { title = member.Nick } title += " (#" + channel.Name + ", " + guild.Name + ")" } hash := message.Author.Avatar if hash == "" { hash = "default" } imagePath, err := getCachedProfileImage(hash, message.Author.AvatarURLWithType(discord.PNGImage)) if err != nil { slog.Warn("failed to get profile image from cache for notification", "err", err, "hash", hash) } shouldChime := cfg.Notifications.Sound.Enabled && (!cfg.Notifications.Sound.OnlyOnPing || mentions.Has(ningen.MessageMentions|ningen.MessageNotifies)) if err := sendDesktopNotification(title, content, imagePath, shouldChime, cfg.Notifications.Duration); err != nil { return err } return nil } var avatarHTTPClient = &http.Client{Timeout: 10 * time.Second} func getCachedProfileImage(avatarHash discord.Hash, url string) (string, error) { dir := filepath.Join(consts.CacheDir(), "avatars") if err := os.MkdirAll(dir, 0700); err != nil { return "", err } safeHash := filepath.Base(string(avatarHash)) path := filepath.Join(dir, safeHash+".png") if _, err := os.Stat(path); err == nil { return path, nil } parsed, err := neturl.Parse(url) if err != nil || parsed.Scheme != "https" { return "", fmt.Errorf("refusing non-HTTPS avatar URL") } resp, err := avatarHTTPClient.Get(url) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected status %d fetching avatar", resp.StatusCode) } file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { return "", err } defer file.Close() if _, err := io.Copy(file, io.LimitReader(resp.Body, 10*1024*1024)); err != nil { return "", err } return path, nil }