WildCat's Blog

钝鸟先飞,大器晚成

Designer + Programmer


Interact Wechaty with Ruby on Rails from scratch

Ruby on Rails is a extremely powerful web framework with a long history, which can simplify our development process, make 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:

architecture_1

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:

architecture_2

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) 

最近的文章

微信小程序令人失望的问题

微信小程序已经发布接近半年了,自己也关注了很久微信小程序的开发,还专门为此注册了公司。令人失望的是,微信小程序从发布到现在,成吨的技术低级问题都没有解决。本文抛砖引玉,供大家参考。文末还有个人对微信小程序的理解与担忧。失望的问题 模拟器与真机很多不一样,很多 margin、padding 为 0 的地方,会多出不明所以的留白(在 Sketch 中对齐的两张截图): 如果上图不够明显,我把模拟器截图的透明度提高,覆盖在真机截图上面: 模拟器应该是使用 Chromium ...…

继续阅读
更早的文章

如何在中国的一座小城市注册公司(译文)

这是一篇译文,译者与笔者都是同一人。原文请见 How to register a company in a small city in China。 因为注册一个完全功能的微信公众账号需要一家中国公司(虽然最近微信开始准备允许境外公司注册微信公众账号,但是就一个国外的朋友反馈说,已经等了六周还是没有回复),我决定在中国开一家公司。这篇文章将会介绍在中国的一座小城市注册公司的流程。如果你在其他城市,情况可能会有所不同,因为地方政府的政策会有所不同。 另外,笔者拥有一点关于金融、公司财...…

继续阅读