add GetSpace method

This commit is contained in:
Valentine 2024-07-24 03:46:28 +03:00
parent 571a63bb50
commit f8c200b312
4 changed files with 340 additions and 6 deletions

View file

@ -1,5 +1,11 @@
# Changelog
## v0.0.9
24.07.2024
- Added method `GetSpace`
## v0.0.8
09.07.2024

View file

@ -34,6 +34,7 @@ You can use this library to get tweets, profiles, and trends trivially.
- [Get trends](#get-trends)
- [Get following](#get-following)
- [Get followers](#get-followers)
- [Get space](#get-space)
- [Create tweet](#create-tweet)
- [Delete tweet](#delete-tweet)
- [Create retweet](#create-retweet)
@ -237,7 +238,6 @@ tweets, cursor, err := scraper.FetchTweets("taylorswift13", 20, cursor)
`GetMediaTweets` returns a channel with the specified number of user tweets that contain media. Its using the `FetchMediaTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for tweet := range scraper.GetMediaTweets(context.Background(), "taylorswift13", 50) {
if tweet.Error != nil {
@ -263,7 +263,6 @@ tweets, cursor, err := scraper.FetchMediaTweets("taylorswift13", 20, cursor)
`GetBookmarks` returns a channel with the specified number of bookmarked tweets. Its using the `FetchBookmarks` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for tweet := range scraper.GetBookmarks(context.Background(), 50) {
if tweet.Error != nil {
@ -289,7 +288,6 @@ tweets, cursor, err := scraper.FetchBookmarks(20, cursor)
`GetHomeTweets` returns a channel with the specified number of latest home tweets. Its using the `FetchHomeTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for tweet := range scraper.GetHomeTweets(context.Background(), 50) {
if tweet.Error != nil {
@ -315,7 +313,6 @@ tweets, cursor, err := scraper.FetchHomeTweets(20, cursor)
`GetForYouTweets` returns a channel with the specified number of for you home tweets. Its using the `FetchForYouTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for tweet := range scraper.GetForYouTweets(context.Background(), 50) {
if tweet.Error != nil {
@ -341,7 +338,6 @@ tweets, cursor, err := scraper.FetchForYouTweets(20, cursor)
`SearchTweets` returns a channel with the specified number of tweets that contain media. Its using the `FetchSearchTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for tweet := range scraper.SearchTweets(context.Background(),
"twitter scraper data -filter:retweets", 50) {
@ -385,7 +381,6 @@ profile, err := scraper.GetProfile("taylorswift13")
`SearchProfiles` returns a channel with the specified number of tweets that contain media. Its using the `FetchSearchProfiles` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
```golang
for profile := range scraper.SearchProfiles(context.Background(), "Twitter", 50) {
if profile.Error != nil {
@ -431,6 +426,37 @@ var cursor string
users, cursor, err := scraper.FetchFollowers("Support", 20, cursor)
```
### Get space
> [!IMPORTANT]
> Requires authentication!
500 requests / 15 minutes
Use to retrvie data about space and it's participants. You can get up to 1000 participants of space. If method returns less, it's probably because listeners is anonymous.
```golang
space, err := scraper.GetSpace("space_id")
```
You can get `space_id` from space url which can be retrived from tweet. For example:
```golang
tweet, err := testScraper.GetTweet("1815884577040445599")
if err != nil {
t.Fatal(err)
}
var spaceId string
spaceUrl := tweet.URLs[0] // https://twitter.com/i/spaces/1mnxeAMPEqqxX
if strings.HasPrefix(spaceUrl, "https://twitter.com/i/spaces/") {
spaceId = strings.Replace(spaceUrl, "https://twitter.com/i/spaces/", "", 1) // 1mnxeAMPEqqxX
}
space, err := scraper.GetSpace(spaceId)
```
### Create tweet
> [!IMPORTANT]

279
spaces.go Normal file
View file

@ -0,0 +1,279 @@
package twitterscraper
import (
"errors"
"net/url"
"time"
)
func (s *Scraper) GetSpace(id string) (*Space, error) {
if !s.isLogged {
return nil, errors.New("scraper is not logged in")
}
req, err := s.newRequest("GET", "https://twitter.com/i/api/graphql/d03OdorPdZ_sH9V3D1_yWQ/AudioSpaceById")
if err != nil {
return nil, err
}
variables := map[string]interface{}{
"id": id,
"isMetatagsQuery": false,
"withReplays": true,
"withListeners": true,
}
features := map[string]interface{}{
"spaces_2022_h2_spaces_communities": true,
"spaces_2022_h2_clipping": true,
"creator_subscriptions_tweet_preview_api_enabled": true,
"rweb_tipjar_consumption_enabled": true,
"responsive_web_graphql_exclude_directive_enabled": true,
"verified_phone_label_enabled": false,
"communities_web_enable_tweet_community_results_fetch": true,
"c9s_tweet_anatomy_moderator_badge_enabled": true,
"articles_preview_enabled": true,
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
"tweetypie_unmention_optimization_enabled": true,
"responsive_web_edit_tweet_api_enabled": true,
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
"view_counts_everywhere_api_enabled": true,
"longform_notetweets_consumption_enabled": true,
"responsive_web_twitter_article_tweet_consumption_enabled": true,
"tweet_awards_web_tipping_enabled": false,
"creator_subscriptions_quote_tweet_preview_enabled": false,
"freedom_of_speech_not_reach_fetch_enabled": true,
"standardized_nudges_misinfo": true,
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
"rweb_video_timestamps_enabled": true,
"longform_notetweets_rich_text_read_enabled": true,
"longform_notetweets_inline_media_enabled": true,
"responsive_web_graphql_timeline_navigation_enabled": true,
"responsive_web_enhance_cards_enabled": false,
}
query := url.Values{}
query.Set("variables", mapToJSONString(variables))
query.Set("features", mapToJSONString(features))
req.URL.RawQuery = query.Encode()
var spaceData space
err = s.RequestAPI(req, &spaceData)
if err != nil {
return nil, err
}
space := spaceData.parse()
if space.ID == "" {
return nil, errors.New("some erorr happend")
}
return space, nil
}
type Topic struct {
ID string
Title string
}
type SpaceUser struct {
UserID string
Username string
Name string
Avatar string
IsVerified bool
ConnectedAt time.Time
}
type SpaceParticipants struct {
TotalCount int
CurrentCount int
Admins []*SpaceUser
Speakers []*SpaceUser
Listeners []*SpaceUser
}
type Space struct {
ID string
State string
Title string
ContentType string
Topics []Topic
Participants SpaceParticipants
CreatedAt time.Time
ScheduledStart time.Time
StartedAt time.Time
UpdatedAt time.Time
}
type spaceUser struct {
PeriscopeUserID string `json:"periscope_user_id"`
Start int64 `json:"start"`
TwitterScreenName string `json:"twitter_screen_name"`
DisplayName string `json:"display_name"`
AvatarURL string `json:"avatar_url"`
IsVerified bool `json:"is_verified"`
IsMutedByAdmin bool `json:"is_muted_by_admin"`
IsMutedByGuest bool `json:"is_muted_by_guest"`
UserResults struct {
RestID string `json:"rest_id"`
Result struct {
Typename string `json:"__typename"`
IdentityProfileLabelsHighlightedLabel struct {
} `json:"identity_profile_labels_highlighted_label"`
IsBlueVerified bool `json:"is_blue_verified"`
Legacy struct {
} `json:"legacy"`
} `json:"result"`
} `json:"user_results"`
}
type space struct {
Data struct {
AudioSpace struct {
Metadata struct {
RestID string `json:"rest_id"`
State string `json:"state"`
Title string `json:"title"`
MediaKey string `json:"media_key"`
CreatedAt int64 `json:"created_at"`
ScheduledStart int64 `json:"scheduled_start"`
StartedAt int64 `json:"started_at"`
UpdatedAt int64 `json:"updated_at"`
ContentType string `json:"content_type"`
CreatorResults struct {
Result struct {
Typename string `json:"__typename"`
ID string `json:"id"`
RestID string `json:"rest_id"`
HasGraduatedAccess bool `json:"has_graduated_access"`
IsBlueVerified bool `json:"is_blue_verified"`
ProfileImageShape string `json:"profile_image_shape"`
Legacy legacyUser `json:"legacy"`
} `json:"result"`
} `json:"creator_results"`
ConversationControls int `json:"conversation_controls"`
DisallowJoin bool `json:"disallow_join"`
IsEmployeeOnly bool `json:"is_employee_only"`
IsLocked bool `json:"is_locked"`
IsMuted bool `json:"is_muted"`
IsSpaceAvailableForClipping bool `json:"is_space_available_for_clipping"`
IsSpaceAvailableForReplay bool `json:"is_space_available_for_replay"`
MentionedUsers []struct {
RestID string `json:"rest_id"`
} `json:"mentioned_users"`
NarrowCastSpaceType int `json:"narrow_cast_space_type"`
NoIncognito bool `json:"no_incognito"`
TotalReplayWatched int `json:"total_replay_watched"`
TotalLiveListeners int `json:"total_live_listeners"`
Topics []struct {
Topic struct {
TopicID string `json:"topic_id"`
Name string `json:"name"`
} `json:"topic"`
} `json:"topics"`
TweetResults struct {
Result tweet `json:"result"`
} `json:"tweet_results"`
MaxGuestSessions int `json:"max_guest_sessions"`
MaxAdminCapacity int `json:"max_admin_capacity"`
} `json:"metadata"`
IsSubscribed bool `json:"is_subscribed"`
Participants struct {
Total int `json:"total"`
Admins []spaceUser `json:"admins"`
Speakers []spaceUser `json:"speakers"`
Listeners []spaceUser `json:"listeners"`
} `json:"participants"`
Sharings struct {
Items []struct {
SharingID string `json:"sharing_id"`
CreatedAtMs int64 `json:"created_at_ms"`
UpdatedAtMs int64 `json:"updated_at_ms"`
SharedItem struct {
Typename string `json:"__typename"`
TweetResults struct {
Result struct {
Typename string `json:"__typename"`
RestID string `json:"rest_id"`
Core tweet `json:"result"`
} `json:"tweet_results"`
} `json:"shared_item"`
UserResults struct {
Result struct {
Typename string `json:"__typename"`
ID string `json:"id"`
RestID string `json:"rest_id"`
AffiliatesHighlightedLabel struct {
} `json:"affiliates_highlighted_label"`
HasGraduatedAccess bool `json:"has_graduated_access"`
IsBlueVerified bool `json:"is_blue_verified"`
ProfileImageShape string `json:"profile_image_shape"`
Legacy legacyUser `json:"legacy"`
TipjarSettings struct {
IsEnabled bool `json:"is_enabled"`
CashAppHandle string `json:"cash_app_handle"`
} `json:"tipjar_settings"`
} `json:"result"`
} `json:"user_results"`
} `json:"shared_item"`
} `json:"items"`
} `json:"sharings"`
} `json:"audioSpace"`
} `json:"data"`
}
func (user *spaceUser) parse() *SpaceUser {
result := &SpaceUser{
UserID: user.UserResults.RestID,
Username: user.TwitterScreenName,
Name: user.DisplayName,
Avatar: user.AvatarURL,
IsVerified: user.IsVerified,
ConnectedAt: time.UnixMilli(user.Start),
}
return result
}
func (space *space) parse() *Space {
result := &Space{
ID: space.Data.AudioSpace.Metadata.RestID,
State: space.Data.AudioSpace.Metadata.State,
Title: space.Data.AudioSpace.Metadata.Title,
ContentType: space.Data.AudioSpace.Metadata.ContentType,
Topics: []Topic{},
Participants: SpaceParticipants{
TotalCount: space.Data.AudioSpace.Metadata.TotalLiveListeners,
CurrentCount: space.Data.AudioSpace.Participants.Total,
},
CreatedAt: time.UnixMilli(space.Data.AudioSpace.Metadata.CreatedAt),
ScheduledStart: time.UnixMilli(space.Data.AudioSpace.Metadata.ScheduledStart),
StartedAt: time.UnixMilli(space.Data.AudioSpace.Metadata.StartedAt),
UpdatedAt: time.UnixMilli(space.Data.AudioSpace.Metadata.UpdatedAt),
}
for _, topic := range space.Data.AudioSpace.Metadata.Topics {
result.Topics = append(result.Topics, Topic{
ID: topic.Topic.TopicID,
Title: topic.Topic.Name,
})
}
for _, admin := range space.Data.AudioSpace.Participants.Admins {
result.Participants.Admins = append(result.Participants.Admins, admin.parse())
}
for _, speaker := range space.Data.AudioSpace.Participants.Speakers {
result.Participants.Speakers = append(result.Participants.Speakers, speaker.parse())
}
for _, listener := range space.Data.AudioSpace.Participants.Listeners {
result.Participants.Listeners = append(result.Participants.Listeners, listener.parse())
}
return result
}

23
spaces_test.go Normal file
View file

@ -0,0 +1,23 @@
package twitterscraper_test
import (
"errors"
"testing"
)
func TestGetSpace(t *testing.T) {
if skipAuthTest {
t.Skip("Skipping test due to environment variable")
}
spaceId := "1OdJrXPVLEnKX"
space, err := testScraper.GetSpace(spaceId)
if err != nil {
t.Fatal(err)
}
if space.ID != spaceId {
t.Fatal(errors.New("returned space id is not requested"))
}
}