Converting a 200 line Ruby script to Kotlin - Possible? Sensible?


#1

People,

I have a Java chatbot running my Fedora 25 x86_64 server and people can chat with it via FaceBook:

FB talks to the Ruby CGI script which communicates with the chatbot and I am thinking that I would convert this Ruby script to Kotlin as an exercise but also I would like to develop the resulting app as a general-purpose gateway for other social networks and then further develop it to act as dispatcher for NLP and AI modules. I attach the Ruby script below - could people comment on the degree of difficulty of conversion and whether it is even sensible to attempt it?

Thanks,
Phil.

#!/usr/bin/env ruby

require 'byebug'
require 'cgi'
require 'faraday'
require 'json'
require 'logger'

cgi = CGI.new
puts cgi.header

# system('gem install faraday > /dev/null 2>&1') # this is already installed

@logger = Logger.new('logs/fb_chatbot.log', 'monthly')
# There are 6 levels: Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::Error, Logger::FATAL, and Logger::UNKOWN.
# @logger.level = Logger::DEBUG
@logger.level = Logger::INFO

@logger.debug( "Reached the CGI script!")

FB_ACCESS_TOKEN = 'xxx'
CHATBOT_HOST = 'localhost'
CHATBOT_PORT = 9192

@xmit = {
  :commands => {
    :users_name => '',
    :users_lastname => '',
    :users_fullname => '',
    :name => ''
  },
  :topic_slug => 'phirhodev',
  :session_id => '',
  :message => ''
}

# byebug

def send_message_to_facebook(user_id, message)
  json_data = {
    recipient: {
      id: user_id
    },
    message: {
      text: message
    }
  }
  url = 'https://graph.facebook.com/v2.6/me/messages?access_token=' + FB_ACCESS_TOKEN

  conn = Faraday.new(url)
  response =
    conn.post do |req|
      req.headers['Content-Type'] = 'application/json'
      req.body = json_data.to_json
    end

  @logger.debug(user_id)
  @logger.debug(JSON.parse(response.body))
  response.status == 200
end

def send_indicator_to_facebook(user_id)
  json_data = {
    recipient: {
      id: user_id
    },
    sender_action: 'typing_on'
  }

  url = 'https://graph.facebook.com/v2.6/me/messages?access_token=' + FB_ACCESS_TOKEN
  conn = Faraday.new(url)
  response =
    conn.post do |req|
      req.headers['Content-Type'] = 'application/json'
      req.body = json_data.to_json
    end

  @logger.debug(user_id)
  @logger.debug(JSON.parse(response.body))
  response.status == 200
end

def load_facebook_profile_details(user_id)
  # check for cached entry
  profile_path = 'logs/' + user_id + '.json'
  @logger.debug( 'Reached here ----------> 5a' )

  if File.file? profile_path
    data = JSON.parse(File.read(profile_path))
  else
    # fetch profile data
    @logger.info('loading first time profile data')
    url = 'https://graph.facebook.com/v2.6/' + user_id + '?fields=first_name,last_name,locale,timezone,gender&access_token=' + FB_ACCESS_TOKEN
    response = Faraday.get(url)
    data = JSON.parse(response.body)
    @logger.debug(user_id)
    @logger.debug(data)

    return 0 if response.status != 200

    File.open(profile_path, 'w') { |f| f.write(data.to_json) }
  end

  @xmit[:commands][:users_name] = data['first_name']
  @xmit[:commands][:users_lastname] = data['last_name']
  @xmit[:commands][:users_fullname] = data['first_name'] + ' ' + data['last_name']
  @xmit[:commands][:name] = data['first_name']
  return(1)
end

def send_message_to_bot(request)
  # Prepare data
  @xmit[:session_id] = '@@PHIRHOBOT_' + request['sender']['id']
  @xmit[:message] = request['message']['text']

  # Send request
  unless TCPSocket.gethostbyname(CHATBOT_HOST)
    @logger.debug('Hostname could not be resolved. Exiting')
    return 0
  end

  @logger.debug(@xmit)
#  @logger.debug(@xmit.to_json)
#  @logger.debug(@xmit.to_s)
#  @logger.debug(@xmit.to_json.to_s)
#  @logger.debug(@xmit.to_s.to_json)

  begin
    client_socket = TCPSocket.open(CHATBOT_HOST, CHATBOT_PORT)
  rescue
    @logger.debug('Could not connect to chatbot')
    return 0
  end

  client_socket.puts @xmit.to_json
  data = ''

  while true
    buf = client_socket.recv(1024)
	break if buf == ""
    data += buf
  end

  client_socket.close
  return data.sub(/\n@@END_OF_REPLY\n/, '')
end

resp_text = ''

# byebug

