t 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +820 -0
- data/LICENSE.md +1 -1
- data/bin/t +7 -15
- data/lib/t/cli.rb +317 -310
- data/lib/t/collectable.rb +4 -4
- data/lib/t/delete.rb +28 -35
- data/lib/t/list.rb +17 -18
- data/lib/t/printable/messaging.rb +54 -0
- data/lib/t/printable/rendering.rb +100 -0
- data/lib/t/printable.rb +58 -181
- data/lib/t/rcfile.rb +9 -1
- data/lib/t/requestable.rb +13 -7
- data/lib/t/requestable_api/account_endpoints.rb +93 -0
- data/lib/t/requestable_api/dm_endpoints.rb +41 -0
- data/lib/t/requestable_api/dm_helpers.rb +107 -0
- data/lib/t/requestable_api/dm_parsing.rb +76 -0
- data/lib/t/requestable_api/helpers.rb +86 -0
- data/lib/t/requestable_api/http.rb +113 -0
- data/lib/t/requestable_api/list_endpoints.rb +70 -0
- data/lib/t/requestable_api/list_normalization.rb +74 -0
- data/lib/t/requestable_api/mutations.rb +88 -0
- data/lib/t/requestable_api/resolution.rb +108 -0
- data/lib/t/requestable_api/tweet_endpoints.rb +85 -0
- data/lib/t/requestable_api/tweet_normalization.rb +87 -0
- data/lib/t/requestable_api/user_endpoints.rb +82 -0
- data/lib/t/requestable_api/user_normalization.rb +68 -0
- data/lib/t/requestable_api.rb +69 -0
- data/lib/t/search.rb +43 -58
- data/lib/t/set.rb +8 -9
- data/lib/t/stream.rb +91 -131
- data/lib/t/utils.rb +55 -52
- data/lib/t/version.rb +2 -2
- data/t.gemspec +3 -2
- metadata +37 -7
- data/lib/t/core_ext/kernel.rb +0 -13
- data/lib/t/core_ext/string.rb +0 -15
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module Helpers
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def normalize_id_list(ids)
|
|
7
|
+
Array(ids).flatten.compact.map(&:to_s)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def single_or_array(input, values)
|
|
11
|
+
input.is_a?(Array) ? values : values.first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def timeline_v2_params(opts)
|
|
15
|
+
params = {
|
|
16
|
+
"tweet.fields": V2_TWEET_FIELDS,
|
|
17
|
+
expansions: V2_TWEET_EXPANSIONS,
|
|
18
|
+
"user.fields": V2_USER_FIELDS,
|
|
19
|
+
"place.fields": V2_PLACE_FIELDS,
|
|
20
|
+
}
|
|
21
|
+
count = opts[:count] || DEFAULT_NUM_RESULTS
|
|
22
|
+
params[:max_results] = count.to_i.clamp(1, MAX_SEARCH_RESULTS).to_s
|
|
23
|
+
excludes = build_v2_excludes(opts)
|
|
24
|
+
params[:exclude] = excludes unless excludes.empty?
|
|
25
|
+
params[:until_id] = opts[:max_id].to_s if opts[:max_id]
|
|
26
|
+
params[:since_id] = opts[:since_id].to_s if opts[:since_id]
|
|
27
|
+
params
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_v2_excludes(opts)
|
|
31
|
+
excludes = []
|
|
32
|
+
excludes << "replies" if opts[:exclude_replies]
|
|
33
|
+
excludes << "retweets" if opts.key?(:include_rts) && opts[:include_rts] == false
|
|
34
|
+
excludes.join(",")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user_lookup_params
|
|
38
|
+
{
|
|
39
|
+
"user.fields": V2_USER_FIELDS,
|
|
40
|
+
expansions: V2_USER_EXPANSIONS,
|
|
41
|
+
"tweet.fields": V2_TWEET_FIELDS,
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def list_lookup_params
|
|
46
|
+
{
|
|
47
|
+
"list.fields": V2_LIST_FIELDS,
|
|
48
|
+
expansions: "owner_id",
|
|
49
|
+
"user.fields": V2_USER_FIELDS,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def v2_tweet_params
|
|
54
|
+
{
|
|
55
|
+
"tweet.fields": V2_TWEET_FIELDS,
|
|
56
|
+
expansions: V2_TWEET_EXPANSIONS,
|
|
57
|
+
"user.fields": V2_USER_FIELDS,
|
|
58
|
+
"place.fields": V2_PLACE_FIELDS,
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def entity_like?(obj)
|
|
63
|
+
obj.is_a?(Hash)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def value_id(value)
|
|
67
|
+
return nil unless entity_like?(value)
|
|
68
|
+
|
|
69
|
+
value["id_str"] || value["id"]&.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def strip_at(value)
|
|
73
|
+
value.to_s.delete_prefix("@")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def slugify_list_name(value)
|
|
77
|
+
slug = value.to_s.downcase.gsub(/[^a-z0-9_]+/, "-")
|
|
78
|
+
slug.gsub(/\A-+|-+\z/, "")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def numeric_identifier?(value)
|
|
82
|
+
value.to_s.match?(/\A\d+\z/)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
|
|
3
|
+
module T
|
|
4
|
+
module RequestableAPI
|
|
5
|
+
module HTTP
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def v1_client
|
|
9
|
+
@v1_client ||= begin
|
|
10
|
+
client # ensure credentials are initialized
|
|
11
|
+
X::Client.new(**@requestable_api_credentials, base_url: BASE_URL_V1)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def upload_client
|
|
16
|
+
@upload_client ||= begin
|
|
17
|
+
client # ensure credentials are initialized
|
|
18
|
+
X::Client.new(**@requestable_api_credentials, base_url: BASE_URL_UPLOAD)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bearer_client
|
|
23
|
+
@bearer_client ||= begin
|
|
24
|
+
client # ensure credentials are initialized
|
|
25
|
+
key = @requestable_api_credentials[:api_key]
|
|
26
|
+
secret = @requestable_api_credentials[:api_key_secret]
|
|
27
|
+
basic = Base64.strict_encode64("#{key}:#{secret}")
|
|
28
|
+
uri = URI("https://api.twitter.com/oauth2/token")
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = true
|
|
31
|
+
request = Net::HTTP::Post.new(uri)
|
|
32
|
+
request["Authorization"] = "Basic #{basic}"
|
|
33
|
+
request["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"
|
|
34
|
+
request.body = "grant_type=client_credentials"
|
|
35
|
+
response = http.request(request)
|
|
36
|
+
token = JSON.parse(response.body)["access_token"]
|
|
37
|
+
X::Client.new(bearer_token: token)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def upload_media(file)
|
|
42
|
+
binary = if file.respond_to?(:read)
|
|
43
|
+
file.rewind if file.respond_to?(:rewind)
|
|
44
|
+
file.read
|
|
45
|
+
else
|
|
46
|
+
File.binread(file.to_s)
|
|
47
|
+
end
|
|
48
|
+
response = t_post_v1_form("media/upload.json", {media_data: Base64.strict_encode64(binary)}, request_client: upload_client)
|
|
49
|
+
media_id = response["media_id_string"] || response["media_id"] || value_id(response)
|
|
50
|
+
raise X::Error.new("Media upload did not return a media_id") if media_id.to_s.empty?
|
|
51
|
+
|
|
52
|
+
media_id.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def t_get_v2(path, params = {})
|
|
56
|
+
client.get(t_endpoint(path, params))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def t_get_v1(path, params = {})
|
|
60
|
+
v1_client.get(t_endpoint(path, params))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def t_post_v2_json(path, body = {})
|
|
64
|
+
client.post(t_normalize_path(path), JSON.generate(t_compact_hash(body)), headers: JSON_HEADERS)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def t_post_bearer_json(path, body = {})
|
|
68
|
+
bearer_client.post(t_normalize_path(path), JSON.generate(t_compact_hash(body)), headers: JSON_HEADERS)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def t_post_v1_form(path, params = {}, request_client: v1_client)
|
|
72
|
+
request_client.post(t_normalize_path(path), URI.encode_www_form(t_form_pairs(params)), headers: FORM_HEADERS)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def t_delete_v2(path, params = {})
|
|
76
|
+
client.delete(t_endpoint(path, params))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def t_endpoint(path, params)
|
|
80
|
+
query = URI.encode_www_form(t_form_pairs(params))
|
|
81
|
+
query.empty? ? t_normalize_path(path) : "#{t_normalize_path(path)}?#{query}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def t_form_pairs(hash)
|
|
85
|
+
hash.each_with_object([]) do |(key, value), pairs|
|
|
86
|
+
next if value.nil?
|
|
87
|
+
|
|
88
|
+
pairs << [key.to_s, t_scalar_value(value)]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def t_scalar_value(value)
|
|
93
|
+
case value
|
|
94
|
+
when TrueClass then "true"
|
|
95
|
+
when FalseClass then "false"
|
|
96
|
+
else value.to_s
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def t_compact_hash(hash)
|
|
101
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
102
|
+
next if value.nil?
|
|
103
|
+
|
|
104
|
+
memo[key] = value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def t_normalize_path(path)
|
|
109
|
+
path.to_s.sub(%r{\A/+}, "")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module ListEndpoints
|
|
4
|
+
def x_lists(user = nil)
|
|
5
|
+
user_id = user.nil? ? current_user_id : resolve_user_id(user)
|
|
6
|
+
collect_owned_lists(user_id)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def x_list(owner_or_id, list_name = nil)
|
|
10
|
+
list_id = if list_name.nil?
|
|
11
|
+
numeric_identifier?(owner_or_id) ? owner_or_id.to_s : resolve_list_id(current_user_id, owner_or_id.to_s)
|
|
12
|
+
else
|
|
13
|
+
owner_id = numeric_identifier?(owner_or_id) ? owner_or_id.to_s : resolve_user_id(owner_or_id)
|
|
14
|
+
resolve_list_id(owner_id, list_name.to_s)
|
|
15
|
+
end
|
|
16
|
+
extract_lists(t_get_v2("lists/#{list_id}", list_lookup_params)).first || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def x_create_list(name, opts = {})
|
|
20
|
+
body = {
|
|
21
|
+
name: name.to_s,
|
|
22
|
+
description: opts[:description].to_s,
|
|
23
|
+
private: opts[:mode].to_s == "private",
|
|
24
|
+
}
|
|
25
|
+
extract_lists(t_post_v2_json("lists", body)).first || {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def x_destroy_list(list)
|
|
29
|
+
list_id = entity_like?(list) ? value_id(list) : list.to_s
|
|
30
|
+
t_delete_v2("lists/#{list_id}")
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def x_add_list_members(list_name, users)
|
|
35
|
+
list_id = resolve_list_id(current_user_id, list_name.to_s)
|
|
36
|
+
Array(users).flatten.each do |entry|
|
|
37
|
+
t_post_v2_json("lists/#{list_id}/members", user_id: resolve_user_id(entry))
|
|
38
|
+
end
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def x_remove_list_members(list_name, users)
|
|
43
|
+
list_id = resolve_list_id(current_user_id, list_name.to_s)
|
|
44
|
+
Array(users).flatten.each do |entry|
|
|
45
|
+
t_delete_v2("lists/#{list_id}/members/#{resolve_user_id(entry)}")
|
|
46
|
+
end
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def x_list_member?(owner, list_name, user)
|
|
51
|
+
owner_id = numeric_identifier?(owner) ? owner.to_s : resolve_user_id(owner)
|
|
52
|
+
list_id = resolve_list_id(owner_id, list_name.to_s)
|
|
53
|
+
user_id = resolve_user_id(user)
|
|
54
|
+
fetch_list_member_ids(list_id).include?(user_id.to_s)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def x_list_members(owner, list_name)
|
|
58
|
+
owner_id = numeric_identifier?(owner) ? owner.to_s : resolve_user_id(owner)
|
|
59
|
+
list_id = resolve_list_id(owner_id, list_name)
|
|
60
|
+
lookup_users_by_ids(fetch_list_member_ids(list_id))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def x_list_timeline(owner, list_name, opts = {})
|
|
64
|
+
owner_id = numeric_identifier?(owner) ? owner.to_s : resolve_user_id(owner)
|
|
65
|
+
list_id = resolve_list_id(owner_id, list_name)
|
|
66
|
+
extract_tweets(t_get_v2("lists/#{list_id}/tweets", timeline_v2_params(opts)))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module ListNormalization
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def extract_lists(value)
|
|
7
|
+
return value if value.is_a?(Array)
|
|
8
|
+
return value.fetch("lists", []) if value["lists"].is_a?(Array)
|
|
9
|
+
|
|
10
|
+
users_by_id = index_items_by_id(value.dig("includes", "users"))
|
|
11
|
+
data = value["data"]
|
|
12
|
+
return [] unless data.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
data.map { |list| normalize_v2_list(list, users_by_id) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def normalize_v2_list(list, users_by_id)
|
|
18
|
+
return list if list.key?("slug") || list.key?("full_name")
|
|
19
|
+
|
|
20
|
+
object = build_v2_list_core(list)
|
|
21
|
+
apply_v2_list_mode(object, list)
|
|
22
|
+
apply_v2_list_owner(object, list, users_by_id)
|
|
23
|
+
object
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_v2_list_core(list)
|
|
27
|
+
object = {}
|
|
28
|
+
apply_v2_id(object, list)
|
|
29
|
+
apply_v2_list_slug(object, list)
|
|
30
|
+
%w[created_at description].each { |f| object[f] = list[f] if list[f] }
|
|
31
|
+
object["member_count"] = list["member_count"] if list.key?("member_count")
|
|
32
|
+
object["subscriber_count"] = list["subscriber_count"] || list["follower_count"]
|
|
33
|
+
object
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def apply_v2_id(object, source)
|
|
37
|
+
id = value_id(source)
|
|
38
|
+
return unless id
|
|
39
|
+
|
|
40
|
+
object["id"] = id.to_i
|
|
41
|
+
object["id_str"] = id.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_v2_list_slug(object, list)
|
|
45
|
+
slug = list["slug"] || list["name"]
|
|
46
|
+
return unless slug
|
|
47
|
+
|
|
48
|
+
object["slug"] = slug
|
|
49
|
+
object["name"] = slug
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def apply_v2_list_mode(object, list)
|
|
53
|
+
if list.key?("mode")
|
|
54
|
+
object["mode"] = list["mode"]
|
|
55
|
+
elsif list.key?("private")
|
|
56
|
+
object["mode"] = list["private"] ? "private" : "public"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def apply_v2_list_owner(object, list, users_by_id)
|
|
61
|
+
id = value_id(list)
|
|
62
|
+
slug = object["slug"]
|
|
63
|
+
owner_id = list["owner_id"]
|
|
64
|
+
if owner_id && users_by_id[owner_id]
|
|
65
|
+
owner = normalize_v2_user(users_by_id[owner_id])
|
|
66
|
+
object["user"] = owner
|
|
67
|
+
owner_name = owner["screen_name"]
|
|
68
|
+
object["full_name"] = "@#{owner_name}/#{slug}" if owner_name && slug
|
|
69
|
+
end
|
|
70
|
+
object["uri"] = "https://x.com/i/lists/#{id}" if id
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module Mutations
|
|
4
|
+
def x_block(users)
|
|
5
|
+
mutate_users(users) { |target_id, me_id| t_post_v2_json("users/#{me_id}/blocking", target_user_id: target_id) }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def x_unblock(users)
|
|
9
|
+
mutate_users(users) { |target_id, me_id| t_delete_v2("users/#{me_id}/blocking/#{target_id}") }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def x_mute(users)
|
|
13
|
+
mutate_users(users) { |target_id, me_id| t_post_v2_json("users/#{me_id}/muting", target_user_id: target_id) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def x_unmute(users)
|
|
17
|
+
mutate_users(users) { |target_id, me_id| t_delete_v2("users/#{me_id}/muting/#{target_id}") }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def x_follow(users)
|
|
21
|
+
mutate_users(users) { |target_id, me_id| t_post_v2_json("users/#{me_id}/following", target_user_id: target_id) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def x_unfollow(users)
|
|
25
|
+
mutate_users(users) { |target_id, me_id| t_delete_v2("users/#{me_id}/following/#{target_id}") }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def x_report_spam(users)
|
|
29
|
+
Array(users).flatten.map do |entry|
|
|
30
|
+
resolved_user = resolve_user(entry)
|
|
31
|
+
key = numeric_identifier?(entry) ? :user_id : :screen_name
|
|
32
|
+
t_post_v1_form("users/report_spam.json", {key => (key == :user_id ? resolved_user["id"] : resolved_user["screen_name"])})
|
|
33
|
+
resolved_user
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def x_muted_ids
|
|
38
|
+
ids = []
|
|
39
|
+
params = {max_results: "1000", "user.fields": "id,username"}
|
|
40
|
+
me_id = current_user_id
|
|
41
|
+
MAX_PAGE.times do
|
|
42
|
+
response = t_get_v2("users/#{me_id}/muting", params)
|
|
43
|
+
ids.concat(extract_ids(response))
|
|
44
|
+
token = response.dig("meta", "next_token")
|
|
45
|
+
break if token.nil?
|
|
46
|
+
|
|
47
|
+
params = params.merge(pagination_token: token)
|
|
48
|
+
end
|
|
49
|
+
ids
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def x_favorite(status_ids)
|
|
53
|
+
me_id = current_user_id
|
|
54
|
+
tweets = normalize_id_list(status_ids).map do |id|
|
|
55
|
+
t_post_v2_json("users/#{me_id}/likes", tweet_id: id)
|
|
56
|
+
{"id" => id.to_i, "id_str" => id.to_s}
|
|
57
|
+
end
|
|
58
|
+
single_or_array(status_ids, tweets)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def x_unfavorite(status_ids)
|
|
62
|
+
me_id = current_user_id
|
|
63
|
+
tweets = normalize_id_list(status_ids).map do |id|
|
|
64
|
+
t_delete_v2("users/#{me_id}/likes/#{id}")
|
|
65
|
+
{"id" => id.to_i, "id_str" => id.to_s}
|
|
66
|
+
end
|
|
67
|
+
single_or_array(status_ids, tweets)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def x_retweet(status_ids)
|
|
71
|
+
me_id = current_user_id
|
|
72
|
+
tweets = normalize_id_list(status_ids).map do |id|
|
|
73
|
+
t_post_v2_json("users/#{me_id}/retweets", tweet_id: id)
|
|
74
|
+
{"id" => id.to_i, "id_str" => id.to_s}
|
|
75
|
+
end
|
|
76
|
+
single_or_array(status_ids, tweets)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def x_destroy_status(status_ids)
|
|
80
|
+
statuses = normalize_id_list(status_ids).map do |id|
|
|
81
|
+
t_delete_v2("tweets/#{id}")
|
|
82
|
+
{"id" => id.to_i, "id_str" => id.to_s}
|
|
83
|
+
end
|
|
84
|
+
single_or_array(status_ids, statuses)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module Resolution
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def mutate_users(users)
|
|
7
|
+
me_id = current_user_id
|
|
8
|
+
Array(users).flatten.map do |entry|
|
|
9
|
+
resolved_user = resolve_user(entry)
|
|
10
|
+
yield resolved_user["id"].to_s, me_id
|
|
11
|
+
resolved_user
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def current_user
|
|
16
|
+
@current_user ||= x_verify_credentials
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def current_user_id
|
|
20
|
+
value_id(current_user).to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resolve_user(entry)
|
|
24
|
+
return current_user if entry.nil?
|
|
25
|
+
return entry if entity_like?(entry)
|
|
26
|
+
return x_user(entry.to_s) if numeric_identifier?(entry)
|
|
27
|
+
|
|
28
|
+
x_user(strip_at(entry.to_s))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_user_id(entry)
|
|
32
|
+
return current_user_id if entry.nil?
|
|
33
|
+
return value_id(entry).to_s if entity_like?(entry)
|
|
34
|
+
|
|
35
|
+
numeric_identifier?(entry) ? entry.to_s : value_id(x_user(strip_at(entry.to_s))).to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve_list_id(owner_id, list_name)
|
|
39
|
+
desired = slugify_list_name(list_name)
|
|
40
|
+
lists = collect_owned_lists(owner_id)
|
|
41
|
+
matched = lists.find do |list|
|
|
42
|
+
slug = (list["slug"] || list["name"]).to_s
|
|
43
|
+
slug.casecmp?(list_name.to_s) || slugify_list_name(slug) == desired
|
|
44
|
+
end
|
|
45
|
+
(value_id(matched) || list_name).to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_relationship_ids(user_id, relationship)
|
|
49
|
+
endpoint = relationship == "followers" ? "users/#{user_id}/followers" : "users/#{user_id}/following"
|
|
50
|
+
params = {max_results: "1000", "user.fields": "id,username"}
|
|
51
|
+
ids = []
|
|
52
|
+
MAX_PAGE.times do
|
|
53
|
+
response = t_get_v2(endpoint, params)
|
|
54
|
+
ids.concat(extract_ids(response))
|
|
55
|
+
token = response.dig("meta", "next_token")
|
|
56
|
+
break if token.nil?
|
|
57
|
+
|
|
58
|
+
params = params.merge(pagination_token: token)
|
|
59
|
+
end
|
|
60
|
+
ids
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_list_member_ids(list_id)
|
|
64
|
+
params = {max_results: "100", "user.fields": "id,username"}
|
|
65
|
+
ids = []
|
|
66
|
+
MAX_PAGE.times do
|
|
67
|
+
response = t_get_v2("lists/#{list_id}/members", params)
|
|
68
|
+
ids.concat(extract_ids(response))
|
|
69
|
+
token = response.dig("meta", "next_token")
|
|
70
|
+
break if token.nil?
|
|
71
|
+
|
|
72
|
+
params = params.merge(pagination_token: token)
|
|
73
|
+
end
|
|
74
|
+
ids
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_owned_lists(user_id)
|
|
78
|
+
params = {
|
|
79
|
+
max_results: "100",
|
|
80
|
+
"list.fields": V2_LIST_FIELDS,
|
|
81
|
+
expansions: "owner_id",
|
|
82
|
+
"user.fields": V2_USER_FIELDS,
|
|
83
|
+
}
|
|
84
|
+
lists = []
|
|
85
|
+
MAX_PAGE.times do
|
|
86
|
+
response = t_get_v2("users/#{user_id}/owned_lists", params)
|
|
87
|
+
lists.concat(extract_lists(response))
|
|
88
|
+
token = response.dig("meta", "next_token")
|
|
89
|
+
break if token.nil?
|
|
90
|
+
|
|
91
|
+
params = params.merge(pagination_token: token)
|
|
92
|
+
end
|
|
93
|
+
lists
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def lookup_users_by_ids(ids)
|
|
97
|
+
ids = Array(ids).compact
|
|
98
|
+
return [] if ids.empty?
|
|
99
|
+
|
|
100
|
+
users = []
|
|
101
|
+
ids.each_slice(100) do |chunk|
|
|
102
|
+
users.concat(extract_users(t_get_v2("users", user_lookup_params.merge(ids: chunk.join(",")))))
|
|
103
|
+
end
|
|
104
|
+
users
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module RequestableAPI
|
|
3
|
+
module TweetEndpoints
|
|
4
|
+
def x_retweets_of_me(opts = {})
|
|
5
|
+
extract_tweets(t_get_v2("users/reposts_of_me", timeline_v2_params(opts)))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def x_retweeted_by_me(opts = {})
|
|
9
|
+
x_retweets_of_me(opts)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def x_retweeted_by_user(user, opts = {})
|
|
13
|
+
x_user_timeline(user, opts).select { |tweet| tweet["full_text"].to_s.start_with?("RT @") }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def x_retweeters_ids(tweet_id)
|
|
17
|
+
ids = []
|
|
18
|
+
params = {"user.fields": "id,username", max_results: "100"}
|
|
19
|
+
MAX_PAGE.times do
|
|
20
|
+
response = t_get_v2("tweets/#{tweet_id}/retweeted_by", params)
|
|
21
|
+
ids.concat(extract_ids(response))
|
|
22
|
+
token = response.dig("meta", "next_token")
|
|
23
|
+
break if token.nil?
|
|
24
|
+
|
|
25
|
+
params = params.merge(pagination_token: token)
|
|
26
|
+
end
|
|
27
|
+
ids
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def x_status(status_id, _opts = {})
|
|
31
|
+
extract_tweets(t_get_v2("tweets/#{status_id}", v2_tweet_params)).first || {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def x_home_timeline(opts = {})
|
|
35
|
+
me_id = current_user_id
|
|
36
|
+
extract_tweets(t_get_v2("users/#{me_id}/timelines/reverse_chronological", timeline_v2_params(opts)))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def x_user_timeline(user, opts = {})
|
|
40
|
+
user_id = resolve_user_id(user)
|
|
41
|
+
extract_tweets(t_get_v2("users/#{user_id}/tweets", timeline_v2_params(opts)))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def x_mentions(opts = {})
|
|
45
|
+
me_id = current_user_id
|
|
46
|
+
extract_tweets(t_get_v2("users/#{me_id}/mentions", timeline_v2_params(opts)))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def x_favorites(user = nil, opts = {})
|
|
50
|
+
if user.is_a?(Hash) && opts.empty?
|
|
51
|
+
opts = user
|
|
52
|
+
user = nil
|
|
53
|
+
end
|
|
54
|
+
user_id = user.nil? ? current_user_id : resolve_user_id(user)
|
|
55
|
+
extract_tweets(t_get_v2("users/#{user_id}/liked_tweets", timeline_v2_params(opts)))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def x_search(query, opts = {})
|
|
59
|
+
count = [opts.fetch(:count, MAX_SEARCH_RESULTS).to_i, MAX_SEARCH_RESULTS].min
|
|
60
|
+
params = {
|
|
61
|
+
query: query.to_s,
|
|
62
|
+
max_results: count.to_s,
|
|
63
|
+
}.merge(v2_tweet_params)
|
|
64
|
+
params[:until_id] = opts[:max_id].to_s if opts[:max_id]
|
|
65
|
+
params[:since_id] = opts[:since_id].to_s if opts[:since_id]
|
|
66
|
+
extract_tweets(t_get_v2("tweets/search/recent", params))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def x_update(status, opts = {})
|
|
70
|
+
body = {text: status.to_s}
|
|
71
|
+
body[:reply] = {in_reply_to_tweet_id: opts[:in_reply_to_status_id].to_s} if opts[:in_reply_to_status_id]
|
|
72
|
+
body[:media] = {media_ids: Array(opts[:media_ids]).map(&:to_s)} if opts[:media_ids]
|
|
73
|
+
response = t_post_v2_json("tweets", body)
|
|
74
|
+
id = value_id(response) || value_id(response["data"])
|
|
75
|
+
{"id" => id.to_i, "id_str" => id.to_s, "text" => status.to_s,
|
|
76
|
+
"full_text" => status.to_s, "user" => current_user}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def x_update_with_media(status, file, opts = {})
|
|
80
|
+
media_id = upload_media(file)
|
|
81
|
+
x_update(status, opts.merge(media_ids: [media_id]))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|