package discord import ( "bytes" "io" "strings" "plumber/internal/events" ) const ( embedTitleLimit = 256 embedDescriptionLimit = 4096 threadNameLimit = 100 embedColor = 0xe96a26 ) // Attachment is a local file Discord should upload with the message. type Attachment struct { Name string ContentType string Kind string Reader io.Reader } // Message is a Discord-ready snapshot of a site post event. type Message struct { Title string URL string Description string City string Author string ImageURLs []string VideoURLs []string Files []Attachment ThreadName string } func formatMessage(ev events.PostEvent) Message { return formatCreated(events.PostCreated{PostEvent: ev}) } func formatCreated(e events.PostCreated) Message { title := strings.TrimSpace(e.Title) if title == "" { title = "Reply" } author := strings.TrimSpace(e.AuthorName) if author == "" { author = "Someone" } msg := Message{ Title: truncateRunes(title, embedTitleLimit), URL: strings.TrimSpace(e.Permalink), Description: truncateRunes(strings.TrimSpace(e.Body), embedDescriptionLimit), City: strings.TrimSpace(e.City), Author: author, ThreadName: threadName(author, e.Title), Files: filesFromMedia(e.Media), } for _, img := range e.Images { url := strings.TrimSpace(img.URL) if url == "" { continue } if img.Kind == "video" { msg.VideoURLs = append(msg.VideoURLs, url) continue } msg.ImageURLs = append(msg.ImageURLs, url) } return msg } func filesFromMedia(media []events.Media) []Attachment { if len(media) == 0 { return nil } files := make([]Attachment, 0, len(media)) for _, m := range media { var r io.Reader switch { case len(m.Bytes) > 0: r = bytes.NewReader(m.Bytes) case m.Body != nil: r = m.Body default: continue } name := strings.TrimSpace(m.Name) if name == "" { name = "upload" } files = append(files, Attachment{ Name: name, ContentType: m.ContentType, Kind: m.Kind, Reader: r, }) } return files } func threadName(author, title string) string { author = strings.TrimSpace(author) if author == "" { author = "Someone" } title = strings.TrimSpace(title) if title == "" { title = "Question" } return truncateRunes(author+" asks: "+title, threadNameLimit) } func truncateRunes(s string, max int) string { if max <= 0 { return "" } runes := []rune(s) if len(runes) <= max { return s } return string(runes[:max]) } func isRoot(ev events.PostEvent) bool { return strings.TrimSpace(ev.ParentID) == "" } func messageContent(msg Message) string { var parts []string if body := strings.TrimSpace(msg.Description); body != "" { parts = append(parts, body) } var meta []string if msg.City != "" { meta = append(meta, msg.City) } if msg.Author != "" { meta = append(meta, msg.Author) } if len(meta) > 0 { parts = append(parts, strings.Join(meta, " ยท ")) } if u := publicURL(msg.URL); u != "" { parts = append(parts, u) } if len(msg.Files) == 0 { for _, mediaURL := range msg.ImageURLs { if u := publicURL(mediaURL); u != "" { parts = append(parts, u) } } for _, mediaURL := range msg.VideoURLs { if u := publicURL(mediaURL); u != "" { parts = append(parts, u) } } } return truncateRunes(strings.Join(parts, "\n"), 2000) } func publicURL(raw string) string { raw = strings.TrimSpace(raw) if !strings.HasPrefix(raw, "https://") { return "" } if strings.Contains(raw, "localhost") || strings.Contains(raw, "127.0.0.1") { return "" } return raw }