Last active
September 1, 2016 11:28
-
-
Save mhluska/fbf63682f20da34488c043aca24caacb to your computer and use it in GitHub Desktop.
Basic Tinder bot with sentiment analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'dotenv' | |
require 'tinderbot' | |
require 'sentimental' | |
Dotenv.load! | |
class Bot | |
MAX_DAYS_SINCE_REPLY = 5 | |
MAX_DAYS_SINCE_MESSAGE = 2 | |
MAX_DISTANCE_MI = 100 | |
MAX_DELAY_MS = 1000 | |
def initialize(token, id) | |
@tinder_client = Tinderbot::Client.new | |
@tinder_client.sign_in(@tinder_client.get_authentication_token(token, id)) | |
@profile = @tinder_client.profile | |
@sentiment_analyzer = Sentimental.new(threshold: 0.625) | |
@sentiment_analyzer.load_defaults | |
@last_matches = [] | |
@last_bot_unmatches = [] | |
end | |
def get_matches! | |
@tinder_client.updates['matches'] | |
end | |
def try_intro(match) | |
return if sent_intro?(match) | |
return if human_messaged?(match) | |
name = match['person']['name'] | |
raise 'Name is missing' if name.nil? | |
send_message(match['id'], intro_message(name)) | |
end | |
def unmatch_if_too_far(match) | |
user_id = match['person']['_id'] | |
distance = @tinder_client.user(user_id).original_tinder_json['distance_mi'] | |
return if distance < MAX_DISTANCE_MI | |
unmatch(match, "distance is #{distance} mi") | |
end | |
def unmatch_if_poor_bio(match) | |
return if human_messaged?(match) | |
text = match['person']['bio'] + all_messages_from(match) | |
raise 'Implement redacted regex' | |
regex_match = text.match(/no.*[redacted]/im) || | |
text.match(/(?:[redacted])/im) | |
return unless regex_match | |
unmatch(match, "poor bio or message: #{regex_match[0]}") | |
end | |
def unmatch_if_negative_sentiment(match) | |
return if human_messaged?(match) | |
messages = messages_after_intro(match) | |
return unless messages | |
return if includes_external_contact?(messages) | |
return unless sentiment(messages) == :negative | |
unmatch(match, "negative sentiment: #{messages}") | |
end | |
def unmatch_if_stale(match) | |
return unless human_messaged?(match) | |
return if awaiting_reply?(match) | |
last_message = match['messages'].last | |
time_since_ms = ((Time.now.to_f * 1000).to_i - last_message['timestamp']) | |
days_since = time_since_ms / 1000.0 / 60 / 60 / 24 | |
return if awaiting_reply?(match) && days_since < MAX_DAYS_SINCE_REPLY | |
return if !awaiting_reply?(match) && days_since < MAX_DAYS_SINCE_MESSAGE | |
unmatch(match, "match stale: #{days_since.to_i} days old") | |
end | |
def notify_human_if_positive_sentiment(match) | |
return if human_messaged?(match) | |
messages = messages_after_intro(match) | |
return unless messages | |
if includes_external_contact?(messages) | |
puts "DEBUG: External contact provided: #{extract_external_contact(messages)}" | |
end | |
return unless [:neutral, :positive].include?(sentiment(messages)) | |
notify_human(match) | |
end | |
def run | |
loop do | |
matches = get_matches! | |
match_ids = matches.map { |m| m['id'] } | |
unmatched = @last_matches - match_ids - @last_bot_unmatches | |
run_facial_analysis(unmatched) | |
@last_matches = match_ids | |
@last_bot_unmatches = [] | |
matches.each do |match| | |
sleep(random_delay) | |
next if unmatch_if_too_far(match) | |
next if unmatch_if_poor_bio(match) | |
next if unmatch_if_negative_sentiment(match) | |
next if unmatch_if_stale(match) | |
try_intro(match) | |
notify_human_if_positive_sentiment(match) | |
end | |
end | |
end | |
private | |
def run_facial_analysis(unmatched_ids) | |
return if unmatched_ids.empty? | |
puts 'DEBUG: Unmatches via app detected!' | |
puts unmatched_ids | |
unmatched = @last_matches.select { |m| unmatched_ids.include?(m['id']) } | |
# TODO(maros): Finish implementing this. | |
end | |
def extract_external_contact(messages) | |
match = messages.match(/[\+\d\-]{5,}/) | |
return match[0] if match | |
end | |
# Returns true if a WhatsApp number or otherwise is provided. | |
def includes_external_contact?(messages) | |
!extract_external_contact(messages).nil? | |
end | |
def random_delay | |
(rand(MAX_DELAY_MS) + 1) / MAX_DELAY_MS.to_f | |
end | |
def intro_message(name) | |
"Hey #{name}, hows it going? Want to meet tonight?" | |
end | |
def human_messaged?(match) | |
messages = match['messages'] | |
.select { |m| m['from'] == @profile.id } | |
.map { |m| m['message'] } | |
return false if messages.length == 0 | |
name = match['person']['name'] | |
messages.length != 1 || messages.first != intro_message(name) | |
end | |
def send_message(id, message) | |
@tinder_client.send_message(id, message) | |
puts "Sending to #{id}: #{message}" | |
end | |
def sentiment(message) | |
@sentiment_analyzer.sentiment(message) | |
end | |
def unmatch(match, reason) | |
name = match['person']['name'] | |
puts "Removing user #{name} because #{reason.slice(0, 128)}" | |
@tinder_client.remove(match['id']) | |
@last_bot_unmatches << match['id'] | |
true | |
end | |
def all_messages_from(match) | |
match['messages'] | |
.select { |m| m['from'] == match['person']['_id'] } | |
.map { |m| m['message'] } | |
.join(' ') | |
end | |
def awaiting_reply?(match) | |
messages = match['messages'] | |
messages.length == 0 || messages.last['from'] != @profile.id | |
end | |
def last_message_to(match) | |
match['messages'].select { |m| m['from'] == @profile.id }.last | |
end | |
def sent_intro?(match) | |
last_to = last_message_to(match) | |
return false unless last_to | |
name = match['person']['name'] | |
last_to['message'] == intro_message(name) | |
end | |
def messages_after_intro(match) | |
last_to = last_message_to(match) | |
return unless last_to | |
raise 'Message not introduction!' unless sent_intro?(match) | |
messages = match['messages'] | |
.select{ |m| m['from'] != @profile.id && m['timestamp'] > last_to['timestamp'] } | |
.map { |m| m['message'] } | |
return if messages.empty? | |
messages.join(' ') | |
end | |
def notify_human(match) | |
puts "Notifying human about match #{match['person']['name']}: #{messages_after_intro(match)}" | |
end | |
end | |
bot = Bot.new(ENV['FACEBOOK_AUTH_TOKEN'], ENV['FACEBOOK_USER_ID']) | |
bot.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment