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"`
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

View file

@ -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)
}
})
}
}