diff --git a/Gemfile.lock b/Gemfile.lock index 5994202b3..2effab715 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -451,7 +451,7 @@ GEM will_paginate (3.1.0) will_paginate-bootstrap (0.2.5) will_paginate (>= 3.0.3) - wit (3.3.1) + wit (3.4.0) wuparty (1.2.6) httparty (>= 0.6.1) mime-types (~> 1.16) diff --git a/Vagrantfile b/Vagrantfile index d5fed0a54..e28d83d8a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -114,7 +114,9 @@ Vagrant.configure(2) do |config| nginx-full \ openjdk-7-jre \ phantomjs \ - elasticsearch + elasticsearch\ + redis-server + mysqladmin -u root -ppassword password ''; sudo update-rc.d elasticsearch defaults; diff --git a/app/controllers/v2/event_invitations_controller.rb b/app/controllers/v2/event_invitations_controller.rb index ae47be542..8d4fbb1f5 100644 --- a/app/controllers/v2/event_invitations_controller.rb +++ b/app/controllers/v2/event_invitations_controller.rb @@ -58,28 +58,27 @@ def create_event(event_invitation) end def send_notifications(event_invitation) - event = event_invitation.event event_invitation.invitees.each do |invitee| case invitee.preferred_contact_method.upcase when 'SMS' - send_sms(invitee, event) + send_sms(invitee, event_invitation) when 'EMAIL' - send_email(invitee, event) + send_email(invitee, event_invitation) end end end - def send_email(person, event) + def send_email(person, event_invitation) EventInvitationMailer.invite( email_address: person.email_address, - event: event, + event: event_invitation, person: person ).deliver_later end - def send_sms(person, event) + def send_sms(person, event_invitation) # we send a bunch at once, delay it. Plus this has extra logic - Delayed::Job.enqueue SendEventInvitationsSmsJob.new(person, event) + Delayed::Job.enqueue(SendEventInvitationsSmsJob.new(person, event_invitation)) end # TODO: add a nested :event diff --git a/app/controllers/v2/sms_reservations_controller.rb b/app/controllers/v2/sms_reservations_controller.rb index 66fd7ddba..293e901ff 100644 --- a/app/controllers/v2/sms_reservations_controller.rb +++ b/app/controllers/v2/sms_reservations_controller.rb @@ -15,8 +15,7 @@ def create if remove? # do the remove people thing. person.deactivate! - elsif declined? # currently not used. - #send_decline_notifications(person, event) + ::RemoveSms.new(to: person).send elsif confirm? # confirmation for the days reservations if person.v2_reservations.for_today_and_tomorrow.size > 0 person.v2_reservations.for_today_and_tomorrow.each(&:confirm!) @@ -43,9 +42,11 @@ def create # we don't know what event_id we're talking about here send_error_notification && return if str_context.nil? context = JSON.parse(str_context) - new_context = ::WitClient.run_actions "#{person.id}_#{context['event_id']}", message, context - Redis.current.set("wit_context:#{person.id}", new_context.to_json) - Redis.current.expire("wit_context:#{person.id}", 3600) + puts "in sms_reservation_controller" + pp context + puts message + puts "!!!!!!!!!!!!!!!!!!!!!!!" + ::WitClient.run_actions "#{person.id}_#{context['event_id']}", message, context end render text: 'OK' end @@ -90,12 +91,6 @@ def resend_available_slots(person, event) ::TimeSlotNotAvailableSms.new(to: person, event: event).send end - def declined? - # this is no longer in use. still might be handt though... - # up to 10k events. - message.downcase =~ /^\d{1,5}-decline?/ - end - def confirm? message.downcase.include?('confirm') end diff --git a/app/jobs/send_event_invitations_sms_job.rb b/app/jobs/send_event_invitations_sms_job.rb index 20ec7182b..b9104d301 100644 --- a/app/jobs/send_event_invitations_sms_job.rb +++ b/app/jobs/send_event_invitations_sms_job.rb @@ -11,7 +11,7 @@ class SendEventInvitationsSmsJob < Struct.new(:to, :event) def enqueue(job) # job.delayed_reference_id = # job.delayed_reference_type = '' - Rails.logger.info '[TwilioSender] job enqueued' + Rails.logger.info '[SendEventInvitationsSms] job enqueued' job.save! end @@ -30,11 +30,14 @@ def perform # context = Redis.current.get("wit_context:#{to.id}") # if context.nil? && !time_requeue? # no context, free to send - # context is symbols here, but will be string keys after json. - context = { person_id: to.id, event_id: event.id, state: 'yes_no' }.to_json - Redis.current.set("wit_context:#{to.id}", context) - Redis.current.expire("wit_context:#{to.id}", 7200) # two hours - EventInvitationSms.new(to: to, event: event).send + # context is symbols here, but will be string keys after json. + context = { person_id: to.id, + event_id: event.id, + 'reference_time' => event.start_datetime, + 'reference_time_slot' => event.bot_duration }.to_json + Redis.current.set("wit_context:#{to.id}", context) + Redis.current.expire("wit_context:#{to.id}", 7200) # two hours + EventInvitationSms.new(to: to, event: event).send # elsif time_requeue? # Delayed::Job.enqueue(SendEventInvitationsSmsJob.new(to, event), run_at: run_in_business_hours) # else # we have a context, wait till we time out. diff --git a/app/models/concerns/calendarable.rb b/app/models/concerns/calendarable.rb index d2d894289..78daa9608 100644 --- a/app/models/concerns/calendarable.rb +++ b/app/models/concerns/calendarable.rb @@ -26,23 +26,23 @@ def end_datetime end def to_time_and_weekday - "#{start_datetime.strftime('%l:%M %p')} - #{end_datetime.strftime('%l:%M %p')} #{start_datetime.strftime('%A %d')}" + "#{start_datetime.strftime('%l:%M %p').lstrip} - #{end_datetime.strftime('%l:%M %p').lstrip} #{start_datetime.strftime('%a %d')}" end def to_weekday_and_time - "#{start_datetime.strftime('%l:%M %p')} #{start_datetime.strftime('%l:%M %p')} - #{end_datetime.strftime('%H:%M')}" + "#{start_datetime.strftime('%a %d')} #{start_datetime.strftime('%l:%M %p').lstrip} - #{end_datetime.strftime('%l:%M %p').lstrip}" end def start_datetime_human - "#{start_datetime.strftime('%l:%M%p, %a %b')} #{start_datetime.strftime('%d').to_i.ordinalize}" + "#{start_datetime.strftime('%l:%M%p, %a %b').lstrip} #{start_datetime.strftime('%d').to_i.ordinalize}" end def slot_time_human - "#{start_datetime.strftime('%l:%M%p')}-#{end_datetime.strftime('%l:%M%p, %a %b')} #{start_datetime.strftime('%d').to_i.ordinalize}" + "#{start_datetime.strftime('%l:%M%p').lstrip}-#{end_datetime.strftime('%l:%M%p, %a %b').lstrip} #{start_datetime.strftime('%d').to_i.ordinalize}" end def bot_duration - "#{start_datetime.strftime('%A')} from #{start_datetime.strftime('%l:%M%p')} to #{end_datetime.strftime('%l:%M%p')}" + "from #{start_datetime.strftime('%l:%M%p').lstrip} to #{end_datetime.strftime('%l:%M%p').lstrip}" end private diff --git a/app/models/person.rb b/app/models/person.rb index 6699bcaf9..97922a5b3 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -317,7 +317,7 @@ def deactivate(method = nil) end def update_neighborhood - n = Neighborhood.zip_to_neighborhood(postal_code) + n = zip_to_neighborhood(postal_code) unless n.blank? self.neighborhood = n save diff --git a/app/sms/event_invitation_sms.rb b/app/sms/event_invitation_sms.rb index 95bdb5e94..3c6c2005d 100644 --- a/app/sms/event_invitation_sms.rb +++ b/app/sms/event_invitation_sms.rb @@ -13,9 +13,9 @@ def initialize(to:, event:) # TODO: Chunk this into 160 characters and send individualls def body body = "#{event.description}\n" - body << "If you're available for #{event.duration / 60} minutes" - body << ' during that time please' - body << " text back 'Yes'. If not, 'No'\n" + body << "If you're available for #{event.duration / 60} minutes at " + body << "#{event.slot_time_human} please" + body << " text back 'Yes'.\nIf not, 'No'\n" body << "You can text 'remove me' to unsubscribe" end end diff --git a/app/sms/remove_sms.rb b/app/sms/remove_sms.rb new file mode 100644 index 000000000..4bef93f0e --- /dev/null +++ b/app/sms/remove_sms.rb @@ -0,0 +1,14 @@ +# TODO: needs a spec. The spec for SmsReservationsController covers it, +# but a unit test would make coverage more robust +class RemoveSms < ApplicationSms + attr_reader :to + + def initialize(to:) + super + @to = to # only really people here. + end + + def body + "You have been removed from this list. If you think this is in error, please contact #{ENV['MAIL_ADMIN']}" + end +end diff --git a/config/initializers/wit_ai.rb b/config/initializers/wit_ai.rb index 7d8559420..e2b80ef6e 100644 --- a/config/initializers/wit_ai.rb +++ b/config/initializers/wit_ai.rb @@ -2,41 +2,114 @@ access_token = ENV['WIT_ACCESS_TOKEN'] + + # note, context: it's a hash with string keys. allways string keys # that's because we save it in json in redis for reuse actions = { say: lambda do |_session_id, context, msg| # this is where we text. + puts "in say" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" person = Person.find_by(id: context['person_id']) ::WitSms.new(to: person, msg: msg).send end, merge: lambda do |_session_id, context, entities, _msg| - # not sure what else we'd do here. - # maybe some content munging to make things easier? - # like make our dates nice and pretty etc. + # this is where all the work happens. I get it now. + # wit relies on us to know what contexts and entites result in + # entirely new contexts and entities. + + # this is such a hack + + context.merge!(entities) # how we know what happened + + puts "before merge" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + # handle initial yes or no + if context['want_interview'].nil? && context['yes_no'] + case context['yes_no'][0]['value'] + when 'yes' + context.merge!({ 'want_interview' => true }) + when 'no' + context.merge!({ 'refuse_interview' => true }) + end + context.delete('yes_no') # start afresh + end + + # handle confirming date + if context['date_is_valid'] && context['yes_no'] + case context['yes_no'][0]['value'] + when 'yes' + context.delete('date_not_confirmed') # might be second attempt + context.merge!({ 'date_confirmed' => true }) + when 'no' + context.delete('datetime') + context.delete('date_is_valid') + context.delete('human_date') + context.merge!({ 'date_not_confirmed' => true }) + end + context.delete('yes_no') # start afresh + end + + # when the person sends a command of any sort + if context['command'] + case context['command'][0]['value'] + when 'decline' + context.delete('want_interview') # can decline at any time + context.merge!({ 'refuse_interview' => true }) + end + context.delete('command') # start afresh + end + + # case when we have a valid date + if context['datetime'] # should we check it's an interval? + context.delete('date_is_invalid') + context.merge!({'date_is_valid'=>true}) + end - # state is usefull for setting up the initial yes_or_no - context.delete('state') if context['state'] - return context.merge(entities) + # might not need this. + if context['date_not_confirmed'] + # must remove this context if we get a new datetime + # currently, if a person doesn't confirm the date, we enter into a loop. + # how do I know that this is a persons second attempt to add a datetime? + end + + + # # when the person sends an unintelligible date + # if context['want_interview'] && context['datetime'].nil? + # # they may have previously sent a good date. + # context.delete('date_is_valid') if context['date_is_valid'] + # context.merge!({'date_is_invalid' => true}) + # end + + Redis.current.set("wit_context:#{context['person_id']}", context.to_json) + Redis.current.expire("wit_context:#{context['person_id']}", 3600) + puts "after merge" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + return context end, error: lambda do |_session_id, _context, error| p error.message end, get_event: lambda do |_session_id, context| # session ID and context will have event id in it. + puts "in get_event" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ei = V2::EventInvitation.find_by(id: context['event_id']) - context.merge({ 'reference_time' => ei.start_datetime, - 'reference_time_slot' => ei.bot_duration }) - end, - select_slot_in_time: lambda do |_session_id, context| - # currently unused - context.merge({ 'first_slot_in_time' => 'friday at 2pm' }) + context.merge!({ 'reference_time' => ei.start_datetime, + 'reference_time_slot' => ei.bot_duration}) end, reserve_slot: lambda do |_session_id, context| # datetime is the time the person chose. - + puts "in reserve slot" pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ei = V2::EventInvitation.find_by(id: context['event_id']) person = Person.find_by(id: context['person_id']) @@ -58,13 +131,16 @@ selected_time = r.start_datetime_human msg = "A #{duration} minute interview has been booked for:\n#{selected_time}\nWith: #{r.user.name}, \nTel: #{r.user.phone_number}\n.You'll get a reminder that morning." else + pp r.errors msg = 'It appears there are no more times available. There will be more opportunities soon!' end Redis.current.del("wit_context:#{person.id}") - context.merge({ 'response_msg' => msg }) + context.merge!({ 'reservation_msg' => msg }) else - context.merge({ 'response_msg' => "I'm sorry, I'm just a silly robot. I didn't understand that. Please respond with something that looks like: #{context['reference_time_slot']}" }) + # lets ask for the date again + context.merge!({ 'date_is_invalid' => true }) end + context end, confirm_reservation: lambda do |_session_id, context| # person confirms reservation @@ -82,10 +158,49 @@ # a person's calendar for the next few days context end, + date_not_confirmed: lambda do |_session_id, context| + # a person's calendar for the next few days + context.merge!({'date_not_confirmed'=>true}) + end, + date_confirmed: lambda do |_session_id, context| + # a person's calendar for the next few days + context.merge!({'date_confirmed'=>true}) + end, decline_invitation: lambda do |_session_id, context| + puts "in decline" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + context.delete('want_interview') if context('want_interview') # probably destroy the context and turn end the current session + Redis.current.del("wit_context:#{context['person_id']}") + context.merge('decline_invitation'=>true) + end, + date_to_human: lambda do |_session_id, context| + + puts "in date_to_human" + pp context + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + context.delete('yes_no') + ei = V2::EventInvitation.find_by(id: context['event_id']) + times = get_times(context,ei.end_datetime) + if times + puts 'we have times!' + human_arr = [] + # handling multiple times + times.each do |time| + human_arr << "#{time[:from].strftime('%A').lstrip} from #{time[:from].strftime('%l:%M%p').lstrip} to #{time[:untill].strftime('%l:%M%p').lstrip}" + end + human_date = human_arr.join(' and ') + context.delete('date_is_invalid') + context.merge!({ 'human_date' => human_date, 'date_is_valid'=> true }) + else + puts "why didn't we get times here?" + context.delete('human_date') + context.delete('date_is_valid') + context.merge!({ 'date_is_invalid' => true }) + end context - end + end, } def find_slot_given_times(slots,times) @@ -105,7 +220,7 @@ def get_times(context, default_end) if datetime['type'] == 'value' from = Time.zone.parse(datetime['value']) untill = default_end - else + else # we have an interval from = Time.zone.parse(datetime['from']['value']) # wit.ai uses the end of a duration. we want the start. # for them 1pm to 5pm == 1pm untill 6pm, including the whole of 5pm @@ -121,7 +236,7 @@ def get_times(context, default_end) # session_id: it's the person_id and the event_id, how we know what we are talking about # reference time: the start of the event # reference_time_slot: string of the start and end time of event -# response_msg: either you got a slot in your time or you didn't -# +# reservation_msg: either you got a slot in your time or you didn't +# human_date: a human array of dates, joined by 'and' ::WitClient = Wit.new access_token, actions diff --git a/conversation_management_options b/conversation_management_options index eb49bcd2a..ac6b5b793 100644 --- a/conversation_management_options +++ b/conversation_management_options @@ -13,6 +13,17 @@ logic for "attendable" state overlap checking: 2) doesn't present as an option to user if person has booked a reservation for tim +one of the ways to get a nice date from wit.ai: + curl \ + -H 'Authorization: Bearer JJMZIZCG4ZIKI3WEWPJLZQ5VPDO3KXDC' \ + 'https://api.wit.ai/message?v=20160524&q=from%202pm%20to%204pm&context=%7B%22reference_time%22%3A%222016-05-28T14%3A00%3A41.573-04%3A00%22%7D' + +where: + context = Rack::Utils.escape({reference_time: DateTime}.to_json) + q = query string + + + Conversation Management: Option 1