diff --git a/cmd/integration-test/integration.go b/cmd/integration-test/integration.go index f928d94..2b45764 100644 --- a/cmd/integration-test/integration.go +++ b/cmd/integration-test/integration.go @@ -26,6 +26,7 @@ var ( // "smtp": &smtp{}, // "pushover": &pushover{}, "gotify": &gotify{}, + "notion": ¬ion{}, } ) diff --git a/cmd/integration-test/providers.go b/cmd/integration-test/providers.go index 7290039..40ab502 100644 --- a/cmd/integration-test/providers.go +++ b/cmd/integration-test/providers.go @@ -75,6 +75,12 @@ func (h *gotify) Execute() error { return run("gotify") } +type notion struct{} + +func (h *notion) Execute() error { + return run("notion") +} + func errIncorrectResultsCount(results []string) error { return fmt.Errorf("incorrect number of results %s", strings.Join(results, "\n\t")) } diff --git a/cmd/integration-test/test-config.yaml b/cmd/integration-test/test-config.yaml index ea83b29..2cc5cfc 100644 --- a/cmd/integration-test/test-config.yaml +++ b/cmd/integration-test/test-config.yaml @@ -47,3 +47,8 @@ gotify: gotify_token: "${GOTIFY_APP_TOKEN}" gotify_format: "{{data}}" gotify_disabletls: true +notion: + - id: "notion" + notion_api_key: "${notion_api_key}" + notion_database_id: "${notion_database_id}" + notion_in_page_title: "${notion_in_page_title}" # optional diff --git a/pkg/providers/notion/notion.go b/pkg/providers/notion/notion.go new file mode 100644 index 0000000..5fb4c8b --- /dev/null +++ b/pkg/providers/notion/notion.go @@ -0,0 +1,63 @@ +package notion + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "go.uber.org/multierr" + + "github.com/projectdiscovery/gologger" + sliceutil "github.com/projectdiscovery/utils/slice" +) + +type Provider struct { + Notion []*Options `yaml:"notion,omitempty"` + counter int +} + +type Options struct { + ID string `yaml:"id,omitempty"` + NotionAPIKey string `yaml:"notion_api_key,omitempty"` + NotionDatabaseId string `yaml:"notion_database_id,omitempty"` + NotionInPageTitle string `yaml:"notion_in_page_title,omitempty"` // optional +} + +func New(options []*Options, ids []string) (*Provider, error) { + provider := &Provider{} + + for _, o := range options { + if len(ids) == 0 || sliceutil.Contains(ids, o.ID) { + provider.Notion = append(provider.Notion, o) + } + } + + provider.counter = 0 + + return provider, nil +} + +func (p *Provider) Send(message, CliFormat string) error { + var NotionErr error + for _, pr := range p.Notion { + + newPage := func() Page { + if pr.NotionInPageTitle != "" { + titleWithTimestamp := fmt.Sprintf("%s %s", pr.NotionInPageTitle, time.Now().Format("2006-01-02 15:04:05 MST")) + return pr.CreateInpageText(titleWithTimestamp, message) + } + return pr.CreateNormalText(message) + + }() + + if success := pr.CreatePage(newPage); success != nil { + err := errors.Wrap(fmt.Errorf("failed to send notion notification"), + fmt.Sprintf("failed to send notion notification for id: %s", pr.ID)) + NotionErr = multierr.Append(NotionErr, err) + continue + } + + gologger.Verbose().Msgf("Notion notification sent for id: %s", pr.ID) + } + return NotionErr +} diff --git a/pkg/providers/notion/notion_api.go b/pkg/providers/notion/notion_api.go new file mode 100644 index 0000000..86f0aab --- /dev/null +++ b/pkg/providers/notion/notion_api.go @@ -0,0 +1,92 @@ +package notion + +import ( + "fmt" + "net/http" + "strings" + + "github.com/projectdiscovery/notify/pkg/utils/httpreq" +) + +const notionApiUrl string = "https://api.notion.com/v1/" + +func (options *Options) CreatePage(page Page) error { + url := strings.Join([]string{notionApiUrl, "pages/"}, "") + apiKey := options.NotionAPIKey + body := APIRequest{ + Parent: map[string]interface{}{"database_id": page.ParentId}, + Properties: page.Properties, + Children: page.Children, + } + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Notion-Version": {"2022-02-22"}, + "Authorization": {fmt.Sprintf("Bearer %s", apiKey)}, + } + + var response *APIResponse + + err := httpreq.NewClient().Post(url, &body, headers, &response) + if err != nil { + return err + } + if response.Object != "page" { + return fmt.Errorf("error while sending notion message: %s ", response.Message) + } + + return nil +} + +func (options *Options) CreateNormalText(text string) Page { + newPage := Page{ + ParentId: options.NotionDatabaseId, + Properties: map[string]interface{}{ + "Name": map[string]interface{}{ + "title": []map[string]interface{}{ + { + "text": map[string]string{ + "content": text, + }, + }, + }, + }, + }, + } + + return newPage +} + +func (options *Options) CreateInpageText(tite string, text string) Page { + newPage := Page{ + ParentId: options.NotionDatabaseId, + Properties: map[string]interface{}{ + "Name": map[string]interface{}{ + "title": []map[string]interface{}{ + { + "text": map[string]string{ + "content": tite, + }, + }, + }, + }, + }, + Children: []map[string]interface{}{ + { + "type": "code", + "code": map[string]interface{}{ + "rich_text": []map[string]interface{}{ + { + "type": "text", + "text": map[string]string{ + "content": text, + }, + }, + }, + "language": "bash", + }, + }, + }, + } + return newPage +} diff --git a/pkg/providers/notion/notion_types.go b/pkg/providers/notion/notion_types.go new file mode 100644 index 0000000..cb839ac --- /dev/null +++ b/pkg/providers/notion/notion_types.go @@ -0,0 +1,20 @@ +package notion + + +type Page struct { + ParentId string `json:"parent_id"` + Properties map[string]interface{} `json:"properties"` + Children []map[string]interface{} `json:"children"` +} + +type APIRequest struct { + Parent map[string]interface{} `json:"parent,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` + Children []map[string]interface{} `json:"children,omitempty"` +} + +type APIResponse struct { + Object string `json:"object,omitempty"` + RequestID string `json:"request_id,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/pkg/providers/providers.go b/pkg/providers/providers.go index f90856c..169579a 100644 --- a/pkg/providers/providers.go +++ b/pkg/providers/providers.go @@ -10,6 +10,7 @@ import ( "github.com/projectdiscovery/notify/pkg/providers/discord" "github.com/projectdiscovery/notify/pkg/providers/googlechat" "github.com/projectdiscovery/notify/pkg/providers/gotify" + "github.com/projectdiscovery/notify/pkg/providers/notion" "github.com/projectdiscovery/notify/pkg/providers/pushover" "github.com/projectdiscovery/notify/pkg/providers/slack" "github.com/projectdiscovery/notify/pkg/providers/smtp" @@ -30,6 +31,7 @@ type ProviderOptions struct { GoogleChat []*googlechat.Options `yaml:"googlechat,omitempty"` Custom []*custom.Options `yaml:"custom,omitempty"` Gotify []*gotify.Options `yaml:"gotify,omitempty"` + Notion []*notion.Options `yaml:"notion,omitempty"` } // Provider is an interface implemented by providers @@ -123,6 +125,15 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err client.providers = append(client.providers, provider) } + if providerOptions.Notion != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "notion")) { + + provider, err := notion.New(providerOptions.Notion, options.IDs) + if err != nil { + return nil, errors.Wrap(err, "could not create notion provider client") + } + client.providers = append(client.providers, provider) + } + return client, nil } diff --git a/pkg/utils/httpreq/httpreq.go b/pkg/utils/httpreq/httpreq.go index 017d50f..00638bb 100644 --- a/pkg/utils/httpreq/httpreq.go +++ b/pkg/utils/httpreq/httpreq.go @@ -24,6 +24,7 @@ func (c *Client) Get(url string, response interface{}) error { if err != nil { return fmt.Errorf("error creating request: %v", err) } + defer res.Body.Close() if err := jsoniter.NewDecoder(res.Body).Decode(&response); err != nil { return fmt.Errorf("error trying to unmarshal the response: %v", err) }