botinsta 0.1.1
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 +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +117 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/botinsta.gemspec +33 -0
- data/example/example.rb +12 -0
- data/lib/botinsta.rb +56 -0
- data/lib/botinsta/actions.rb +108 -0
- data/lib/botinsta/class_methods.rb +21 -0
- data/lib/botinsta/data/media_data.rb +42 -0
- data/lib/botinsta/data/page_data.rb +36 -0
- data/lib/botinsta/data/user_data.rb +31 -0
- data/lib/botinsta/helpers.rb +156 -0
- data/lib/botinsta/login.rb +48 -0
- data/lib/botinsta/modes.rb +66 -0
- data/lib/botinsta/pages.rb +79 -0
- data/lib/botinsta/requests.rb +43 -0
- data/lib/botinsta/version.rb +3 -0
- data/spec/botinsta_spec.rb +5 -0
- data/spec/spec_helper.rb +103 -0
- metadata +241 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# Class handling media data.
|
2
|
+
# Takes a data object extended Hashie::Extensions::DeepFind
|
3
|
+
class MediaData
|
4
|
+
|
5
|
+
attr_reader :id, :owner, :text, :shortcode, :tags
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
|
9
|
+
@id = data.deep_find('id')
|
10
|
+
@owner = data.deep_find('owner')['id']
|
11
|
+
@is_video = data.deep_find('is_video')
|
12
|
+
@comments_disabled = data.deep_find('comments_disabled')
|
13
|
+
@text = data.deep_find('text')
|
14
|
+
@tags = @text.scan(/#[a-zA-Z0-9]+/)
|
15
|
+
@shortcode = data.deep_find('shortcode')
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def comments_disabled?
|
20
|
+
@comments_disabled
|
21
|
+
end
|
22
|
+
|
23
|
+
def blacklisted_tag?(tag_blacklist)
|
24
|
+
!(@tags & tag_blacklist).empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def video?
|
28
|
+
@is_video
|
29
|
+
end
|
30
|
+
|
31
|
+
def insert_into_db(table)
|
32
|
+
table.insert(media_id: @id, user_id: @owner, shortcode: @shortcode, like_time: Time.now)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_from_db(table)
|
36
|
+
table.where(media_id: @id).delete
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists_in_db?(table)
|
40
|
+
!table.where(media_id: @id).empty?
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Class handling media data.
|
2
|
+
# Takes a data object extended Hashie::Extensions::DeepFind
|
3
|
+
class PageData
|
4
|
+
|
5
|
+
attr_reader :hashtag_id, :hashtag_name, :end_cursor,
|
6
|
+
:medias, :media_count, :top_medias, :top_media_count,
|
7
|
+
:all_media, :all_media_count
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
|
11
|
+
@hashtag_id = data.deep_find('hashtag')['id']
|
12
|
+
@hashtag_name = data.deep_find('hashtag')['name']
|
13
|
+
@has_next_page = data.deep_find('page_info')['has_next_page']
|
14
|
+
@end_cursor = data.deep_find('end_cursor')
|
15
|
+
@top_medias = data['data']['hashtag']['edge_hashtag_to_top_posts']['edges']
|
16
|
+
@top_media_count = @top_medias.count
|
17
|
+
@medias = data['data']['hashtag']['edge_hashtag_to_media']['edges']
|
18
|
+
@media_count = @medias.count + @top_media_count
|
19
|
+
@all_media = @top_medias + @medias
|
20
|
+
@all_media_count = @all_media.count
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_page?
|
25
|
+
@has_next_page
|
26
|
+
end
|
27
|
+
|
28
|
+
def end_cursor_nil?
|
29
|
+
@end_cursor.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def medias_empty?
|
33
|
+
@medias.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Class handling user data.
|
2
|
+
# Takes a data object extended Hashie::Extensions::DeepFind
|
3
|
+
class UserData
|
4
|
+
|
5
|
+
attr_reader :id, :username, :full_name, :follower_count, :following_count
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@id = data.deep_find('pk')
|
9
|
+
@username = data.deep_find('username')
|
10
|
+
@full_name = data.deep_find('full_name')
|
11
|
+
@following_count = data.deep_find('following_count')
|
12
|
+
@follower_count = data.deep_find('follower_count')
|
13
|
+
@is_private = data.deep_find('is_private')
|
14
|
+
end
|
15
|
+
|
16
|
+
def private?
|
17
|
+
@is_private
|
18
|
+
end
|
19
|
+
|
20
|
+
def insert_into_db(table)
|
21
|
+
table.insert(user_id: @id, username: @username, follow_time: Time.now)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_from_db(table)
|
25
|
+
table.where(user_id: @id).delete
|
26
|
+
end
|
27
|
+
|
28
|
+
def exists_in_db?(table)
|
29
|
+
!table.where(user_id: @id).empty?
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# Some helper methods
|
2
|
+
# to use in main methods for the bot.
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
# Prints out the current time
|
6
|
+
# @example
|
7
|
+
# print_time_stamp # => "2018-09-19 12:14:43"
|
8
|
+
def print_time_stamp
|
9
|
+
print "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\t"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Prints success message for a specified action.
|
13
|
+
#
|
14
|
+
# @option params [Symbol] :action The action performed.
|
15
|
+
# @option params [String] :data The id that the action is performed on.
|
16
|
+
# @option params [Integer] :number The number of times that the action has been performed.
|
17
|
+
def print_success_message(**params)
|
18
|
+
action = case params[:action]
|
19
|
+
when :like then 'liked media'
|
20
|
+
when :unlike then 'unliked media'
|
21
|
+
when :follow then 'followed user'
|
22
|
+
when :unfollow then 'unfollowed user'
|
23
|
+
when :comment then 'commented on media'
|
24
|
+
end
|
25
|
+
success_string = "Successfully #{action} ##{params[:number]} ".colorize(:green) + params[:data].to_s
|
26
|
+
print_time_stamp
|
27
|
+
puts success_string
|
28
|
+
end
|
29
|
+
|
30
|
+
# Prints error message for a specified action.
|
31
|
+
#
|
32
|
+
# @option params [Symbol] :action The action performed.
|
33
|
+
# @option params [String] :data The id that the action is performed on.
|
34
|
+
def print_error_message(**params)
|
35
|
+
action = case params[:action]
|
36
|
+
when :like then 'like media'
|
37
|
+
when :unlike then 'unlike media'
|
38
|
+
when :follow then 'follow user'
|
39
|
+
when :unfollow then 'unfollow user'
|
40
|
+
when :comment then 'comment on media'
|
41
|
+
end
|
42
|
+
error_string = "There was an error trying to #{action} ".colorize(:red) + params[:data].to_s
|
43
|
+
print_time_stamp
|
44
|
+
puts error_string
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# Prints login status message for the account
|
49
|
+
#
|
50
|
+
# @option params [Symbol] :result Login status
|
51
|
+
# @option params [String] :username
|
52
|
+
def print_login_message(**params)
|
53
|
+
result = case params[:result]
|
54
|
+
when :success then 'Successfully logged in as '
|
55
|
+
when :error then 'There was an error trying to login as '
|
56
|
+
end
|
57
|
+
print_time_stamp
|
58
|
+
puts result.colorize(:red) + params[:username]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Prints messages when trying to take some action.
|
62
|
+
#
|
63
|
+
# @option params [Symbol] :action The action that the bot is trying to take.
|
64
|
+
# @option params [String, Integer] :data The data which will be
|
65
|
+
# affected by the specified action.
|
66
|
+
def print_try_message(**params)
|
67
|
+
action = case params[:action]
|
68
|
+
when :login then 'Trying to login ...'
|
69
|
+
when :logout then 'Trying to logout ...'
|
70
|
+
when :like then 'Trying to like media '
|
71
|
+
when :unlike then 'Trying to unlike media '
|
72
|
+
when :follow then 'Trying to follow user '
|
73
|
+
when :unfollow then 'trying to unfollow user '
|
74
|
+
end
|
75
|
+
print_time_stamp
|
76
|
+
puts action.colorize(:light_red) + "#{params[:data].to_s unless params[:data].nil?}"
|
77
|
+
sleep(1)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Prints action sum. Used before logging out.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# bot.print_action_sum # => 2018-09-19 17:41:11 Liked: 0 Followed: 0 Unfollowed: 0
|
84
|
+
def print_action_sum
|
85
|
+
string = 'Liked: ' + @total_likes.to_s.colorize(:red) +
|
86
|
+
' Followed: ' + @total_follows.to_s.colorize(:red) +
|
87
|
+
' Unfollowed: ' + @total_unfollows.to_s.colorize(:red)
|
88
|
+
print_time_stamp
|
89
|
+
puts string
|
90
|
+
end
|
91
|
+
|
92
|
+
# Prints out when the current is set to some specified tag.
|
93
|
+
#
|
94
|
+
# @param tag [String] current tag.
|
95
|
+
def print_tag_message(tag)
|
96
|
+
print_time_stamp
|
97
|
+
puts 'Current tag is set to '.colorize(:blue) + '#' + tag
|
98
|
+
end
|
99
|
+
|
100
|
+
def sleep_rand(min, max)
|
101
|
+
sleep_time = rand(min..max)
|
102
|
+
sleep(1)
|
103
|
+
print_time_stamp
|
104
|
+
puts "Sleeping for #{sleep_time - 1} seconds ...".colorize(:red)
|
105
|
+
sleep(sleep_time - 1)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Handles the creation and connection of a database and its tables.
|
109
|
+
# Sets @table_follows and @table_likes to the related tables for further use.
|
110
|
+
def handle_database_creation
|
111
|
+
database = Sequel.sqlite('./actions_db.db') # memory database, requires sqlite3
|
112
|
+
database.create_table? :"#{@username}_follows" do
|
113
|
+
primary_key :id
|
114
|
+
String :user_id
|
115
|
+
String :username
|
116
|
+
Time :follow_time
|
117
|
+
end
|
118
|
+
database.create_table? :"#{@username}_likes" do
|
119
|
+
primary_key :id
|
120
|
+
String :media_id
|
121
|
+
String :user_id
|
122
|
+
String :shortcode
|
123
|
+
Time :like_time
|
124
|
+
end
|
125
|
+
@table_follows = database[:"#{@username}_follows"]
|
126
|
+
@table_likes = database[:"#{@username}_likes"]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Reassigns the database related variables to the first entry
|
130
|
+
# in the database.
|
131
|
+
# Used after a deletion from the database.
|
132
|
+
def refresh_db_related
|
133
|
+
return if @table_follows.empty?
|
134
|
+
|
135
|
+
@first_db_entry = @table_follows.first
|
136
|
+
@last_follow_time = @first_db_entry[:follow_time]
|
137
|
+
end
|
138
|
+
|
139
|
+
# The method that is used to delete a user from the database
|
140
|
+
# after unfollowing the user.
|
141
|
+
#
|
142
|
+
# @todo This method needs to be replaced with a better and DRY one.
|
143
|
+
# @param user_id [String, Integer] id of the user to be unfollowed.
|
144
|
+
def delete_from_db(user_id)
|
145
|
+
@table_follows.where(user_id: user_id).delete
|
146
|
+
end
|
147
|
+
|
148
|
+
# Calculates if a day is past since the first follow entry in the database.
|
149
|
+
#
|
150
|
+
# @param last_time [Time] a Time instance, used with @last_follow_time.
|
151
|
+
# @return [Boolean] true if a day is past since
|
152
|
+
# the first follow entry in the database, false otherwise.
|
153
|
+
def one_day_past?(last_time)
|
154
|
+
((Time.now - last_time) / 86_400) >= 1
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Contains login and logout methods for the bot.
|
2
|
+
module Login
|
3
|
+
|
4
|
+
# Login method to log the user in.
|
5
|
+
# Prints success message on successful login,
|
6
|
+
# error message otherwise.
|
7
|
+
# @example Login example
|
8
|
+
# bot.login # => 2018-09-19 17:39:45 Trying to login ...
|
9
|
+
# # => 2018-09-19 17:39:47 Successfully logged in as andreyuhai
|
10
|
+
def login
|
11
|
+
@agent = Mechanize.new
|
12
|
+
|
13
|
+
# Navigate to classic login page
|
14
|
+
login_page = @agent.get 'https://www.instagram.com/accounts/login/?force_classic_login'
|
15
|
+
|
16
|
+
# Get the login form
|
17
|
+
login_form = login_page.forms.first
|
18
|
+
|
19
|
+
# Fill in the login form
|
20
|
+
login_form['username'] = @username
|
21
|
+
login_form['password'] = @password
|
22
|
+
|
23
|
+
# Submit the form and if couldn't login raise an exception.
|
24
|
+
print_try_message(action: :login)
|
25
|
+
response = login_form.submit
|
26
|
+
if response.code != 200 && response.body.include?('not-logged-in')
|
27
|
+
login_status = false
|
28
|
+
else
|
29
|
+
print_login_message(result: :success, username: @username)
|
30
|
+
login_status = true
|
31
|
+
end
|
32
|
+
raise StandardError unless login_status
|
33
|
+
rescue StandardError
|
34
|
+
print_login_message(result: :error, username: @username)
|
35
|
+
# TODO: logger to log these kind of stuff
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
# Prints action sum and then logs the user out.
|
40
|
+
# @example Logout example
|
41
|
+
# bot.logout # => 2018-09-19 17:41:11 Liked: 0 Followed: 0 Unfollowed: 0
|
42
|
+
# # => 2018-09-19 17:41:11 Trying to logout ...
|
43
|
+
def logout
|
44
|
+
print_action_sum
|
45
|
+
print_try_message(action: :logout)
|
46
|
+
@agent.get 'https://instagram.com/accounts/logout/'
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Contains bot modes.
|
2
|
+
# The bot has only one mode for now, which is tag based mode.
|
3
|
+
# In this mode bot works on a tag basis. Gets medias from specified tags,
|
4
|
+
# likes them and follows the owner of the media until it fulfills
|
5
|
+
# its like and follow limits for the day.
|
6
|
+
module Modes
|
7
|
+
|
8
|
+
def tag_based_mode
|
9
|
+
@tags.each do |tag|
|
10
|
+
like_count = 0; follow_count = 0
|
11
|
+
is_first_page = true
|
12
|
+
print_tag_message tag
|
13
|
+
until like_count == @likes_per_tag && follow_count == @follows_per_tag
|
14
|
+
|
15
|
+
if is_first_page
|
16
|
+
set_query_id(tag)
|
17
|
+
get_first_page_data(tag)
|
18
|
+
is_first_page = false
|
19
|
+
break if @page.medias_empty?
|
20
|
+
elsif like_count == @page.media_count && @page.has_next_page?
|
21
|
+
get_next_page_data(tag)
|
22
|
+
end
|
23
|
+
@page.medias.each do |media|
|
24
|
+
media.extend Hashie::Extensions::DeepFind
|
25
|
+
@media = MediaData.new(media)
|
26
|
+
next if @media.blacklisted_tag?(@tag_blacklist)
|
27
|
+
|
28
|
+
# Here is the code for liking stuff.
|
29
|
+
if like_count != @likes_per_tag
|
30
|
+
if like_if_not_in_db(@media)
|
31
|
+
like_count += 1
|
32
|
+
else
|
33
|
+
print_error_message(action: :like, data: @media.id)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Here is the code for following users.
|
38
|
+
if follow_count != @follows_per_tag
|
39
|
+
if get_user_page_data(@media.owner) && follow_if_not_in_db(@user)
|
40
|
+
follow_count += 1
|
41
|
+
else
|
42
|
+
print_error_message(action: :follow, data: @user.username)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Here is the code for unfollowing users
|
47
|
+
if !@table_follows.empty? && one_day_past?(@last_follow_time) && @total_unfollows != @unfollows_per_run
|
48
|
+
if unfollow_user(@first_db_entry[:user_id])
|
49
|
+
@total_unfollows += 1
|
50
|
+
print_success_message(action: :unfollow, number: @total_unfollows,
|
51
|
+
data: @first_db_entry[:username])
|
52
|
+
delete_from_db(@first_db_entry[:user_id])
|
53
|
+
refresh_db_related
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
break if like_count == @likes_per_tag && follow_count == @follows_per_tag
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
rescue Interrupt
|
63
|
+
logout
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Contains methods for getting pages, query_id and JS link.
|
2
|
+
# To like a media from every tag we first need its query_id (a.k.a) query_hash
|
3
|
+
#
|
4
|
+
module Pages
|
5
|
+
# Sets the query id for the current tag.
|
6
|
+
#
|
7
|
+
# @param tag [String] Current tag.
|
8
|
+
# @return @query_id [String] Returns the instance variable @query_id
|
9
|
+
def set_query_id(tag)
|
10
|
+
response = @agent.get get_js_link tag
|
11
|
+
# RegExp for getting the right query id. Because there are a few of them.
|
12
|
+
match_data = /byTagName\.get\(t\)\.pagination},queryId:"(?<queryId>[0-9a-z]+)/.match(response.body)
|
13
|
+
@query_id = match_data[:queryId]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the .js link of the TagPageContainer
|
17
|
+
# from which we will extract the query_id.
|
18
|
+
#
|
19
|
+
# @param (see #set_query_id)
|
20
|
+
# @return [String] Full link of the TagPageContainer.js
|
21
|
+
def get_js_link(tag)
|
22
|
+
response = @agent.get "https://instagram.com/explore/tags/#{tag}"
|
23
|
+
# Parsing the returned page to select the script which has 'TagPageContainer.js' in its src
|
24
|
+
parsed_page = Nokogiri::HTML(response.body)
|
25
|
+
script_array = parsed_page.css('script').select {|script| script.to_s.include?('TagPageContainer.js')}
|
26
|
+
script = script_array.first
|
27
|
+
|
28
|
+
'https://instagram.com' + script['src']
|
29
|
+
end
|
30
|
+
|
31
|
+
# Gets first page JSON string for the tag to extract data
|
32
|
+
# (i.e. media IDs and owner IDs) and creates a PageData
|
33
|
+
# instance.
|
34
|
+
#
|
35
|
+
# @param (see #set_query_id)
|
36
|
+
def get_first_page_data(tag)
|
37
|
+
print_time_stamp
|
38
|
+
puts 'Getting the first page for the tag '.colorize(:blue) + "##{tag}"
|
39
|
+
response = @agent.get "https://www.instagram.com/explore/tags/#{tag}/?__a=1"
|
40
|
+
data = JSON.parse(response.body.sub(/graphql/, 'data'))
|
41
|
+
data.extend Hashie::Extensions::DeepFind
|
42
|
+
@page = PageData.new(data)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gets next page JSON string for when we liked all the media
|
46
|
+
# on the first page and creates a PageData instance.
|
47
|
+
# This is where we need query_id and
|
48
|
+
# end_cursor string of the current page.
|
49
|
+
#
|
50
|
+
# @param (see #set_query_id)
|
51
|
+
def get_next_page_data(tag)
|
52
|
+
print_time_stamp
|
53
|
+
puts 'Getting the next page for the tag '.colorize(:red) + "#{tag}"
|
54
|
+
next_page_link = "https://www.instagram.com/graphql/query/?query_hash=#{@query_id}&"\
|
55
|
+
"variables={\"tag_name\":\"#{tag}\"," \
|
56
|
+
"\"first\":10,\"after\":\"#{@page.end_cursor}\"}"
|
57
|
+
response = @agent.get next_page_link
|
58
|
+
data = JSON.parse(response.body)
|
59
|
+
data.extend Hashie::Extensions::DeepFind
|
60
|
+
@page = PageData.new(data)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Gets user page JSON string and parses it
|
64
|
+
# to create a UserData instance.
|
65
|
+
#
|
66
|
+
# @param user_id [String] User id of the media owner.
|
67
|
+
def get_user_page_data(user_id)
|
68
|
+
url_user_detail = "https://i.instagram.com/api/v1/users/#{user_id}/info/"
|
69
|
+
begin
|
70
|
+
response = @agent.get url_user_detail
|
71
|
+
rescue Mechanize::ResponseCodeError
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
data = JSON.parse(response.body)
|
75
|
+
data.extend Hashie::Extensions::DeepFind
|
76
|
+
@user = UserData.new(data)
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|