if cgi.params
  @logger.debug( 'Reached here ----------> 1' )
  fb_params = nil
  fb_params = cgi.params.keys.first if cgi.params.is_a? Hash
#  fb_params = 
#  {:entry => [
#    {:messaging => [
#    {:timestamp => 1494081902315,
#     :message => {:text => 'test 1',
#                  :mid => 'mid.$cAALftLGKxSFiD33e61b3jiUhe4-x',
#                  :seq => 675},
#     :recipient => {:id => '709466372544009'},
#     :sender => {:id =>  '1270899366263587'}
#    }],
#  :id => '709466372544009',
#  :time => 1494081902575}],
#  :object => 'page'}
# 
#  fb_params = fb_params.to_json

  fb_params = JSON.parse(fb_params) rescue nil
  @logger.debug( fb_params )

  @logger.debug( 'Reached here ----------> 2' )

  if fb_params && fb_params['entry'] && fb_params['entry'].first && fb_params['entry'].first['messaging']
    @logger.debug( 'Reached here ----------> 3' )
    msg_data = fb_params['entry'].first['messaging'].first
    @logger.info("Received message from user #{msg_data['sender']['id']}")
    @logger.info(msg_data['message']['text'])

    # Send processing/typing indicator to facebook
    send_indicator_to_facebook(msg_data['sender']['id'])
    @logger.debug( 'Reached here ----------> 4' )

    # Load profile details for user id
    load_facebook_profile_details(msg_data['sender']['id'])
    @logger.debug( 'Reached here ----------> 5b' )

    # Send request to chatbot
    cb_response = send_message_to_bot(msg_data)
    @logger.debug( 'Reached here ----------> 6' )
    @logger.info('Chatbot response:')
    @logger.info(cb_response)

    cb_response ||= 'Error communicating with chatbot server!'

    # Send chatbot response back to facebook
    send_result = send_message_to_facebook(msg_data['sender']['id'], cb_response)

    @logger.info('Failed to send reply to facebook api') unless send_result
  elsif cgi.params['hub.verify_token'] == ['verify_206SouthDowlingSt']
    resp_text = cgi.params['hub.challenge']
  else
    @logger.info('No valid request found')
  end
end

puts resp_text

#2

You really should use proper markdown formatting to make your example readable. Indent your code with 4 spaces or add a line with “```” above and below your code.

```
your code ...

```

#3

@medium,

I just pasted from the Vim buffer which was nicely formatted - it looks like comment lines got turned into headings . . is there a convenient way to convert .rb into .md ?

Thanks.


#4

@philip_rhoades code block should be surrounded with tripple ` characters to preserve formatting like follows:

    ```
    <your code block>
    ```

#5

Ah . . that’s better - sorry I didn’t get what was meant the first time . .


#6

It should be not to hard to convert this to Kotlin. You would need a http client library (Apache Commons has a good one), some basic tcp libs from the JDK and some kind of server, for example a simple servlet (no need for a more sophisticated framework here) and a servlet container like Tomcat or Jetty. For such a simple scenario Spark web framework could be a good fit, too.


#7

Thanks!


#8

Avoid heading down the servlet route (handles concurrency badly - full of blocking APIs, uses too many resources) unless you need to do it for legacy reasons related to Java. As @medium mentioned Spark is a good option. You might want to also consider using Vert.x which is also a good choice. Vert.x (a simple toolkit) allows you to easily roll your own web server and web client, unit test web services, live code reloading (works with .kt files), and has very good Kotlin support (has excellent Kotlin specific documentation that is full of Kotlin examples, Kotlin APIs etc) which is more mature than Spark’s.

Below is a simple Vert.x web services example (plain HTTP server):

import io.vertx.core.AbstractVerticle

class Server : AbstractVerticle() {
    override fun start() {
        vertx.createHttpServer().requestHandler { req ->
            req.response()
                .putHeader("content-type", "text/plain")
                .end("Hello from Vert.x")
        }.listen(8080)
    }
}

#9

People,

Thanks for the comments - they are much appreciated! I am just about to start working my way through Tim’s book:

https://www.amazon.com.au/d/Programming-Beginners-software-engineering-Kotlin-ebook/B06ZYB7NHY/ref=sr_1_2?ie=UTF8&qid=1503213050&sr=8-2&keywords=kotlin+beginners

but will probably not be able to resist the temptation to try out your suggestions out - sooner rather than later!

Regards,
Phil.


#10

I suggested Servlets, because I think that they are fairly simple and straight forward. However with some frameworks abstracting away most basic things like the setup of a container, higher level frameworks are easy to use. So, when we are already there, I want to mention Spring Webflux, which is a new reactive web framework that will be introduced with Spring 5 in September and which comes with a dedicated Kotlin API. It has a client API, too.