jiunjiun(均均)

[Ruby] 發票機 - 列印電子發票

| Comments

最近接了一個案子,需要透過出單機列印電子發票

原本是想請出單機廠商協助這部份的開發,無奈是出單機廠商提供的程式

只有MS C#,但我們系統需要在Linux環境下去操作

反正後來廠商沒辦法處理這部份,最後只能從最原始的方式去操作

這中間的開發過程有點艱苦,資料很有限的狀況下去寫

我們的出單機的機型是 TP805,指令上跟 EPSON 可能有些微不同

不保證每一台出單機都適用,有需要的可以參考下面Source Code

下面範例是使用 Ruby,使用如下

printer = Invoices::Printer.new

# 列印發票
printer.run  

# 列印交易明細
printer.detail

編寫的方式,可能不是很完整

有問題歡迎提出,再來調整

[Rails] 智付通Spgateway - 信用卡定期定額

| Comments

智付通的信用卡定期定額,會用AES的方式去加密,回傳你的訂單資訊

在回傳的時候,遇到幾個問題:

  • 無法 decrypt
  • decrypt 後有跳脫字元

無法 decrypt

我是使用 MCrypt,需要在你的 Mac 安裝

brew install libmcrypt

需要先在你的專案 Gemfile 加入

gem "ruby-mcrypt"

接著在你的專案裡加入 aes.rb,路徑我是放在 lib/spg/aes.rb

# encoding: utf-8
require 'mcrypt'

module Spg
  class Aes
    attr_accessor :key, :iv

    def self.encrypt data
      new.encrypt data
    end

    def self.decrypt data
      new.decrypt data
    end

    def initialize
      self.key = Settings.spg.hash_key
      self.iv  = Settings.spg.hash_iv
    end

    def encrypt data
      crypto.encrypt(data).unpack("H*").last
    end

    def decrypt data
      crypto.padding = :zeros

      decrypt = crypto.decrypt([data].pack("H*"))
      decrypt.encode('UTF-8', invalid: :replace, replace: '').match(/{.*}/)[0]
    end

    private
    def crypto
      @crypto ||= Mcrypt.new(:rijndael_128, :cbc, key, iv, :pkcs)
    end
  end
end

initializeself.keyself.iv 填入 key, iv
encrypt 的時候比較沒什麼問題,但在 decrypt 一開始沒有加

crypto.padding = :zeros

在回傳錯誤的訊息,都是正常的,但在回傳正確的訊息的,他就會 decrypt 失敗
後來是看 日本的Qiita的文章才找到答案
PHPのMcrypt関数でencryptしたデータをRubyでdecryptする
加了 crypto.padding = :zeros 就正常了

decrypt 後有跳脫字元

然後以為都沒有問題了,在JSON.prase的時候就噴掉,想說是不是寫錯什麼,後來查完是會在你的 decrypt
後面加了很多 \x01D\x01D\x01D\x01D\x01D\x01D\x01D\x01D 真的很煩人

接著就把這些跳脫拔掉,因為我不知道會有多少種,至少我在測的時候是發現這個 \x01D

decrypt.encode('UTF-8', invalid: :replace, replace: '').match(/{.*}/)[0]

sample.rb

text = 'Hello, World!'
encrypt = Spg::Aes.encrypt(text)
decrypt = Spg::Aes.decrypt(encrypt)

參考:

LineBot - Sinatra

| Comments

前陣子看到 Line Bot,全新改版,想說來寫看看
基本上官方文件比起之前 簡單很多也詳細很多
也有出很多版本 Java, Ruby, Go, PHP,接下來會以Ruby做為範例

從這邊開始註冊:https://business.line.me/zh-hant/services/bot

申請完後,先選LINE@ MANAGER 設定你的機器人

點選畫面左邊 帳號設定 > Bot設定 設定你的機器人,在 Webhook傳訊 選擇允許
那下面有一個 是否可讓Bot加入群組聊天室 再看需不需要把這個功能做開啟
最早是沒有這個功能,現在有這個功能後,就可以把機器人拉到你跟朋友之間的群組做使用
設定完後 儲存

接下來開始 Code 的部份,這邊以 Ruby 做為範例
可以從官方的 line-bot-sdk-ruby,裡面已經有很簡單的範例,只要複製貼後上就可以了
連結:https://github.com/line/line-bot-sdk-ruby

