scala irc botでのbotの作り方

はじめに

この記事は、私がGitHubで公開しているscala-irc-botというプログラムの使い方とボットの書き方のチュートリアルです。 ScalaIRCボットを書きたいという需要が非常に少ない目的のために書きました。

目次

  • scala irc botの紹介
  • 使い方(デプロイ方法)
  • プラグインの作成方法
  • 設定なし
  • 設定あり

scala irc botの紹介

scala irc botってなんですか

https://github.com/scala-irc-bot

私がScalairc botを書けるように作成しているプログラムです。 実際にIRCに接続するのにはPircBotというJava製のIRCクライアントを使用しています。

現在の最新バージョンは0.2.0-SNAPSHOTです。バージョンは大きく設計が変わるときに更新されます。プラグインに影響のない修正ではバージョンを上げません(mavenリポジトリの更新が面倒だからです)。

現在の使い方は sbt run によって起動させるのを推奨しています。 性質的に常時起動しておくプログラムなのでデーモンのように使うほうがいいのかなあ。。。

irc botってなんですか

IRCでの誰かの発言などのイベントなどに反応して発言したり作業したり愛したり恋したりしてくれるプログラムです。 ときには喧嘩して殴りあったりするけれど、でもそんなお前のこと、そんなに嫌いじゃないぜ?っていう人格を持っています。

ircってなんですか

RFC1459

デプロイ方法

次のものを用意してください。

git clone

本体のコードはgithubで管理しているので、cloneで持ってきます。

git clone https://github.com/scala-irc-bot/scala-irc-bot.git

設定ファイルの記述

cloneしたディレクトリに移動し、configディレクトリの中に、Config.scalaというファイルを作成します。

Config.scala

import net.mtgto.irc.Config
import net.mtgto.irc.config.BotConfig
new Config {
  val hostname = "irc.example.com" // IRCサーバの名前
  val port = 6667 // IRCサーバのポート
  val password = None // 必要ならSome("password")のように記述する
  val encoding = "utf-8" // 使用するエンコーディング
  val messageDelay = 2000 // メッセージ間の遅延(ミリ秒)
  val timerDelay = 60000 // タイマーイベント間隔(ミリ秒)
  val nickname = "scala-irc-bot" // ボットのニックネーム
  val username = "scala-irc-bot" // ボットのログイン名
  val realname = "mtgto@example.com" // ボットのリアルネーム
  // channels
  val channels = Array("#mtgto", "#test") // JOINさせたいチャンネル
  // bots
  val bots = Array[(String, Option[BotConfig])](
    // ここにプラグインの設定を記述する
  )
}

起動する

コンソールからsbtで起動します。

sbt run

(初回のみ)コンパイルが実行され、設定ファイルやネットワークなどに問題が起きなければ、しばらく経つとIRCサーバにscala irc botが接続してきます。

ですが今のままではボットプラグインを何も入れてないので、何もしてくれません。 コンソール画面にexitと入力して終了させましょう。

プラグインの作成方法 (シンプル)

ここからはまずはシンプルなプラグインを作成していきます。 ここでいうシンプルなプラグインというのは、

  1. 設定ファイルでボットプラグインごとのオプションを設定できない

っていう前提のものです。下の方にオプションを使う方法も載せてあります。

Scala + sbtで作る

今回作る環境は、

  • sbt (バージョンはユーザの環境に依る)
  • Scala 2.10.0
  • PircBot 1.8.0

です。

最小構成では build.sbt Echobot.scala の2ファイルで構成されます。

echobot/build.sbt

// -*- scala -*-
name := "EchoBot"

organization := "net.mtgto"

version := "0.1.0-SNAPSHOT"

scalaVersion := "2.10.0"

resolvers += "mtgto repos" at "http://scala-irc-bot.github.com/scala-irc-bot/maven/"

libraryDependencies := Seq(
  "net.mtgto" %% "scala-irc-bot" % "0.2.0-SNAPSHOT" % "provided"
)

scalacOptions ++= Seq("-deprecation", "-unchecked", "-encoding", "UTF8")

echobot/Echobot.scala

