Archived
1
0
Fork 0

Create new thread when product is available again (#28)

Instead of spamming a twitter thread with generic replies confusing the
community because the original message was posted long time ago, the bot now
creates a new thread with all product information and an incrementing counter
for uniqueness.
This commit is contained in:
Julien Riou 2021-04-23 12:22:18 +02:00
parent c796cb20f2
commit d6ee2922d7
No known key found for this signature in database
GPG key ID: FF42D23B580C89F7
2 changed files with 76 additions and 24 deletions

View file

@ -23,6 +23,7 @@ type Tweet struct {
TweetID int64 `gorm:"not null;unique"` TweetID int64 `gorm:"not null;unique"`
Hash string `gorm:"unique"` Hash string `gorm:"unique"`
LastTweetID int64 `gorm:"index"` LastTweetID int64 `gorm:"index"`
Counter int64 `gorm:"not null;default:1"`
ProductURL string `gorm:"index"` ProductURL string `gorm:"index"`
Product Product `gorm:"not null;references:URL"` Product Product `gorm:"not null;references:URL"`
} }
@ -143,7 +144,7 @@ func (c *TwitterNotifier) buildHashtags(productName string) string {
func (c *TwitterNotifier) NotifyWhenAvailable(shopName string, productName string, productPrice float64, productCurrency string, productURL string) error { func (c *TwitterNotifier) NotifyWhenAvailable(shopName string, productName string, productPrice float64, productCurrency string, productURL string) error {
// format message // format message
hashtags := c.buildHashtags(productName) hashtags := c.buildHashtags(productName)
message := formatAvailableTweet(shopName, productName, productPrice, productCurrency, productURL, hashtags) message := formatAvailableTweet(shopName, productName, productPrice, productCurrency, productURL, hashtags, 0)
// compute message checksum to avoid duplicates // compute message checksum to avoid duplicates
var tweet Tweet var tweet Tweet
@ -164,7 +165,7 @@ func (c *TwitterNotifier) NotifyWhenAvailable(shopName string, productName strin
log.Infof("tweet %d sent for product '%s'", tweetID, productURL) log.Infof("tweet %d sent for product '%s'", tweetID, productURL)
// save thread to database // save thread to database
tweet = Tweet{TweetID: tweetID, ProductURL: productURL, Hash: hash} tweet = Tweet{TweetID: tweetID, ProductURL: productURL, Hash: hash, Counter: 1}
trx = c.db.Create(&tweet) trx = c.db.Create(&tweet)
if trx.Error != nil { if trx.Error != nil {
return fmt.Errorf("could not save tweet %d to database for product '%s': %s", tweet.TweetID, productURL, trx.Error) return fmt.Errorf("could not save tweet %d to database for product '%s': %s", tweet.TweetID, productURL, trx.Error)
@ -173,48 +174,49 @@ func (c *TwitterNotifier) NotifyWhenAvailable(shopName string, productName strin
} else { } else {
if !c.enableReplies { // tweet already has been sent in the past
log.Debugf("twitter replies are disabled, skipping available notification for product '%s'", productURL) // creating new thread with a counter
return nil tweet.Counter++
} message = formatAvailableTweet(shopName, productName, productPrice, productCurrency, productURL, hashtags, tweet.Counter)
tweetID, err := c.createTweet(message)
// select tweet to reply
lastTweetID := CoalesceInt64(tweet.LastTweetID, tweet.TweetID)
if lastTweetID == 0 {
return fmt.Errorf("could not find original tweet ID to create reply for product '%s'", productURL)
}
// tweet already has been sent in the past and replies are enabled
// continuing thread
tweetID, err := c.replyToTweet(lastTweetID, "Good news, it's available again!")
if err != nil { if err != nil {
return fmt.Errorf("could not reply to tweet %d for product '%s': %s", lastTweetID, productURL, err) return fmt.Errorf("could not create new twitter thread for product '%s': %s", productURL, err)
} }
log.Infof("reply to tweet %d sent with id %d for product '%s'", lastTweetID, tweetID, productURL) log.Infof("tweet %d sent for product '%s'", tweetID, productURL)
// save thread to database // save thread to database
tweet.LastTweetID = tweetID tweet.LastTweetID = tweetID
if trx = c.db.Save(&tweet); trx.Error != nil { if trx = c.db.Save(&tweet); trx.Error != nil {
return fmt.Errorf("could not save tweet %d to database for product '%s': %s", tweet.TweetID, productURL, trx.Error) return fmt.Errorf("could not save tweet %d to database for product '%s': %s", tweet.TweetID, productURL, trx.Error)
} }
log.Debugf("tweet %d saved in database", tweet.TweetID) log.Debugf("tweet %d saved to database", tweet.TweetID)
} }
return nil return nil
} }
// formatAvailableTweet creates a message based on product characteristics // formatAvailableTweet creates a message based on product characteristics
func formatAvailableTweet(shopName string, productName string, productPrice float64, productCurrency string, productURL string, hashtags string) string { func formatAvailableTweet(shopName string, productName string, productPrice float64, productCurrency string, productURL string, hashtags string, counter int64) string {
// format message // format message
formattedPrice := formatPrice(productPrice, productCurrency) formattedPrice := formatPrice(productPrice, productCurrency)
message := fmt.Sprintf("%s: %s for %s is available at %s %s", shopName, productName, formattedPrice, productURL, hashtags) message := fmt.Sprintf("%s: %s for %s is available at %s %s", shopName, productName, formattedPrice, productURL, hashtags)
if counter > 1 {
message = fmt.Sprintf("%s (%d)", message, counter)
}
// truncate tweet if too big // truncate tweet if too big
if utf8.RuneCountInString(message) > tweetMaxSize { if utf8.RuneCountInString(message) > tweetMaxSize {
messageWithoutProduct := fmt.Sprintf("%s: for %s is available at %s %s", shopName, formattedPrice, productURL, hashtags)
if counter > 1 {
messageWithoutProduct = fmt.Sprintf("%s (%d)", messageWithoutProduct, counter)
}
// maximum tweet size - other characters - additional "…" to say product name has been truncated // maximum tweet size - other characters - additional "…" to say product name has been truncated
productNameSize := tweetMaxSize - utf8.RuneCountInString(fmt.Sprintf("%s: for %s is available at %s %s", shopName, formattedPrice, productURL, hashtags)) - 1 productNameSize := tweetMaxSize - utf8.RuneCountInString(messageWithoutProduct) - 1
format := fmt.Sprintf("%%s: %%.%ds… for %%s is available at %%s %%s", productNameSize) format := fmt.Sprintf("%%s: %%.%ds… for %%s is available at %%s %%s", productNameSize)
message = fmt.Sprintf(format, shopName, productName, formattedPrice, productURL, hashtags) message = fmt.Sprintf(format, shopName, productName, formattedPrice, productURL, hashtags)
if counter > 1 {
message = fmt.Sprintf("%s (%d)", message, counter)
}
} }
return message return message

View file

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"testing" "testing"
"unicode/utf8"
) )
func TestBuildHashtags(t *testing.T) { func TestBuildHashtags(t *testing.T) {
@ -64,19 +65,68 @@ func TestFormatAvailableTweet(t *testing.T) {
productCurrency string productCurrency string
productURL string productURL string
hashtags string hashtags string
counter int64
expected string expected string
}{ }{
{"shop.com", "my awesome product", 999.99, "USD", "https://shop.com/awesome", "#awesome #product", "shop.com: my awesome product for $999.99 is available at https://shop.com/awesome #awesome #product"}, {
{"shop.com", "my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long name", 999.99, "USD", "https://shop.com/awesome", "#awesome #product", "shop.com: my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very… for $999.99 is available at https://shop.com/awesome #awesome #product"}, // short product name
"shop.com",
"my awesome product",
999.99,
"USD",
"https://shop.com/awesome",
"#awesome #product",
0,
"shop.com: my awesome product for $999.99 is available at https://shop.com/awesome #awesome #product",
},
{
// long product name
"shop.com",
"my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long name",
999.99,
"USD",
"https://shop.com/awesome",
"#awesome #product",
0,
"shop.com: my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very… for $999.99 is available at https://shop.com/awesome #awesome #product",
},
{
// short product name with a counter
"shop.com",
"my awesome product",
999.99,
"USD",
"https://shop.com/awesome",
"#awesome #product",
2,
"shop.com: my awesome product for $999.99 is available at https://shop.com/awesome #awesome #product (2)",
},
{
// long product name with a counter
"shop.com",
"my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long name",
999.99,
"USD",
"https://shop.com/awesome",
"#awesome #product",
2,
"shop.com: my awesome product with very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very … for $999.99 is available at https://shop.com/awesome #awesome #product (2)",
},
} }
for i, tc := range tests { for i, tc := range tests {
t.Run(fmt.Sprintf("TestFormatAvailableTweet#%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("TestFormatAvailableTweet#%d", i), func(t *testing.T) {
got := formatAvailableTweet(tc.shopName, tc.productName, tc.productPrice, tc.productCurrency, tc.productURL, tc.hashtags) got := formatAvailableTweet(tc.shopName, tc.productName, tc.productPrice, tc.productCurrency, tc.productURL, tc.hashtags, tc.counter)
if got != tc.expected { if got != tc.expected {
t.Errorf("for %s, got '%s', want '%s'", tc.productName, got, tc.expected) t.Errorf("for %s, got '%s', want '%s'", tc.productName, got, tc.expected)
} else { } else {
t.Logf("for %s, got '%s', want '%s'", tc.productName, got, tc.expected) t.Logf("for %s, got '%s', want '%s'", tc.productName, got, tc.expected)
} }
// ensure generated tweet doesn't exceed maximum length
if utf8.RuneCountInString(got) > tweetMaxSize {
t.Errorf("for %s, got '%s' which exceeed tweet maximum lenght of %d chars", tc.productName, got, tweetMaxSize)
}
}) })
} }
} }