botinsta 0.1.1

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