オルトプラスエンジニアの日常をお伝えします!

Wantedlyとぼくと自動化

この記事は Altplus Advent Calendar 2017 の7日目のエントリです。

※Qiitaに書いた記事を転載しています。

こんにちは、自動化大好き オルトプラスラボの嶋田大輔(id:cimadai) です。またお会いしましたね。

概要

今日は私の会社で(もしかしたら皆さんの会社でも)よくある、ちょっとした手間を一つ減らして効率化していこう、という内容を書きます。

どのような問題なのか

私の会社では、Wantedlyを活用していて、特にブログに注力しています。 Wantedlyのブログは会社の採用に繋がるということで有用なツールの一つとして多くの企業に使われていると思います。

多くの企業に使われているということはそれだけ多数の記事があるため、効果的に認知を高めるにはその中でどうにか目立たせないといけません。

幸いWantedlyにははてなのホッテントリのような、短期間に「いいね」が多数集まった記事を「急上昇」として取り上げる機能があります。

そこで、記事が書かれた後は社員に対して「いいね」をするよう「お願い」があります。1

もちろん、いい方に来て頂きたいので当然「いいね」するわけなのですが、記事を開き下までスクロールし、その間に目にとまる文章があれば読んでしまったりして割と時間泥棒だったりするのです。

ということで、どうにか記事を読むことなくできるだけ手間なく「いいね」できる方法はないかと思い、この記事を書くに至りました。

なにをしたのか

AutedlyというChrome Extensionを作り、記事を開いたら自動的に「いいね」するようにしました。

github.com

解説

Chrome Extensionを作る方法はいろいろあるので別記事に譲りますが、ここではどんなことをしたのかを解説します。

manifest.json

{
    "manifest_version": 2,
    "name": "Autedly",
    "version": "0.2.0.0",
    "description": "Auto like when you open wantedly page.",
    "background": {
        "scripts": ["background.js"],
        "persistent": true
    },
    "content_scripts": [
        {
            "matches": [
                "*://www.wantedly.com/companies/altplus/*"
            ],
            "css": ["jquery.toast.min.css"],
            "js": ["jquery-2.1.4.min.js", "jquery.toast.min.js", "content.js" ],
            "run_at": "document_start"
        }
    ],
    "web_accessible_resources": [
        "background.js"
    ],
    "permissions": ["*://*/*"]
}

↑はExtensionの定義をしています。 簡単なDOM操作のためにjQueryを使い、ユーザーに対するフィードバックにjQuery Toastというライブラリを使いました。

実際の処理は以下になります。 Wantedlyではブログがたくさん並んでいる企業ページと、各記事個別のページがあるので、それぞれで処理を分けています。

コード中のコメントにも書いてあるのですが、Wantedlyの記事読み込みタイミングや「いいね」状態の反映のタイミングが非同期でなかなか掴めなかったので、setTimeoutを利用しています。 ここはかなりイケてない部分なので気が向いたらどうにか直したいところです。

その後chrome.runtime.sendMessageでbackground taskに対して処理を依頼しています。

content.js

function likeIfNeeded($target) {
    var url = location.href;
    var re = /.*\/companies\/altplus\/post_articles\/(\d+)/i;
    var found = url.match(re);
    if (found) {
        var articleId = found[1];
        var likeUrl = 'https://www.wantedly.com/api/internal/posts/' + articleId + '/like';

        var $likeButton = $target.find(".post-like-button");
        if (!$likeButton.is(".liked")) {
            var csrfToken = $("meta[name=csrf-token]").attr("content");
            chrome.runtime.sendMessage({command: "like", csrfToken: csrfToken, likeUrl: likeUrl});
            // TODO: 単純にlikedを付けるだけだとその後もう一度ボタンを押した時に動作がおかしい。
            $likeButton.addClass("liked");
            $.toast({
                text: "「いいね!」しました。",
                position: "top-right"
            });
        } else {
            console.log("Already liked.");
        }
    }
}

$(function () {
    // 企業ページから各記事を開いた時
    $(document).on("click", ".stream-post", function() {
        var $self = $(this);
        // setTimeoutはイケてない
        setTimeout(function () { likeIfNeeded($self); }, 1000);
    });
    // 記事を直接開いた時
    // setTimeoutはイケてない
    setTimeout(function () { likeIfNeeded($("body")); }, 1000);
});

background taskでは、content.jsから受け取った「いいね」をつけるURLに対してXHRでPOSTを行い、「いいね」しています。 すでに「いいね」済みの記事に対して実行すると442が返ってくるのですが、基本的にcontent.js側で判定しておりめったにこないはずですので、Wantedlyサーバーに負荷がかかりすぎるということはないでしょう。 2