package net.mtgto

import net.mtgto.irc.{Bot, Client}
import net.mtgto.irc.event._

/**
 * 受け取ったメッセージをNOTICEで送り返すボット
 */
class Echobot extends Bot {
  /**
   * 誰かがPRIVMSGを書いた時に通知される
   */
  override def onMessage(client: Client, message: Message) = {
    // メッセージが送信されたチャンネルに同じメッセージをNOTICEで送り返す
    client.sendNotice(message.channel, message.text)
  }
}

ファイルを置いたら、

sbt package

と実行すると、 target/scala-2.10.0/echobot_2.10.0-0.1.0-SNAPSHOT.jar が作成されます。

このjarファイルがコンパイルされたプラグインです。

プラグインをインストール

まず、作成した echobot_2.10.0-0.1.0-SNAPSHOT.jarscala-irc-bot/botsフォルダの中に置いてください。 次に config/Config.scala にボットプラグインの設定を記述します。

  val bots = Array[(String, Option[BotConfig])](
    // ここにプラグインの設定を記述する
    ("net.mtgto.Echobot", None)
  )

sbt run すると画像のようにこちらの発言をNOTICEで繰り返してくれるようになります。

f:id:mtgto:20121216223101p:plain

botテンプレートを使う (g8)

giter8用のテンプレートを作ってあるので、これを使うこともできます。

g8 scala-irc-bot/bot
version [0.1.0-SNAPSHOT]:
organization [com.example]: net.mtgto
name [IRC Bot Project]: EchoBot

こちらは先ほどの手順で作成した時に加えて、テスト用にSpecs2、ビルド用にsbt-assemblyの設定をしてあります。 慣れてきた場合や他ライブラリに依存するプラグインを作るときにはこちらをおすすめします。

プラグインの作成方法 (設定可能)

設定ファイルでボットプラグインごとのオプションを設定することができます。

設定ファイルに書いたオプションを読み込むには、net.mtgto.irc.config.BotConfigを実装したクラスを作り、そのオブジェクトをプラグインのメインクラスのコンストラクタにもたせてあげればよいです。

実例を示してみます。今回は設定ファイルに書いたキーワードが書かれた時だけ反応するようにしてみましょう。 KeywordEchobotを作成します。

KeywordEchobot.scala

package net.mtgto

import net.mtgto.irc.{Bot, Client}
import net.mtgto.irc.event._
import net.mtgto.irc.config.BotConfig

/**
 * BotConfigを実装したクラスを定義する必要がある。
 */
case class KeywordEchobotConfig(
  val keywords: Seq[String]
) extends BotConfig

/**
 * 特定のメッセージをNOTICEで送り返すボット
 *
 * コンストラクタでEchobotConfigを受け取る
 */
class KeywordEchobot(val config: KeywordEchobotConfig) extends Bot {
  /**
   * 誰かがPRIVMSGを書いた時に通知される
   */
  override def onMessage(client: Client, message: Message) = {
    if (config.keywords.contains(message.text)) {
      // メッセージが送信されたチャンネルに同じメッセージをNOTICEで送り返す
      client.sendNotice(message.channel, message.text)
    }
  }
}

これを先ほどと同じように sbt package でビルドして、target/scala-2.9.2/keywordechobot_2.10.0-0.1.0-SNAPSHOT.jarscala-irc-bot/bots にコピーします。

あとは config/Config.scala に設定を書いてあげます。

val bots = Array[(String, Option[BotConfig])](
  ("net.mtgto.Echobot", Some(net.mtgto.KeywordEchobotConfig(Seq("こんばんは", "おはようございます"))))
)

Seqに好きな言葉を列挙してあげます。

あとは sbt run すると、設定ファイルにある言葉が書かれた時にだけ応答してくれるようになります。

f:id:mtgto:20121216223145p:plain

まとめ

簡単でしたが、scala irc botの導入方法とプラグインの作成方法を書きました。 多分ここまで読んでいる方はいらっしゃらないと思うのですが、さらにプラグインを作ってみたいという奇特な方がいらっしゃるようでしたらこの上なく幸せです。 まだサンプルプログラムが少ないのですが、私も幾つかプラグインを作っているのでよろしければそれを見てみてください。

https://github.com/scala-irc-bot

CocoaPodsでGHUnit + OCMockなiOS開発環境を構築する

なにこれ

新しいプロジェクト作るたびにいつも構築手順を忘れてるのでメモ。 おまけでコンソールからのテスト実行(Jenkinsとの統合が目的)のやり方も書いた。

対象者

  • Objective-CによるiOS開発者
  • CocoaPodsを知っている・使いたい
  • GHUnitを知っている・使いたい
  • (JenkinsでCIしたい)←必須じゃない

検証環境

2012年12月9日時点で最新のもの。

手順

1. Xcodeでプロジェクト作成

Unit TestはGHUnitを使うので不要。 このドキュメントでは MyProject とする。

2. 新しいターゲット作成

プロジェクト設定を開いて、テスト実行用のターゲットを作成する。 ターゲットの種類はiOSのEmpty Application、名前は Tests にしておく。 名前は変更してもいいけど、以下の説明で Tests と出てきたら自分で読み替えること。

自動生成されるファイルのうち、次のものだけ残して全部消す(ファイルごと)

  • Tests-Info.plist
  • InfoPlist.strings
  • main.m
  • Tests-Prefix.pch

3. Podfile作成

MyProject.xcodeproj と同じ所に Podfile を作成する。

platform :ios, '5.0'

pod 'AFNetworking',  '~> 1.0.1'
pod 'CocoaLumberjack', '~> 1.6'

target :Tests, :exclusive => true do
  pod 'GHUnitIOS', '~> 0.5.5'
  pod 'OCMock', '~> 2.0.1'
end

一行目でiOS 5.0以上を指定しているが、実際には作成するプロジェクトに合わせて指定する。 AFNetworkingとCocoaLumberjackは実際に作ったPodfileから取ってきただけのただのサンプル。実際に必要な物を指定して欲しい。 このとき target :Tests を2.で作ったターゲット名にするのを忘れないこと。

終わったらコンソールからworkspaceを作成してもらう。

pod install

4. xcodeproj閉じてxcworkspace開く

両方同じxcodeprojを参照するのでわけわからないことになる前に xcodeproj を閉じる。

5. ターゲット TestsBuild Settings いじる

Other Linker Flags-ObjC -all_load を追加する。 俺の環境では -ObjC はすでに追加されていた。

6. main.m いじる

//
//  main.m
//  Tests
//
//  Created by <ユーザ名> on 12/8/12.
//  Copyright (c) 2012 <組織名>. All rights reserved.
//

#import <UIKit/UIKit.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate");
    }
}

ここまでやってiPhone Simulatorで起動するとGHUnitのテスト実行画面が表示される。

7. ユニットテスト作ってみる

GHUnit

ここまでうまくいってるか確認するために一個テスト作ってみる。 Testsに一個Objective-Cのクラスを作る。 GHUnitのガイド にあるコードのままだとコンパイルが通らないのでちょいと手直し。

MyTest.h

#import <GHUnitIOS/GHUnit.h>

@interface MyTest : GHTestCase

@end

MyTest.m

#import "MyTest.h"

@implementation MyTest

- (void)testStrings {
    NSString *string1 = @"a string";
    GHTestLog(@"I can log to the GHUnit test console: %@", string1);

    // Assert string1 is not NULL, with no custom error description
    GHAssertNotNil(string1, nil);

    // Assert equal objects, add custom error description
    NSString *string2 = @"a string";
    GHAssertEqualObjects(string1, string2, @"A custom error message. string1 should be equal to: %@.", string2);
}

@end

修正前はGHAssertNotNULLを使ってたのでGHAssertNotNilに変えた。

もっかいiPhone Simulator起動してみてテストできることを確認する。

OCMockも確認する

MyMockTest.h

#import <GHUnitIOS/GHUnit.h>
#import <OCMock/OCMock.h>

@interface MyTest : GHTestCase

@end

MyMockTest.m

