Episodes

Label Maker with Ruby

Episode #343 by Teacher's Avatar David Kimura

Summary

Learn how to turn your tweets into physical printouts on a label maker via the Cups/PPD API. It's a silly concept, but can have some great real world application with interfacing with printers.
rails api 26:30

Resources

Download Source Code

Summary

# Terminal
bundle add cupsffi
bundle add dotenv-rails
bundle add rufus-scheduler
WORKER=true bin/rails s
rails g model tweet tweet_id:integer:index tweeted_at:datetime username content:text printed:boolean

# irb
CupsPPD.new("Brother_QL_810W", nil).options

# .env
USER_ID=your-id-here
BEARER_TOKEN=your-token-here

# config/application.rb
Dotenv::Railtie.load

# config/initializers/cron.rb
require 'rufus/scheduler'

scheduler = Rufus::Scheduler.new

scheduler.every '1m' do
  Twitter.fetch if ENV['WORKER']
end

# models/printer.rb
require 'cupsffi'

class Printer
  def self.print(id)
    new(id).print
  end

  def initialize(id)
    @id = id
  end

  def print
    job = printer.print_data(text, "text/plain", options)
    loop do
      puts job.status
      break unless [:pending, :processing].include?(job.status)
      sleep 1
    end
    tweet.update(printed: true) if job.status == :completed
  end

  private

  def tweet
    @tweet ||= Tweet.find(@id)
  end

  def message
    [tweet.username, "said on", tweet.tweeted_at, ":", tweet.content].join(" ")
  end

  def printer
    # printers = CupsPrinter.get_all_printer_names
    # Brother_QL_810W
    @printer ||= CupsPrinter.new("Brother_QL_810W")
  end

  def options
    {
      'cupsPrintQuality': 'High',
      'MediaType': 'roll',
      'PageSize': "Custom.#{width}x#{height}mm"
    }
  end

  def width
    62
  end

  def height
    (text_array.size * 7).to_i
  end

  def text_array
    message.scan(/(.{1,22})(?:\s|$)/m)
  end

  def text
    "\r\n #{text_array.join("\r\n ")}".encode(
      "ascii",
      invalid: :replace,
      undef: :replace,
      replace: ''
    )
  end
end

# models/twitter.rb
require 'net/http'
class Twitter
  USER_ID = ENV["USER_ID"]
  BEARER_TOKEN = ENV["BEARER_TOKEN"]

  def self.fetch
    new.fetch
  end

  def fetch
    Rails.logger.info "Fetching tweets #{Time.current}"

    return if tweets['data'].nil?
    tweets['data'].reverse.each do |tweet|
      user = tweets.dig('includes', 'users').find { |u| u['id'] == tweet['author_id'] }
      record = Tweet.find_or_create_by(
        tweet_id: tweet['id'],
        tweeted_at: tweet['created_at'],
        username: user['name'],
        content: tweet['text']
      )
      next if record.printed?
      Printer.new(record.id).print
    end
  end

  private

  def tweets
    @tweets ||= begin
      uri = URI(url)
      request = Net::HTTP::Get.new(uri)
      request["Authorization"] = "Bearer #{BEARER_TOKEN}"
      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
        http.request(request)
      end
      JSON.parse(response.body)
    end
  end

  def url
    params = []
    params << "tweet.fields=created_at"
    params << "expansions=author_id"
    params << "since_id=#{last_tweet_id}" if last_tweet_id
    "https://api.twitter.com/2/users/#{USER_ID}/mentions?#{params.join("&")}"
  end

  def last_tweet_id
    @last_tweet_id ||= Tweet.order(tweet_id: :desc).first&.tweet_id
  end
end