add GetSpace method
This commit is contained in:
parent
571a63bb50
commit
f8c200b312
4 changed files with 340 additions and 6 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.0.9
|
||||||
|
|
||||||
|
24.07.2024
|
||||||
|
|
||||||
|
- Added method `GetSpace`
|
||||||
|
|
||||||
## v0.0.8
|
## v0.0.8
|
||||||
|
|
||||||
09.07.2024
|
09.07.2024
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -34,6 +34,7 @@ You can use this library to get tweets, profiles, and trends trivially.
|
||||||
- [Get trends](#get-trends)
|
- [Get trends](#get-trends)
|
||||||
- [Get following](#get-following)
|
- [Get following](#get-following)
|
||||||
- [Get followers](#get-followers)
|
- [Get followers](#get-followers)
|
||||||
|
- [Get space](#get-space)
|
||||||
- [Create tweet](#create-tweet)
|
- [Create tweet](#create-tweet)
|
||||||
- [Delete tweet](#delete-tweet)
|
- [Delete tweet](#delete-tweet)
|
||||||
- [Create retweet](#create-retweet)
|
- [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. It’s using the `FetchMediaTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`GetMediaTweets` returns a channel with the specified number of user tweets that contain media. It’s using the `FetchMediaTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for tweet := range scraper.GetMediaTweets(context.Background(), "taylorswift13", 50) {
|
for tweet := range scraper.GetMediaTweets(context.Background(), "taylorswift13", 50) {
|
||||||
if tweet.Error != nil {
|
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. It’s using the `FetchBookmarks` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`GetBookmarks` returns a channel with the specified number of bookmarked tweets. It’s using the `FetchBookmarks` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for tweet := range scraper.GetBookmarks(context.Background(), 50) {
|
for tweet := range scraper.GetBookmarks(context.Background(), 50) {
|
||||||
if tweet.Error != nil {
|
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. It’s using the `FetchHomeTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`GetHomeTweets` returns a channel with the specified number of latest home tweets. It’s using the `FetchHomeTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for tweet := range scraper.GetHomeTweets(context.Background(), 50) {
|
for tweet := range scraper.GetHomeTweets(context.Background(), 50) {
|
||||||
if tweet.Error != nil {
|
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. It’s using the `FetchForYouTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`GetForYouTweets` returns a channel with the specified number of for you home tweets. It’s using the `FetchForYouTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for tweet := range scraper.GetForYouTweets(context.Background(), 50) {
|
for tweet := range scraper.GetForYouTweets(context.Background(), 50) {
|
||||||
if tweet.Error != nil {
|
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. It’s using the `FetchSearchTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`SearchTweets` returns a channel with the specified number of tweets that contain media. It’s using the `FetchSearchTweets` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for tweet := range scraper.SearchTweets(context.Background(),
|
for tweet := range scraper.SearchTweets(context.Background(),
|
||||||
"twitter scraper data -filter:retweets", 50) {
|
"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. It’s using the `FetchSearchProfiles` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
`SearchProfiles` returns a channel with the specified number of tweets that contain media. It’s using the `FetchSearchProfiles` method under the hood. Read how this method works in [Methods that returns channels](#methods-that-returns-channels).
|
||||||
|
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
for profile := range scraper.SearchProfiles(context.Background(), "Twitter", 50) {
|
for profile := range scraper.SearchProfiles(context.Background(), "Twitter", 50) {
|
||||||
if profile.Error != nil {
|
if profile.Error != nil {
|
||||||
|
|
@ -431,6 +426,37 @@ var cursor string
|
||||||
users, cursor, err := scraper.FetchFollowers("Support", 20, cursor)
|
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
|
### Create tweet
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|
|
||||||
279
spaces.go
Normal file
279
spaces.go
Normal 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
23
spaces_test.go
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue