10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

超シンプル!リモートワークの電話連絡を自動化してみた話

Last updated at Posted at 2024-12-11

DALL·E 2024-12-11 21.15.18.png

はじめに

「フルリモートワーク」と聞くと、効率的な働き方を想像する人も多いでしょう。しかし、実際にはリモート特有の課題がつきものです。特に「電話対応」というアナログな部分では、情報共有の手間がかかりがちです。この記事では、そんな課題を楽しく解決した私の体験をご紹介します。

ハウインターナショナルのリモートワーク環境

私が所属するハウインターナショナルは、フルリモートワークを実現している会社です。本社は福岡県飯塚市にありますが、東京、愛知、沖縄、さらには「闘魂の国」からもメンバーが働いています。この分散したチーム環境において、電話対応をどのように効率化するかが大きな課題でした。

電話対応の流れは次のようなものでした:

  1. 電話代行サービスが受けた電話の内容(誰から誰への電話だったのか)をメールで送信
  2. メールを受け取った担当者が内容を確認
  3. Slackで該当者にメンションを付けて連絡

このプロセスはシンプルに見えますが、手動の手間が課題でした。

image.png

解決策:自動化への挑戦

私はこの手動プロセスの「2番と3番」を自動化することに挑戦しました。その結果、新しいフローは次のように改善されました:

  1. 電話代行サービスが電話の内容をメールで送信
  2. ボットが、そのメールを解析し、Slackで該当する担当者にメンションを付けて電話があったことを自動通知

たったこれだけの改善ですが、業務効率は格段に向上しました。

image.png

担当者は、リンボーダンスをはじめました!

メール処理の奥深さ

自動化の鍵となるのは「メールの受信処理」です。一見簡単そうに思えるこの部分が、実は奥深いものでした。

Gmail APIを活用

ハウインターナショナルは、Google Workspaceを利用しているのでGmail APIを使いました。
Rubyの心得があるので、Rubyを使いました。

以前は、 https://developers.google.com/gmail/api/quickstart/ruby というページがありましたが、2024-12-11に見たらありませんでした!

google-api-ruby-client Gem自体は、開発が続けられているようです。

以下は簡単なコード例です:

Gemfile

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"

gem "google-api-client"

私はすごく古いものを使っちまっているようです。以下の記事を参考にアップグレードするとよいと思います。

lib/awesome/gmail.rb

Gmailを受信する処理です。

lib/awesome/gmail.rb
require "google/apis/gmail_v1"
require "googleauth"
require "googleauth/stores/file_token_store"
require "fileutils"

OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze
APPLICATION_NAME = "Gmail API Ruby Quickstart".freeze
CREDENTIALS_PATH = "credentials.json".freeze

TOKEN_PATH = "token.yaml".freeze
SCOPE = Google::Apis::GmailV1::AUTH_GMAIL_READONLY

MAX_RESULTS = 20

module Awesome
  class Gmail
    class << self
      def authorize
        client_id = Google::Auth::ClientId.from_file CREDENTIALS_PATH
        token_store = Google::Auth::Stores::FileTokenStore.new file: TOKEN_PATH
        authorizer = Google::Auth::UserAuthorizer.new client_id, SCOPE, token_store
        user_id = "default"
        credentials = authorizer.get_credentials user_id
        if credentials.nil?
          url = authorizer.get_authorization_url base_url: OOB_URI
          puts "Open the following URL in the browser and enter the " \
               "resulting code after authorization:\n" + url
          code = gets
          credentials = authorizer.get_and_store_credentials_from_code(
            user_id: user_id, code: code, base_url: OOB_URI
          )
        end
        credentials
      end

      def list
        gmail = Google::Apis::GmailV1::GmailService.new
        gmail.authorization = authorize

        messages = []
        next_page = nil
        begin
          result = gmail.list_user_messages('me', max_results: MAX_RESULTS, page_token: next_page)
          messages += result.messages
          break if messages.size >= MAX_RESULTS
          next_page = result.next_page_token
        end while next_page

        messages.map(&:id)
      end

      def get(id)
        gmail = Google::Apis::GmailV1::GmailService.new
        gmail.authorization = authorize

        result = gmail.get_user_message('me', id)
        payload = result.payload
        headers = payload.headers

        date = headers.any? { |h| h.name == 'Date' } ? headers.find { |h| h.name == 'Date' }.value : ''
        from = headers.any? { |h| h.name == 'From' } ? headers.find { |h| h.name == 'From' }.value : ''
        to = headers.any? { |h| h.name == 'To' } ? headers.find { |h| h.name == 'To' }.value : ''
        subject = headers.any? { |h| h.name == 'Subject' } ? headers.find { |h| h.name == 'Subject' }.value : ''

        body = payload.body.data
        if body.nil? && payload.parts && payload.parts.any?
          body = payload.parts.map { |part| part.body.data }.join
        end

        {id: result.id, date: date, from: from, to: to, subject: subject, body: body}
      end
    end
  end
