249 lines
5.2 KiB
Go
249 lines
5.2 KiB
Go
|
package pushover
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"mime/multipart"
|
||
|
"net/http"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
var deviceNameRegexp *regexp.Regexp
|
||
|
|
||
|
func init() {
|
||
|
deviceNameRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]{1,25}$`)
|
||
|
}
|
||
|
|
||
|
// Message represents a pushover message.
|
||
|
type Message struct {
|
||
|
// Required
|
||
|
Message string
|
||
|
|
||
|
// Optional
|
||
|
Title string
|
||
|
Priority int
|
||
|
URL string
|
||
|
URLTitle string
|
||
|
Timestamp int64
|
||
|
Retry time.Duration
|
||
|
Expire time.Duration
|
||
|
CallbackURL string
|
||
|
DeviceName string
|
||
|
Sound string
|
||
|
HTML bool
|
||
|
Monospace bool
|
||
|
|
||
|
// attachment
|
||
|
attachment io.Reader
|
||
|
}
|
||
|
|
||
|
// NewMessage returns a simple new message.
|
||
|
func NewMessage(message string) *Message {
|
||
|
return &Message{Message: message}
|
||
|
}
|
||
|
|
||
|
// NewMessageWithTitle returns a simple new message with a title.
|
||
|
func NewMessageWithTitle(message, title string) *Message {
|
||
|
return &Message{Message: message, Title: title}
|
||
|
}
|
||
|
|
||
|
// AddAttachment adds an attachment to the message it's programmer's
|
||
|
// responsibility to close the reader.
|
||
|
func (m *Message) AddAttachment(attachment io.Reader) error {
|
||
|
m.attachment = attachment
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Validate the message values.
|
||
|
func (m *Message) validate() error {
|
||
|
// Message should no be empty
|
||
|
if m.Message == "" {
|
||
|
return ErrMessageEmpty
|
||
|
}
|
||
|
|
||
|
// Validate message length
|
||
|
if utf8.RuneCountInString(m.Message) > MessageMaxLength {
|
||
|
return ErrMessageTooLong
|
||
|
}
|
||
|
|
||
|
// Validate Title field length
|
||
|
if utf8.RuneCountInString(m.Title) > MessageTitleMaxLength {
|
||
|
return ErrMessageTitleTooLong
|
||
|
}
|
||
|
|
||
|
// Validate URL field
|
||
|
if utf8.RuneCountInString(m.URL) > MessageURLMaxLength {
|
||
|
return ErrMessageURLTooLong
|
||
|
}
|
||
|
|
||
|
// Validate URL title field
|
||
|
if utf8.RuneCountInString(m.URLTitle) > MessageURLTitleMaxLength {
|
||
|
return ErrMessageURLTitleTooLong
|
||
|
}
|
||
|
|
||
|
// URLTitle should not be set with an empty URL
|
||
|
if m.URL == "" && m.URLTitle != "" {
|
||
|
return ErrEmptyURL
|
||
|
}
|
||
|
|
||
|
// Validate priorities
|
||
|
if m.Priority > PriorityEmergency || m.Priority < PriorityLowest {
|
||
|
return ErrInvalidPriority
|
||
|
}
|
||
|
|
||
|
// Validate emergency priority
|
||
|
if m.Priority == PriorityEmergency {
|
||
|
if m.Retry == 0 || m.Expire == 0 {
|
||
|
return ErrMissingEmergencyParameter
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test device name
|
||
|
if m.DeviceName != "" {
|
||
|
// Accept comma separated device names
|
||
|
devices := strings.Split(m.DeviceName, ",")
|
||
|
for _, d := range devices {
|
||
|
if !deviceNameRegexp.MatchString(d) {
|
||
|
return ErrInvalidDeviceName
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Return a map filled with the relevant data.
|
||
|
func (m *Message) toMap(pToken, rToken string) map[string]string {
|
||
|
ret := map[string]string{
|
||
|
"token": pToken,
|
||
|
"user": rToken,
|
||
|
"message": m.Message,
|
||
|
"priority": strconv.Itoa(m.Priority),
|
||
|
}
|
||
|
|
||
|
if m.Title != "" {
|
||
|
ret["title"] = m.Title
|
||
|
}
|
||
|
|
||
|
if m.URL != "" {
|
||
|
ret["url"] = m.URL
|
||
|
}
|
||
|
|
||
|
if m.URLTitle != "" {
|
||
|
ret["url_title"] = m.URLTitle
|
||
|
}
|
||
|
|
||
|
if m.Sound != "" {
|
||
|
ret["sound"] = m.Sound
|
||
|
}
|
||
|
|
||
|
if m.DeviceName != "" {
|
||
|
ret["device"] = m.DeviceName
|
||
|
}
|
||
|
|
||
|
if m.Timestamp != 0 {
|
||
|
ret["timestamp"] = strconv.FormatInt(m.Timestamp, 10)
|
||
|
}
|
||
|
|
||
|
if m.HTML {
|
||
|
ret["html"] = "1"
|
||
|
}
|
||
|
|
||
|
if m.Monospace {
|
||
|
ret["monospace"] = "1"
|
||
|
}
|
||
|
|
||
|
if m.Priority == PriorityEmergency {
|
||
|
ret["retry"] = strconv.FormatFloat(m.Retry.Seconds(), 'f', -1, 64)
|
||
|
ret["expire"] = strconv.FormatFloat(m.Expire.Seconds(), 'f', -1, 64)
|
||
|
if m.CallbackURL != "" {
|
||
|
ret["callback"] = m.CallbackURL
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
// Send sends the message using the pushover and the recipient tokens.
|
||
|
func (m *Message) send(pToken, rToken string) (*Response, error) {
|
||
|
url := fmt.Sprintf("%s/messages.json", APIEndpoint)
|
||
|
|
||
|
var f func(string, string, string) (*http.Request, error)
|
||
|
if m.attachment == nil {
|
||
|
// Use a URL-encoded request if there's no need to attach files
|
||
|
f = m.urlEncodedRequest
|
||
|
} else {
|
||
|
// Use a multipart request if a file should be sent
|
||
|
f = m.multipartRequest
|
||
|
}
|
||
|
|
||
|
// Post the from and check the headers of the response
|
||
|
req, err := f(pToken, rToken, url)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
resp := &Response{}
|
||
|
if err := do(req, resp, true); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return resp, nil
|
||
|
}
|
||
|
|
||
|
// multipartRequest returns a new multipart POST request with a file attached.
|
||
|
func (m *Message) multipartRequest(pToken, rToken, url string) (*http.Request, error) {
|
||
|
body := &bytes.Buffer{}
|
||
|
|
||
|
if m.attachment == nil {
|
||
|
return nil, ErrMissingAttachment
|
||
|
}
|
||
|
|
||
|
// Write the body as multipart form data
|
||
|
w := multipart.NewWriter(body)
|
||
|
|
||
|
// Write the file in the body
|
||
|
fw, err := w.CreateFormFile("attachment", "attachment")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
written, err := io.Copy(fw, m.attachment)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if written > MessageMaxAttachmentByte {
|
||
|
return nil, ErrMessageAttachmentTooLarge
|
||
|
}
|
||
|
|
||
|
// Handle params
|
||
|
for k, v := range m.toMap(pToken, rToken) {
|
||
|
if err := w.WriteField(k, v); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err := w.Close(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest("POST", url, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
||
|
|
||
|
return req, nil
|
||
|
}
|
||
|
|
||
|
// urlEncodedRequest returns a new url encoded request.
|
||
|
func (m *Message) urlEncodedRequest(pToken, rToken, endpoint string) (*http.Request, error) {
|
||
|
return newURLEncodedRequest("POST", endpoint, m.toMap(pToken, rToken))
|
||
|
}
|