botinsta 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|