#import "MyMockTest.h"

@implementation MyMockTest

- (void)testMock {
    id mock = [OCMockObject mockForClass:[NSString class]];
    [[[mock stub] andReturnValue:OCMOCK_VALUE((NSUInteger){100})] length];

    GHAssertEquals(100U, [mock length], @"ocmock works");
}

@end

iPhone Simulator起動してみてテストできることを確認する。

8. コンソールからテスト実行してみる

Jenkins用なんで興味ない人はスルー。 基本は GHUnitのガイド 通り。

i. シェルスクリプトを取ってくる

xcodeproj, xcworkspaceがあるところで以下を実行。

wget https://raw.github.com/gabriel/gh-unit/master/Scripts/RunTests.sh
wget https://raw.github.com/gabriel/gh-unit/master/Scripts/RunIPhoneSecurityd.sh

ii. テスト実行時にスクリプトを実行するようにする

プロジェクト設定内のTestsBuild Phasesを開き、Add Build Phase -> Add Run Scriptを選択。 入力欄にsh RunTests.shと入れる。

iii. Makefile

GHUnitのドキュメントにはworkspaceのときの例がないのでそのままだと動かない。 適当に作ってみる。

Makefile

clean:
	-rm -rf build/*

test:
	GHUNIT_CLI=1 xcodebuild ONLY_ACTIVE_ARCH=NO -workspace <xcworkspaceファイルへのパス> -scheme Tests -configuration Debug -sdk iphonesimulator build

ワークスペースへのパスに書き換えるのを忘れないこと。 連続空白に見えるところはタブ文字にすること。

iv. (Base SDKが6.0のときのみ) main.mの修正

2012年12月9日現在、iOS SDKが6.0だと、コンソールから動かせない問題がある(GHUnitのIssue at GitHub)。 暫定措置だが一番下のgfxさんのコメント通りにmain.mを書き換えるとコンソールから動かせるようになる。

v. テストを実行する

make test

echo $?して0だったら、おめでとう。すべてのテストをパスしたぞ!

9. Jenkinsからテスト実行

まず前章で作ったRunTests.sh, RunIPhoneSecurityd.sh, Makefileをgit commitしておく。 公開前提のプロジェクトだと、git cloneしたユーザにpod installを実行させるのでxcworkspaceをソースコードリポジトリ管理しないと思う。

なのでその流儀に則り、Jenkinsでもpod installすることにする。

i. ジョブを作る

名前はMyProjectにしてみた。フリースタイル・プロジェクトを選択。

ii. 設定

ソースコード管理システムでソースの位置を指定する。 俺はローカルホストにJenkinsを立ててるので、GitのRepository URLがfile:///からはじまる。

ビルドに シェルの実行 を追加する。

WORKSPACE_FILE=MyProject.xcworkspace
if [ ! -d "$WORKSPACE_FILE" ]; then
  pod install
  open -R "$WORKSPACE_FILE"
  echo "We need to init the workspace file. Please try re-run test after you open workspace file."
  exit 1
fi
make clean && WRITE_JUNIT_XML=YES JUNIT_XML_DIR=tmp/test-results make test

最初のビルドではxcworkspaceの初期化が終わっていないため、 xcodebuild: error: The workspace 'MyProject' does not contain a scheme named 'Tests'. と失敗してしまう。

少し悩んだが、ユーザにXcodeでxcworkspaceを開かせることにした。 一度Xcodeで開いてしまえばもうこの作業はやらなくてOKだ。

open -R <ファイルパス> はFinderでファイルを表示するコマンド。

ビルド後の処理 から JUnitテスト結果の集計 を選び、tmp/test-results/*.xmlと指定する。

ビルドしてみると一回目は失敗すると思う。 xcworkspaceがFinderで表示されていると思うので、ダブルクリックして初期化してから再度ビルドすればテストが実行されるはずだ。

このへんはバッドノウハウなんで、もっといい方法あるって人は教えて欲しい。

サンプル

https://github.com/mtgto/CocoaPodsSample

ここまでの説明があってるかの確認のために作ってみた。

参考

全部英語のページ。