package zoptions import ( "fmt" "io" "os" "time" "gitlab.bareksa.com/backend/zen/internal/bufferpool" ) type NotifyParameters struct { // AlwaysSend makes sure this message when set to true // is always sent no matter the condition. // // It will cause `zen` to ignore every other // parameters and just send the message. AlwaysSend bool // InitialBackoff is the starting backoff. // // If not specified, the default duration is time.Minute. InitialBackoff time.Duration // MaxBackoff is the maximum backoff. If unset, the default value // is time.Hour * 24. MaxBackoff time.Duration // BackoffFormula is a string that represents the formula to calculate the backoff time // when multiple message with the same key is fired off repeatedly. // // It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go // for the language spec. // // Expression must return a duration type. // // If invalid cel expression, empty string, or wrong return value, default formula will be used. // // Available variables: // // - `repeat`: the number of times the notification has been sent. type: int. // - `last`: the last time the notification was sent. type: timestamp. // - `prev_backoff`: the backoff value from previous evaluation. 0 if first seen. Type: duration. // - `initial_backoff`: the initial_backoff value. Type: duration // - `max_backoff`: the maximum backoff value. Type: duration // // Available Non-Standard functions: // // // pow multiplies the base with the exponent // pow(base: double, exp: double) -> double // // // mult_dur_double multiplies the duration with the double value // mult_dur_double(dur: duration, mult: double) -> duration // // // Example (and also default formula): // // mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff // ? max_backoff // : mult_dur_double(initial_backoff, pow(1.5, double(repeat))) BackoffFormula string // Attachments is a list of NamedReader (files or buffer) that will be sent as attachments. // // Attachments are guaranteed to be closed wether the notification is sent successfully or not // to avoid resource leaks. Attachments []NamedReadCloser } type NotifyOption interface { Apply(parameters *NotifyParameters) } // NotifyOptionFunc is a shortcut to create a NotifyOption from a function. type NotifyOptionFunc func(parameters *NotifyParameters) func (f NotifyOptionFunc) Apply(parameters *NotifyParameters) { f(parameters) } type NotifiyOptionBuilder []NotifyOption func (no NotifiyOptionBuilder) Apply(parameters *NotifyParameters) { for _, opt := range no { opt.Apply(parameters) } } // AlwaysSend makes sure this message when set to true // is always sent no matter the condition. // // It will cause `zen` to ignore every other // parameters and just send the message. func (no NotifiyOptionBuilder) AlwaysSend(alwaysSend bool) NotifiyOptionBuilder { return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { parameters.AlwaysSend = alwaysSend })) } // InitialBackoff is the starting backoff. // // If not specified, the default duration is time.Minute. func (no NotifiyOptionBuilder) InitialBackoff(backoff time.Duration) NotifiyOptionBuilder { return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { parameters.InitialBackoff = backoff })) } // MaxBackoff is the maximum backoff. If unset, the default value // is time.Hour * 24. func (no NotifiyOptionBuilder) MaxBackoff(maxBackoff time.Duration) NotifiyOptionBuilder { return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { parameters.MaxBackoff = maxBackoff })) } // Attachments is a list attachments (files or buffers) that will be sent with the notification. // // Attachments are guaranteed to be closed wether the notification is sent successfully or not // to avoid resource leaks. func (no NotifiyOptionBuilder) Attachments(attachments ...NamedReadCloser) NotifiyOptionBuilder { return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { parameters.Attachments = append(parameters.Attachments, attachments...) })) } // BackoffFormula is a string that represents the formula to calculate the backoff time // when multiple message with the same key is fired off repeatedly. // // It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go // for the language spec. // // Expression must return a duration type. // // If invalid cel expression, empty string, or wrong return value, default formula will be used. // // Available variables: // // - `repeat`: the number of times the notification has been sent. type: int. // - `last`: the last time the notification was sent. type: timestamp. // - `prev_backoff`: the backoff value from previous evaluation. 0 if this message is first seen. Type: duration. // - `initial_backoff`: the initial_backoff value. Type: duration // - `max_backoff`: the maximum backoff value. Type: duration // // Available Non-Standard functions: // // // pow multiplies the base with the exponent // pow(base: double, exp: double) -> double // // // mult_dur_double multiplies the duration with the double value // mult_dur_double(dur: duration, mult: double) -> duration // // Example (and also default formula): // // mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff // ? max_backoff // : mult_dur_double(initial_backoff, pow(1.5, double(repeat))) func (no NotifiyOptionBuilder) BackoffFormula(formula string) NotifiyOptionBuilder { return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) { parameters.BackoffFormula = formula })) } func Notify() NotifiyOptionBuilder { return NotifiyOptionBuilder{} } // NamedReadCloser is an interface that extends io.ReadCloser with // Filename and MimeType methods. // // This is used to pass a file with its metadata to the notification // system. type NamedReadCloser interface { io.ReadCloser // Filename returns the name of the file. // // Filename will be sluggified and used as the filename // in the notification. Filename() string // MimeType returns the mime type of the file. // // MimeType is used by Zen to determine the type of the file. // // Use constant values from: https://github.com/ldez/mimetype // for maximum compatibility. // // Attachment like images, videos, etc, using well known mime types // that are supported by browsers can // be displayed by the browser. // // Mimetypes that are supported by browsers: // // - image/png // - image/jpeg // - image/gif // - image/svg+xml // - video/mp4 // - video/webm // - video/ogg // - audio/mpeg // - audio/ogg // - audio/wav // // Documents like pdf, docx, xlsx, will be given a download link and proper icons. // How they are handled is up to the browser. // // If the mimetype is not supported, it will be given a download link with a generic icon. MimeType() string } // NewNamedReadCloser creates a new NamedReader from the given filename, mimeType and reader. // // If reader implements io.Closer, it will be closed when the NamedReadCloser is closed. // // Otherwise, the reader will be wrapped with io.NopCloser. // // NewNamedReadCloser claims ownership of the reader and the caller should not use the reader // after passing it to NewNamedReadCloser. // // To create a NamedReadCloser that can copy the reader as it's consumed, e.g. the reader // is used for HTTP.Request.Body for example, use TeeNamedReader. func NewNamedReadCloser(filename, mimeType string, reader io.Reader) NamedReadCloser { var rc io.ReadCloser if c, ok := reader.(io.ReadCloser); ok { rc = c } else { rc = io.NopCloser(reader) } return &namedReadCloser{ filename: filename, mimeType: mimeType, reader: rc, } } // NewNamedReadCloserFromFile same as NewNamedReader but reads the file from the filesystem. // // If the file does not exist or there is an error in permissions on reading the file, // the error will be logged but otherwise ignored. // // Example: // // NewNamedReadCloserFromFile("path/to/file.txt", "text/plain") func NewNamedReadCloserFromFile(path string, mimetype string) NamedReadCloser { var rc io.ReadCloser f, err := os.Open(path) if err != nil { rc = &errorReader{err} } else { if stat, err := f.Stat(); err == nil && stat.IsDir() { f.Close() rc = &errorReader{fmt.Errorf("file %s is a directory", path)} } else { rc = f } } return &namedReadCloser{ filename: path, mimeType: mimetype, reader: rc, } } type readCloser struct { tee io.Reader close func() error } // Read implements io.Reader. func (re *readCloser) Read(p []byte) (n int, err error) { return re.tee.Read(p) } // Close implements io.Closer. func (re *readCloser) Close() error { return re.close() } // TeeNamedReadCloser copies everything read from returned rc to namedReadCloser as it's consumed. // // Useful for e.g. HTTP Request Body. // // if reader is io.ReadCloser, it will be closed when the returned rc is closed (NOT namedReadCloser!). // Otherwise, the reader will be wrapped with io.NopCloser. // // Example HTTP Request Body: // // // tee will forward the close method if the original reader is a ReadCloser. // tee, named := zoptions.TeeNamedReadCloser("file.txt", "text/plain", body) // req, _ := http.NewRequest("POST", "http://example.com", tee) // use tee as the body. // // resp, err := http.DefaultClient.Do(req) // // named now contains copy of values of the reader // // and can be used to send the file as attachment. func TeeNamedReadCloser(name, mimeType string, reader io.Reader) (rc io.ReadCloser, namedReadCloser NamedReadCloser) { buf := bufferpool.Get() if e, ok := reader.(interface{ Len() int }); ok { buf.Grow(e.Len()) } var closeFunc func() error if c, ok := reader.(io.ReadCloser); ok { closeFunc = c.Close } else { closeFunc = func() error { return nil } } tee := io.TeeReader(reader, buf) teeReader := &readCloser{ tee: tee, close: closeFunc, } nrc := NewNamedReadCloser(name, mimeType, buf) return teeReader, nrc } type namedReadCloser struct { filename string mimeType string reader io.ReadCloser } func (na *namedReadCloser) Read(p []byte) (n int, err error) { return na.reader.Read(p) } func (na *namedReadCloser) Close() error { return na.reader.Close() } func (na *namedReadCloser) Filename() string { return na.filename } func (na *namedReadCloser) MimeType() string { return na.mimeType } type errorReader struct { err error } func (er *errorReader) Read(p []byte) (n int, err error) { return 0, er.err } func (er *errorReader) Close() error { return nil }