こんなことをしなくともcontent.js側でjQueryを使っているんだから$.ajaxでPOSTすればいいじゃないかとも思ったのですがHTTP OnlyのCookieが必要なのでこうなりました。

background.js

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if (request.command == "like") {
        xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            switch ( xhr.readyState ) {
                case 4: // データ受信完了.
                    if( xhr.status == 200 || xhr.status == 422 ) {
                        console.log('LIKED!');
                    }
            }
        };

        xhr.open("POST", request.likeUrl, true);
        xhr.setRequestHeader("x-csrf-token" , request.csrfToken);
        xhr.setRequestHeader("Content-Type" , "application/x-www-form-urlencoded; charset=UTF-8");
        xhr.setRequestHeader("x-requested-with" , "XMLHttpRequest");
        xhr.send("location=post_action_bar");
    }
    return true;
});

動作

f:id:d-shimada:20171206013048g:plain

「いいね」がない状態でページを読み込むと自動で「いいね」が付けられる、という動作風景です。 右上にヒョコっとでるのがjQuery Toastです。 4日目の記事にも書いたのですが、最近はあまりjQueryを多用しなくなったので久しぶりのjQueryという感じでしたがこういうちょっと書捨て、みたいな感じだととても楽ですね!

社内限定公開

さて、このような感じで作ったExtensionですが、気軽に使えないと困ってしまうので社内限定で公開してみました。

オルトプラスの方は「こちら」からインストールできます。

f:id:d-shimada:20171206013201p:plain ※社内限定公開なので検索しても出てこないです。

社内限定であれば無料ででき、かなり気軽に配付できることができたので楽ちんでした。

公開方法は以下の記事がとても参考になりました。

blog.serverworks.co.jp

まとめ

ちょっとの時間のロスが通年でみれば大きなロスになることってありますよね。 こうしたExtensionを駆使することでWantedlyに負荷をかけることもなく、会社は「いいね」が集まって嬉しい、自分は時間が節約できて嬉しいといういいこと尽くめなプチハックでした。

それではまた別の日に!


  1. もちろん重要性はわかってますよ!ぼくは真面目な社員ですからね!

  2. なので大目に見て頂けないですかね。。

識別子と順序付け

この記事はAltplus Advent Calendar 2017の6日目のエントリです。

こんにちは、職業Scalalian、C++愛好家の竹田です。

識別子は整数型で表現すること多いですが、整数型などをそのまま使用してしまうと、”順序付け可能である”や”配列のインデックスで使用できる”などの整数型の性質を前提にコーディングしてしまい危険です。

例えば、”順番通り”に並んでいて、ID番の要素にそのIDと関連付けられているデータを使用してコーディングをしてしまうと、様々なバグの温床になります。

#include <vector>
#include <iostream>

using ID = int;
using Probability = double;
using Image = int;

static std::vector<Probability> probabilityForEachID = {0.1, 0.2, 0.3, 0.4};
static std::vector<Image> imageForEachID = {10, 20, 30, 40};

Probability badProbability(ID id){
    return probabilityForEachID[id];
}

Image badImage(ID id) {
    return imageForEachID[id];
}

void displayImageAndProbability(Probability const& p, Image const& i){
    std::cout << "probability = " << p << " , image = " << i << std::endl;
}

int main(){
    displayImageAndProbability(badProbability(0), badImage(0));
}

このコードでは、probabilityForEachIDimageForEachIDの、i番目の要素がID iに関連するデータでなければいけないという暗黙のルールを守っている間は、想定通りに動作します。しかし、データの追加やコード中の操作でprobabilityForEachIDimageForEachIDの要素の並び順が変わってしまうと、インデックスとIDが対応しているという暗黙のルールが破られてしまい、IDに関連するデータが正確に引けなくなってしまいます。

識別子

問題の原点は、識別子に必要以上の性質を与えてしまったことにあります。Wikipediaで識別子の定義を引くと「識別子(しきべつし、英: identifier)とは、ある実体の集合の中で、特定の元を他の元から曖昧さ無く区別することを可能とする、その実体に関連する属性の集合のこと」(By Wikipedia)とあります。

識別子とは、集まりの中である個を他の個と区別するための情報です。"区別"をC++で表現するには、等価演算子が相応しいので、等価演算子をサポートして識別子を表現するクラスを導入して、どのようにコードが変わるか見てみます。

#include <unordered_map>
#include <iostream>

struct ID{
    int id;
};

