209 lines
5.3 KiB
Go
209 lines
5.3 KiB
Go
/*
|
|
Copyright 2021 The Dapr Authors
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package twitter
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/dghubble/go-twitter/twitter"
|
|
"github.com/dghubble/oauth1"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/dapr/components-contrib/bindings"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
// Binding represents Twitter input/output binding.
|
|
type Binding struct {
|
|
client *twitter.Client
|
|
query string
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewTwitter returns a new Twitter event input binding.
|
|
func NewTwitter(logger logger.Logger) bindings.InputOutputBinding {
|
|
return &Binding{logger: logger}
|
|
}
|
|
|
|
// Init initializes the Twitter binding.
|
|
func (t *Binding) Init(metadata bindings.Metadata) error {
|
|
ck, f := metadata.Properties["consumerKey"]
|
|
if !f || ck == "" {
|
|
return fmt.Errorf("consumerKey not set")
|
|
}
|
|
cs, f := metadata.Properties["consumerSecret"]
|
|
if !f || cs == "" {
|
|
return fmt.Errorf("consumerSecret not set")
|
|
}
|
|
at, f := metadata.Properties["accessToken"]
|
|
if !f || at == "" {
|
|
return fmt.Errorf("accessToken not set")
|
|
}
|
|
as, f := metadata.Properties["accessSecret"]
|
|
if !f || as == "" {
|
|
return fmt.Errorf("accessSecret not set")
|
|
}
|
|
|
|
// set query only in an input binding case
|
|
q, f := metadata.Properties["query"]
|
|
if f {
|
|
t.query = q
|
|
}
|
|
|
|
config := oauth1.NewConfig(ck, cs)
|
|
token := oauth1.NewToken(at, as)
|
|
|
|
httpClient := config.Client(oauth1.NoContext, token)
|
|
|
|
t.client = twitter.NewClient(httpClient)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Operations returns list of operations supported by twitter binding.
|
|
func (t *Binding) Operations() []bindings.OperationKind {
|
|
return []bindings.OperationKind{bindings.GetOperation}
|
|
}
|
|
|
|
// Read triggers the Twitter search and events on each result tweet.
|
|
func (t *Binding) Read(ctx context.Context, handler bindings.Handler) error {
|
|
if t.query == "" {
|
|
return errors.New("metadata property 'query' is empty")
|
|
}
|
|
|
|
demux := twitter.NewSwitchDemux()
|
|
demux.Tweet = func(tweet *twitter.Tweet) {
|
|
t.logger.Debugf("raw tweet: %+v", tweet)
|
|
data, marshalErr := json.Marshal(tweet)
|
|
if marshalErr != nil {
|
|
t.logger.Errorf("error marshaling tweet: %+v", tweet)
|
|
|
|
return
|
|
}
|
|
handler(ctx, &bindings.ReadResponse{
|
|
Data: data,
|
|
Metadata: map[string]string{
|
|
"query": t.query,
|
|
},
|
|
})
|
|
}
|
|
|
|
demux.StreamLimit = func(limit *twitter.StreamLimit) {
|
|
t.logger.Warnf("disconnect: %+v", limit)
|
|
}
|
|
|
|
demux.StreamDisconnect = func(disconnect *twitter.StreamDisconnect) {
|
|
t.logger.Errorf("stream disconnect: %+v", disconnect)
|
|
}
|
|
|
|
filterParams := &twitter.StreamFilterParams{
|
|
Track: []string{t.query},
|
|
StallWarnings: twitter.Bool(true),
|
|
}
|
|
|
|
t.logger.Debug("starting stream for query: %s", t.query)
|
|
stream, err := t.client.Streams.Filter(filterParams)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error executing stream filter: %+v", filterParams)
|
|
}
|
|
|
|
t.logger.Debug("starting handler...")
|
|
go demux.HandleChan(stream.Messages)
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
t.logger.Debug("stopping handler...")
|
|
stream.Stop()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Invoke handles all operations.
|
|
func (t *Binding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
t.logger.Debugf("operation: %v", req.Operation)
|
|
if req.Metadata == nil {
|
|
return nil, fmt.Errorf("metadata not set")
|
|
}
|
|
// required
|
|
q, f := req.Metadata["query"]
|
|
if !f || q == "" {
|
|
return nil, fmt.Errorf("query not set")
|
|
}
|
|
|
|
// optionals
|
|
l, f := req.Metadata["lang"]
|
|
if !f || l == "" {
|
|
l = "en"
|
|
}
|
|
|
|
r, f := req.Metadata["result"]
|
|
if !f || r == "" {
|
|
// mixed : Include both popular and real time results in the response
|
|
// recent : return only the most recent results in the response
|
|
// popular : return only the most popular results in the response
|
|
r = "recent"
|
|
}
|
|
|
|
var sinceID int64
|
|
s, f := req.Metadata["since_id"]
|
|
if f && s != "" {
|
|
i, err := strconv.ParseInt(s, 10, 64)
|
|
if err == nil {
|
|
sinceID = i
|
|
}
|
|
}
|
|
|
|
sq := &twitter.SearchTweetParams{
|
|
Count: 100, // max
|
|
Lang: l,
|
|
SinceID: sinceID,
|
|
Query: q,
|
|
ResultType: r,
|
|
IncludeEntities: twitter.Bool(true),
|
|
}
|
|
|
|
t.logger.Debug("starting stream for: %+v", sq)
|
|
search, _, err := t.client.Search.Tweets(sq)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error executing search filter: %+v", sq)
|
|
}
|
|
if search == nil || search.Statuses == nil {
|
|
return nil, errors.Wrapf(err, "nil search result from: %+v", sq)
|
|
}
|
|
|
|
t.logger.Debugf("raw response: %+v", search.Statuses)
|
|
data, marshalErr := json.Marshal(search.Statuses)
|
|
if marshalErr != nil {
|
|
t.logger.Errorf("error marshaling tweet: %v", marshalErr)
|
|
|
|
return nil, errors.Wrapf(err, "error parsing response from: %+v", sq)
|
|
}
|
|
|
|
req.Metadata["max_tweet_id"] = search.Metadata.MaxIDStr
|
|
req.Metadata["tweet_count"] = strconv.Itoa(search.Metadata.Count)
|
|
req.Metadata["search_ts"] = time.Now().UTC().String()
|
|
|
|
ir := &bindings.InvokeResponse{
|
|
Data: data,
|
|
Metadata: req.Metadata,
|
|
}
|
|
|
|
return ir, nil
|
|
}
|