Implement expandable ("Show more...") tweets

This commit is contained in:
Alessio 2023-06-08 18:51:50 -03:00
parent eb566c0612
commit 0868d8d6d8
8 changed files with 81 additions and 3 deletions

View File

@ -41,6 +41,7 @@ create table tweets (rowid integer primary key,
id integer unique not null check(typeof(id) = 'integer'), id integer unique not null check(typeof(id) = 'integer'),
user_id integer not null check(typeof(user_id) = 'integer'), user_id integer not null check(typeof(user_id) = 'integer'),
text text not null, text text not null,
is_expandable bool not null default 0,
posted_at integer, posted_at integer,
num_likes integer, num_likes integer,
num_retweets integer, num_retweets integer,

View File

@ -23,16 +23,21 @@ func (p Profile) SaveTweet(t scraper.Tweet) error {
_, err := db.NamedExec(` _, err := db.NamedExec(`
insert into tweets (id, user_id, text, posted_at, num_likes, num_retweets, num_replies, num_quote_tweets, in_reply_to_id, insert into tweets (id, user_id, text, posted_at, num_likes, num_retweets, num_replies, num_quote_tweets, in_reply_to_id,
quoted_tweet_id, mentions, reply_mentions, hashtags, space_id, tombstone_type, is_stub, is_content_downloaded, quoted_tweet_id, mentions, reply_mentions, hashtags, space_id, tombstone_type, is_expandable,
is_stub, is_content_downloaded,
is_conversation_scraped, last_scraped_at) is_conversation_scraped, last_scraped_at)
values (:id, :user_id, :text, :posted_at, :num_likes, :num_retweets, :num_replies, :num_quote_tweets, :in_reply_to_id, values (:id, :user_id, :text, :posted_at, :num_likes, :num_retweets, :num_replies, :num_quote_tweets, :in_reply_to_id,
:quoted_tweet_id, :mentions, :reply_mentions, :hashtags, nullif(:space_id, ''), :quoted_tweet_id, :mentions, :reply_mentions, :hashtags, nullif(:space_id, ''),
(select rowid from tombstone_types where short_name=:tombstone_type), :is_stub, :is_content_downloaded, (select rowid from tombstone_types where short_name=:tombstone_type),
:is_expandable,
:is_stub, :is_content_downloaded,
:is_conversation_scraped, :last_scraped_at) :is_conversation_scraped, :last_scraped_at)
on conflict do update on conflict do update
set text=(case set text=(case
when is_stub then when is_stub then
:text :text
when not is_expandable and :is_expandable then
:text
else else
text text
end end
@ -49,6 +54,7 @@ func (p Profile) SaveTweet(t scraper.Tweet) error {
(select rowid from tombstone_types where short_name=:tombstone_type) (select rowid from tombstone_types where short_name=:tombstone_type)
end end
), ),
is_expandable=is_expandable or :is_expandable,
is_content_downloaded=(is_content_downloaded or :is_content_downloaded), is_content_downloaded=(is_content_downloaded or :is_content_downloaded),
is_conversation_scraped=(is_conversation_scraped or :is_conversation_scraped), is_conversation_scraped=(is_conversation_scraped or :is_conversation_scraped),
last_scraped_at=max(last_scraped_at, :last_scraped_at) last_scraped_at=max(last_scraped_at, :last_scraped_at)
@ -119,6 +125,7 @@ func (p Profile) GetTweetById(id scraper.TweetID) (scraper.Tweet, error) {
err := db.Get(&t, ` err := db.Get(&t, `
select id, user_id, text, posted_at, num_likes, num_retweets, num_replies, num_quote_tweets, in_reply_to_id, quoted_tweet_id, select id, user_id, text, posted_at, num_likes, num_retweets, num_replies, num_quote_tweets, in_reply_to_id, quoted_tweet_id,
mentions, reply_mentions, hashtags, ifnull(space_id, '') space_id, ifnull(tombstone_types.short_name, "") tombstone_type, mentions, reply_mentions, hashtags, ifnull(space_id, '') space_id, ifnull(tombstone_types.short_name, "") tombstone_type,
is_expandable,
is_stub, is_content_downloaded, is_conversation_scraped, last_scraped_at is_stub, is_content_downloaded, is_conversation_scraped, last_scraped_at
from tweets left join tombstone_types on tweets.tombstone_type = tombstone_types.rowid from tweets left join tombstone_types on tweets.tombstone_type = tombstone_types.rowid
where id = ? where id = ?

View File

@ -75,6 +75,7 @@ func TestNoWorseningTweet(t *testing.T) {
tweet.IsContentDownloaded = true tweet.IsContentDownloaded = true
tweet.IsStub = false tweet.IsStub = false
tweet.IsConversationScraped = true tweet.IsConversationScraped = true
tweet.IsExpandable = true
tweet.LastScrapedAt = scraper.TimestampFromUnix(1000) tweet.LastScrapedAt = scraper.TimestampFromUnix(1000)
tweet.Text = "Yes text" tweet.Text = "Yes text"
tweet.NumLikes = 10 tweet.NumLikes = 10
@ -90,6 +91,7 @@ func TestNoWorseningTweet(t *testing.T) {
tweet.IsContentDownloaded = false tweet.IsContentDownloaded = false
tweet.IsStub = true tweet.IsStub = true
tweet.IsConversationScraped = false tweet.IsConversationScraped = false
tweet.IsExpandable = false
tweet.LastScrapedAt = scraper.TimestampFromUnix(500) tweet.LastScrapedAt = scraper.TimestampFromUnix(500)
tweet.Text = "" tweet.Text = ""
err = profile.SaveTweet(tweet) err = profile.SaveTweet(tweet)
@ -106,6 +108,7 @@ func TestNoWorseningTweet(t *testing.T) {
assert.False(new_tweet.IsStub, "Should have preserved non-stub status") assert.False(new_tweet.IsStub, "Should have preserved non-stub status")
assert.True(new_tweet.IsContentDownloaded, "Should have preserved is-content-downloaded status") assert.True(new_tweet.IsContentDownloaded, "Should have preserved is-content-downloaded status")
assert.True(new_tweet.IsConversationScraped, "Should have preserved is-conversation-scraped status") assert.True(new_tweet.IsConversationScraped, "Should have preserved is-conversation-scraped status")
assert.True(new_tweet.IsExpandable)
assert.Equal(int64(1000), new_tweet.LastScrapedAt.Unix(), "Should have preserved last-scraped-at time") assert.Equal(int64(1000), new_tweet.LastScrapedAt.Unix(), "Should have preserved last-scraped-at time")
assert.Equal(new_tweet.Text, "Yes text", "Text should not get clobbered if it becomes unavailable") assert.Equal(new_tweet.Text, "Yes text", "Text should not get clobbered if it becomes unavailable")
assert.Equal(10, new_tweet.NumLikes) assert.Equal(10, new_tweet.NumLikes)
@ -149,6 +152,36 @@ func TestUntombstoningTweet(t *testing.T) {
assert.Equal(new_tweet.Text, "Some text", "Should have created the text") assert.Equal(new_tweet.Text, "Some text", "Should have created the text")
} }
// The tweet is an expanding tweet, but was saved before expanding tweets were implemented
func TestUpgradingExpandingTweet(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
profile_path := "test_profiles/TestTweetQueries"
profile := create_or_load_profile(profile_path)
tweet := create_dummy_tweet()
tweet.IsExpandable = false
tweet.Text = "Some long but cut-off text..."
// Save the tweet
err := profile.SaveTweet(tweet)
require.NoError(err)
// Now that we have expanding tweets
tweet.IsExpandable = true
tweet.Text = "Some long but cut-off text, but now it no longer is cut off!"
err = profile.SaveTweet(tweet)
require.NoError(err)
// Reload the tweet
new_tweet, err := profile.GetTweetById(tweet.ID)
require.NoError(err)
assert.True(new_tweet.IsExpandable, "Should now be is_expanding after re-scrape")
assert.Equal(new_tweet.Text, "Some long but cut-off text, but now it no longer is cut off!", "Should have extended the text")
}
/** /**
* The "unavailable" tombstone type is not reliable, you should be able to update away from it but * The "unavailable" tombstone type is not reliable, you should be able to update away from it but
* not toward it * not toward it

View File

@ -8,7 +8,7 @@ import (
"offline_twitter/terminal_utils" "offline_twitter/terminal_utils"
) )
const ENGINE_DATABASE_VERSION = 16 const ENGINE_DATABASE_VERSION = 17
type VersionMismatchError struct { type VersionMismatchError struct {
EngineVersion int EngineVersion int
@ -97,6 +97,7 @@ var MIGRATIONS = []string{
foreign key(space_id) references spaces(id) foreign key(space_id) references spaces(id)
);`, );`,
`create index if not exists index_tweets_user_id on tweets (user_id);`, `create index if not exists index_tweets_user_id on tweets (user_id);`,
`alter table tweets add column is_expandable bool not null default 0;`,
} }
/** /**

View File

@ -176,6 +176,7 @@ type APITweet struct {
UserHandle string UserHandle string
Card APICard `json:"card"` Card APICard `json:"card"`
TombstoneText string TombstoneText string
IsExpandable bool
} }
func (t *APITweet) NormalizeContent() { func (t *APITweet) NormalizeContent() {

View File

@ -151,6 +151,15 @@ type _Result struct {
Core *APIV2UserResult `json:"core"` Core *APIV2UserResult `json:"core"`
Card APIV2Card `json:"card"` Card APIV2Card `json:"card"`
QuotedStatusResult *APIV2Result `json:"quoted_status_result"` QuotedStatusResult *APIV2Result `json:"quoted_status_result"`
NoteTweet struct {
IsExpandable bool `json:"is_expandable"`
NoteTweetResults struct {
Result struct {
ID string `json:"id"`
Text string `json:"text"`
} `json:"result"`
} `json:"note_tweet_results"`
} `json:"note_tweet"`
} }
type APIV2Result struct { type APIV2Result struct {
@ -177,6 +186,14 @@ func (api_result APIV2Result) ToTweetTrove(ignore_null_entries bool) TweetTrove
api_result.Result._Result = api_result.Result.Tweet api_result.Result._Result = api_result.Result.Tweet
} }
// Handle expandable tweets
if api_result.Result.NoteTweet.IsExpandable {
api_result.Result.Legacy.FullText = api_result.Result.NoteTweet.NoteTweetResults.Result.Text
api_result.Result.Legacy.DisplayTextRange = []int{} // Override the "display text"
api_result.Result.Legacy.IsExpandable = true
}
// Process the tweet itself
main_tweet_trove := api_result.Result.Legacy.ToTweetTrove() main_tweet_trove := api_result.Result.Legacy.ToTweetTrove()
ret.MergeWith(main_tweet_trove) ret.MergeWith(main_tweet_trove)

View File

@ -637,6 +637,23 @@ func TestRetweetWithVisibilityResults(t *testing.T) {
assert.Equal(rt.TweetID, TweetID(1595973736833892356)) assert.Equal(rt.TweetID, TweetID(1595973736833892356))
} }
func TestExpandableTweet(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
data, err := os.ReadFile("test_responses/api_v2/expandable_tweet.json")
require.NoError(err)
var tweet_result APIV2Result
err = json.Unmarshal(data, &tweet_result)
require.NoError(err)
trove := tweet_result.ToTweetTrove(true)
main_tweet, is_ok := trove.Tweets[TweetID(1649600354747572225)]
require.True(is_ok)
assert.True(main_tweet.IsExpandable)
assert.Equal(main_tweet.Text, "This entire millenial media era has come and gone. Where are the lindy articles from all these websites? The ideas? \n\nIt was just a decade and a half of nothing. \n\na complete waste of time. \n\nAnd it ends with the blue checks being stripped. \n\nA fitting ending to a time not worth saving") //nolint:lll // It's a string
}
// In a user feed, an "entry" can contain multiple tweets when making authenticated requests. // In a user feed, an "entry" can contain multiple tweets when making authenticated requests.
// They should parse out as all the tweets. // They should parse out as all the tweets.
func TestEntryWithConversationThread(t *testing.T) { func TestEntryWithConversationThread(t *testing.T) {

View File

@ -114,6 +114,7 @@ func ParseSingleTweet(apiTweet APITweet) (ret Tweet, err error) {
ret.UserID = UserID(apiTweet.UserID) ret.UserID = UserID(apiTweet.UserID)
ret.UserHandle = UserHandle(apiTweet.UserHandle) ret.UserHandle = UserHandle(apiTweet.UserHandle)
ret.Text = apiTweet.FullText ret.Text = apiTweet.FullText
ret.IsExpandable = apiTweet.IsExpandable
// Process "posted-at" date and time // Process "posted-at" date and time
if apiTweet.TombstoneText == "" { // Skip time parsing for tombstones if apiTweet.TombstoneText == "" { // Skip time parsing for tombstones