bool operator==(ID lhs, ID rhs) {
    return lhs.id == rhs.id;
}

bool operator!=(ID lhs, ID rhs) {
    return !(lhs == rhs);
}

namespace std {
    // IDをunordered_mapで使用するための特殊化。
    template<>
    struct hash<ID>{
        size_t operator()(ID const& x) const
        {
            return hash<int>{}(x.id);
        }
    };
}

using Probability = double;
using Image = int;

static std::unordered_map<ID, Probability> probabilityForEachID 
= {{ID{0}, 0.1}, {ID{2}, 0.3}, {ID{1}, 0.2}, {ID{3},0.4}};
static std::unordered_map<ID, Image> imageForEachID 
= {{ID{1}, 20}, {ID{2}, 30}, {ID{3}, 40}, {ID{0}, 10}};

Probability betterProbability(ID id){
    return probabilityForEachID.at(id);
}

Image betterImage(ID id) {
    return imageForEachID.at(id);
}

void displayImageAndProbability(Probability const& p, Image const& i){
    std::cout << "probability = " << p << " , image = " << i << std::endl;
}

int main(){
    displayImageAndProbability(betterProbability(ID{0}), betterImage(ID{0}));
}

このコードでは、IDの同値関係(operator==)とHashを利用して、unordered_mapのキーにIDを利用しています。probabilityForEachIDimageForEachIDのデータの順番をシャッフルしていますが、意図通りに動作します。

unordered_mapは、Hashマップで実装されています。メモリ使用量やメモリ・アクセス速度が気になる場合は、以下のようにstd::vector<std::pair<ID, Data>>を使用することができます。ただしこの方法では、識別子の一意性などが保証できないため、同じIDに対するデータが複数ある状況になりえるので、データ用のコンテナを操作するところでは、事後条件のチェックやユニットテストを通じて、一意性を保証するようにしましょう。

またBoostが使える状況では、std::vector<std::pair>のような性質でmapのIFを提供しているflat_mapもあるので、flat_mapの使用も検討しましょう。

#include <vector>
#include <iostream>
#include <algorithm>

struct ID{
    int id;
};

bool operator==(ID lhs, ID rhs) {
    return lhs.id == rhs.id;
}

bool operator!=(ID lhs, ID rhs) {
    return !(lhs == rhs);
}

using Probability = double;
using Image = int;

static std::vector<std::pair<ID, Probability>> probabilityForEachID 
= {{ID{0}, 0.1}, {ID{2}, 0.3}, {ID{1}, 0.2}, {ID{3},0.4}};
static std::vector<std::pair<ID, Image>> imageForEachID 
= {{ID{1}, 20}, {ID{2}, 30}, {ID{3}, 40}, {ID{0}, 10}};

Probability betterProbability(ID id){
    auto const found = std::find_if(probabilityForEachID.begin(), probabilityForEachID.end(),
                [&](auto const& p){ return p.first == id; });
    if(found == probabilityForEachID.end()){
        throw std::out_of_range("Not found.");
    }
    return found->second;
}

Image betterImage(ID id) {
    auto const found = std::find_if(imageForEachID.begin(), imageForEachID.end(),
                [&](auto const& p){ return p.first == id; });
    if(found == imageForEachID.end()){
        throw std::out_of_range("Not found.");
    }
    return found->second;
}

void displayImageAndProbability(Probability const& p, Image const& i){
    std::cout << "probability = " << p << " , image = " << i << std::endl;
}

int main(){
    displayImageAndProbability(betterProbability(ID{0}), betterImage(ID{0}));
}

最後に

あるコンセプト(識別子)の表現に必要以上の性質(int型で表現して、順序付けやインデックスとしての使用できる性質)を与えてしまうと、バグの温床になることを見てきました。

自分で型を定義して適切な性質を与えることで、安全で見通し良いコードを書くことができます。

Solidityコードをデバッグする

この記事は Altplus Advent Calendar 2017 の5日目のエントリです。※Qiitaに書いた記事を転載しています。

はじめまして、オルトプラスラボの菊池(id:takuya-kikuchi)です。今年の4月からこちらで楽しく過ごしています。

最近思い出作りにSolidityを書き始めたので、開発環境の使い方のメモです。

Solidityとは

Ethereum上で動作させるスマートコントラクトを記述するための言語です。 jsっぽい?けど静的型付き言語。 Soliity言語で書いたコントラクトは、以下の手順を踏むことでEthereumネットワークにデプロイされ、他者が使用できる状態になります。

  1. コーディング
  2. コンパイル
  3. デプロイ

