fuck_facebook 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/fuck-facebook +66 -0
- data/bin/setup +8 -0
- data/fuck_facebook.gemspec +36 -0
- data/lib/fuck_facebook.rb +5 -0
- data/lib/fuck_facebook/version.rb +3 -0
- data/src/authenticator.rb +58 -0
- data/src/config.rb +9 -0
- data/src/cookie_handler.rb +63 -0
- data/src/facebook_connection.rb +47 -0
- data/src/fb_duration.rb +36 -0
- data/src/initializers/mail_initializer.rb +12 -0
- data/src/message_handler.rb +63 -0
- data/src/models/message.rb +18 -0
- data/src/models/message_thread.rb +11 -0
- data/src/models/user.rb +11 -0
- data/src/senders.rb +13 -0
- data/src/senders/email.rb +14 -0
- data/src/senders/stdout.rb +9 -0
- data/src/storage.rb +40 -0
- data/src/user_input.rb +16 -0
- data/src/xpaths.rb +80 -0
- data/xpaths.yaml +21 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8034fd3f3afb91d27f11300b42e15e07c92808c473daec3459f82c14ab772ff6
|
4
|
+
data.tar.gz: f656f97d23da62a99d618ec197ddf8f3b9f3d8c818d71ed16d27cb35b8d25123
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: afc81df11373573a7ce572a09574f7c5159f278f3173a924202b1f0f11cd913922f89caaac117901eca27c4634576180733050da46f839c2b714b40ce4d8b068
|
7
|
+
data.tar.gz: 97e1f728a14dd6f67af0985a2d382753579fd160c16866b85946e19d855d15ec2c8ed9ed24d921cdddde2c2064e76541a145abac090360bd2670bec57f15cf82
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.1
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fuck_facebook (0.1.0)
|
5
|
+
activesupport (~> 6.1)
|
6
|
+
highline (~> 2.0)
|
7
|
+
mail (~> 2.7)
|
8
|
+
selenium-webdriver (~> 3.142)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
activesupport (6.1.4)
|
14
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
15
|
+
i18n (>= 1.6, < 2)
|
16
|
+
minitest (>= 5.1)
|
17
|
+
tzinfo (~> 2.0)
|
18
|
+
zeitwerk (~> 2.3)
|
19
|
+
ast (2.4.2)
|
20
|
+
childprocess (3.0.0)
|
21
|
+
coderay (1.1.3)
|
22
|
+
concurrent-ruby (1.1.9)
|
23
|
+
highline (2.0.3)
|
24
|
+
i18n (1.8.10)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
mail (2.7.1)
|
27
|
+
mini_mime (>= 0.1.1)
|
28
|
+
method_source (1.0.0)
|
29
|
+
mini_mime (1.1.0)
|
30
|
+
minitest (5.14.4)
|
31
|
+
parallel (1.20.1)
|
32
|
+
parser (3.0.1.1)
|
33
|
+
ast (~> 2.4.1)
|
34
|
+
pry (0.14.1)
|
35
|
+
coderay (~> 1.1)
|
36
|
+
method_source (~> 1.0)
|
37
|
+
rainbow (3.0.0)
|
38
|
+
regexp_parser (2.1.1)
|
39
|
+
rexml (3.2.5)
|
40
|
+
rubocop (1.17.0)
|
41
|
+
parallel (~> 1.10)
|
42
|
+
parser (>= 3.0.0.0)
|
43
|
+
rainbow (>= 2.2.2, < 4.0)
|
44
|
+
regexp_parser (>= 1.8, < 3.0)
|
45
|
+
rexml
|
46
|
+
rubocop-ast (>= 1.7.0, < 2.0)
|
47
|
+
ruby-progressbar (~> 1.7)
|
48
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
49
|
+
rubocop-ast (1.7.0)
|
50
|
+
parser (>= 3.0.1.1)
|
51
|
+
ruby-progressbar (1.11.0)
|
52
|
+
rubyzip (2.3.2)
|
53
|
+
selenium-webdriver (3.142.7)
|
54
|
+
childprocess (>= 0.5, < 4.0)
|
55
|
+
rubyzip (>= 1.2.2)
|
56
|
+
tzinfo (2.0.4)
|
57
|
+
concurrent-ruby (~> 1.0)
|
58
|
+
unicode-display_width (2.0.0)
|
59
|
+
zeitwerk (2.4.2)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
fuck_facebook!
|
66
|
+
pry
|
67
|
+
rubocop
|
68
|
+
|
69
|
+
BUNDLED WITH
|
70
|
+
2.2.15
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Keeyan Nejad
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'fuck_facebook'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/fuck-facebook
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'mail'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
require_relative '../src/facebook_connection'
|
8
|
+
require_relative '../src/config'
|
9
|
+
require_relative '../src/initializers/mail_initializer'
|
10
|
+
require_relative '../src/senders'
|
11
|
+
|
12
|
+
options = {
|
13
|
+
headless: true,
|
14
|
+
clear_cookies: false,
|
15
|
+
unread_messages: false,
|
16
|
+
messages: false,
|
17
|
+
sender: 'stdout'
|
18
|
+
}
|
19
|
+
|
20
|
+
OptionParser.new do |opts|
|
21
|
+
opts.banner = 'Usage: fuck-facebook.rb [options]'
|
22
|
+
|
23
|
+
opts.on('-h', '--[no-]headless', 'Run in a headless browser') do |v|
|
24
|
+
options[:headless] = v
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('--clear-cookies') do |v|
|
28
|
+
options[:clear_cookies] = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('--unread-messages') do |v|
|
32
|
+
options[:unread_messages] = v
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('--messages') do |v|
|
36
|
+
options[:messages] = v
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('--login') do |v|
|
40
|
+
options[:login] = v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('--sender=SENDER') do |sender|
|
44
|
+
options[:sender] = sender
|
45
|
+
end
|
46
|
+
end.parse!
|
47
|
+
|
48
|
+
facebook_connection = FacebookConnection.new(headless: options[:headless])
|
49
|
+
|
50
|
+
facebook_connection.cookie_handler.clear! if options[:clear_cookies]
|
51
|
+
|
52
|
+
facebook_connection.login if options[:login]
|
53
|
+
|
54
|
+
messages =
|
55
|
+
if options[:unread_messages]
|
56
|
+
facebook_connection.message_handler.unread
|
57
|
+
elsif options[:messages]
|
58
|
+
facebook_connection.message_handler.messages
|
59
|
+
else
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
|
63
|
+
sender = Senders.by_name(options[:sender])
|
64
|
+
sender.send_messages(messages)
|
65
|
+
|
66
|
+
facebook_connection.close
|
data/bin/setup
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'lib/fuck_facebook/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'fuck_facebook'
|
5
|
+
spec.version = FuckFacebook::VERSION
|
6
|
+
spec.authors = ['Keeyan Nejad']
|
7
|
+
spec.email = ['keeyan@keeyan.xyz']
|
8
|
+
|
9
|
+
spec.summary = 'A collection of tools to help you use Facebook when you don\'t want to'
|
10
|
+
spec.description = <<~DESCRIPTION
|
11
|
+
For now you can just set up a cronjob to email you about new messages,
|
12
|
+
but more features will come as I find more things that annoy me about Facebook
|
13
|
+
DESCRIPTION
|
14
|
+
spec.homepage = 'https://gitlab.com/keeyan/fuck_facebook'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.0.1')
|
17
|
+
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
|
+
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
21
|
+
spec.metadata['source_code_uri'] = 'https://gitlab.com/keeyan/fuck_facebook'
|
22
|
+
spec.metadata['changelog_uri'] = 'https://gitlab.com/keeyan/fuck_facebook/-/blob/main/CHANGELOG.md'
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
28
|
+
end
|
29
|
+
spec.executables = %w[fuck-facebook]
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.add_dependency 'activesupport', '~> 6.1'
|
33
|
+
spec.add_dependency 'highline', '~> 2.0'
|
34
|
+
spec.add_dependency 'mail', '~> 2.7'
|
35
|
+
spec.add_dependency 'selenium-webdriver', '~> 3.142'
|
36
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
|
3
|
+
require_relative 'xpaths'
|
4
|
+
require_relative 'config'
|
5
|
+
require_relative 'user_input'
|
6
|
+
|
7
|
+
class Authenticator
|
8
|
+
attr_reader :driver, :xpaths, :user_input
|
9
|
+
|
10
|
+
def initialize(driver)
|
11
|
+
@driver = driver
|
12
|
+
@xpaths = Xpaths.new(driver)
|
13
|
+
@user_input = UserInput.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def login
|
17
|
+
driver.navigate.to 'https://www.facebook.com/'
|
18
|
+
return if logged_in?
|
19
|
+
|
20
|
+
accept_cookies
|
21
|
+
submit_credentials
|
22
|
+
submit_2fa
|
23
|
+
click_remember_password
|
24
|
+
end
|
25
|
+
|
26
|
+
def logged_in?
|
27
|
+
driver.navigate.to 'https://www.facebook.com/'
|
28
|
+
xpaths.element_exists?(:link_to_profile)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def click_remember_password
|
34
|
+
driver.navigate.to('https://www.facebook.com/settings?tab=security')
|
35
|
+
driver.switch_to.frame(xpaths.element(:settings, :security_iframe))
|
36
|
+
xpaths.element(:settings, :save_login_information_edit_button).click
|
37
|
+
xpaths.element(:settings, :save_login_information_button).click
|
38
|
+
end
|
39
|
+
|
40
|
+
def submit_credentials
|
41
|
+
xpaths.element(:email_address_input).send_keys(user_input.get(:email_address))
|
42
|
+
xpaths.element(:password_input).send_keys(user_input.get(:password, secret: true))
|
43
|
+
xpaths.element(:login_button).click
|
44
|
+
end
|
45
|
+
|
46
|
+
def submit_2fa
|
47
|
+
return unless xpaths.element(:checkpoint, :prompt).text == 'Two-factor authentication required'
|
48
|
+
|
49
|
+
xpaths.element(:checkpoint, :otp_input).send_keys(user_input.get(:otp))
|
50
|
+
xpaths.element(:checkpoint, :otp_continue_button).click
|
51
|
+
xpaths.element(:checkpoint, :save_browser_continue_button).click
|
52
|
+
end
|
53
|
+
|
54
|
+
def accept_cookies
|
55
|
+
accept_cookies_button = xpaths.element(:accept_cookies_button)
|
56
|
+
accept_cookies_button.click
|
57
|
+
end
|
58
|
+
end
|
data/src/config.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class CookieHandler
|
5
|
+
attr_reader :driver
|
6
|
+
|
7
|
+
def initialize(driver)
|
8
|
+
@driver = driver
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear!
|
12
|
+
File.delete(cookies_filename) if File.exist? cookies_filename
|
13
|
+
|
14
|
+
driver.manage.delete_all_cookies
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
FileUtils.mkdir_p(cookies_dir)
|
19
|
+
File.write(cookies_filename, current_cookies.to_json)
|
20
|
+
end
|
21
|
+
|
22
|
+
def load
|
23
|
+
current_domain = nil
|
24
|
+
saved_cookies.each do |saved_cookie|
|
25
|
+
new_domain = remove_leading_period(saved_cookie[:domain])
|
26
|
+
|
27
|
+
driver.navigate.to "https://#{new_domain}" unless new_domain == current_domain
|
28
|
+
current_domain = new_domain
|
29
|
+
|
30
|
+
driver.manage.add_cookie(saved_cookie)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def current_cookies
|
35
|
+
driver.manage.all_cookies
|
36
|
+
end
|
37
|
+
|
38
|
+
def saved_cookies
|
39
|
+
return {} unless File.exist? cookies_filename
|
40
|
+
|
41
|
+
saved_cookies = JSON.parse(File.read(cookies_filename), symbolize_names: true)
|
42
|
+
|
43
|
+
saved_cookies.each do |saved_cookie|
|
44
|
+
saved_cookie[:expires] = Time.parse(saved_cookie[:expires])
|
45
|
+
end
|
46
|
+
|
47
|
+
saved_cookies
|
48
|
+
end
|
49
|
+
|
50
|
+
def cookies_filename
|
51
|
+
@cookies_filename ||= File.join(cookies_dir, 'cookies.json')
|
52
|
+
end
|
53
|
+
|
54
|
+
def cookies_dir
|
55
|
+
@cookies_dir ||= File.join(ENV['HOME'], '.local/share/fuck-facebook/')
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def remove_leading_period(str)
|
61
|
+
str[1..]
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
|
3
|
+
require_relative './authenticator'
|
4
|
+
require_relative './message_handler'
|
5
|
+
require_relative './cookie_handler'
|
6
|
+
|
7
|
+
class FacebookConnection
|
8
|
+
attr_reader :headless
|
9
|
+
|
10
|
+
def initialize(headless:)
|
11
|
+
@headless = headless
|
12
|
+
|
13
|
+
cookie_handler.load
|
14
|
+
end
|
15
|
+
|
16
|
+
def login
|
17
|
+
authenticator = Authenticator.new(driver)
|
18
|
+
authenticator.login
|
19
|
+
end
|
20
|
+
|
21
|
+
def close
|
22
|
+
cookie_handler.save
|
23
|
+
end
|
24
|
+
|
25
|
+
def message_handler
|
26
|
+
@message_handler ||= MessageHandler.new(driver)
|
27
|
+
end
|
28
|
+
|
29
|
+
def driver
|
30
|
+
return @driver if defined? @driver
|
31
|
+
|
32
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
33
|
+
options.add_argument('--disable-notifications')
|
34
|
+
options.add_argument('--headless') if headless
|
35
|
+
options.add_option('detach', true) unless headless
|
36
|
+
|
37
|
+
@driver = Selenium::WebDriver.for :chrome, options: options
|
38
|
+
|
39
|
+
@driver.manage.window.resize_to(1920, 1080)
|
40
|
+
|
41
|
+
@driver
|
42
|
+
end
|
43
|
+
|
44
|
+
def cookie_handler
|
45
|
+
@cookie_handler ||= CookieHandler.new(driver)
|
46
|
+
end
|
47
|
+
end
|
data/src/fb_duration.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support/core_ext/numeric/time'
|
2
|
+
|
3
|
+
class FbDuration
|
4
|
+
DURATION_LETTER_TO_METHOD = {
|
5
|
+
'm' => :minutes,
|
6
|
+
'd' => :days,
|
7
|
+
'h' => :hours,
|
8
|
+
'w' => :weeks,
|
9
|
+
'y' => :years
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
DURATION_LETTER_TO_BEGINNING_OF_METHOD = {
|
13
|
+
'm' => :beginning_of_minute,
|
14
|
+
'd' => :beginning_of_day,
|
15
|
+
'h' => :beginning_of_hour,
|
16
|
+
'w' => :beginning_of_week,
|
17
|
+
'y' => :beginning_of_year
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def self.parse_to_time(facebook_format_duration)
|
21
|
+
duration = parse(facebook_format_duration)
|
22
|
+
time_to_beginning_of_duration_letter(Time.now - duration, facebook_format_duration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse(facebook_format_duration)
|
26
|
+
segments = facebook_format_duration.split
|
27
|
+
|
28
|
+
segments[0].to_i.send(DURATION_LETTER_TO_METHOD[segments[1]])
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.time_to_beginning_of_duration_letter(time, facebook_format_duration)
|
32
|
+
duration_letter = facebook_format_duration.split[1]
|
33
|
+
|
34
|
+
time.send(DURATION_LETTER_TO_BEGINNING_OF_METHOD[duration_letter])
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative '../config'
|
2
|
+
|
3
|
+
Mail.defaults do
|
4
|
+
delivery_method :smtp, {
|
5
|
+
address: Config.option(:smtp, :address),
|
6
|
+
port: Config.option(:smtp, :port),
|
7
|
+
user_name: Config.option(:smtp, :user_name),
|
8
|
+
password: Config.option(:smtp, :password),
|
9
|
+
authentication: Config.option(:smtp, :authentication),
|
10
|
+
enable_starttls_auto: true
|
11
|
+
}
|
12
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'fb_duration'
|
2
|
+
require_relative 'storage'
|
3
|
+
require_relative 'models/message'
|
4
|
+
require_relative 'models/user'
|
5
|
+
require_relative 'models/message_thread'
|
6
|
+
|
7
|
+
class MessageHandler
|
8
|
+
attr_reader :driver
|
9
|
+
|
10
|
+
def initialize(driver)
|
11
|
+
@driver = driver
|
12
|
+
end
|
13
|
+
|
14
|
+
def unread
|
15
|
+
messages.reject(&:read?)
|
16
|
+
end
|
17
|
+
|
18
|
+
def messages
|
19
|
+
visit_messages_page
|
20
|
+
|
21
|
+
xpaths.wait_for_element(:messages, :chats)
|
22
|
+
|
23
|
+
chats = xpaths.elements(:messages, :chats)
|
24
|
+
|
25
|
+
messages = messages_for_chats(chats)
|
26
|
+
|
27
|
+
messages_after_last_message_time(messages)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def messages_for_chats(chats)
|
33
|
+
chats.map do |chat|
|
34
|
+
message_thread = MessageThread.new(title: xpaths.child_element(chat, :messages, :title_relative).text)
|
35
|
+
text = xpaths.child_element(chat, :messages, :text_relative).text
|
36
|
+
read = !xpaths.element_has_child?(chat, :messages, :mark_as_read_button_relative)
|
37
|
+
message_time_ago = xpaths.child_element(chat, :messages, :time_sent_ago_relative).text
|
38
|
+
timestamp = FbDuration.parse_to_time(message_time_ago)
|
39
|
+
|
40
|
+
Message.new(message_thread: message_thread, text: text, read: read, timestamp: timestamp)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def messages_after_last_message_time(messages)
|
45
|
+
last_message_time = Storage.get(:last_message_time, default: Time.new(2004, 2, 4))
|
46
|
+
|
47
|
+
messages_to_return = messages.select { _1.timestamp > last_message_time }
|
48
|
+
|
49
|
+
last_message_time = messages.map(&:timestamp).max
|
50
|
+
|
51
|
+
Storage.set(:last_message_time, last_message_time)
|
52
|
+
|
53
|
+
messages_to_return
|
54
|
+
end
|
55
|
+
|
56
|
+
def xpaths
|
57
|
+
@xpaths ||= Xpaths.new(driver)
|
58
|
+
end
|
59
|
+
|
60
|
+
def visit_messages_page
|
61
|
+
driver.navigate.to('https://www.facebook.com/messages/')
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Message
|
2
|
+
attr_accessor :message_thread, :text, :read, :timestamp
|
3
|
+
|
4
|
+
def initialize(message_thread:, text:, timestamp:, read: true)
|
5
|
+
@message_thread = message_thread
|
6
|
+
@text = text
|
7
|
+
@read = read
|
8
|
+
@timestamp = timestamp
|
9
|
+
end
|
10
|
+
|
11
|
+
def read?
|
12
|
+
read
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{message_thread}: #{text}"
|
17
|
+
end
|
18
|
+
end
|
data/src/models/user.rb
ADDED
data/src/senders.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Senders
|
2
|
+
class Email
|
3
|
+
def self.send_messages(messages)
|
4
|
+
messages.each do |message|
|
5
|
+
Mail.deliver do
|
6
|
+
from Config.option(:smtp, :from_email)
|
7
|
+
to Config.option(:smtp, :to_email)
|
8
|
+
subject "New Facebook message on chat #{message.message_thread}"
|
9
|
+
body message.text
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/src/storage.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class Storage
|
5
|
+
STORAGE_FILE_PATH = "#{ENV['HOME']}/.local/share/fuck-facebook/storage.yaml".freeze
|
6
|
+
|
7
|
+
def self.get(*keys, default: nil)
|
8
|
+
value = storage.dig(*keys)
|
9
|
+
|
10
|
+
return value if value
|
11
|
+
|
12
|
+
default
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.set(*keys, value)
|
16
|
+
create_storage_file_if_not_exists!
|
17
|
+
|
18
|
+
new_storage = storage
|
19
|
+
|
20
|
+
current_level_hash = new_storage
|
21
|
+
keys.each_with_index do |key, index|
|
22
|
+
current_level_hash[key] = index + 1 == keys.count ? value : current_level_hash.fetch(key, {})
|
23
|
+
current_level_hash = current_level_hash[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
File.write(STORAGE_FILE_PATH, new_storage.to_yaml)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.storage
|
30
|
+
create_storage_file_if_not_exists!
|
31
|
+
|
32
|
+
YAML.load_file(STORAGE_FILE_PATH, symbolize_names: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.create_storage_file_if_not_exists!
|
36
|
+
dirname = File.dirname(STORAGE_FILE_PATH)
|
37
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
38
|
+
File.write(STORAGE_FILE_PATH, '{}') unless File.exist?(STORAGE_FILE_PATH)
|
39
|
+
end
|
40
|
+
end
|
data/src/user_input.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'highline'
|
2
|
+
|
3
|
+
class UserInput
|
4
|
+
attr_reader :highline, :saved_answers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@highline = HighLine.new
|
8
|
+
@saved_answers = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(value, text = nil, secret: false, remember: true)
|
12
|
+
return saved_answers[value] if saved_answers.key?(value) && remember
|
13
|
+
|
14
|
+
saved_answers[value] = highline.ask("#{text || value}: ") { _1.echo = !secret }
|
15
|
+
end
|
16
|
+
end
|
data/src/xpaths.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Xpaths
|
4
|
+
attr_reader :driver
|
5
|
+
|
6
|
+
def initialize(driver)
|
7
|
+
@driver = driver
|
8
|
+
end
|
9
|
+
|
10
|
+
def element(*path_elements)
|
11
|
+
element_xpath = self.for(*path_elements)
|
12
|
+
|
13
|
+
driver.find_element(:xpath, element_xpath)
|
14
|
+
end
|
15
|
+
|
16
|
+
def child_element(element, *path_elements)
|
17
|
+
element.find_element(:xpath, self.for(*path_elements))
|
18
|
+
end
|
19
|
+
|
20
|
+
def element_exists?(*path_elements)
|
21
|
+
element(*path_elements)
|
22
|
+
true
|
23
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def element_has_child?(element, *path_elements)
|
28
|
+
child_element(element, *path_elements)
|
29
|
+
true
|
30
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def elements(*path_elements)
|
35
|
+
element_xpath = self.for(*path_elements)
|
36
|
+
|
37
|
+
driver.find_elements(:xpath, element_xpath)
|
38
|
+
end
|
39
|
+
|
40
|
+
def element_containing_text(text)
|
41
|
+
results = elements_containing_text(text)
|
42
|
+
|
43
|
+
raise "Too many elements found containing text '#{text}'" if results.count > 1
|
44
|
+
raise "No elements found containing text '#{text}'" if results.count.zero?
|
45
|
+
|
46
|
+
results[0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def elements_containing_text(text)
|
50
|
+
driver.find_elements(:xpath, "//*[text() = '#{text}']")
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait_for_element(*path_elements)
|
54
|
+
found_element = nil
|
55
|
+
|
56
|
+
Selenium::WebDriver::Wait.new.until do
|
57
|
+
found_element = element(*path_elements)
|
58
|
+
true
|
59
|
+
rescue Selenium::WebDriver::Error::TimeoutError
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
found_element
|
64
|
+
end
|
65
|
+
|
66
|
+
def for(*path_elements)
|
67
|
+
result = XPATHS.dig(*path_elements)
|
68
|
+
|
69
|
+
raise 'Could not find xpath' if result.empty?
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
private_class_method def self.xpaths_from_file
|
75
|
+
xpaths_file = File.join(File.dirname(__FILE__), '../xpaths.yaml')
|
76
|
+
YAML.load_file(xpaths_file, symbolize_names: true)
|
77
|
+
end
|
78
|
+
|
79
|
+
XPATHS = xpaths_from_file
|
80
|
+
end
|
data/xpaths.yaml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
accept_cookies_button: '/html/body/div[3]/div[2]/div/div/div/div/div[3]/button[2]'
|
2
|
+
email_address_input: '/html/body/div[1]/div[2]/div[1]/div/div/div/div[2]/div/div[1]/form/div[1]/div[1]/input'
|
3
|
+
password_input: '/html/body/div[1]/div[2]/div[1]/div/div/div/div[2]/div/div[1]/form/div[1]/div[2]/div/input'
|
4
|
+
login_button: '/html/body/div[1]/div[2]/div[1]/div/div/div/div[2]/div/div[1]/form/div[2]/button'
|
5
|
+
settings:
|
6
|
+
security_iframe: '/html/body/div[1]/div/div[1]/div/div[3]/div/div/div[1]/div[1]/div[2]/div/div/iframe'
|
7
|
+
save_login_information_edit_button: '/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/div[2]/div/div/div/div[3]/div[3]/table/tbody/tr/td[3]/button'
|
8
|
+
save_login_information_button: '//div[text() = "Save your login information"]'
|
9
|
+
link_to_profile: '//a[@href="/me/"]'
|
10
|
+
checkpoint:
|
11
|
+
otp_input: '/html/body/div[1]/div[3]/div[1]/div/form/div/div[2]/div[3]/span/input'
|
12
|
+
otp_continue_button: '/html/body/div[1]/div[3]/div[1]/div/form/div/div[3]/div[1]/button'
|
13
|
+
save_browser_radio_input: '/html/body/div[1]/div[3]/div[1]/div/form/div/div[2]/div[2]/div[1]/input'
|
14
|
+
save_browser_continue_button: '/html/body/div[1]/div[3]/div[1]/div/form/div/div[3]/div[1]/button'
|
15
|
+
prompt: '/html/body/div[1]/div[3]/div[1]/div/form/div/div[1]'
|
16
|
+
messages:
|
17
|
+
chats: '/html/body/div[1]/div/div[1]/div/div[3]/div/div/div[1]/div[1]/div[1]/div/div/div/div[3]/div[1]/div[2]/div/child::*[@role = "row"]'
|
18
|
+
title_relative: 'div[1]/div/a/div/div[2]/div[1]/div/div/div[1]/span/span/span/span'
|
19
|
+
text_relative: 'div[1]/div/a/div/div[2]/div[1]/div/div/div[2]/span/span/div/span[1]/span'
|
20
|
+
time_sent_ago_relative: 'div[1]/div/a/div/div[2]/div[1]/div/div/div[2]/span/span/div/span[3]'
|
21
|
+
mark_as_read_button_relative: './/*[@aria-label="Mark as read"]'
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fuck_facebook
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Keeyan Nejad
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-07-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: highline
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mail
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.7'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: selenium-webdriver
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.142'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.142'
|
69
|
+
description: |
|
70
|
+
For now you can just set up a cronjob to email you about new messages,
|
71
|
+
but more features will come as I find more things that annoy me about Facebook
|
72
|
+
email:
|
73
|
+
- keeyan@keeyan.xyz
|
74
|
+
executables:
|
75
|
+
- fuck-facebook
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".gitignore"
|
80
|
+
- ".rspec"
|
81
|
+
- ".rubocop.yml"
|
82
|
+
- ".ruby-version"
|
83
|
+
- CHANGELOG.md
|
84
|
+
- Gemfile
|
85
|
+
- Gemfile.lock
|
86
|
+
- LICENSE.txt
|
87
|
+
- README.md
|
88
|
+
- Rakefile
|
89
|
+
- bin/console
|
90
|
+
- bin/fuck-facebook
|
91
|
+
- bin/setup
|
92
|
+
- fuck_facebook.gemspec
|
93
|
+
- lib/fuck_facebook.rb
|
94
|
+
- lib/fuck_facebook/version.rb
|
95
|
+
- src/authenticator.rb
|
96
|
+
- src/config.rb
|
97
|
+
- src/cookie_handler.rb
|
98
|
+
- src/facebook_connection.rb
|
99
|
+
- src/fb_duration.rb
|
100
|
+
- src/initializers/mail_initializer.rb
|
101
|
+
- src/message_handler.rb
|
102
|
+
- src/models/message.rb
|
103
|
+
- src/models/message_thread.rb
|
104
|
+
- src/models/user.rb
|
105
|
+
- src/senders.rb
|
106
|
+
- src/senders/email.rb
|
107
|
+
- src/senders/stdout.rb
|
108
|
+
- src/storage.rb
|
109
|
+
- src/user_input.rb
|
110
|
+
- src/xpaths.rb
|
111
|
+
- xpaths.yaml
|
112
|
+
homepage: https://gitlab.com/keeyan/fuck_facebook
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata:
|
116
|
+
allowed_push_host: https://rubygems.org
|
117
|
+
homepage_uri: https://gitlab.com/keeyan/fuck_facebook
|
118
|
+
source_code_uri: https://gitlab.com/keeyan/fuck_facebook
|
119
|
+
changelog_uri: https://gitlab.com/keeyan/fuck_facebook/-/blob/main/CHANGELOG.md
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: 3.0.1
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubygems_version: 3.2.15
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: A collection of tools to help you use Facebook when you don't want to
|
139
|
+
test_files: []
|