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:
parent
c796cb20f2
commit
d6ee2922d7
2 changed files with 76 additions and 24 deletions
|
@ -23,6 +23,7 @@ type Tweet struct {
|
|||
TweetID int64 `gorm:"not null;unique"`
|
||||
Hash string `gorm:"unique"`
|
||||
LastTweetID int64 `gorm:"index"`
|
||||
Counter int64 `gorm:"not null;default:1"`
|
||||
ProductURL string `gorm:"index"`
|
||||
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 {
|
||||
// format message
|
||||
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
|
||||
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)
|
||||
|
||||
// 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)
|
||||
if trx.Error != nil {
|
||||
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 {
|
||||
|
||||
if !c.enableReplies {
|
||||
log.Debugf("twitter replies are disabled, skipping available notification for product '%s'", productURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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!")
|
||||
// tweet already has been sent in the past
|
||||
// creating new thread with a counter
|
||||
tweet.Counter++
|
||||
message = formatAvailableTweet(shopName, productName, productPrice, productCurrency, productURL, hashtags, tweet.Counter)
|
||||
tweetID, err := c.createTweet(message)
|
||||
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
|
||||
tweet.LastTweetID = tweetID
|
||||
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)
|
||||
}
|
||||
log.Debugf("tweet %d saved in database", tweet.TweetID)
|
||||
log.Debugf("tweet %d saved to database", tweet.TweetID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
formattedPrice := formatPrice(productPrice, productCurrency)
|
||||
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
|
||||
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
|
||||
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)
|
||||
message = fmt.Sprintf(format, shopName, productName, formattedPrice, productURL, hashtags)
|
||||
if counter > 1 {
|
||||
message = fmt.Sprintf("%s (%d)", message, counter)
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
|
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestBuildHashtags(t *testing.T) {
|
||||
|
@ -64,19 +65,68 @@ func TestFormatAvailableTweet(t *testing.T) {
|
|||
productCurrency string
|
||||
productURL string
|
||||
hashtags string
|
||||
counter int64
|
||||
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 {
|
||||
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 {
|
||||
t.Errorf("for %s, got '%s', want '%s'", tc.productName, got, tc.expected)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue