fuck_facebook 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Style/Documentation:
4
+ Enabled: false
5
+ Style/ClassAndModuleChildren:
6
+ Enabled: false
7
+ Style/FrozenStringLiteralComment:
8
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-07-15
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry'
7
+ end
8
+
9
+ group :development do
10
+ gem 'rubocop'
11
+ end
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
@@ -0,0 +1,3 @@
1
+ # Fuck Facebook
2
+
3
+ A collection of tools to help you use Facebook when you don't want to
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,5 @@
1
+ require_relative 'fuck_facebook/version'
2
+
3
+ module FuckFacebook
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,3 @@
1
+ module FuckFacebook
2
+ VERSION = '0.1.0'.freeze
3
+ 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,9 @@
1
+ require 'yaml'
2
+
3
+ class Config
4
+ CONFIG = YAML.load_file("#{ENV['HOME']}/.config/fuck-facebook/config.yaml", symbolize_names: true)
5
+
6
+ def self.option(*path)
7
+ CONFIG.dig(*path)
8
+ end
9
+ end
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ class MessageThread
2
+ attr_accessor :title
3
+
4
+ def initialize(title:)
5
+ @title = title
6
+ end
7
+
8
+ def to_s
9
+ title
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class User
2
+ attr_accessor :name
3
+
4
+ def initialize(name:)
5
+ @name = name
6
+ end
7
+
8
+ def to_s
9
+ name
10
+ end
11
+ end
data/src/senders.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'senders/email'
2
+ require_relative 'senders/stdout'
3
+
4
+ module Senders
5
+ SENDERS = {
6
+ stdout: Stdout,
7
+ email: Email
8
+ }.freeze
9
+
10
+ def self.by_name(name)
11
+ SENDERS[name.to_sym]
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ module Senders
2
+ class Stdout
3
+ def self.send_messages(messages)
4
+ messages.each do |message|
5
+ puts message
6
+ end
7
+ end
8
+ end
9
+ 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: []