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.
@@ -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