這部份我有寫一個簡單的 sample code
GitHub:https://github.com/jiunjiun/LineBot

可以透過 Deploy To Heroku 部署一個App到 Heroku

部署完後你要把 Channel SecretChannel Access Token 加到 你的專案

可以從 LINE Developers 到你的Bot後台

你可以從畫面上找到你的 Channel SecretChannel Access Token(螢幕太小截兩張圖)


把這兩個值設定到 Heroku 的 config

$ heroku config:set LINE_CHANNEL_SECRET={YOUR_Channel_Secret}
$ heroku config:set LINE_CHANNEL_TOKEN={YOUR_Channel_Access_Token}

接著回到 LINE DevelopersEdit 把 Heroku App 網址帶到 Webhook URL

https://{YOUR_HEROKU_SERVER_ID}.herokuapp.com/callback
  • {YOUR_HEROKU_SERVER_ID} 這邊是填你的 Heroku App ID

儲存完後,再按 VERIFY 驗證你的server,出現 Success 就完成了

回去你的 LINE@ MANAGER帳號設定 > 基本設定 就可以加好友了。
那就完成了

如果有什麼問題再問我吧~

Sample Code:https://github.com/jiunjiun/LineBot

其他相關的 API 可以參考官方的資料
https://devdocs.line.me/en/
裡面寫得很清楚,包含貼圖、Template messages,都可以玩玩看
:D

-

參考資料:
https://github.com/line/line-bot-sdk-ruby
https://github.com/kkdai/LineBotTemplate

[Rails] Dropzonejs

| Comments

在網站開發常會需要圖片上傳

  • 然後 還要可以直接拖拉(Drag&Drop)
  • 然後 交換順序

透過 Dropzonejs 快速的把功能做出來

前置作業

Gemfile
# upload
gem 'carrierwave'

# use sortable
gem 'jquery-ui-rails' 

gem 'dropzonejs-rails'
bundle install

建立 Photo model

