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