1
0
mirror of synced 2024-11-29 08:36:12 +03:00

Merge pull request #39 from dgellow/add-account-linking-handler

Implement support for AccountLinking process
This commit is contained in:
Harrison Shoebridge 2018-03-26 10:41:55 +11:00 committed by GitHub
commit 65752d0a98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 319 additions and 33 deletions

View File

@ -6,4 +6,6 @@ go:
go_import_path: github.com/paked/messenger go_import_path: github.com/paked/messenger
install: go get -t ./... install: go get -t ./...
script: go test -v ./... script:
- go test -v ./...
- go build ./examples/...

View File

@ -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. 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 ## Tips

View File

@ -20,4 +20,7 @@ const (
OptInAction OptInAction
// ReferralAction represents ?ref parameter in m.me URLs // ReferralAction represents ?ref parameter in m.me URLs
ReferralAction ReferralAction
// AccountLinkingAction means that the event concerns changes in account linking
// status.
AccountLinkingAction
) )

View File

@ -1,6 +0,0 @@
{
"verify-token": "",
"should-verify": false,
"page-token": "",
"app-secret": ""
}

View File

@ -1,29 +1,35 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os"
"time" "time"
"github.com/paked/configure"
"github.com/paked/messenger" "github.com/paked/messenger"
) )
var ( var (
conf = configure.New() verifyToken = flag.String("verify-token", "mad-skrilla", "The token used to verify facebook (required)")
verifyToken = conf.String("verify-token", "mad-skrilla", "The token used to verify facebook") verify = flag.Bool("should-verify", false, "Whether or not the app should verify itself")
verify = conf.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")
pageToken = conf.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)")
appSecret = conf.String("app-secret", "", "The app secret from the facebook developer portal") host = flag.String("host", "localhost", "The host used to serve the messenger bot")
port = conf.Int("port", 8080, "The port used to serve the messenger bot") port = flag.Int("port", 8080, "The port used to serve the messenger bot")
) )
func main() { func main() {
conf.Use(configure.NewFlag()) flag.Parse()
conf.Use(configure.NewEnvironment())
conf.Use(configure.NewJSONFromFile("config.json"))
conf.Parse() if *verifyToken == "" || *appSecret == "" || *pageToken == "" {
fmt.Println("missing arguments")
fmt.Println()
flag.Usage()
os.Exit(-1)
}
// Create a new messenger client // Create a new messenger client
client := messenger.New(messenger.Options{ client := messenger.New(messenger.Options{
@ -55,7 +61,7 @@ func main() {
fmt.Println("Read at:", m.Watermark().Format(time.UnixDate)) fmt.Println("Read at:", m.Watermark().Format(time.UnixDate))
}) })
fmt.Printf("Serving messenger bot on localhost:%d\n", *port) addr := fmt.Sprintf("%s:%d", *host, *port)
log.Println("Serving messenger bot on", addr)
http.ListenAndServe(fmt.Sprintf("localhost:%d", *port), client.Handler()) log.Fatal(http.ListenAndServe(addr, client.Handler()))
} }

View 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,
)
}

View File

@ -60,6 +60,19 @@ type PostBack struct {
Referral Referral `json:"referral"` 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. // Watermark is the RawWatermark timestamp rendered as a time.Time.
func (d Delivery) Watermark() time.Time { func (d Delivery) Watermark() time.Time {
return time.Unix(d.RawWatermark/int64(time.Microsecond), 0) return time.Unix(d.RawWatermark/int64(time.Microsecond), 0)

View File

@ -59,6 +59,10 @@ type OptInHandler func(OptIn, *Response)
// ReferralHandler is a handler used postback callbacks. // ReferralHandler is a handler used postback callbacks.
type ReferralHandler func(ReferralMessage, *Response) 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. // Messenger is the client which manages communication with the Messenger Platform API.
type Messenger struct { type Messenger struct {
mux *http.ServeMux mux *http.ServeMux
@ -68,6 +72,7 @@ type Messenger struct {
postBackHandlers []PostBackHandler postBackHandlers []PostBackHandler
optInHandlers []OptInHandler optInHandlers []OptInHandler
referralHandlers []ReferralHandler referralHandlers []ReferralHandler
accountLinkingHandlers []AccountLinkingHandler
token string token string
verifyHandler func(http.ResponseWriter, *http.Request) verifyHandler func(http.ResponseWriter, *http.Request)
verify bool verify bool
@ -131,6 +136,11 @@ func (m *Messenger) HandleReferral(f ReferralHandler) {
m.referralHandlers = append(m.referralHandlers, f) 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. // Handler returns the Messenger in HTTP client form.
func (m *Messenger) Handler() http.Handler { func (m *Messenger) Handler() http.Handler {
return m.mux return m.mux
@ -370,6 +380,14 @@ func (m *Messenger) dispatch(r Receive) {
message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0)
f(message, resp) 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 return OptInAction
} else if info.ReferralMessage != nil { } else if info.ReferralMessage != nil {
return ReferralAction return ReferralAction
} else if info.AccountLinking != nil {
return AccountLinkingAction
} }
return UnknownAction return UnknownAction
} }

View File

@ -43,6 +43,8 @@ type MessageInfo struct {
OptIn *OptIn `json:"optin"` OptIn *OptIn `json:"optin"`
ReferralMessage *ReferralMessage `json:"referral"` ReferralMessage *ReferralMessage `json:"referral"`
AccountLinking *AccountLinking `json:"account_linking"`
} }
type OptIn struct { type OptIn struct {