end

このコードにより、Gmailのメッセージを簡単に取得できます。

lib/awesome/slack.rb

メール解析後、Slackに通知を送る仕組みを構築しました。このプロセスではIncoming Webhook(非推奨)を活用しています:

lib/awesome/slack.rb
require 'net/http'
require 'uri'
require 'json'

URL = ENV['GMAIL_TO_SLACK_WEB_HOOK_URL']
CHANNEL = ENV['GMAIL_TO_SLACK_CHANNEL']

module Awesome
  class Slack
    class << self
      def post(text)
        uri = URI.parse(URL)
        https = Net::HTTP.new(uri.host, uri.port)

        https.use_ssl = true
        req = Net::HTTP::Post.new(uri.request_uri)

        req['Content-Type'] = 'application/json'
        payload = {
          text: text,
          channel: CHANNEL,
          username: 'awesome_bot',
          icon_emoji: ':telephone:',
          link_names: 1
        }.to_json
        req.body = payload
        res = https.request(req)

      end
    end
  end
end

これにより、電話の情報がSlackで即座に通知されるようになりました。開発中はSlackの通知が来るたびに、成功を実感し楽しい気持ちになりました。

lib/main.rb

司令塔となるプログラムです。
1分ごとに実行しています。だって、電話は常にかかってきますし、その用件はいつも急用ですので。
ボットは疲れを知りません。どんどん働いてもらいましょう。

lib/main.rb
require 'awesome/gmail'
require 'awesome/slack'
require 'time'
require 'csv'
require 'set'

LAST_AT = 'last_at'
FROM = ENV['GMAIL_TO_SLACK_FROM']

if File.exist?(LAST_AT)
  last_at = Time.parse(open(LAST_AT, &:read))
else
  last_at = Time.now - 60 * 5
end

ids = Awesome::Gmail.list()
filtered = ids.map { |id| Awesome::Gmail.get(id) }.filter { |mail| !mail[:date].empty? && (Time.parse(mail[:date]) > last_at) }
              .filter{ |mail| mail[:from].include?(FROM) }

names = {}
CSV.foreach('users.csv') do |row|
  names[row[0]] = row[1]
end
NAMES = names

filtered.each do |mail|
  body = mail[:body].force_encoding('UTF-8')
  usernames = Set.new
  NAMES.each do |key, username|
    usernames << username if body.include?(key)
  end

  text = usernames.to_a.join(' ') + "\n" + body
  Awesome::Slack.post(text)
end

last_mail = filtered.sort_by{ |mail| Time.parse(mail[:date]) }.last
if last_mail
  open(LAST_AT, "w") { |f| f << last_mail[:date] }
end

本文に該当者の名前が含まれていたら、メンションを本文の先頭に挿し込むことをしています。

ちなみに、users.csvはこんな感じ(寛至)のCSVファイルです。

users.csv
山内,@yamauchi
やまうち,@yamauchi
ヤマウチ,@yamauchi
闘魂,@inoki
猪木,@inoki
いのき,@inoki
イノキ,@inoki

どんなに優れたことをしたのか

大仰かもしれませんが語らせてもらいます。

このプロジェクトは、以下の点で優れています:

  1. 業務効率化:電話対応のプロセスを大幅に簡略化(担当者がメールを監視していた負担を減らし、他のクリエイティブな仕事に時間を割り当てることができます)
  2. 開発の楽しさ:メール解析やSlack通知というリアルタイム性のある機能の構築
  3. チームへの影響:手作業の負担を減らし、リモートワークの環境をさらに最適化

image.png

担当者は、優雅にバレエをはじめました!

楽しさと達成感

開発を通じて、単なる業務効率化以上の楽しさを感じることができました。メンバーから「通知が便利になった」と感謝の声をもらえたときの喜びはひとしおです。

まとめ

電話対応という一見地味な部分でも、自動化と技術の力で大きな変化をもたらすことができます。この体験を通じて、(おこがましいかもしれませんが)WHI様のミッションである「『はたらく』を楽しくする」精神を形にできたと感じています。

ぜひ皆さんも、身の回りの課題を楽しく解決する方法を見つけてみてください!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?