architecture2

Author: @imWildCat, University of Portsmouth

Ruby on Rails is a extremely powerful web framework with a long history, which can simplify our development process, making it more enjoyable. As it known to all, many well-known sites are built on this framework, such as GitHub, Unsplash, Airbnb, Dribbble and Product Hunt1. For most developers without so much experience about Rails, setting up a development environment for this stack is not a easy task. This blog would introduce how to interact WeChaty with Rails with an example of a group message logger, trying to Keep it simple, stupid (the KISS principle).

Note: This blog will mainly illustrate the tutorial on macOS. The situations can be very different on other platforms such as Windows and Linux. Due to the limitations of this author’s time, these topics cannot be covered. Moreover, the final version of code has been published on GitHub: https://github.com/imWildCat/blog-post-interact-wechaty-with-rails-from-scratch

Prerequisites

Ruby and Rails

It is better to manage Ruby versions with rbenv. On macOS,

# Install brew (https://brew.sh):
➜ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# Install rbenv (a Ruby version manager, https://github.com/rbenv/rbenv):
➜ brew install rbenv ruby-build
# Install Ruby (2.4.1)
➜ rbenv install 2.4.1
➜ gem install rails --pre --no-ri --no-rdoc

Check your Ruby runtime:

➜ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]
# Or something like that

Node.js

# Install node.js with nvm (if you have installed node.js, skip this step):
➜ brew install nvm && nvm install v7.9.0

Check your node.js runtime:

➜ node -v
v7.9.0
# Any node.js version above 6.0 is fine :)

PostgreSQL (Optional)

PostgeSQL is a database, usually used for Rails apps. On macOS, it is recommended to use Postgres.app. It can also be installed by brew:

➜ brew cask install postgres

However, it doesn’t matter if you use MySQL or even SQLite. This tutorial offers this flexibility to use any database supported by ActiveRecord (the ORM framework of Rails).

Get started for Wechaty

First of all, we prefer to create a directory for our project:

mkdir chatieme
cd chatieme

In this directory, firstly, create a directory for Wechaty, naming it chatieme-worker and setting up the project:

mkdir chatieme-worker
cd chatieme-worker
npm init -y

Add dependencies for wechaty:

npm i --save wechaty chromedriver request

Then create a new file named bot.js with this code base:

const { Wechaty } = require('wechaty');

function startBot() {
    const bot = Wechaty.instance({ profile: 'chatieme' });
    bot.on('scan', (url, code) => console.log(`Scan QrCode to login: ${code}\n${url}`))
        .on('login', user => console.log(`User ${user} logined`))
        .on('message', message => console.log(`Message: ${message}`))
        .init();
}

startBot();

Since we’d like to send the message to Rails app, we have to build a JSON object:

const { Wechaty } = require('wechaty');

function startBot() {
    const bot = Wechaty.instance({ profile: 'chatieme' });
    bot.on('scan', (url, code) => console.log(`Scan QrCode to login: ${code}\n${url}`))
        .on('login', user => console.log(`User ${user} logined`))
        .on('message', onMessage)
        .init();
}

function onMessage(message) {
    const room = message.room();
    const sender = message.from();
    const receiver = message.to();
    const content = message.content();

    if (!room || !receiver) {
        // onPersonalMessage(message);
        return;
    }

    const topic = room.topic();
    const from_name = sender.name();

    const data = {topic, from_name, content};
    console.log(data);
}

startBot();

Run the bot node bot.js, logging in with your WeChat account. The output can be like:

{ topic: 'wechaty',
  from_name: 'WildCat',
  content: 'Hello, World!' }

Get started for Rails

Go back to the parent directory, create a rails project with rails command line, then enter this directory:

cd ..
rails new chatieme-worker
cd chatieme-worker

Then you could create the database, regardless what database you use:

➜ rails db:create
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

First of all, a model for message should be created. Rails offers command line tool to accelerate the process of creating code base (i.e. ‘rails –help’)2:

➜ rails generate model Message topic:string from_name:string content:text
Running via Spring preloader in process 38447
      invoke  active_record
      create    db/migrate/20170421120013_create_messages.rb
      create    app/models/message.rb
      invoke    test_unit
      create      test/models/message_test.rb
      create      test/fixtures/messages.yml

In fact, the model file app/models/message.rb doen’t contain any code about the structrure of the database. The migration file db/migrate/20170421115650_create_messages.rb (in your case, the file name should be different in relation to your date & time) contains the structure:

class CreateMessages < ActiveRecord::Migration[5.1]
  def change
    create_table :messages do |t|
      t.string :topic
      t.string :from_name
      t.text :content

      t.timestamps
    end
  end
end

Ruby is a ‘natural’ programming. You can easily understand these codes with a little bit experience of other programming language.

Then migrate the database to latest version:

rails db:migrate

You could see the structure of the sqlite database file db/development.sqlite3:

database_initial_structure

While using rails console to open a REPL3 for your Rails project, we can create a data record easily by Message.create! topic: 'My Group Name', from_name: 'HailCat', content: 'Hiya, World!':

