Merge pull request #39 from dgellow/add-account-linking-handler
Implement support for AccountLinking process
This commit is contained in:
commit
65752d0a98
@ -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/...
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"verify-token": "",
|
||||
"should-verify": false,
|
||||
"page-token": "",
|
||||
"app-secret": ""
|
||||
}
|
@ -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()))
|
||||
}
|
246
examples/linked-account/main.go
Normal file
246
examples/linked-account/main.go
Normal file
@ -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 = `<p class="alert alert-danger">Incorrect credentials</p>`
|
||||
}
|
||||
|
||||
template := `
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Login to your account</h1>
|
||||
<p class="alert alert-primary">
|
||||
Valid credentials are "%s" as the username and "%s" as the password
|
||||
</p>
|
||||
%s
|
||||
<form action="%s" method="POST">
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<input type="hidden" name="account_linking_token" value="%s">
|
||||
<input type="hidden" name="redirect_uri" value="%s">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
validUsername,
|
||||
validPassword,
|
||||
failedInfo,
|
||||
loginPath,
|
||||
linkingToken,
|
||||
redirectURI,
|
||||
)
|
||||
}
|
13
message.go
13
message.go
@ -60,6 +60,19 @@ type PostBack struct {
|
||||
Referral Referral `json:"referral"`
|
||||
}
|
||||
|
||||
type AccountLinking struct {
|
||||
// Sender is who the message was sent from.
|
||||
Sender Sender `json:"-"`
|
||||
// Recipient is who the message was sent to.
|
||||
Recipient Recipient `json:"-"`
|
||||
// Time is when the message was sent.
|
||||
Time time.Time `json:"-"`
|
||||
// Status represents the new account linking status.
|
||||
Status string `json:"status"`
|
||||
// AuthorizationCode is a pass-through code set during the linking process.
|
||||
AuthorizationCode string `json:"authorization_code"`
|
||||
}
|
||||
|
||||
// Watermark is the RawWatermark timestamp rendered as a time.Time.
|
||||
func (d Delivery) Watermark() time.Time {
|
||||
return time.Unix(d.RawWatermark/int64(time.Microsecond), 0)
|
||||
|
42
messenger.go
42
messenger.go
@ -59,19 +59,24 @@ type OptInHandler func(OptIn, *Response)
|
||||
// ReferralHandler is a handler used postback callbacks.
|
||||
type ReferralHandler func(ReferralMessage, *Response)
|
||||
|
||||
// AccountLinkingHandler is a handler used to react to an account
|
||||
// being linked or unlinked.
|
||||
type AccountLinkingHandler func(AccountLinking, *Response)
|
||||
|
||||
// Messenger is the client which manages communication with the Messenger Platform API.
|
||||
type Messenger struct {
|
||||
mux *http.ServeMux
|
||||
messageHandlers []MessageHandler
|
||||
deliveryHandlers []DeliveryHandler
|
||||
readHandlers []ReadHandler
|
||||
postBackHandlers []PostBackHandler
|
||||
optInHandlers []OptInHandler
|
||||
referralHandlers []ReferralHandler
|
||||
token string
|
||||
verifyHandler func(http.ResponseWriter, *http.Request)
|
||||
verify bool
|
||||
appSecret string
|
||||
mux *http.ServeMux
|
||||
messageHandlers []MessageHandler
|
||||
deliveryHandlers []DeliveryHandler
|
||||
readHandlers []ReadHandler
|
||||
postBackHandlers []PostBackHandler
|
||||
optInHandlers []OptInHandler
|
||||
referralHandlers []ReferralHandler
|
||||
accountLinkingHandlers []AccountLinkingHandler
|
||||
token string
|
||||
verifyHandler func(http.ResponseWriter, *http.Request)
|
||||
verify bool
|
||||
appSecret string
|
||||
}
|
||||
|
||||
// New creates a new Messenger. You pass in Options in order to affect settings.
|
||||
@ -131,6 +136,11 @@ func (m *Messenger) HandleReferral(f ReferralHandler) {
|
||||
m.referralHandlers = append(m.referralHandlers, f)
|
||||
}
|
||||
|
||||
// HandleAccountLinking adds a new AccountLinkingHandler to the Messenger
|
||||
func (m *Messenger) HandleAccountLinking(f AccountLinkingHandler) {
|
||||
m.accountLinkingHandlers = append(m.accountLinkingHandlers, f)
|
||||
}
|
||||
|
||||
// Handler returns the Messenger in HTTP client form.
|
||||
func (m *Messenger) Handler() http.Handler {
|
||||
return m.mux
|
||||
@ -370,6 +380,14 @@ func (m *Messenger) dispatch(r Receive) {
|
||||
message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0)
|
||||
f(message, resp)
|
||||
}
|
||||
case AccountLinkingAction:
|
||||
for _, f := range m.accountLinkingHandlers {
|
||||
message := *info.AccountLinking
|
||||
message.Sender = info.Sender
|
||||
message.Recipient = info.Recipient
|
||||
message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0)
|
||||
f(message, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -431,6 +449,8 @@ func (m *Messenger) classify(info MessageInfo, e Entry) Action {
|
||||
return OptInAction
|
||||
} else if info.ReferralMessage != nil {
|
||||
return ReferralAction
|
||||
} else if info.AccountLinking != nil {
|
||||
return AccountLinkingAction
|
||||
}
|
||||
return UnknownAction
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ type MessageInfo struct {
|
||||
OptIn *OptIn `json:"optin"`
|
||||
|
||||
ReferralMessage *ReferralMessage `json:"referral"`
|
||||
|
||||
AccountLinking *AccountLinking `json:"account_linking"`
|
||||
}
|
||||
|
||||
type OptIn struct {
|
||||
|
Loading…
Reference in New Issue
Block a user