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 + +