diff --git a/service/metaworkplace/README.md b/service/metaworkplace/README.md new file mode 100644 index 00000000..dd61213d --- /dev/null +++ b/service/metaworkplace/README.md @@ -0,0 +1,68 @@ +## Meta Workplace Service + +### Usage +* You must have a Meta Workplace access token to use this. +* You must be part of an existing thread/chat with a user to use this. +* If you are not in a thread/chat with a user already you must use AddUsers(). +* Unless AddUsers() is called again subsequent calls to SendMessage() will send to the same thread/chat. + +```go +package main + +import ( + "context" + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/metaworkplace" + "log" +) + +func main() { + // Create a new notifier + notifier := notify.New() + + accesstoken := "your_access_token" + + // Create a new Workplace service. + wps := metaworkplace.New(accesstoken) + + wps.AddUsers("some_user_id") + wps.AddThreads("t_somethreadid", "t_somethreadid") + + notifier.UseServices(wps) + + err := notifier.Send(context.Background(), "", "our first message") + if err != nil { + log.Fatal(err) + } + + err = notifier.Send(context.Background(), "", "our second message") + if err != nil { + log.Fatal(err) + } + + wps.AddUsers("some_other_user_id") + + err = notifier.Send(context.Background(), "", "our third message") + if err != nil { + log.Fatal(err) + } +} +``` + + +#### Please consider the following: +* Meta Workplace service does not allow sending messages to group chats +* Meta Workplace service does not allow sending messages to group posts +* Meta Workplace service does not allow sending messages to users that are not already in a thread/chat with you +* Testing still needs to be added; the scenarios below will all fail. Those without a user/thread id should fail. Those +with a valid user/thread id should succeed and return an error for the invalid user/thread id(s). Here are the scenarios: + * wps.AddUsers("", "123456789012345") + wps.AddUsers("", "123456789012345", "") + wps.AddUsers("") + * wps.AddThreads("", "123456789012345") + wps.AddThreads("", "123456789012345", "") + wps.AddThreads("") + +## Contributors +- [Melvin Hillsman](github.com/mrhillsman) +- [Ainsley Clark](github.com/ainsleyclark) diff --git a/service/metaworkplace/metaworkplace.go b/service/metaworkplace/metaworkplace.go new file mode 100644 index 00000000..f1c4938d --- /dev/null +++ b/service/metaworkplace/metaworkplace.go @@ -0,0 +1,171 @@ +package metaworkplace + +import ( + "bytes" + "context" + "encoding/json" + "github.com/pkg/errors" + "io" + "log" + "net/http" + "time" +) + +const ( + // ENDPOINT is the base URL of the Workplace API to send messages. + ENDPOINT = "https://graph.facebook.com/me/messages" +) + +// metaWorkplaceService is the internal implementation of the Meta Workplace notification service. +// +//go:generate mockery --name=metaWorkplaceService --output=. --case=underscore --inpackage +type metaWorkplaceService interface { + send(payload interface{}) *MetaWorkplaceResponse +} + +// ValidateConfig checks if the required configuration data is present. +func (sc *MetaWorkplaceServiceConfig) ValidateConfig() error { + if sc.AccessToken == "" { + return errors.New("a valid Meta Workplace access token is required") + } + return nil +} + +// New returns a new instance of a Meta Workplace notification service. +func New(token string) *MetaWorkplaceService { + serviceConfig := MetaWorkplaceServiceConfig{ + AccessToken: token, + Endpoint: ENDPOINT, + } + + err := serviceConfig.ValidateConfig() + if err != nil { + log.Fatalf("failed to validate Meta Workplace service configuration: %v", err) + } + + return &MetaWorkplaceService{ + MetaWorkplaceServiceConfig: serviceConfig, + userIDs: []string{}, + threadIDs: []string{}, + client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// AddThreads takes Workplace thread IDs and adds them to the internal thread ID list. The Send method will send +// a given message to all those threads. +func (mw *MetaWorkplaceService) AddThreads(threadIDs ...string) { + mw.threadIDs = append(mw.threadIDs, threadIDs...) +} + +// AddUsers takes user IDs and adds them to the internal user ID list. The Send method will send +// a given message to all those users. +func (mw *MetaWorkplaceService) AddUsers(userIDs ...string) { + mw.userIDs = append(mw.userIDs, userIDs...) +} + +// send takes a payload, sends it to the Workplace API, and returns the response. +func (mw *MetaWorkplaceService) send(payload interface{}) *MetaWorkplaceResponse { + data, err := json.Marshal(payload) + if err != nil { + response := MetaWorkplaceResponse{ + MessageID: "", + ThreadKey: "", + Error: &MetaWorkplaceErrorResponse{ + Message: "failed to marshal payload", + }, + } + return &response + } + + buff := bytes.NewBuffer(data) + + req, err := http.NewRequest(http.MethodPost, mw.Endpoint, buff) + if err != nil { + log.Println("failed to create new HTTP request") + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+mw.AccessToken) + + res, err := mw.client.Do(req) + if err != nil { + log.Printf("failed to send HTTP request: %v", err) + } + + defer func(Body io.ReadCloser) { + err = Body.Close() + if err != nil { + log.Printf("failed to close response body: %v", err) + } + }(res.Body) + + data, err = io.ReadAll(res.Body) + if err != nil { + log.Printf("failed to read response body: %v", err) + } + + var response MetaWorkplaceResponse + + err = json.Unmarshal(data, &response) + if err != nil { + log.Printf("failed to unmarshal response body: %v", err) + } + + return &response +} + +// Send takes a message and sends it to the provided user and/or thread IDs. +func (mw *MetaWorkplaceService) Send(ctx context.Context, subject string, message string) error { + if len(mw.userIDs) == 0 && len(mw.threadIDs) == 0 { + return errors.New("no user or thread IDs provided") + } + + if len(mw.threadIDs) != 0 { + for _, threadID := range mw.threadIDs { + select { + case <-ctx.Done(): + return ctx.Err() + default: + payload := metaWorkplaceThreadPayload{ + Message: metaWorkplaceMessage{Text: message}, + Recipient: metaWorkplaceThread{ThreadID: threadID}, + } + err := mw.send(payload) + if err.Error != nil { + log.Printf("%+v\n", err.Error) + return errors.New("failed to send message to Workplace thread: " + threadID) + } + } + } + } + + if len(mw.userIDs) != 0 { + for _, userID := range mw.userIDs { + select { + case <-ctx.Done(): + return ctx.Err() + default: + payload := metaWorkplaceUserPayload{ + Message: metaWorkplaceMessage{Text: message}, + Recipient: metaWorkplaceUsers{UserIDs: []string{userID}}, + } + err := mw.send(payload) + if err.Error != nil { + log.Printf("%+v\n", err.Error) + return errors.New("failed to send message to Workplace user: " + userID) + } + + // Capture the thread ID for the user and add it to the thread ID list. Subsequent + // messages will be sent to the thread instead of creating a new thread for the user. + mw.threadIDs = append(mw.threadIDs, err.ThreadKey) + } + } + + // Clear the user ID list. Subsequent messages will be sent to the thread instead of creating a new thread for the user. + mw.userIDs = []string{} + } + + return nil +} diff --git a/service/metaworkplace/metaworkplace_test.go b/service/metaworkplace/metaworkplace_test.go new file mode 100644 index 00000000..bcbf1704 --- /dev/null +++ b/service/metaworkplace/metaworkplace_test.go @@ -0,0 +1 @@ +package metaworkplace diff --git a/service/metaworkplace/types.go b/service/metaworkplace/types.go new file mode 100644 index 00000000..42ebf516 --- /dev/null +++ b/service/metaworkplace/types.go @@ -0,0 +1,69 @@ +package metaworkplace + +import ( + "net/http" +) + +type ( + // MetaWorkplaceErrorResponse is a custom error type for the Meta Workplace service. This struct should be filled when + // a status code other than 200 is received from the Meta Workplace API. + MetaWorkplaceErrorResponse struct { + Message string `json:"message"` + Type string `json:"type"` + Code int `json:"code"` + ErrorSubcode int `json:"error_subcode"` + FbtraceID string `json:"fbtrace_id"` + } + + // MetaWorkplaceResponse is a custom response type for the Meta Workplace service. Only the MessageID and + // ThreadKey fields should be filled when a 200 response is received from the Meta Workplace API. + MetaWorkplaceResponse struct { + MessageID string `json:"message_id"` + ThreadKey string `json:"thread_key"` + Error *MetaWorkplaceErrorResponse `json:"error"` + } + + // MetaWorkplaceService struct holds necessary data to communicate with Meta Workplace users and/or threads. + MetaWorkplaceService struct { + MetaWorkplaceServiceConfig + userIDs []string + threadIDs []string + client *http.Client + } + + // MetaWorkplaceServiceConfig holds the required configuration data when creating a connection to the Meta + // Workplace API. + MetaWorkplaceServiceConfig struct { + AccessToken string + Endpoint string + } + + // metaWorkplaceUserPayload is a custom payload type for the Meta Workplace service. This struct should be filled + // when sending a message to a user. + metaWorkplaceUserPayload struct { + Message metaWorkplaceMessage `json:"message"` + Recipient metaWorkplaceUsers `json:"recipient"` + } + + // metaWorkplaceUserPayload is a custom payload type for the Meta Workplace service. This struct should be filled + // when sending a message to a thread. + metaWorkplaceThreadPayload struct { + Message metaWorkplaceMessage `json:"message"` + Recipient metaWorkplaceThread `json:"recipient"` + } + + // metaWorkplaceUsers holds the user IDs to send a message to. + metaWorkplaceUsers struct { + UserIDs []string `json:"ids"` + } + + // metaWorkplaceThread holds the thread ID to send a message to. + metaWorkplaceThread struct { + ThreadID string `json:"thread_key"` + } + + // metaWorkplaceMessage holds the message to send. + metaWorkplaceMessage struct { + Text string `json:"text"` + } +)