➜ rails console
Running via Spring preloader in process 39120
Loading development environment (Rails 5.1.0.rc2)
irb(main):001:0> Message.create! topic: 'My Group Name', from_name: 'HailCat', content: 'Hiya, World!'
   (0.1ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "messages" ("topic", "from_name", "content", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["topic", "My Group Name"], ["from_name", "HailCat"], ["content", "Hiya, World!"], ["created_at", "2017-04-21 12:09:49.440133"], ["updated_at", "2017-04-21 12:09:49.440133"]]
   (0.6ms)  commit transaction
=> #<Message id: 1, topic: "My Group Name", from_name: "HailCat", content: "Hiya, World!", created_at: "2017-04-21 12:09:49", updated_at: "2017-04-21 12:09:49">

At present, the code base has been set up and the directories would look like:

cd ..
➜ tree -L 2
.
├── chatieme-server
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── README.md
│   ├── Rakefile
│   ├── app
│   ├── bin
│   ├── config
│   ├── config.ru
│   ├── db
│   ├── lib
│   ├── log
│   ├── package.json
│   ├── public
│   ├── test
│   ├── tmp
│   └── vendor
└── chatieme-worker
    ├── bot.js
    ├── chatieme.wechaty.json
    ├── node_modules
    └── package.json
13 directories, 9 files

Let Wechaty communicate with Rails

Basicially, Rails is a web framework so that the most usual way for the communication is by HTTP (web). We hope there can be an architecture like this:

architecture1

Both Rails and Wechaty can be regarded as micro services, which can also be dockerized4 in the coming blogs. The most consierable advantage of this kind of architecture is that more than one Wechaty instances can share a single Rails app:

architecture2

So, let’s do it.

Set up API of Rails

We need to create a controller to handle the HTTP requests about creating new Message record:

➜ rails generate controller Messages
Running via Spring preloader in process 41917
      create  app/controllers/messages_controller.rb
      invoke  erb
      create    app/views/messages
      invoke  test_unit
      create    test/controllers/messages_controller_test.rb
      invoke  helper
      create    app/helpers/messages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/messages.coffee
      invoke    scss
      create      app/assets/stylesheets/messages.scss

Edit app/controllers/messages_controller.rb:

class MessagesController < ApplicationController

    skip_before_action :verify_authenticity_token, :only => [:create]

    def create
        message_params = params.permit(:topic, :from_name, :content)
        new_message = Message.new message_params
        if new_message.save
            render json: {error: nil}
        else
            render json: {error: true, error_message: new_message.errors.full_message}
        end
    end

end

Then edit config/routes.rb:

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  post 'messages', to: 'messages#create'
end

Note: I would not explain detailed Rails app development because it is off the main topic.

Run the development server of Rails:

➜ rails server
=> Booting Puma
=> Rails 5.1.0.rc2 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

We can test the API provided by Rails app using curl:

➜ curl -X POST -F 'topic=This is topic' -F 'from_name=HailCat' -F 'content=This is content' http://localhost:3000/messages
{"error":null}%

POST the data from Wechaty to Rails

We could take advantages of the library request, posting the data to Rails:

const { Wechaty } = require('wechaty');
const request = require('request');

function startBot() {
    const bot = Wechaty.instance({ profile: 'chatieme' });
    bot.on('scan', (url, code) => console.log(`Scan QrCode to login: ${code}\n${url}`))
        .on('login', user => console.log(`User ${user} logined`))
        .on('message', onMessage)
        .init();
}

function onMessage(message) {
    const room = message.room();
    const sender = message.from();
    const receiver = message.to();
    const content = message.content();

    if (!room || !receiver) {
        // onPersonalMessage(message);
        return;
    }

    const topic = room.topic();
    const from_name = sender.name();

    const data = { topic, from_name, content };
    console.log(data);

    // New code starts
    request.post({
        url: 'http://localhost:3000/messages',
        form: data
    },
        function (err, httpResponse, body) {
           console.log(body);
        });
    // New code ends
}

startBot();

Restart Wechaty node bot.js. Wechaty can post any group messages to Rails. A simple logger has been finished.

Add admin panel for Rails

Recording messages in database is far from enough, initially, we need a admin panel helping us manage the data. RailsAdmin is a choice to build a simple control panel with a few lines of code. To get started, add gem 'rails_admin' into your Gemfile of Rails project:

# Gemfile
# ... A lot of other codes above
gem 'rails_admin'
gem 'erubis'

Stop Rails development server and run bundle install to install the dependency:

➜ bundle install
... a lot of lines above ...
Using rails 5.1.0.rc2
Using sass-rails 5.0.6
Installing rails_admin 1.1.1
Bundle complete! 17 Gemfile dependencies, 79 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

Run rails g rails_admin:install. Then run rails server to start Rails development server.

Visit http://localhost:3000/admin, you can see an awesome admin pannel of this logger:

admin panel

Conclusion

This blog should be a very specific tutorial for developers who want to build a message logger for WeChat. Rails is a fullstack web framework so that it can be very complex. Explaining the comcepts of Rails is not the main purpose of this blog. However, if you would like to build a powerful logger, it is worthy doing that. Powered by Rails, you can build the system with less time consumption and more joy.

Thanks for your reading. Feel free to drop any questions.

References

  1. Rails, StackShare: https://stackshare.io/rails 

  2. The Rails Command Line — Ruby on Rails Guides: http://guides.rubyonrails.org/command_line.html 

  3. Read–eval–print loop - Wikipedia: https://en.wikipedia.org/wiki/Read–eval–print_loop 

  4. Docker (software) - Wikipedia: https://en.wikipedia.org/wiki/Docker_(software)