手順1〜2はテキストエディタとターミナルでもさほど問題ありませんが、3のデプロイ作業がなかなかに面倒です。手作業であたたかみのあるデプロイも人生には大事なのかもしれませんが、そういうところを楽させてくれるのが、Remixです。

Remix(browser-solidity)を使う

Remixは、もともとbrowser-solidtyと呼ばれていたものです。名前の通り、ブラウザ上で動作するSolidityのIDEです。github上からダウンロードすればローカルでも動かせます。

エディタ

なかなか高機能なエディタで、auto compileなんかも走らせることができます。なので、コンパイルエラーなんかがあれば常時アラートしてくれます。

https://camo.qiitausercontent.com/e89409282625c8dd7f13ae46d7fa60ba0d23b51b/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f30303361643836652d326439652d376637612d653635652d3862326437306566373961332e706e67

ContractのDebugもできる

私はなかなか脳内デバッガーの精度が低く、ステップ実行してコードの動きを確認したくなることが多々あります。Remixであればそれも可能です。

コントラクトの中身はあまり本筋と関係ありませんが、今回はこのようなコントラクトを書いてみました。 Postで気になるあの人に向けたメッセージをブロックチェーン上に刻むことができ、Readで自分宛のメッセージを読むことができます。

pragma solidity ^0.4.0;

contract SampleContract {
    address public owner;
    mapping(address => string) public messages;

    // コンストラクタ
    function SampleContract() public {
        owner = msg.sender;
    }

    // 任意のアドレスに向けたメッセージを記す。
    function Post(address dest, string message) public {
        messages[dest] = message;
    }
    
    // メッセージを読む。何も存在しなければabort
    function Read() public constant returns (string message) {
        bytes memory strMem = bytes(messages[msg.sender]);
        require(strMem.length > 0);
        return messages[msg.sender];
    }
}

デバッグ手順

1. EnvironmentをJavaScript VMに変更

画面右側の[Run]タブにて、[Environment]を[JavaScript VM]とします。 他のEnvironmentを選択しているとデバッグできませんのでご注意。

https://camo.qiitausercontent.com/286e7fc7b5ab0f7eb2de76d51c0b9303b1c44d27/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f63646665383364352d646131622d306566312d653032652d6163613465633066653339372e706e67

2. コントラクトのデプロイ

おなじく[Run]タブ内でデプロイしたいコントラクトを選択し、[Create]ボタンを押せばデプロイ完了です。

https://camo.qiitausercontent.com/f38d1dccccd6eb9746fdfca629ec17acd4a5ab6a/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f66383361353862372d393639632d613565322d343330372d6236383061656231613431312e706e67

3. コントラクトの実行

Remix上でデプロイしたコントラクトが一覧表示されていますので、こちらから任意のfunctionを選んで実行します。

https://camo.qiitausercontent.com/e820a51831490d0d87b11f2ad363bf0faf873a2d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f31633331303834622d383437332d396533612d303163322d3633393735383463393832332e706e67

4. デバッグ

エディタ下部のコンソールに次のようにコントラクトの実行結果が表示されていますので、右側の[Debug]をクリックします。

https://camo.qiitausercontent.com/30717b7e2af96f41c0b5758095906ea712b2a36d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f39636665646466302d383131662d353238622d313537652d3430363039636336653865652e706e67

すると、右側のタブは自動的に[Debugger]に切り替わり、デバッグ対象のfunctionのエントリポイントで自動的に一時停止します。

https://camo.qiitausercontent.com/d8c75eb667fbb122daf07a8aa3c61c836403bdd7/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f32323934326661642d663431612d653138362d373463632d6334363433383439363233332e706e67

あとは、表示されているボタンを使ってステップイン/ステップオーバー/ブレークポイントまで飛ばす・・・といった、 一般的なデバッガで可能な操作を行うことができます。

https://camo.qiitausercontent.com/26c95890f3ccb127549d0f3a0a011a5c7debf63c/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f64396564376266312d303061352d343734632d383734362d3564336266646230363665342e706e67

ローカル変数の中身も表示可能。 Watchみたいなのはない・・・のかな?

おわりに

以上、Solidityコードをババっと書いてちゃちゃっとデバッグする方法でした。PCのメモリ上にデプロイされているだけなのであまり面白みはありませんが、Solidityでどんなことができるのか、というのを知るには良いのではないかと思います。

きちんとコントラクトのデバッグができたら、Ethereumネットワークにデプロイして遊んでみましょう。(まずはプライベートネットから!)

参考

Ethereum入門

Remix