rails g model photo
db/migrate/*_create_photos.rb
class CreatePhotos < ActiveRecord::Migration
  def change
    create_table :photos do |t|
      t.string  :name
      t.string  :file
      t.string  :size
      t.string  :content_type
      t.integer :position

      t.timestamps null: false
    end
  end
end

db migrate

rake db:create ; rake db:migrate

建立 photo controller

rails g controller photos

設定 CarrierWave Uploader

rails generate uploader Photo

開始來寫

設定routes

config/routes.rb
Rails.application.routes.draw do
  root to: 'photos#index'
  resources :photos do
    collection do
      post :update_position
    end
  end
  # ...
end

update_position 是之後我們會有一個 更新圖片順序的功能

-

設定assets 加入dropzonejquery-ui

  • app/assets/javascripts/application.js
    app/assets/javascripts/application.js
    //= require jquery-ui/sortable
    //= require dropzone
    
    這邊jquery-ui我們只需要用到sortable
  • app/assets/stylesheets/application.css
    app/assets/stylesheets/application.css
    *= require dropzone/dropzone
    

-

設定models/photo

app/models/photo.rb
class Photo < ActiveRecord::Base
  mount_uploader :file, PhotoUploader

  before_create :update_file_attributes
  after_destroy :remove_file

  private
  def update_file_attributes
    if self.file.present?
      self.name         = file.file.filename
      self.size         = file.file.size
      self.content_type = file.file.content_type
      self.position     = Photo.count + 1
    end
  end

  def remove_file
    FileUtils.remove_dir("#{Rails.root}/public/uploads/room_photo/file/#{id}", true)
  end
end

設定 mount_uploader :file, PhotoUploader
before_create :update_file_attributes 圖片上傳後做一些設定,包含圖片position
after_destroy :remove_file 圖片刪除後,為了不在local佔太多空間,也一起把他砍掉!!

-

設定photo index page

app/views/photos/index.html.erb
<form action="/photos" class="dropzone" id="myDropzone">
  <div class="fallback">
    <input name="file" type="file" multiple />
  </div>
</form>

基本上這樣寫,dropzone已經會抓到並且有一個基本的畫面可以上傳,不過我們還需要刪除拖拉順序,還需要到js去改寫

-

設定photo controller

app/controllers/photos_controller.rb
class PhotosController < ApplicationController
  before_action :set_photo, :only => [:destroy]

  def index
    respond_to do |format|
      format.html
      format.json {
        photos = Photo.order(:position).map {|photo| {pid: photo.id, url: photo.file.url, name: photo.name, size: photo.size} }
        render json: photos
      }
    end
  end

  def create
    photo = Photo.new(photo_params)
    if photo.save
      result = {success: true, pid: photo.id}
    else
      result = {success: false, error: photo.errors.full_messages.join(';')}
    end
    render json: result
  end

  def destroy
    @photo.destroy
    respond_to do |format|
      format.json {
        render json: {success: true}
      }
    end
  end

  def update_position
    positions = params[:positions]
    if positions.present?
      photo_update_attributes = positions.values.map {|val| {position: val} }
      Photo.update(positions.keys, photo_update_attributes)
    end

    respond_to do |format|
      format.json { render json: {success: true} }
    end
  end

  private
  def set_photo
    @photo = Photo.find(params[:id])
  end

  def photo_params
    params.require(:photo).permit(:file)
  end
end

這邊說明一下

  • index - 在上傳之後重新整理還可以在 dorpzone 看到我們上傳的圖片,會透過json的方式把圖片資訊帶回去
  • update_position - 可以更改上傳圖片的順序

-

設定photo js

app/assets/javascripts/photos.coffee
$ ->
  Dropzone.options.myDropzone =
    acceptedFiles: 'image/*'
    dictCancelUpload: '取消上傳'
    dictRemoveFile: '刪除圖片'
    dictInvalidFileType: '檔案格式錯誤,請上傳圖片'
    paramName: 'photo[file]'
    maxFilesize: 5
    addRemoveLinks: true
    init: ->
      thisDropzone = this
      $.getJSON 'photos.json', (data) ->
        $.each data, (index, val) ->
          mockFile =
            name: val.name
            size: val.size
            pid: val.pid

          thisDropzone.options.addedfile.call thisDropzone, mockFile
          thisDropzone.options.thumbnail.call thisDropzone, mockFile, val.url
          $('#myDropzone .dz-preview').last().attr('data-pid', val.pid)
          return
        return
      return
    sending: (file, xhr) ->
      $.rails.CSRFProtection(xhr)
    success: (file, response) ->
      file.pid = response.pid
      $('#myDropzone .dz-preview').last().attr('data-pid', val.pid)
    removedfile: (file) ->
      return if file.pid and !confirm('確定刪除嗎?')
      $.ajax
        url: "photos/#{file.pid}"
        type: 'DELETE'
        success: (result) ->
          $(file.previewElement).remove() if (result.success is true)
          return


  $('#myDropzone').sortable
    items: '.dz-preview'
    cursor: 'move'
    opacity: 0.5
    distance: 20
    tolerance: 'pointer'
    stop: (event, ui) ->
      positions = {}
      $('#myDropzone .dz-preview').each (index)->
        pid = $(this).data('pid')
        positions[pid] = index

      $.post 'photos/update_position',
        {positions: positions}, (data) ->
          true
        , 'json'
      return

說明一下

Dropzone
  • Dropzone.options.myDropzone 後面的.myDropzone 是我們form設定的id
    <form action="/photos" class="dropzone" id="myDropzone">
    
  • acceptedFiles: 'image/*':允許的上傳格式,這樣設定就只能上傳圖片
  • dictCancelUploaddictRemoveFiledictInvalidFileType:一些文字說明
  • paramName: 'photo[file]':傳到後端的參數名稱
  • maxFilesize: 5:檔案上傳最大容量(MB),這樣設定最大就5MB
  • addRemoveLinks: true:是否開啟刪除連結,這邊我們把他打開
  • init:這邊最主要就是去檢查之前上傳的圖片,並且把他顯示出來
    • mockFilenamesize會顯示在畫面上,那pid是為了之後要刪除圖片所需要的
  • sending:這邊的寫法主要是Rails CSRF關係,所以我們需要加上這個
  • success:在上傳圖片的時候接著再按下刪除會因為沒有pid無法刪除,這邊只是加個pid,其中這部份不確定有沒有比較好的作法,歡迎大家指點
  • removedfile:刪除圖片

更細節的設定可以看 Dropzonejs configuration

jQuery UI Sortable

這部份就是完成交換順序的功能,只要在移動完後也就是stop event把順序打回去修改

更細節的設定可以看 jQuery UI Sortable

這樣就完成了 拖拉(Drag&Drop)、交換圖片順序!!!!

其實中間感覺還有很多地方可以調整寫的得好,有任何問題歡迎指點指教

詳細的 code,已經放在 GitHub上面
dropzone-rails-example

參考連結:
Dropzonejs
jQuery UI Sortable

感謝:
Rick大神Hank大神+0大神

[Rails] Can't verify CSRF token authenticity

| Comments

在使用一些前端套件常會需要Post後端,像使用jqueryajax

這個時候server會回應

Can't verify CSRF token authenticity

主要是開發Rails時,Rails本身就安全機制(XSS, CSRF, ...),相關的安全資訊ihower都有做介紹,這邊就不提了
連結:ihower - https://ihower.tw/rails4/security.html

像上面錯誤就是缺少CSRF的Token,那Token在哪裡??
你可以從app/views/application.html.erb裡面看到

<%= csrf_meta_tags %>
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="MbihocBhGJuJYYRkzvJt2Sdppk8kaa8T6TyLFVgmGmnRhoNIhZqrAFTtYdW7hUSddQn3cBW4GRCbf3vCTXtu+g==">

你可以用最簡單的方式去抓token

var token = $( 'meta[name="csrf-token"]' ).attr( 'content' );

或是透過jquery_ujs內建

var token = $.rails.csrfToken();

那在ajax的beforeSend加入token

$.ajax({ url: 'YOUR URL HERE',
  type: 'POST',
  beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
  data: {},
  success: function(response) {
    ...
  }
});

或是

$.ajax({ url: 'YOUR URL HERE',
  type: 'POST',
  beforeSend: $.rails.CSRFProtection,
  data: {},
  success: function(response) {
    ...
  }
});

如果你覺每一個ajax都要寫很麻煩,加上ajax setup

$.ajaxSetup({
  headers: {
    'X-CSRF-Token': $.rails.csrfToken()
  }
});

參考連結:
https://ihower.tw/rails4/security.html
http://stackoverflow.com/questions/7203304/warning-cant-verify-csrf-token-authenticity-rails

Handlebars

| Comments

前陣子在公司的Project有看到還蠻好用的工具

在跟後端要資料的,常會在重複block塞data(json)到html

以往以前都是用jQuery組起來,還蠻纏手

Handlebars就是來解決這個問題

bootstrap css#table來做範例

html

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-2.2.2.min.js"   integrity="sha256-36cp2Co+/62rEAAYHLmRCPIych47CvdM+uTBJwSzWjI="   crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.js"></script>

<table class="table">
  <thead>
    <tr>
      <th>#</th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Username</th>
    </tr>
  </thead>
  <tbody>
    // data
  </tbody>
</table>

<script id="entry-template" type="text/x-handlebars-template">
  {{#each this}}
  <tr>
    <th scope="row">{{index}}</th>
    <td>{{first_name}}</td>
    <td>{{last_name}}</td>
    <td>{{username}}</td>
  </tr>
  {{/each}}
</script>

<script>
$(function(){
  var data = [
    {
      index: 1,
      first_name: 'Mark',
      last_name: 'Otto',
      username: '@mdo'
    },
    {
      index: 2,
      first_name: 'Jacob',
      last_name: 'Thornton',
      username: '@fat'
    },
    {
      index: 3,
      first_name: 'Larry',
      last_name: 'the Bird',
      username: '@twitter'
    }
  ];

  var source   = $("#entry-template").html();
  var template = Handlebars.compile(source);
  var td       = template(data);
  $(td).appendTo('table tbody');
});
</script>

把重複的template寫在 <script id="entry-template" type="text/x-handlebars-template">裡面

其中{{index}}, {{first_name}}, {{last_name}}, {{username}}是對應你的data的hash

因為我們的data是用array去包著裡面的hash,所以在外面還要再包一個{{#each this}}

參考連結:
http://handlebarsjs.com/
https://gist.github.com/pheuter/3515945

How to get current_user in model?

| Comments

前陣子遇到一個狀況,在model處理時,需要將curent_user寫進去,但model沒辦法直接讀取

找了一下可以透過下面的方式在model去抓current_user

先到 application_controller.rb,將每次 current_user 暫時的放到Thread
並且每次都要確定清掉,不然會抓到舊的user,所以這裡用 around_action

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  ....
  around_action :set_current_user
  
  def set_current_user
    User.current = current_user
    yield
  ensure
    User.current = nil
  end
end
app/models/user.rb
class User < ActiveRecord::Base
  ...
  
  def self.current
    Thread.current[:user]
  end

  def self.current=(user)
    Thread.current[:user] = user
  end
end

設定完後在 model 都可以直接用 User.current 直接取得現在 current_user

參考資料:
https://amitrmohanty.wordpress.com/2014/01/20/how-to-get-current_user-in-model-and-observer-rails/
http://stackoverflow.com/questions/2513383/access-current-user-in-model

[CDN] CloudFlare

| Comments

前陣子把我的APB Shuttle,也掛上CDN(CloudFlare)

這專案大概快變成我第一個來測試的玩具XDDD

使用的方式很簡單以及相關的設定,這邊就不提了

中間我就在想如果有人連到 http 那我要怎麼自動導到 https 還在想是不是寫什麼js去做

其實在Nginx config server裡面加上下面這一段就可以了

server {
   listen         80;
   server_name    example.com www.example.com;

   if ($http_x_forwarded_proto = "http") {
     return 301 https://$server_name$request_uri;
   }

   ... directives to generate a response
}

參考資料:
http://serverfault.com/questions/653976/redirect-loop-using-cloudflares-flexible-ssl

[Ruby] Geocoding

| Comments

最近剛好需要從地址(景點)去找是哪個國家,找了一下好像找不太到類似的gem

看了一下還是只有Google Maps Geocoding API 剛好有提供比較接近的功能

下面是自己寫的範例,可以參考 Gist

# coding: utf-8

require 'json'
require 'open-uri'
require 'cgi'

class Geocoder
  # GOOGLE_KEY = 'your_google_key' # Google Maps Geocoding API
  # GoogleGeocodeUrl = "https://maps.googleapis.com/maps/api/geocode/json?key=#{GOOGLE_KEY}&address="
  GoogleGeocodeUrl = "https://maps.googleapis.com/maps/api/geocode/json?address="

  def self.geo_info(address)
    address = CGI::escape(address).gsub("+", "%20")
    url = "#{GoogleGeocodeUrl}#{address}"

    results = JSON.parse(open(url).read)
    status  = results['status']
    results = results['results'].first
  end
end

geo_info = Geocoder.geo_info("台北101")
p geo_info
說明一下

下面是 URL encode

address = CGI::escape(address).gsub("+", "%20")

這邊注意一下

這裡有兩個GoogleGeocodeUrl,其實用下面這一行

GoogleGeocodeUrl = "https://maps.googleapis.com/maps/api/geocode/json?address="

是可以work的,只不過有測試一下,在單筆通常都會成功都會回來,
在短時間大量使用這個API會有失敗的狀況,用API Key有90%機率會成功,但也不是100%這點比較需要注意

參考資料:
https://developers.google.com/maps/documentation/geocoding/intro?hl=zh-tw
http://blog.darkthread.net/post-2012-06-15-geocoding-api.aspx

Hubot script

| Comments

最近上班都靠他Hubot打起精神,只要我們的小編大大提出什麼問題

我就直接反應到Hubot,寫著寫著,寫了不少無聊的東西

其中有關於Facebook Debugger

由來

只要有剛上的文章,我們的小編都要貼到Facebook
在分享文章時找不到圖片或是標題的時候,都會去Debugger
所以才幫小編寫了一個功能,直接在Slack用說的
想說這功能,有人用得到就把他打包丟到NPM給別人使用

Hubot script

自己要打包一個Hubot的套件,你可以透過generator-hubot-script,幫你產生Hubot script,並且包含測試的部份

安裝

npm install -g generator-hubot-script

建立一個資料夾,並到那個目錄

mkdir qwe; cd qwe

Build Hubot script

yo hubot-script

他會問你的這個Hubot的名字,通常你設定他會預設你目錄的資料夾名稱,在這邊就會帶 qwe

[?] Base name of script:

後面就看你對這個機器人有什麼描述, Keywords

[?] Description: A hubot script that does the things
[?] Keywords: hubot, hubot-scripts

設定完他就開始跑,跑完你就可以從 src/qwe.coffee開始寫你的 Hubot script

最後你寫完你要發佈出去NPM,只要簡單的一行指令

grunt release

收工~

在這裡分享前面提到的 fb-debugger,還有Facebook stats
NPM: hubot-fb-debugger
Github: hubot-fb-debugger

歡迎大家分享自己的機器人 XDD

參考資料: