diff --git a/.travis.yml b/.travis.yml index 4d8ff29..ab5b8bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,6 @@ go: go_import_path: github.com/paked/messenger install: go get -t ./... -script: go test -v ./... +script: + - go test -v ./... + - go build ./examples/... diff --git a/README.md b/README.md index f1e7550..8cb178c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a Go library for making bots to be used on Facebook messenger. It is built on the [Messenger Platform](https://developers.facebook.com/docs/messenger-platform). One of the main goals of the project is to implement it in an idiomatic and easy to use fashion. -[You can find an example of how to use it here](https://github.com/paked/messenger/blob/master/cmd/bot/main.go) +You can find [examples for this library here](https://github.com/paked/messenger/blob/master/examples/). ## Tips diff --git a/actions.go b/actions.go index 53de023..bccac68 100644 --- a/actions.go +++ b/actions.go @@ -20,4 +20,7 @@ const ( OptInAction // ReferralAction represents ?ref parameter in m.me URLs ReferralAction + // AccountLinkingAction means that the event concerns changes in account linking + // status. + AccountLinkingAction ) diff --git a/cmd/bot/config.json.example b/cmd/bot/config.json.example deleted file mode 100644 index 264a73c..0000000 --- a/cmd/bot/config.json.example +++ /dev/null @@ -1,6 +0,0 @@ -{ - "verify-token": "", - "should-verify": false, - "page-token": "", - "app-secret": "" -} diff --git a/cmd/bot/main.go b/examples/basic/main.go similarity index 59% rename from cmd/bot/main.go rename to examples/basic/main.go index 4be7931..f86d6e0 100644 --- a/cmd/bot/main.go +++ b/examples/basic/main.go @@ -1,29 +1,35 @@ package main import ( + "flag" "fmt" + "log" "net/http" + "os" "time" - "github.com/paked/configure" "github.com/paked/messenger" ) var ( - conf = configure.New() - verifyToken = conf.String("verify-token", "mad-skrilla", "The token used to verify facebook") - verify = conf.Bool("should-verify", false, "Whether or not the app should verify itself") - pageToken = conf.String("page-token", "not skrilla", "The token that is used to verify the page on facebook") - appSecret = conf.String("app-secret", "", "The app secret from the facebook developer portal") - port = conf.Int("port", 8080, "The port used to serve the messenger bot") + verifyToken = flag.String("verify-token", "mad-skrilla", "The token used to verify facebook (required)") + verify = flag.Bool("should-verify", false, "Whether or not the app should verify itself") + pageToken = flag.String("page-token", "not skrilla", "The token that is used to verify the page on facebook") + appSecret = flag.String("app-secret", "", "The app secret from the facebook developer portal (required)") + host = flag.String("host", "localhost", "The host used to serve the messenger bot") + port = flag.Int("port", 8080, "The port used to serve the messenger bot") ) func main() { - conf.Use(configure.NewFlag()) - conf.Use(configure.NewEnvironment()) - conf.Use(configure.NewJSONFromFile("config.json")) + flag.Parse() - conf.Parse() + if *verifyToken == "" || *appSecret == "" || *pageToken == "" { + fmt.Println("missing arguments") + fmt.Println() + flag.Usage() + + os.Exit(-1) + } // Create a new messenger client client := messenger.New(messenger.Options{ @@ -55,7 +61,7 @@ func main() { fmt.Println("Read at:", m.Watermark().Format(time.UnixDate)) }) - fmt.Printf("Serving messenger bot on localhost:%d\n", *port) - - http.ListenAndServe(fmt.Sprintf("localhost:%d", *port), client.Handler()) + addr := fmt.Sprintf("%s:%d", *host, *port) + log.Println("Serving messenger bot on", addr) + log.Fatal(http.ListenAndServe(addr, client.Handler())) } diff --git a/examples/linked-account/main.go b/examples/linked-account/main.go new file mode 100644 index 0000000..cfde079 --- /dev/null +++ b/examples/linked-account/main.go @@ -0,0 +1,246 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/paked/messenger" +) + +const ( + webhooksPath = "/webhooks" + loginPath = "/signin" + logoutPath = "/signout" + + validUsername = "john" + validPassword = "secret" +) + +var ( + verifyToken = flag.String("verify-token", "", "The token used to verify facebook (required)") + pageToken = flag.String("page-token", "", "The token that is used to verify the page on facebook.") + appSecret = flag.String("app-secret", "", "The app secret from the facebook developer portal (required)") + host = flag.String("host", "localhost", "The host used to serve the messenger bot") + port = flag.Int("port", 8080, "The port used to serve the messenger bot") + publicHost = flag.String("public-host", "example.org", "The public facing host used to access the messenger bot") +) + +func main() { + flag.Parse() + + if *verifyToken == "" || *appSecret == "" || *pageToken == "" { + fmt.Println("missing arguments") + fmt.Println() + flag.Usage() + + os.Exit(-1) + } + + // Instantiate messenger client + client := messenger.New(messenger.Options{ + AppSecret: *appSecret, + VerifyToken: *verifyToken, + Token: *pageToken, + }) + + // Handle incoming messages + client.HandleMessage(func(m messenger.Message, r *messenger.Response) { + log.Printf("%v (Sent, %v)\n", m.Text, m.Time.Format(time.UnixDate)) + + p, err := client.ProfileByID(m.Sender.ID) + if err != nil { + log.Println("Failed to fetch user profile:", err) + } + + switch strings.ToLower(m.Text) { + case "login": + err = loginButton(r) + case "logout": + err = logoutButton(r) + case "help": + err = help(p, r) + default: + err = greeting(p, r) + } + + if err != nil { + log.Println("Failed to respond:", err) + } + }) + + // Send a feedback to the user after an update of account linking status + client.HandleAccountLinking(func(m messenger.AccountLinking, r *messenger.Response) { + var text string + switch m.Status { + case "linked": + text = "Hey there! You're now logged in :)" + case "unlinked": + text = "You've been logged out of your account." + } + + if err := r.Text(text, messenger.ResponseType); err != nil { + log.Println("Failed to send account linking feedback") + } + }) + + // Setup router + mux := http.NewServeMux() + mux.Handle(webhooksPath, client.Handler()) + mux.HandleFunc(loginPath, func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + loginForm(w, r) + case "POST": + login(w, r) + } + }) + + // Listen + addr := fmt.Sprintf("%s:%d", *host, *port) + log.Println("Serving messenger bot on", addr) + log.Fatal(http.ListenAndServe(addr, mux)) +} + +// loginButton will present to the user a button that can be used to +// start the account linking process. +func loginButton(r *messenger.Response) error { + buttons := &[]messenger.StructuredMessageButton{ + { + Type: "account_link", + URL: "https://" + path.Join(*publicHost, loginPath), + }, + } + return r.ButtonTemplate("Link your account.", buttons, messenger.ResponseType) +} + +// logoutButton show to the user a button that can be used to start +// the process of unlinking an account. +func logoutButton(r *messenger.Response) error { + buttons := &[]messenger.StructuredMessageButton{ + { + Type: "account_unlink", + }, + } + return r.ButtonTemplate("Unlink your account.", buttons, messenger.ResponseType) +} + +// greeting salutes the user. +func greeting(p messenger.Profile, r *messenger.Response) error { + return r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType) +} + +// help displays possibles actions to the user. +func help(p messenger.Profile, r *messenger.Response) error { + text := fmt.Sprintf( + "%s, looking for actions to do? Here is what I understand.", + p.FirstName, + ) + + replies := []messenger.QuickReply{ + { + ContentType: "text", + Title: "Login", + }, + { + ContentType: "text", + Title: "Logout", + }, + } + + return r.TextWithReplies(text, replies, messenger.ResponseType) +} + +// loginForm is the endpoint responsible to displays a login +// form. During the account linking process, after clicking on the +// login button, users are directed to this form where they are +// supposed to sign into their account. When the form is submitted, +// credentials are sent to the login endpoint. +func loginForm(w http.ResponseWriter, r *http.Request) { + values := r.URL.Query() + linkingToken := values.Get("account_linking_token") + redirectURI := values.Get("redirect_uri") + fmt.Fprint(w, templateLogin(loginPath, linkingToken, redirectURI, false)) +} + +// login is the endpoint that handles the actual signing in, by +// checking the credentials, then redirecting to Facebook Messenger if +// they are valid. +func login(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + username := r.FormValue("username") + password := r.FormValue("password") + linkingToken := r.FormValue("account_linking_token") + rawRedirect := r.FormValue("redirect_uri") + + if !checkCredentials(username, password) { + fmt.Fprint(w, templateLogin(loginPath, linkingToken, rawRedirect, true)) + return + } + + redirectURL, err := url.Parse(rawRedirect) + if err != nil { + log.Println("failed to parse url:", err) + return + } + + q := redirectURL.Query() + q.Set("authorization_code", "something") + redirectURL.RawQuery = q.Encode() + + w.Header().Set("Location", redirectURL.String()) + w.WriteHeader(http.StatusFound) +} + +func checkCredentials(username, password string) bool { + return username == validUsername && password == validPassword +} + +// templateLogin constructs the signin form. +func templateLogin(loginPath, linkingToken, redirectURI string, failed bool) string { + failedInfo := "" + if failed { + failedInfo = `
Incorrect credentials
` + } + + template := ` + + + + + ++ Valid credentials are "%s" as the username and "%s" as the password +
+ %s + +