Skip to content

Instantly share code, notes, and snippets.

@mhluska
Last active September 1, 2016 11:28
Show Gist options
  • Save mhluska/fbf63682f20da34488c043aca24caacb to your computer and use it in GitHub Desktop.
Save mhluska/fbf63682f20da34488c043aca24caacb to your computer and use it in GitHub Desktop.
Basic Tinder bot with sentiment analysis
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