├── resources └── jubot.png ├── .gitignore ├── src └── jubot │ ├── redef.clj │ ├── require.clj │ ├── scheduler │ └── keep_awake.clj │ ├── test.clj │ ├── adapter │ ├── util.clj │ ├── repl.clj │ └── slack.clj │ ├── system.clj │ ├── brain │ ├── memory.clj │ └── redis.clj │ ├── adapter.clj │ ├── brain.clj │ ├── core.clj │ ├── scheduler.clj │ └── handler.clj ├── dev ├── user.clj └── dev.clj ├── test └── jubot │ ├── test_test.clj │ ├── scheduler │ └── keep_awake_test.clj │ ├── adapter │ ├── util_test.clj │ ├── repl_test.clj │ └── slack_test.clj │ ├── brain │ ├── memory_test.clj │ └── redis_test.clj │ ├── adapter_test.clj │ ├── system_test.clj │ ├── brain_test.clj │ ├── core_test.clj │ ├── schedule_test.clj │ └── handler_test.clj ├── project.clj ├── doc └── api │ ├── js │ └── page_effects.js │ ├── jubot.require.html │ ├── jubot.adapter.util.html │ ├── jubot.test.html │ ├── jubot.redef.html │ ├── jubot.brain.memory.html │ ├── jubot.adapter.html │ ├── jubot.scheduler.keep-awake.html │ ├── jubot.system.html │ ├── jubot.brain.redis.html │ ├── jubot.brain.html │ ├── jubot.core.html │ ├── jubot.adapter.repl.html │ ├── jubot.handler.html │ ├── css │ └── default.css │ ├── jubot.scheduler.html │ ├── jubot.adapter.slack.html │ └── index.html ├── README.md └── LICENSE /resources/jubot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liquidz/jubot/HEAD/resources/jubot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .env 11 | .beco* 12 | .DS_Store 13 | *.shd 14 | -------------------------------------------------------------------------------- /src/jubot/redef.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.redef 2 | "Redefining function fot stubbing.") 3 | 4 | (def ^{:doc "clojure.core/println"} 5 | println* println) 6 | 7 | (def ^{:doc "System/getenv"} 8 | getenv* #(System/getenv %)) 9 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.tools.namespace.repl :refer [refresh]] 4 | [clojure.repl :refer :all] 5 | [jubot.system :as sys] 6 | [dev :refer [-main]])) 7 | 8 | (def stop sys/stop) 9 | 10 | (defn start [] 11 | (-main "-a" "repl" "-b" "memory")) 12 | 13 | (defn restart [] 14 | (sys/stop) 15 | (refresh :after 'user/start)) 16 | 17 | (defn in [s] 18 | ((-> sys/system :adapter :in) s)) 19 | -------------------------------------------------------------------------------- /test/jubot/test_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.test-test 2 | (:require 3 | [jubot.test :refer :all] 4 | [jubot.brain :as jb] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest test-with-test-brain 8 | (with-test-brain 9 | (are [x y] (= x y) 10 | {"a" "b"} (jb/set "a" "b") 11 | {"a" "b", "b" "c"} (jb/set "b" "c") 12 | "b" (jb/get "a") 13 | "c" (jb/get "b"))) 14 | (with-test-brain 15 | (is (nil? (jb/get "a"))))) 16 | -------------------------------------------------------------------------------- /test/jubot/scheduler/keep_awake_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.scheduler.keep-awake-test 2 | (:require 3 | [jubot.scheduler.keep-awake :refer :all] 4 | [jubot.redef :refer :all] 5 | [clojure.test :refer :all] 6 | [conjure.core :refer [stubbing]] 7 | [clj-http.lite.client :as client])) 8 | 9 | (deftest test-keep-awake-schedule 10 | (stubbing [client/get identity] 11 | (is (nil? (keep-awake-schedule)))) 12 | 13 | (stubbing [getenv* "url" 14 | client/get identity] 15 | (is (= "url" (keep-awake-schedule))))) 16 | -------------------------------------------------------------------------------- /src/jubot/require.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.require 2 | "Jubot utilities for requiring namespaces." 3 | (:require 4 | [clojure.java.io :as io] 5 | [clojure.tools.namespace.find :as find])) 6 | 7 | (defn regexp-require 8 | "Require namespaces which matches specified regular expression. 9 | 10 | Params 11 | ns-regexp - A regular expression which specifies namespaces. 12 | " 13 | [ns-regexp] 14 | (doseq [sym (->> (find/find-namespaces-in-dir (io/file ".")) 15 | (filter #(re-find ns-regexp (str %))) 16 | (remove #(re-find #"-test$" (str %))))] 17 | (require sym))) 18 | -------------------------------------------------------------------------------- /src/jubot/scheduler/keep_awake.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.scheduler.keep-awake 2 | "Built-in schedule to keep awake jubot system on PaaS. (such as Heroku)" 3 | (:require 4 | [jubot.redef :refer :all] 5 | [jubot.scheduler :as js] 6 | [clj-http.lite.client :as client])) 7 | 8 | (def ^{:const true 9 | :doc "Interval to awake system."} 10 | AWAKE_INTERVAL "/30 * * * * * *") 11 | 12 | (def ^{:const true 13 | :doc "Key of environment variables which handle url to awake system."} 14 | AWAKE_URL_KEY "AWAKE_URL") 15 | 16 | (def ^{:doc "Schedule to keep awake system."} 17 | keep-awake-schedule 18 | (js/schedule 19 | AWAKE_INTERVAL #(some-> AWAKE_URL_KEY getenv* client/get))) 20 | -------------------------------------------------------------------------------- /src/jubot/test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.test 2 | "Jubot testing utilities" 3 | (:require 4 | [com.stuartsierra.component :as component] 5 | [jubot.system :refer [system]] 6 | [jubot.brain.memory :refer :all])) 7 | 8 | (defmacro with-test-brain 9 | "Wrap body with a test memory brain. 10 | 11 | Example 12 | (with-test-brain 13 | (jubot.brain/set \"foo\" \"bar\") 14 | (println (jubot.brain/get \"foo\"))) ; => \"bar\" 15 | " 16 | [& body] 17 | `(let [before# system 18 | brain# (map->MemoryBrain {})] 19 | (alter-var-root #'jubot.system/system (constantly {:brain (component/start brain#)})) 20 | (do ~@body) 21 | (alter-var-root #'jubot.system/system (constantly before#)))) 22 | -------------------------------------------------------------------------------- /src/jubot/adapter/util.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.util 2 | "Utilities for jubot adapter." 3 | (:require [clojure.string :as str])) 4 | 5 | (defn parse-text 6 | "Parse passed text and return a map. 7 | 8 | Params 9 | botname - Bot's name. 10 | text - Message from user. 11 | Return 12 | Parsed map. 13 | " 14 | [botname text] 15 | (let [[head tail] (some-> text (str/split #"\s+" 2)) 16 | fuzzy? (nil? (->> head str (re-find #"[@:]"))) 17 | name (some->> head str (re-find #"^@?(.+?):?$") second) 18 | forme? (= botname name)] 19 | (merge {:message-for-me? forme?} 20 | (if (and name (not (and (not forme?) fuzzy?))) 21 | {:to name :text tail} 22 | {:text text})))) 23 | -------------------------------------------------------------------------------- /src/jubot/system.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.system 2 | "Jubot system manager." 3 | (:require 4 | [com.stuartsierra.component :as component])) 5 | 6 | (def ^{:doc "Jubot system var."} 7 | system {}) 8 | 9 | (defn init 10 | "Initialize stuartsierra.component system. 11 | 12 | Params 13 | create-system-fn - A function to create jubot system. 14 | " 15 | [create-system-fn] 16 | (alter-var-root 17 | #'system 18 | (constantly (create-system-fn)))) 19 | 20 | (defn start 21 | "Start stuartsierra.component system. 22 | " 23 | [] 24 | (alter-var-root #'system component/start)) 25 | 26 | (defn stop 27 | "Stop stuartsierra.component system. 28 | " 29 | [] 30 | (alter-var-root 31 | #'system 32 | (fn [s] (when s (component/stop s))))) 33 | -------------------------------------------------------------------------------- /test/jubot/adapter/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.util-test 2 | (:require 3 | [jubot.adapter.util :refer :all] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest test-parse-text 7 | (are [x y] (= x (parse-text "foo" y)) 8 | {:to "foo" :text "bar" :message-for-me? true} "foo bar" 9 | {:to "foo" :text "bar" :message-for-me? true} "foo: bar" 10 | {:to "foo" :text "bar" :message-for-me? true} "@foo bar" 11 | {:to "foo" :text "bar" :message-for-me? true} "@foo: bar" 12 | 13 | {:text "baz bar" :message-for-me? false} "baz bar" 14 | {:to "baz" :text "bar" :message-for-me? false} "baz: bar" 15 | {:to "baz" :text "bar" :message-for-me? false} "@baz bar" 16 | {:to "baz" :text "bar" :message-for-me? false} "@baz: bar")) 17 | -------------------------------------------------------------------------------- /src/jubot/brain/memory.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain.memory 2 | "Jubot brain for memory." 3 | (:require 4 | [com.stuartsierra.component :as component])) 5 | 6 | (defn- set-to-memory 7 | [{mem :mem} k v] 8 | (swap! mem assoc k v)) 9 | 10 | (defn- get-from-memory 11 | [{mem :mem} k] 12 | (get @mem k)) 13 | 14 | (defn- keys-from-memory 15 | [{mem :mem}] 16 | (keys @mem)) 17 | 18 | (defrecord MemoryBrain [mem] 19 | component/Lifecycle 20 | (start [this] 21 | (if mem 22 | this 23 | (do (println ";; start memory brain") 24 | (let [this (assoc this :mem (atom {}))] 25 | (assoc this 26 | :set (partial set-to-memory this) 27 | :get (partial get-from-memory this) 28 | :keys (partial keys-from-memory this)))))) 29 | (stop [this] 30 | (if-not mem 31 | this 32 | (do (println ";; stop memory brain") 33 | (assoc this :mem nil :set nil :get nil))))) 34 | -------------------------------------------------------------------------------- /src/jubot/adapter.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter 2 | "Jubot adapter manager." 3 | (:require 4 | [jubot.system :refer [system]] 5 | [jubot.adapter.repl :refer [map->ReplAdapter]] 6 | [jubot.adapter.slack :refer [map->SlackAdapter]])) 7 | 8 | (defn create-adapter 9 | "Create the specified adapter. 10 | 11 | Params 12 | config-option 13 | :adapter - A adapter name. 14 | Return 15 | Adapter component. 16 | " 17 | [{:keys [adapter] :as config-option}] 18 | (case adapter 19 | "slack" (map->SlackAdapter config-option) 20 | (map->ReplAdapter config-option))) 21 | 22 | (defn out 23 | "Call output function in system adapter. 24 | Before using this function, jubot.system should be started. 25 | 26 | Params 27 | s - A message string. 28 | option - Output option map. 29 | See REPL or Slack adapter documents. 30 | Return 31 | nil. 32 | " 33 | [s & option] 34 | (some-> system :adapter :out (as-> f (apply f s option)))) 35 | -------------------------------------------------------------------------------- /test/jubot/brain/memory_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain.memory-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.brain.memory :refer :all] 5 | [clojure.test :refer :all])) 6 | 7 | (def ^:private brain (map->MemoryBrain {})) 8 | 9 | (deftest test-MemoryBrain 10 | (testing "start brain" 11 | (let [brain (component/start brain)] 12 | (are [k] (contains? brain k) 13 | :mem 14 | :set 15 | :get) 16 | (is (= brain (component/start brain))))) 17 | 18 | (testing "stop brain" 19 | (let [brain (-> brain component/start component/stop)] 20 | (are [k] (nil? (get brain k)) 21 | :mem 22 | :set 23 | :get) 24 | (is (= brain (component/stop brain))))) 25 | 26 | (testing "set/get/keys" 27 | (let [brain (component/start brain)] 28 | (is (= {"foo" "bar"} ((:set brain) "foo" "bar"))) 29 | (is (= "bar" ((:get brain) "foo"))) 30 | (is (= ["foo"] ((:keys brain))))))) 31 | -------------------------------------------------------------------------------- /dev/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require 3 | [jubot.core :refer [jubot]] 4 | [jubot.handler :as jh] 5 | [jubot.adapter :as ja] 6 | [jubot.brain :as jb] 7 | [jubot.scheduler :as js])) 8 | 9 | (defn ping-handler 10 | "jubot ping - reply with 'pong'" 11 | [{:keys [text message-for-me?]}] 12 | (if (and message-for-me? (= "ping" text)) "pong")) 13 | 14 | (defn brain-handler 15 | "jubot set - store data to brain 16 | jubot get - restore data from brain" 17 | [{:keys [message-for-me?] :as arg}] 18 | (when message-for-me? 19 | (jh/regexp arg 20 | #"^set (.+?) (.+?)$" (fn [{[_ k v] :match}] (jb/set k v)) 21 | #"^get (.+?)$" (fn [{[_ k] :match}] (jb/get k))))) 22 | 23 | (defn hear-handler 24 | [{:keys [text user]}] 25 | (when (re-find #"hello" text) 26 | (ja/out (str "hello " user) :as "world"))) 27 | 28 | ;(def dev-schedule 29 | ; (js/schedules 30 | ; "/5 * * * * * *" #(str "Hey!"))) 31 | 32 | (def -main (jubot :ns-regexp #"^dev")) 33 | -------------------------------------------------------------------------------- /test/jubot/adapter_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter-test 2 | (:require 3 | [jubot.adapter :refer :all] 4 | [jubot.system :refer [system]] 5 | [jubot.system-test :refer [with-mocked-system]] 6 | [clojure.test :refer :all] 7 | [conjure.core :refer [stubbing]])) 8 | 9 | (def ^:private botname "test") 10 | 11 | 12 | (deftest test-create-adapter 13 | (testing "slack adapter" 14 | (let [adapter (create-adapter {:name botname :adapter "slack"})] 15 | (are [x y] (= x y) 16 | botname (:name adapter) 17 | jubot.adapter.slack.SlackAdapter (type adapter)))) 18 | 19 | (testing "default(repl) adapter" 20 | (let [adapter (create-adapter {:name botname})] 21 | (are [x y] (= x y) 22 | botname (:name adapter) 23 | jubot.adapter.repl.ReplAdapter (type adapter))))) 24 | 25 | (deftest test-out 26 | (is (nil? (out "foo"))) 27 | (with-mocked-system 28 | {:adapter {:out #(str "[" % "]")}} 29 | (fn [] 30 | (is (= "[foo]" (out "foo")))))) 31 | -------------------------------------------------------------------------------- /test/jubot/system_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.system-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.system :refer :all] 5 | [clojure.test :refer :all])) 6 | 7 | (defn with-mocked-system 8 | [system-value f] 9 | (let [before system] 10 | (alter-var-root #'jubot.system/system (constantly system-value)) 11 | (f) 12 | (alter-var-root #'jubot.system/system (constantly before)))) 13 | 14 | (defrecord TestComponent [foo] 15 | component/Lifecycle 16 | (start [this] (assoc this :foo "bar")) 17 | (stop [this] (assoc this :foo nil))) 18 | 19 | (defn- create-test-system 20 | [] 21 | (component/system-map 22 | :test (map->TestComponent {}))) 23 | 24 | (deftest test-init 25 | (init (fn [] "foo")) 26 | (is (= "foo" system))) 27 | 28 | (deftest test-start 29 | (do (init create-test-system) 30 | (start)) 31 | (is (= "bar" (-> system :test :foo)))) 32 | 33 | (deftest test-stop 34 | (do (init create-test-system) 35 | (start) 36 | (stop)) 37 | (is (nil? (-> system :test :foo)))) 38 | -------------------------------------------------------------------------------- /test/jubot/brain_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain-test 2 | (:require 3 | [jubot.brain :as brain] 4 | [jubot.system :refer :all] 5 | [jubot.system-test :refer [with-mocked-system]] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest test-create-brain 9 | (testing "redis brain" 10 | (is (= jubot.brain.redis.RedisBrain 11 | (type (brain/create-brain {:brain "redis"}))))) 12 | (testing "default(memory) brain" 13 | (is (= jubot.brain.memory.MemoryBrain 14 | (type (brain/create-brain {})))))) 15 | 16 | (deftest test-set 17 | (testing "not started brain" 18 | (is (nil? (brain/set "foo" "bar")))) 19 | 20 | (testing "dummy brain" 21 | (with-mocked-system {:brain {:set list}} 22 | (fn [] 23 | (is (= ["foo" "bar"] (brain/set "foo" "bar"))))))) 24 | 25 | (deftest test-get 26 | (testing "not started brain" 27 | (is (nil? (brain/get "foo")))) 28 | 29 | (testing "dummy brain" 30 | (with-mocked-system {:brain {:get list}} 31 | (fn [] 32 | (is (= ["foo"] (brain/get "foo"))))))) 33 | -------------------------------------------------------------------------------- /src/jubot/brain.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain 2 | "Jubot brain manager." 3 | (:require 4 | [jubot.system :refer [system]] 5 | [jubot.brain.memory :refer [map->MemoryBrain]] 6 | [jubot.brain.redis :refer [map->RedisBrain]]) 7 | (:refer-clojure :exclude [set get keys])) 8 | 9 | (defn create-brain 10 | "Create the specified brain. 11 | 12 | Params 13 | config-option 14 | :brain - A brain's name. 15 | Return 16 | Brain component. 17 | " 18 | [{:keys [brain] :as config-option}] 19 | (case brain 20 | "redis" (map->RedisBrain config-option) 21 | (map->MemoryBrain config-option))) 22 | 23 | (defn set 24 | "Store data to system brain. 25 | Before using this function, jubot.system should be started. 26 | 27 | Params 28 | k - Data key. 29 | v - Data value. 30 | " 31 | [k v] 32 | (some-> system :brain :set (as-> f (f k v)))) 33 | 34 | (defn get 35 | "Get data from system brain. 36 | Before using this function, jubot.system should be started. 37 | 38 | Params 39 | k - Data key. 40 | Return 41 | Stored data. 42 | " 43 | [k] 44 | (some-> system :brain :get (as-> f (f k)))) 45 | 46 | (defn keys 47 | "Get key list from system brain. 48 | Before using this function, jubot.system should be started. 49 | 50 | Return 51 | Key list. 52 | " 53 | [] 54 | (some-> system :brain :keys (as-> f (f)))) 55 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject jubot "0.1.1" 2 | :description "Chatbot framework in Clojure" 3 | :url "https://github.com/liquidz/jubot" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.6.0"] 8 | [com.stuartsierra/component "0.2.3"] 9 | [org.clojure/tools.namespace "0.2.10"] 10 | [org.clojure/tools.cli "0.3.1"] 11 | [compojure "1.3.4"] 12 | [ring/ring-jetty-adapter "1.3.2"] 13 | [ring/ring-defaults "0.1.5"] 14 | [org.clojure/data.json "0.2.6"] 15 | [com.taoensso/carmine "2.10.0"] 16 | [com.taoensso/timbre "3.4.0"] 17 | [clj-http-lite "0.2.1"] 18 | [im.chit/cronj "1.4.3"]] 19 | 20 | :profiles {:dev {:dependencies 21 | [[org.clojars.runa/conjure "2.2.0"] 22 | [ring/ring-mock "0.2.0"]] 23 | :source-paths ["dev"]}} 24 | 25 | :codox {:exclude [user dev] 26 | :src-dir-uri "http://github.com/liquidz/jubot/blob/master/" 27 | :src-linenum-anchor-prefix "L" 28 | :output-dir "doc/api"} 29 | 30 | :aliases {"dev" ["run" "-m" "dev/-main"]}) 31 | -------------------------------------------------------------------------------- /test/jubot/adapter/repl_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.repl-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.adapter.repl :refer :all] 5 | [jubot.redef :refer :all] 6 | [conjure.core :refer [stubbing]] 7 | [clojure.test :refer :all])) 8 | 9 | (def ^:private botname "test") 10 | (def ^:private handler (fn [{:keys [user channel text message-for-me?]}] 11 | (when message-for-me? 12 | (str "user=" user ",channel=" channel ",text=" text)))) 13 | (def ^:private adapter (map->ReplAdapter {:name botname :handler handler})) 14 | (def ^:private process-output* (partial process-output adapter)) 15 | (def ^:private process-input* (partial process-input adapter)) 16 | 17 | (deftest test-process-output 18 | (stubbing [println* identity] 19 | (are [x y] (= x y) 20 | "test=> foo" (process-output* "foo") 21 | "bar=> foo" (process-output* "foo" :as "bar")))) 22 | 23 | (deftest test-process-input 24 | (stubbing [println* identity] 25 | (is (nil? (process-input* "foo"))) 26 | (is (= (str botname "=> user=" username ",channel=,text=foo") 27 | (process-input* (str botname " foo")))))) 28 | 29 | (deftest test-ReplAdapter 30 | (testing "start adapter" 31 | (let [adapter (component/start adapter)] 32 | (is (fn? (:in adapter))) 33 | (is (fn? (:out adapter))))) 34 | 35 | (testing "stop adapter" 36 | (let [adapter (-> adapter component/start component/stop)] 37 | (is (nil? (:in adapter))) 38 | (is (nil? (:out adapter)))))) 39 | -------------------------------------------------------------------------------- /src/jubot/adapter/repl.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.repl 2 | "Jubot adapter for REPL." 3 | (:require 4 | [jubot.adapter.util :refer :all] 5 | [jubot.redef :refer :all] 6 | [com.stuartsierra.component :as component])) 7 | 8 | (def ^{:doc "User name. (default value is \"nobody\")"} 9 | username (or (getenv* "USER") "nobody")) 10 | 11 | (defn process-output 12 | "Process output to REPL. 13 | 14 | Params 15 | this - REPL adapter. 16 | :name - Bot's name. 17 | s - Output text to REPL. 18 | option 19 | :as - Overwrite bot's name if you specify this option. 20 | " 21 | [this s & {:keys [as] :as option}] 22 | (let [name (or as (:name this))] 23 | (println* (str name "=> " s)))) 24 | 25 | (defn process-input 26 | "Process input from REPL. 27 | 28 | Params 29 | this - REPL adapter. 30 | :name - Bot's name. 31 | :handler - A handler function. 32 | s - Input text from REPL. 33 | " 34 | [{:keys [name handler] :as this} s] 35 | (let [option {:user username :channel nil}] 36 | (some->> s 37 | (parse-text name) 38 | (merge option) 39 | handler 40 | (process-output this)))) 41 | 42 | (defrecord ReplAdapter [name handler in out] 43 | component/Lifecycle 44 | (start [this] 45 | (if (and in out) 46 | this 47 | (do (println ";; start repl adapter. bot name is" name) 48 | (assoc this 49 | :in (partial process-input this) 50 | :out (partial process-output this))))) 51 | (stop [this] 52 | (if-not (and in out) 53 | this 54 | (do (println ";; stop repl adapter") 55 | (assoc this :in nil :out nil))))) 56 | -------------------------------------------------------------------------------- /src/jubot/brain/redis.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain.redis 2 | "Jubot brain for Redis." 3 | (:require 4 | [com.stuartsierra.component :as component] 5 | [jubot.redef :refer :all] 6 | [taoensso.timbre :as timbre] 7 | [taoensso.carmine :as car])) 8 | 9 | (def ^{:const true 10 | :doc "Default redis URI."} 11 | DEFAULT_REDIS_URI "redis://localhost:6379/") 12 | 13 | (defn error* 14 | "DO NOT USE THIS FUNCTION DIRECTLY 15 | DI for timbre/error" 16 | [e] (timbre/error e)) 17 | 18 | (defn- set-to-redis 19 | [conn k v] 20 | (try 21 | (car/wcar conn (car/set k v)) 22 | (catch Exception e 23 | (error* e)))) 24 | 25 | (defn- get-from-redis 26 | [conn k] 27 | (try 28 | (car/wcar conn (car/get k)) 29 | (catch Exception e 30 | (error* e)))) 31 | 32 | (defn- keys-from-redis 33 | [conn] 34 | (try 35 | (car/wcar conn (car/keys "*")) 36 | (catch Exception e 37 | (error* e)))) 38 | 39 | (defrecord RedisBrain [conn uri] 40 | component/Lifecycle 41 | (start [this] 42 | (if conn 43 | this 44 | (let [conn {:pool {} 45 | :spec {:uri (or uri 46 | (getenv* "REDISCLOUD_URL") 47 | DEFAULT_REDIS_URI)}}] 48 | (println ";; start redis brain. redis url is" (-> conn :spec :uri)) 49 | (assoc this 50 | :conn conn 51 | :set (partial set-to-redis conn) 52 | :get (partial get-from-redis conn) 53 | :keys (partial keys-from-redis conn))))) 54 | (stop [this] 55 | (if-not conn 56 | this 57 | (do (println ";; stop redis brain") 58 | (assoc this :conn nil :set nil :get nil))))) 59 | -------------------------------------------------------------------------------- /test/jubot/brain/redis_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.brain.redis-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.brain.redis :refer :all] 5 | [taoensso.timbre :as timbre] 6 | [taoensso.carmine :as car] 7 | [conjure.core :refer [stubbing]] 8 | [clojure.test :refer :all])) 9 | 10 | (def ^:private brain (map->RedisBrain {})) 11 | (timbre/set-config! 12 | [:appenders :standard-out :enabled?] false) 13 | 14 | (deftest test-RedisBrain 15 | (testing "start brain" 16 | (let [brain (component/start brain)] 17 | (are [k] (contains? brain k) 18 | :conn 19 | :set 20 | :get) 21 | (is (= brain (component/start brain))))) 22 | 23 | (testing "stop brain" 24 | (let [brain (-> brain component/start component/stop)] 25 | (are [k] (nil? (get brain k)) 26 | :conn 27 | :set 28 | :get) 29 | (is (= brain (component/stop brain))))) 30 | 31 | (testing "specify redis uri" 32 | (is (= DEFAULT_REDIS_URI (-> brain component/start :conn :spec :uri))) 33 | (is (= "foo" (-> (map->RedisBrain {:uri "foo"}) 34 | component/start :conn :spec :uri)))) 35 | 36 | (testing "set/get/keys" 37 | (let [brain (-> brain component/start)] 38 | (stubbing [car/set (fn [& _] (throw (Exception. "test"))) 39 | error* "OK"] 40 | (is (= "OK" ((:set brain) "foo" "bar")))) 41 | 42 | (stubbing [car/get (fn [& _] (throw (Exception. "test"))) 43 | error* "bar"] 44 | (is (= "bar" ((:get brain) "foo")))) 45 | 46 | (stubbing [car/keys (fn [& _] (throw (Exception. "test"))) 47 | error* ["foo"]] 48 | (is (= ["foo"] ((:keys brain)))))))) 49 | -------------------------------------------------------------------------------- /test/jubot/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.core-test 2 | (:require 3 | [jubot.scheduler.keep-awake :refer :all] 4 | [jubot.system :as sys] 5 | [jubot.core :refer :all] 6 | [conjure.core :refer [stubbing]] 7 | [clojure.test :refer :all])) 8 | 9 | 10 | (do (create-ns 'jubot.test.core.a) 11 | (create-ns 'jubot.test.core.b) 12 | (intern 'jubot.test.core.a 'a-handler #(if (= "test" (:text %)) "handler")) 13 | (intern 'jubot.test.core.b 'b-schedule ["entries"])) 14 | 15 | (def ^:private f (jubot :ns-regexp #"^jubot\.test\.core")) 16 | 17 | (deftest test-jubot 18 | (stubbing [sys/start nil] 19 | (testing "default parameters" 20 | (let [{:keys [adapter brain scheduler]} (do (f) sys/system) 21 | {:keys [name handler]} adapter 22 | {:keys [entries]} scheduler] 23 | (are [x y] (= x y) 24 | jubot.adapter.slack.SlackAdapter (type adapter) 25 | jubot.brain.memory.MemoryBrain (type brain) 26 | jubot.scheduler.Scheduler (type scheduler) 27 | DEFAULT_BOTNAME name 28 | "handler" (handler {:text "test"}) 29 | nil (handler {}) 30 | "entries" (first entries)))) 31 | 32 | (testing "specify adapter and botname" 33 | (let [{:keys [adapter brain scheduler]} (do (f "-n" "foo" "-a" "repl") 34 | sys/system)] 35 | (are [x y] (= x y) 36 | jubot.adapter.repl.ReplAdapter (type adapter) 37 | "foo" (:name adapter)))) 38 | 39 | (testing "built-in schedules" 40 | (let [{:keys [scheduler]} (do (f) sys/system) 41 | {:keys [entries]} scheduler 42 | entries (set entries)] 43 | (are [x] (some? (entries x)) 44 | keep-awake-schedule))) 45 | 46 | (testing "built-in help handler" 47 | (let [{:keys [adapter]} (do (f) sys/system) 48 | {:keys [handler]} adapter] 49 | (is (.startsWith (handler {:message-for-me? true :text "help"}) 50 | "Help documents:")))))) 51 | -------------------------------------------------------------------------------- /src/jubot/core.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.core 2 | "Jubot core" 3 | (:require 4 | [com.stuartsierra.component :as component] 5 | [clojure.tools.cli :refer [parse-opts]] 6 | [jubot.system :as sys] 7 | [jubot.adapter :as ja] 8 | [jubot.brain :as jb] 9 | [jubot.scheduler :as js] 10 | [jubot.handler :as jh] 11 | [jubot.require :as jr] 12 | [jubot.scheduler [keep-awake]])) 13 | 14 | (defn create-system-fn 15 | "Returns function to create stuartsierra.component system. 16 | 17 | Params 18 | :name - a bot's name 19 | :handler - a handler function 20 | :entries - a sequence of schedule entries 21 | 22 | Return 23 | (fn [{:keys [adapter brain] :as config-option}]) 24 | " 25 | [& {:keys [name handler entries] :or {entries []}}] 26 | (fn [{:keys [adapter brain] :as config-option}] 27 | (let [built-in-entries (js/collect #"^jubot.scheduler\.") 28 | entries (concat entries built-in-entries)] 29 | (component/system-map 30 | :adapter (ja/create-adapter {:adapter adapter 31 | :name name 32 | :handler handler}) 33 | :brain (jb/create-brain {:brain brain}) 34 | :scheduler (js/create-scheduler {:entries entries}))))) 35 | 36 | (def ^{:const true :doc "Default adapter's name"} DEFAULT_ADAPTER "slack") 37 | (def ^{:const true :doc "Default brain's name"} DEFAULT_BRAIN "memory") 38 | (def ^{:const true :doc "Default bot's name"} DEFAULT_BOTNAME "jubot") 39 | 40 | (def ^:private cli-options 41 | [["-a" "--adapter ADAPTER_NAME" "Select adapter" :default DEFAULT_ADAPTER] 42 | ["-b" "--brain NAME" "Select brain" :default DEFAULT_BRAIN] 43 | ["-n" "--name NAME" "Set bot name" :default DEFAULT_BOTNAME]]) 44 | 45 | (defn jubot 46 | "Returns jubot -main function. 47 | 48 | Params 49 | :ns-regexp - a regular-expression which specifies bot's namespace. 50 | if a handler and entries are omitted, 51 | these are collected automatically from the specified namespace. 52 | 53 | Return 54 | (fn [& args]) 55 | " 56 | [& {:keys [ns-regexp]}] 57 | (fn [& args] 58 | ; require namespaces automatically 59 | (jr/regexp-require ns-regexp) 60 | 61 | (let [{:keys [options _ _ errors]} (parse-opts args cli-options) 62 | {:keys [name adapter brain debug]} options 63 | help-handler (jh/help-handler-fn ns-regexp) 64 | handler (jh/comp help-handler (jh/collect ns-regexp)) 65 | entries (js/collect ns-regexp) 66 | create-system (create-system-fn 67 | :name name 68 | :handler handler 69 | :entries entries)] 70 | (sys/init #(create-system options)) 71 | (sys/start)))) 72 | -------------------------------------------------------------------------------- /src/jubot/scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.scheduler 2 | "Jubot scheduler." 3 | (:require 4 | [com.stuartsierra.component :as component] 5 | [jubot.adapter :as ja] 6 | [cronj.core :as c])) 7 | 8 | (def ^{:const true 9 | :doc "The regular expression for collecting schedules automatically."} 10 | SCHEDULE_REGEXP #"^.*-schedule$") 11 | 12 | (defn schedule 13 | "Generate a schedule from a pair of cronj format string and function. 14 | 15 | Params 16 | cron-expr - Cronj format string. http://docs.caudate.me/cronj/#crontab 17 | f - A function with zero parameter. 18 | Return 19 | A schedule function. 20 | " 21 | [cron-expr f] 22 | (with-meta f {:schedule cron-expr})) 23 | 24 | (defn schedules 25 | "Generate sequence of schedules from pairs of cronj format string and function. 26 | 27 | Params 28 | args - Pairs of cronj format string and function. 29 | Return 30 | Sequence of schedule functions. 31 | " 32 | [& args] 33 | (map #(apply schedule %) (partition 2 args))) 34 | 35 | (defn schedule->task 36 | "Convert a schedule function to cronj task. 37 | 38 | Params 39 | f - A schedule function. 40 | Return 41 | A cronj task. 42 | " 43 | [f] 44 | {:id (str (gensym "task")) 45 | :handler (fn [t opt] (let [ret (f)] 46 | (if (string? ret) (ja/out ret)))) 47 | :schedule (-> f meta :schedule)}) 48 | 49 | (defrecord Scheduler [cj entries] 50 | component/Lifecycle 51 | (start [this] 52 | (if cj 53 | this 54 | (do (println ";; start scheduler") 55 | (let [cj (c/cronj :entries (map schedule->task entries))] 56 | (c/start! cj) 57 | (assoc this :cj cj))))) 58 | 59 | (stop [this] 60 | (if-not cj 61 | this 62 | (do (println ";; stop scheduler") 63 | (c/stop! cj) 64 | (assoc this :cj nil))))) 65 | 66 | (defn create-scheduler 67 | "Create the scheduler. 68 | 69 | Params 70 | :entries - Sequence of schedules. 71 | Return 72 | Scheduler component. 73 | " 74 | [{:keys [entries] :as config-option}] 75 | (map->Scheduler (merge {:entries []} 76 | config-option))) 77 | 78 | (defn public-schedules 79 | "Return sequence of public schedule vars which matched SCHEDULE_REGEXP in specified namespaces. 80 | 81 | Params 82 | ns-regexp - A regular expression which specifies namespaces for searching schedules. 83 | Return 84 | Sequence of schedule vars. 85 | " 86 | [ns-regexp] 87 | (->> (all-ns) 88 | (remove #(re-find #"-test$" (str (ns-name %)))) 89 | (filter #(re-find ns-regexp (str (ns-name %)))) 90 | (mapcat #(vals (ns-publics %))) 91 | (filter #(re-matches SCHEDULE_REGEXP (-> % meta :name str))))) 92 | 93 | (defn collect 94 | "Return sequence of public schedules in specified namespaces. 95 | 96 | Params 97 | ns-regexp - A regular expression which specifies namespaces for searching schedules. 98 | Return 99 | Sequence of schedules. 100 | " 101 | [ns-regexp] 102 | (flatten (map var-get (public-schedules ns-regexp)))) 103 | -------------------------------------------------------------------------------- /src/jubot/handler.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.handler 2 | "Jubot handler utilities." 3 | (:require [clojure.string :as str]) 4 | (:refer-clojure :exclude [comp])) 5 | 6 | (def ^{:const true 7 | :doc "The handler name regular expression 8 | for collecting handler functions automatically."} 9 | HANDLER_REGEXP #"^.*-handler$") 10 | 11 | (defn regexp 12 | "Choose and call handler function by specified regular expression. 13 | 14 | Params 15 | option - An argument that is passed to original handler function. 16 | reg-fn-list - Pair of regular expression and function. 17 | The paired function is called, 18 | if the regular expression is matched. 19 | 20 | In addition to original handler input, 21 | `re-find` result will be passed to 22 | the paired function with `match` key. 23 | Return 24 | Result of a chosen handler function. 25 | " 26 | [{:keys [text] :as option} & reg-fn-list] 27 | {:pre [(zero? (mod (count reg-fn-list) 2))]} 28 | (when text 29 | (reduce 30 | (fn [_ [r f]] 31 | (if (instance? java.util.regex.Pattern r) 32 | (some->> (re-find r text) 33 | (assoc option :match) 34 | f 35 | reduced) 36 | (reduced (f option)))) 37 | nil 38 | (partition 2 reg-fn-list)))) 39 | 40 | (defn comp 41 | "Compose handler functions. 42 | 43 | Params 44 | fs - Sequence of handler functions. 45 | Return 46 | Composition of handler functions. 47 | " 48 | ([] identity) 49 | ([& fs] 50 | {:pre [(every? #(or (fn? %) (var? %)) fs)]} 51 | (let [fs (reverse fs)] 52 | (fn [arg] 53 | (loop [ret ((first fs) arg), fs (next fs)] 54 | (if (and fs (nil? ret)) 55 | (recur ((first fs) arg) (next fs)) 56 | ret)))))) 57 | 58 | (defn public-handlers 59 | "Return sequence of public handler functions which matched HANDLER_REGEXP in specified namespaces. 60 | 61 | Params 62 | ns-regexp - A regular expression which specifies namespaces for searching handler functions. 63 | Return 64 | Sequence of handler functions. 65 | " 66 | [ns-regexp] 67 | (->> (all-ns) 68 | (remove #(re-find #"-test$" (str (ns-name %)))) 69 | (filter #(re-find ns-regexp (str (ns-name %)))) 70 | (mapcat #(vals (ns-publics %))) 71 | (filter #(re-matches HANDLER_REGEXP (-> % meta :name str))))) 72 | 73 | (defn collect 74 | "Return composition of public handler functions in specified namespaces. 75 | 76 | Params 77 | ns-regexp - A regular expression which specifies namespaces for searching handler functions. 78 | Return 79 | A handler function. 80 | " 81 | [ns-regexp] 82 | (if-let [handlers (seq (public-handlers ns-regexp))] 83 | (apply comp handlers) 84 | (constantly nil))) 85 | 86 | (defn help-handler-fn 87 | "Returns handler function to show handler helps. 88 | 89 | Params 90 | :ns-regexp - a regular-expression which specifies bot's namespace. 91 | Return 92 | A handler function. 93 | " 94 | [ns-regexp] 95 | (fn [{text :text, forme? :message-for-me?}] 96 | (when (and forme? (= "help" text)) 97 | (->> (public-handlers ns-regexp) 98 | (map #(-> % meta :doc)) 99 | (remove #(or (nil? %) (= % ""))) 100 | (mapcat str/split-lines) 101 | (map str/trim) 102 | (str/join "\n") 103 | (str "Help documents:\n---\n"))))) 104 | -------------------------------------------------------------------------------- /doc/api/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('#vars a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment("#vars a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('#vars .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function resizeSidebars() { 75 | var nsWidth = sidebarContentWidth('#namespaces') + 30 76 | var varWidth = 0 77 | 78 | if ($('#vars').length != 0) { 79 | varWidth = sidebarContentWidth('#vars') + 30 80 | } 81 | 82 | // snap to grid 83 | var snap = 30; 84 | nsWidth = Math.ceil(nsWidth / snap) * snap; 85 | varWidth = Math.ceil(varWidth / snap) * snap; 86 | 87 | $('#namespaces').css('width', nsWidth) 88 | $('#vars').css('width', varWidth) 89 | $('#vars, .namespace-index').css('left', nsWidth + 1) 90 | $('.namespace-docs').css('left', nsWidth + varWidth + 2) 91 | } 92 | 93 | $(window).ready(resizeSidebars) 94 | $(window).ready(setCurrentVarLink) 95 | $(window).ready(function() { persistScrollPosition('#namespaces')}) 96 | $(window).ready(function() { 97 | $('#content').scroll(setCurrentVarLink) 98 | $(window).resize(setCurrentVarLink) 99 | }) 100 | -------------------------------------------------------------------------------- /test/jubot/schedule_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.schedule-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.scheduler :refer :all] 5 | [jubot.adapter :as ja] 6 | [cronj.core :as c] 7 | [conjure.core :refer [stubbing]] 8 | [clojure.test :refer :all])) 9 | 10 | (defn test-ns-fixture 11 | [f] 12 | (create-ns 'jubot.test.scheduler.a) 13 | (create-ns 'jubot.test.scheduler.b) 14 | (create-ns 'jubot.test.scheduler.b-test) 15 | (intern 'jubot.test.scheduler.a 'a-schedule 16 | (schedule "x" (constantly "xx"))) 17 | (intern 'jubot.test.scheduler.b 'b-schedule 18 | (schedules "y" (constantly "yy"), "z" (constantly "zz"))) 19 | (intern 'jubot.test.scheduler.b-test 'must-not-be-collected-schedule 20 | (schedule "z" (constantly nil))) 21 | (f) 22 | (remove-ns 'jubot.test.scheduler.a) 23 | (remove-ns 'jubot.test.scheduler.b) 24 | (remove-ns 'jubot.test.scheduler.b-test)) 25 | 26 | (use-fixtures :each test-ns-fixture) 27 | 28 | (deftest test-schedule 29 | (let [f (schedule "foo" (constantly "bar"))] 30 | (is (= "bar" (f))) 31 | (is (= "foo" (-> f meta :schedule))))) 32 | 33 | (deftest test-schedules 34 | (let [fs (schedules "foo" (constantly "bar") 35 | "bar" (constantly "baz"))] 36 | (are [x y] (= x y) 37 | "bar" ((first fs)) 38 | "foo" (-> fs first meta :schedule) 39 | "baz" ((second fs)) 40 | "bar" (-> fs second meta :schedule)))) 41 | 42 | (deftest test-schedule->task 43 | (testing "return string" 44 | (stubbing [ja/out #(str "[" % "]")] 45 | (let [s (schedule "foo" (constantly "bar")) 46 | t (schedule->task s)] 47 | (are [x y] (= x y) 48 | "foo" (:schedule t) 49 | "[bar]" ((:handler t) nil nil))))) 50 | 51 | (testing "not return string" 52 | (stubbing [ja/out (fn [_] (throw (Exception. "must not be called")))] 53 | (let [s (schedule "foo" (constantly [1 2 3])) 54 | t (schedule->task s)] 55 | (are [x y] (= x y) 56 | "foo" (:schedule t) 57 | nil ((:handler t) nil nil)))))) 58 | 59 | (def ^:private test-entries 60 | (schedules "foo" (constantly "bar"))) 61 | 62 | (deftest test-create-scheduler 63 | (testing "no entries" 64 | (let [s (create-scheduler {})] 65 | (are [x y] (= x y) 66 | jubot.scheduler.Scheduler (type s) 67 | [] (:entries s)))) 68 | 69 | (testing "with test-entries" 70 | (stubbing [c/start! nil, c/stop! nil] 71 | (let [started (component/start (create-scheduler {:entries test-entries})) 72 | stopped (component/stop started)] 73 | (are [x y] (= x y) 74 | false (nil? (:cj started)) 75 | (count test-entries) (count (:entries started)) 76 | (first test-entries) (first (:entries started)) 77 | true (nil? (:cj stopped))))))) 78 | 79 | (deftest test-public-schedules 80 | (is (= (map resolve '(jubot.test.scheduler.a/a-schedule 81 | jubot.test.scheduler.b/b-schedule)) 82 | (sort #(.compareTo (-> %1 meta :name) (-> %2 meta :name)) 83 | (public-schedules #"^jubot\.test\.scheduler")))) 84 | (is (= (map resolve '(jubot.test.scheduler.a/a-schedule)) 85 | (public-schedules #"^jubot\.test\.scheduler\.a")))) 86 | 87 | (deftest test-collect 88 | (let [ls (sort #(.compareTo (-> %1 meta :schedule) (-> %2 meta :schedule)) 89 | (collect #"^jubot\.test\.scheduler")) 90 | [a b c] ls] 91 | (are [x y] (= x y) 92 | 3 (count ls) 93 | "x" (-> a meta :schedule) 94 | "xx" (a) 95 | "y" (-> b meta :schedule) 96 | "yy" (b) 97 | "z" (-> c meta :schedule) 98 | "zz" (c))) 99 | (is (empty? (collect #"^foobar")))) 100 | -------------------------------------------------------------------------------- /doc/api/jubot.require.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.require documentation

jubot.require

Jubot utilities for requiring namespaces.
3 | 

regexp-require

(regexp-require ns-regexp)
Require namespaces which matches specified regular expression.
4 | 
5 | Params
6 |   ns-regexp - A regular expression which specifies namespaces.
7 | 
-------------------------------------------------------------------------------- /doc/api/jubot.adapter.util.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.adapter.util documentation

jubot.adapter.util

Utilities for jubot adapter.
 3 | 

parse-text

(parse-text botname text)
Parse passed text and return a map.
 4 | 
 5 | Params
 6 |   botname - Bot's name.
 7 |   text    - Message from user.
 8 | Return
 9 |   Parsed map.
10 | 
-------------------------------------------------------------------------------- /doc/api/jubot.test.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.test documentation

jubot.test

Jubot testing utilities
3 | 

with-test-brain

macro

(with-test-brain & body)
Wrap body with a test memory brain.
4 | 
5 | Example
6 |   (with-test-brain
7 |     (jubot.brain/set "foo" "bar")
8 |     (println (jubot.brain/get "foo"))) ; => "bar"
9 | 
-------------------------------------------------------------------------------- /test/jubot/handler_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.handler-test 2 | (:require 3 | [jubot.handler :as handler] 4 | [clojure.string :as str] 5 | [clojure.test :refer :all])) 6 | 7 | (defn- regexp-handler* 8 | [arg] 9 | (handler/regexp 10 | arg 11 | #"^ping$" (constantly "pong") 12 | #"^opt$" (fn [{:keys [user channel]}] 13 | (str "u=" user ",c=" channel)) 14 | #"^set (.+?)$" (fn [{[_ x] :match}] (str "s:" x)) 15 | #"^get (.+?)$" (fn [{[_ x] :match}] (str "g:" x)) 16 | :else (constantly "error"))) 17 | 18 | (defn test-ns-fixture 19 | [f] 20 | (create-ns 'jubot.test.handler.a) 21 | (create-ns 'jubot.test.handler.b) 22 | (create-ns 'jubot.test.handler.b-test) 23 | (intern 'jubot.test.handler.a 24 | (with-meta 'a-handler {:doc "pingpong-help 25 | pingpong-handler"}) 26 | (fn [{text :text}] (if (= "ping" text) "pong"))) 27 | (intern 'jubot.test.handler.b 28 | (with-meta 'b-handler {:doc "foobar-help 29 | foobar-handler"}) 30 | (fn [{text :text}] (if (= "foo" text) "bar"))) 31 | (intern 'jubot.test.handler.b 32 | 'no-doc-handler 33 | (fn [{text :text}] (if (= "bar" text) "baz"))) 34 | (intern 'jubot.test.handler.b-test 'must-not-be-collected-handler 35 | (constantly nil)) 36 | (f) 37 | (remove-ns 'jubot.test.handler.a) 38 | (remove-ns 'jubot.test.handler.b) 39 | (remove-ns 'jubot.test.handler.b-test)) 40 | 41 | (use-fixtures :each test-ns-fixture) 42 | 43 | (deftest test-regexp 44 | (testing "should work fine" 45 | (are [x y] (= x (regexp-handler* {:user "aa" :channel "bb" :text y})) 46 | "pong" "ping" 47 | "u=aa,c=bb" "opt" 48 | "s:foo" "set foo" 49 | "g:bar" "get bar" 50 | "error" "foobar")) 51 | 52 | (testing "nil input" 53 | (is (nil? (regexp-handler* {})))) 54 | 55 | (testing "without else" 56 | (is (nil? (handler/regexp {:text "text"} #"^ping$" (constantly "pong"))))) 57 | 58 | (testing "empty reg-fn-list" 59 | (is (nil? (handler/regexp {:text "text"})))) 60 | 61 | (testing "invalid reg-fn-list" 62 | (is (thrown? AssertionError (handler/regexp {:text "text"} :lonely))))) 63 | 64 | (deftest test-comp 65 | (testing "handler composition" 66 | (let [f (fn [{text :text}] 67 | (case text "aaa" "111", "bbb" "222", "ping" "pong" nil)) 68 | g (fn [{text :text}] 69 | (case text "bbb" "333", "aaa" "444", "foo" "bar" nil))] 70 | 71 | (are [x y] (= x ((handler/comp g f) {:text y})) 72 | "111" "aaa" 73 | "222" "bbb" 74 | "pong" "ping" 75 | "bar" "foo" 76 | nil "xxx") 77 | 78 | (are [x y] (= x ((handler/comp f g) {:text y})) 79 | "444" "aaa" 80 | "333" "bbb" 81 | "pong" "ping" 82 | "bar" "foo" 83 | nil "xxx"))) 84 | 85 | (testing "empty list" 86 | (is (= identity (handler/comp)))) 87 | 88 | (testing "invalid reg-fn-list" 89 | (is (thrown? AssertionError (handler/comp 'a 'b))))) 90 | 91 | (deftest test-public-handlers 92 | (is (= (map resolve '(jubot.test.handler.a/a-handler 93 | jubot.test.handler.b/b-handler 94 | jubot.test.handler.b/no-doc-handler)) 95 | (sort #(.compareTo (-> %1 meta :name) (-> %2 meta :name)) 96 | (handler/public-handlers #"^jubot\.test\.handler")))) 97 | (is (= (map resolve '(jubot.test.handler.a/a-handler)) 98 | (handler/public-handlers #"^jubot\.test\.handler\.a")))) 99 | 100 | (deftest test-collect 101 | (are [x y] (= x ((handler/collect #"^jubot\.test\.handler") {:text y})) 102 | "pong" "ping" 103 | "bar" "foo" 104 | nil "xxx") 105 | (are [x y] (= x ((handler/collect #"^jubot\.test\.handler\.a") {:text y})) 106 | "pong" "ping" 107 | nil "foo" 108 | nil "xxx") 109 | (is (nil? ((handler/collect #"^foobar") {:text "foo"})))) 110 | 111 | (deftest test-help-handler-fn 112 | (let [f (handler/help-handler-fn #"^jubot\.test\.handler") 113 | helps (str/split-lines (f {:message-for-me? true :text "help"}))] 114 | (are [x] (seq (filter #(= % x) helps)) 115 | "pingpong-help" 116 | "pingpong-handler" 117 | "foobar-help" 118 | "foobar-handler") 119 | (is (empty? (filter #(= % "") helps))))) 120 | -------------------------------------------------------------------------------- /doc/api/jubot.redef.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.redef documentation

jubot.redef

Redefining function fot stubbing.
3 | 

getenv*

System/getenv
4 | 

println*

clojure.core/println
5 | 
-------------------------------------------------------------------------------- /test/jubot/adapter/slack_test.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.slack-test 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [jubot.adapter.slack :refer :all] 5 | [jubot.redef :refer :all] 6 | [conjure.core :refer [stubbing]] 7 | [clojure.data.json :as json] 8 | [clojure.test :refer :all] 9 | [clj-http.lite.client :refer [post]] 10 | [ring.adapter.jetty :refer [run-jetty]] 11 | [ring.middleware.defaults :refer :all] 12 | [ring.mock.request :refer [request]])) 13 | 14 | (def ^:private botname "test") 15 | (def ^:private handler (fn [{:keys [user channel text message-for-me?]}] 16 | (when message-for-me? 17 | (str "user=" user ",channel=" channel ",text=" text)))) 18 | (def ^:private nil-handler (constantly nil)) 19 | (def ^:private adapter (map->SlackAdapter {:name botname})) 20 | (def ^:private process-input* (partial process-input adapter)) 21 | (def ^:private process-output* (partial process-output adapter)) 22 | 23 | (deftest test-not-from-slackbot 24 | (are [x y] (= x y) 25 | "bar" (not-from-slackbot "foo" "bar") 26 | nil (not-from-slackbot "foo" nil) 27 | nil (not-from-slackbot "slackbot" "bar"))) 28 | 29 | (deftest test-valid-outgoing-token 30 | (are [x y] (= x y) 31 | "foo" (valid-outgoing-token nil "foo") 32 | nil (valid-outgoing-token "foo" "bar")) 33 | 34 | (stubbing [getenv* "foo"] 35 | (are [x y] (= x y) 36 | nil (valid-outgoing-token nil "foo") 37 | "bar" (valid-outgoing-token "foo" "bar")))) 38 | 39 | (deftest test-process-input 40 | (testing "ignore nil input" 41 | (is (= "" (process-input* handler {:text nil})))) 42 | 43 | (testing "ignore message from slackbot" 44 | (is (= "" (process-input* 45 | handler {:text (str botname " foo") :user_name "slackbot"})))) 46 | 47 | (testing "ignore message with invalid token" 48 | (is (= "" (process-input* 49 | handler {:text (str botname " foo") :token "invalid"})))) 50 | 51 | (testing "ignore message which is not addressed to bot" 52 | (is (= "" (process-input* handler {:text "foo"})))) 53 | 54 | (testing "handler function returns nil" 55 | (is (= "" (process-input* nil-handler {:text (str botname " foo")})))) 56 | 57 | (testing "handler function returns string" 58 | (is (= {:username botname :text "@aaa user=aaa,channel=bbb,text=ccc"} 59 | (json/read-str 60 | (process-input* handler {:text (str botname " ccc") 61 | :user_name "aaa" 62 | :channel_name "bbb"}) 63 | :key-fn keyword))))) 64 | 65 | (deftest test-process-output 66 | (testing "should work fine" 67 | (stubbing [getenv* "localhost" 68 | post list] 69 | (let [[url {{payload :payload} :form-params}] (process-output* "foo") 70 | payload (json/read-str payload :key-fn keyword)] 71 | (is (= "localhost" url)) 72 | (is (= {:username botname :text "foo"} payload))))) 73 | 74 | (testing "INCOMING_URL_KEY is not defined" 75 | (stubbing [getenv* nil] 76 | (is (nil? (process-output* "foo"))))) 77 | 78 | (testing "specify bot-name and icon-url" 79 | (stubbing [getenv* "localhost", post list] 80 | (let [f #(-> (apply process-output* %&) second :form-params :payload 81 | (json/read-str :key-fn keyword)) 82 | text "text" 83 | newname "bar" 84 | icon-url "http://localhost/baz.png"] 85 | (is (= {:username newname :text text} (f text :as newname))) 86 | (is (= {:username newname :icon_url icon-url :text text} 87 | (f text :as newname :icon-url icon-url))))))) 88 | 89 | (deftest test-app 90 | (testing "get" 91 | (is (= 200 (-> (request :get "/") app :status)))) 92 | 93 | (testing "post" 94 | (let [param {:foo "bar"} 95 | app* (-> app 96 | (wrap-defaults api-defaults) 97 | (wrap-adapter "adapter" "handler"))] 98 | (stubbing [process-input list] 99 | (is (= ["adapter" "handler" param] 100 | (-> (request :post "/" param) app* :body))))))) 101 | 102 | (deftest test-SlackAdapter 103 | (stubbing [run-jetty "jetty" 104 | println nil] 105 | (testing "start adapter" 106 | (let [adapter (component/start adapter)] 107 | (are [x y] (= x y) 108 | "jetty" (:server adapter) 109 | true (fn? (:out adapter))) 110 | (is (= adapter (component/start adapter))))) 111 | 112 | (testing "stop adapter" 113 | (let [adapter (-> adapter component/start component/stop)] 114 | (is (nil? (:server adapter))) 115 | (is (= adapter (component/stop adapter))))))) 116 | -------------------------------------------------------------------------------- /doc/api/jubot.brain.memory.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.brain.memory documentation

jubot.brain.memory

Jubot brain for memory.
3 | 

->MemoryBrain

(->MemoryBrain mem)
Positional factory function for class jubot.brain.memory.MemoryBrain.
4 | 

map->MemoryBrain

(map->MemoryBrain m__5869__auto__)
Factory function for class jubot.brain.memory.MemoryBrain, taking a map of keywords to field values.
5 | 
-------------------------------------------------------------------------------- /doc/api/jubot.adapter.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.adapter documentation

jubot.adapter

Jubot adapter manager.
 3 | 

create-adapter

(create-adapter {:keys [adapter], :as config-option})
Create the specified adapter.
 4 | 
 5 | Params
 6 |   config-option
 7 |     :adapter - A adapter name.
 8 | Return
 9 |   Adapter component.
10 | 

out

(out s & option)
Call output function in system adapter.
11 | Before using this function, jubot.system should be started.
12 | 
13 | Params
14 |   s      - A message string.
15 |   option - Output option map.
16 |            See REPL or Slack adapter documents.
17 | Return
18 |   nil.
19 | 
-------------------------------------------------------------------------------- /src/jubot/adapter/slack.clj: -------------------------------------------------------------------------------- 1 | (ns jubot.adapter.slack 2 | "Jubot adapter for Slack. 3 | https://slack.com/ 4 | " 5 | (:require 6 | [jubot.adapter.util :refer :all] 7 | [jubot.redef :refer :all] 8 | [com.stuartsierra.component :as component] 9 | [ring.adapter.jetty :refer [run-jetty]] 10 | [ring.middleware.defaults :refer :all] 11 | [compojure.core :refer [defroutes GET POST]] 12 | [compojure.route :refer [files not-found]] 13 | [clojure.data.json :as json] 14 | [clj-http.lite.client :as client])) 15 | 16 | (def ^:private DEFAULT_PORT 8080) 17 | (def ^:private OUTGOING_TOKEN_KEY "SLACK_OUTGOING_TOKEN") 18 | (def ^:private INCOMING_URL_KEY "SLACK_INCOMING_URL") 19 | 20 | (defn not-from-slackbot 21 | "If the user name of inputted message is \"slackbot\", return text as it is. 22 | If that's not the case, return nil. 23 | 24 | Params 25 | username - The user name of inputted message. 26 | text - Message from user. 27 | Return 28 | Text string or nil. 29 | " 30 | [username text] 31 | (if (not= "slackbot" username) 32 | text)) 33 | 34 | (defn valid-outgoing-token 35 | "If the outgoing token is valid, return text as it is. 36 | If that's not the case, return nil. 37 | 38 | Params 39 | token - Outgoing token passed from slack. 40 | text - Message from user. 41 | Return 42 | Text string or nil. 43 | " 44 | [token text] 45 | (if (= token (getenv* OUTGOING_TOKEN_KEY)) 46 | text)) 47 | 48 | (defn process-output 49 | "Process output to Slack. 50 | 51 | Params 52 | this - Slack adapter. 53 | :name - Bot's name. 54 | text - Output text to Slack. 55 | option 56 | :as - Overwrite bot's name if you specify this option. 57 | :icon-url - Customized icon URL. 58 | " 59 | [this text & {:keys [as icon-url]}] 60 | (let [url (getenv* INCOMING_URL_KEY) 61 | name (or as (:name this)) 62 | payload {:text text :username name} 63 | payload (merge payload (if icon-url {:icon_url icon-url} {}))] 64 | (when url 65 | (client/post url {:form-params {:payload (json/write-str payload)}})))) 66 | 67 | (defn process-input 68 | "Process input from Slack. 69 | 70 | Params 71 | this - REPL adapter. 72 | handler-fn - A handler function. 73 | params - POST parameters. 74 | {:channel_id \"xxxxxxxxx\", 75 | :token \"xxxxxxxxx\", 76 | :channel_name \"test\", 77 | :user_id \"xxxxxxxxx\", 78 | :team_id \"xxxxxxxxx\", 79 | :service_id \"xxxxxxxxx\", 80 | :user_name \"slackbot\", 81 | :team_domain \"uochan\", 82 | :timestamp \"1422058599.000004\", 83 | :text \"foo bar\"} 84 | Return 85 | JSON string or empty string. 86 | " 87 | [this handler-fn params] 88 | (let [{:keys [token user_name channel_name text]} params 89 | botname (:name this) 90 | option {:user user_name :channel channel_name}] 91 | (or (some->> text 92 | (not-from-slackbot user_name) 93 | (valid-outgoing-token token) 94 | (parse-text botname) 95 | (merge option) 96 | handler-fn 97 | (str "@" user_name " ") 98 | (hash-map :username botname :text) 99 | json/write-str) 100 | ""))) 101 | 102 | (defroutes app 103 | (GET "/" {:keys [adapter]} 104 | (str "this is jubot slack adapter." 105 | " bot's name is \"" (:name adapter) "\".")) 106 | (POST "/" {:keys [adapter handler-fn params]} 107 | (process-input adapter handler-fn params)) 108 | (files "/static" {:root "static"}) 109 | (not-found "page not found")) 110 | 111 | (defn wrap-adapter 112 | "Ring middleware to wrap jubot adapter on request. 113 | 114 | Params 115 | handler - A ring handler. 116 | adapter - A jubot adapter. 117 | bot-handler - A jubot handler function. 118 | Return 119 | A ring handler. 120 | " 121 | [handler adapter bot-handler] 122 | #(handler 123 | (assoc % :adapter adapter 124 | :handler-fn bot-handler))) 125 | 126 | 127 | (defrecord SlackAdapter [name server handler] 128 | component/Lifecycle 129 | (start [this] 130 | (if server 131 | this 132 | (do (println ";; start slack adapter. bot name is" name) 133 | (let [server (run-jetty 134 | (-> app 135 | (wrap-defaults api-defaults) 136 | (wrap-adapter this handler)) 137 | {:port (Integer. (or (getenv* "PORT") DEFAULT_PORT)) 138 | :join? false })] 139 | (assoc this 140 | :server server 141 | :out (partial process-output this)))))) 142 | (stop [this] 143 | (if-not server 144 | this 145 | (do (println ";; stop slack adapter") 146 | (try (.stop server) 147 | (catch Exception e 148 | (println ";; error occured when stopping server:" e))) 149 | (assoc this :server nil))))) 150 | -------------------------------------------------------------------------------- /doc/api/jubot.scheduler.keep-awake.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.scheduler.keep-awake documentation

jubot.scheduler.keep-awake

Built-in schedule to keep awake jubot system on PaaS. (such as Heroku)
3 | 

AWAKE_INTERVAL

Interval to awake system.
4 | 

AWAKE_URL_KEY

Key of environment variables which handle url to awake system.
5 | 

keep-awake-schedule

Schedule to keep awake system.
6 | 
-------------------------------------------------------------------------------- /doc/api/jubot.system.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.system documentation

jubot.system

Jubot system manager.
 3 | 

init

(init create-system-fn)
Initialize stuartsierra.component system.
 4 | 
 5 | Params
 6 |   create-system-fn - A function to create jubot system.
 7 | 

start

(start)
Start stuartsierra.component system.
 8 | 

stop

(stop)
Stop stuartsierra.component system.
 9 | 

system

Jubot system var.
10 | 
-------------------------------------------------------------------------------- /doc/api/jubot.brain.redis.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.brain.redis documentation

jubot.brain.redis

Jubot brain for Redis.
3 | 

->RedisBrain

(->RedisBrain conn uri)
Positional factory function for class jubot.brain.redis.RedisBrain.
4 | 

DEFAULT_REDIS_URI

Default redis URI.
5 | 

error*

(error* e)
DO NOT USE THIS FUNCTION DIRECTLY
6 | DI for timbre/error

map->RedisBrain

(map->RedisBrain m__5869__auto__)
Factory function for class jubot.brain.redis.RedisBrain, taking a map of keywords to field values.
7 | 
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jubot 2 | [![Circle CI](https://circleci.com/gh/liquidz/jubot.svg?style=svg)](https://circleci.com/gh/liquidz/jubot) [![Dependency Status](https://www.versioneye.com/user/projects/54ca4610de7924f81a0000dc/badge.svg?style=flat)](https://www.versioneye.com/user/projects/54ca4610de7924f81a0000dc) 3 | 4 | **[API Docs](http://liquidz.github.io/jubot/api/)** 5 | 6 | ![jubot](resources/jubot.png) 7 | **Chatbot framework in Clojure.** 8 | 9 | Currently, jubot supports following adapters and brains: 10 | 11 | * Adapter 12 | * [Slack](https://slack.com/) 13 | * Brain 14 | * [Redis](http://redis.io/) 15 | 16 | ## Why jubot? 17 | 18 | * Simplicity 19 | * Handlers are simple functions, and these are **TESTABLE**. 20 | * Efficiency 21 | * Supports REPL friendly development that you love. 22 | * Extensibility 23 | * Easy to exntend system because jubot uses [stuartsierra/component](https://github.com/stuartsierra/component) as a component system. 24 | 25 | 26 | ## Getting Started 27 | 28 | ```sh 29 | $ lein new jubot YOUR_JUBOT_PROJECT 30 | $ cd YOUR_JUBOT_PROJECT 31 | $ lein repl 32 | user=> (in "jubot help") 33 | ``` 34 | 35 | 36 | ## Handlers 37 | 38 | Handler is a function to process user input. 39 | 40 | ### Ping pong example: 41 | 42 | **NOTE:** This example will response for any addresses because example code does not check `:message-for-me?`. 43 | 44 | ```clj 45 | (defn ping-handler 46 | "jubot ping - reply with 'pong'" 47 | [{:keys [text]}] 48 | (if (= "ping" text) "pong")) 49 | ``` 50 | * Arguments 51 | * `:user`: User name 52 | * `:text`: Input string. 53 | * `:to`: Address username. 54 | * `:message-for-me?`: Is inputted message addredded to bot or not. 55 | * Document string 56 | * Document string will be shown in chatbot help. 57 | ```clj 58 | user=> (in "jubot help") 59 | ``` 60 | 61 | Or you can use [`handler/regexp`](http://liquidz.github.io/jubot/api/jubot.handler.html#var-regexp): 62 | 63 | ``` 64 | (ns foo.bar 65 | (:require 66 | [jubot.handler :as jh])) 67 | 68 | (defn ping-handler 69 | [arg] 70 | (jh/regexp arg 71 | #"^ping$" (constantly "pong"))) 72 | ``` 73 | 74 | ### Which handlers are collected automatically? 75 | 76 | Developers do not need to specify which handlers are used, because jubot collects handler functions automatically. 77 | 78 | * Public functions that matches `/^.*-handler$/` in `ns-prefix` will be collected automatically. 79 | * `ns-prefix` is a namespace regular expression. It is defined in `YOUR_JUBOT_PROJECT.core`. 80 | * However, namespaces that matches `/^.*-test$/` is excluded. 81 | 82 | 83 | ## Schedules 84 | Schedule is a function that is called periodically as a cron. 85 | 86 | ### Good morning/night example: 87 | ```clj 88 | (ns foo.bar 89 | (:require 90 | [jubot.scheduler :as js])) 91 | 92 | (def good-morning-schedule 93 | (js/schedules 94 | "0 0 7 * * * *" (fn [] "good morning") 95 | "0 0 21 * * * *" (fn [] "good night"))) 96 | ``` 97 | * Use [`scheduler/schedule`](http://liquidz.github.io/jubot/api/jubot.scheduler.html#var-schedule) or [`scheduler/schedules`](http://liquidz.github.io/jubot/api/jubot.scheduler.html#var-schedules) to define one or more schedules. 98 | * If the function returns string, jubot sends the string to adapter as a message. In other words, jubot does nothing when the function returns other than string. 99 | * Scheduling format 100 | * Jubot uses [cronj](https://github.com/zcaudate/cronj) for scheduling tasks, and scheduling format's details is here: [cronj format](http://docs.caudate.me/cronj/#crontab) 101 | 102 | ### Which schedules are collected automatically? 103 | As same as handler section, jubot collects schedule functions automatically. 104 | 105 | * Public schedule funtion that matches `/^.*-schedule$/` in `ns-prefix` will be collected automatically. 106 | * Test namespaces that matches `/^.*-test$/` are excluded. 107 | 108 | ## Development in REPL 109 | Jubot provides some useful funcition to develop chatbot in REPL efficiently. 110 | These functions are defined in `dev/user.clj`. 111 | 112 | ```clj 113 | user=> (start) ; start jubot system 114 | user=> (stop) ; stop jubot system 115 | user=> (restart) ; reload sources and restart jubot system 116 | user=> (in "jubot ping") 117 | ``` 118 | 119 | ## Command line arguments 120 | 121 | ### Adapter name: `-a`, `--adapter` 122 | * Default value is "slack" 123 | * Possible adapter names are as follows: 124 | * slack 125 | * repl (for development) 126 | 127 | ### Brain name: `-b`, `--brain` 128 | * Default value is "memory" 129 | * Possible brain names are as follow: 130 | * redis 131 | * memory (for development) 132 | 133 | ### Chatbot name: `-n`, `--name` 134 | * Default value is "jubot" 135 | 136 | ## Deploy to Heroku 137 | 1. Edit `Procfile` as you want 138 | 1. Create and deploy (the following sample uses Redis as a brain) 139 | ```sh 140 | heroku apps:create 141 | heroku addons:add rediscloud 142 | git push heroku master 143 | ``` 144 | 145 | Or use following deployment button based on [jubot-sample](https://github.com/liquidz/jubot-sample). 146 | 147 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/liquidz/jubot-sample) 148 | 149 | ### Slack setting 150 | * Required integration 151 | * Outgoing WebHooks 152 | * Incoming WebHooks 153 | * Required environmental variables 154 | ```sh 155 | heroku config:add SLACK_OUTGOING_TOKEN="aaa" 156 | heroku config:add SLACK_INCOMING_URL="bbb" 157 | ``` 158 | 159 | ### Advanced setting for heroku 160 | * Avoid sleeping app 161 | ```sh 162 | heroku config:add AWAKE_URL="Application url on heroku" 163 | ``` 164 | 165 | ## License 166 | 167 | Copyright (C) 2015 [uochan](http://twitter.com/uochan) 168 | 169 | Distributed under the Eclipse Public License either version 1.0 or (at 170 | your option) any later version. 171 | -------------------------------------------------------------------------------- /doc/api/jubot.brain.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.brain documentation

jubot.brain

Jubot brain manager.
 3 | 

create-brain

(create-brain {:keys [brain], :as config-option})
Create the specified brain.
 4 | 
 5 | Params
 6 |   config-option
 7 |     :brain - A brain's name.
 8 | Return
 9 |   Brain component.
10 | 

get

(get k)
Get data from system brain.
11 | Before using this function, jubot.system should be started.
12 | 
13 | Params
14 |   k - Data key.
15 | Return
16 |   Stored data.
17 | 

keys

(keys)
Get key list from system brain.
18 | Before using this function, jubot.system should be started.
19 | 
20 | Return
21 |   Key list.
22 | 

set

(set k v)
Store data to system brain.
23 | Before using this function, jubot.system should be started.
24 | 
25 | Params
26 |   k - Data key.
27 |   v - Data value.
28 | 
-------------------------------------------------------------------------------- /doc/api/jubot.core.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.core documentation

jubot.core

Jubot core
 3 | 

create-system-fn

(create-system-fn & {:keys [name handler entries], :or {entries []}})
Returns function to create stuartsierra.component system.
 4 | 
 5 | Params
 6 |   :name    - a bot's name
 7 |   :handler - a handler function
 8 |   :entries - a sequence of schedule entries
 9 | 
10 | Return
11 |   (fn [{:keys [adapter brain] :as config-option}])
12 | 

DEFAULT_ADAPTER

Default adapter's name
13 | 

DEFAULT_BOTNAME

Default bot's name
14 | 

DEFAULT_BRAIN

Default brain's name
15 | 

jubot

(jubot & {:keys [ns-regexp]})
Returns jubot -main function.
16 | 
17 | Params
18 |   :ns-regexp - a regular-expression which specifies bot's namespace.
19 |                if a handler and entries are omitted,
20 |                these are collected automatically from the specified namespace.
21 | 
22 | Return
23 |   (fn [& args])
24 | 
-------------------------------------------------------------------------------- /doc/api/jubot.adapter.repl.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.adapter.repl documentation

jubot.adapter.repl

Jubot adapter for REPL.
 3 | 

->ReplAdapter

(->ReplAdapter name handler in out)
Positional factory function for class jubot.adapter.repl.ReplAdapter.
 4 | 

map->ReplAdapter

(map->ReplAdapter m__5869__auto__)
Factory function for class jubot.adapter.repl.ReplAdapter, taking a map of keywords to field values.
 5 | 

process-input

(process-input {:keys [name handler], :as this} s)
Process input from REPL.
 6 | 
 7 | Params
 8 |   this       - REPL adapter.
 9 |     :name    - Bot's name.
10 |     :handler - A handler function.
11 |   s          - Input text from REPL.
12 | 

process-output

(process-output this s & {:keys [as], :as option})
Process output to REPL.
13 | 
14 | Params
15 |   this    - REPL adapter.
16 |     :name - Bot's name.
17 |   s       - Output text to REPL.
18 |   option
19 |     :as   - Overwrite bot's name if you specify this option.
20 | 

username

User name. (default value is "nobody")
21 | 
-------------------------------------------------------------------------------- /doc/api/jubot.handler.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.handler documentation

jubot.handler

Jubot handler utilities.
 3 | 

collect

(collect ns-regexp)
Return composition of public handler functions in specified namespaces.
 4 | 
 5 | Params
 6 |   ns-regexp - A regular expression which specifies namespaces for searching handler functions.
 7 | Return
 8 |   A handler function.
 9 | 

comp

(comp)(comp & fs)
Compose handler functions.
10 | 
11 | Params
12 |   fs - Sequence of handler functions.
13 | Return
14 |   Composition of handler functions.
15 | 

HANDLER_REGEXP

The handler name regular expression
16 | for collecting handler functions automatically.

help-handler-fn

(help-handler-fn ns-regexp)
Returns handler function to show handler helps.
17 | 
18 | Params
19 |   :ns-regexp - a regular-expression which specifies bot's namespace.
20 | Return
21 |   A handler function.
22 | 

public-handlers

(public-handlers ns-regexp)
Return sequence of public handler functions which matched HANDLER_REGEXP in specified namespaces.
23 | 
24 | Params
25 |   ns-regexp - A regular expression which specifies namespaces for searching handler functions.
26 | Return
27 |   Sequence of handler functions.
28 | 

regexp

(regexp {:keys [text], :as option} & reg-fn-list)
Choose and call handler function by specified regular expression.
29 | 
30 | Params
31 |   option      - An argument that is passed to original handler function.
32 |   reg-fn-list - Pair of regular expression and function.
33 |                 The paired function is called,
34 |                 if the regular expression is matched.
35 | 
36 |                 In addition to original handler input,
37 |                 `re-find` result will be passed to
38 |                 the paired function with `match` key.
39 | Return
40 |   Result of a chosen handler function.
41 | 
-------------------------------------------------------------------------------- /doc/api/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 15px; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | margin: 15px 0; 10 | } 11 | 12 | h2 { 13 | font-weight: normal; 14 | font-size: 28px; 15 | padding: 10px 0 2px 0; 16 | margin: 0; 17 | } 18 | 19 | #header, #content, .sidebar { 20 | position: fixed; 21 | } 22 | 23 | #header { 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | height: 20px; 28 | background: #444; 29 | color: #fff; 30 | padding: 5px 7px; 31 | } 32 | 33 | #content { 34 | top: 30px; 35 | right: 0; 36 | bottom: 0; 37 | overflow: auto; 38 | background: #fff; 39 | color: #333; 40 | padding: 0 18px; 41 | } 42 | 43 | .sidebar { 44 | position: fixed; 45 | top: 30px; 46 | bottom: 0; 47 | overflow: auto; 48 | } 49 | 50 | #namespaces { 51 | background: #e2e2e2; 52 | border-right: solid 1px #cccccc; 53 | left: 0; 54 | width: 250px; 55 | } 56 | 57 | #vars { 58 | background: #f2f2f2; 59 | border-right: solid 1px #cccccc; 60 | left: 251px; 61 | width: 200px; 62 | } 63 | 64 | .namespace-index { 65 | left: 251px; 66 | } 67 | 68 | .namespace-docs { 69 | left: 452px; 70 | } 71 | 72 | #header { 73 | background: -moz-linear-gradient(top, #555 0%, #222 100%); 74 | background: -webkit-linear-gradient(top, #555 0%, #333 100%); 75 | background: -o-linear-gradient(top, #555 0%, #222 100%); 76 | background: -ms-linear-gradient(top, #555 0%, #222 100%); 77 | background: linear-gradient(top, #555 0%, #222 100%); 78 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 79 | z-index: 100; 80 | } 81 | 82 | #header h1 { 83 | margin: 0; 84 | padding: 0; 85 | font-size: 12pt; 86 | font-weight: lighter; 87 | text-shadow: -1px -1px 0px #333; 88 | } 89 | 90 | #header a, .sidebar a { 91 | display: block; 92 | text-decoration: none; 93 | } 94 | 95 | #header a { 96 | color: #fff; 97 | } 98 | 99 | .sidebar a { 100 | color: #333; 101 | } 102 | 103 | #header h2 { 104 | float: right; 105 | font-size: 9pt; 106 | font-weight: normal; 107 | margin: 3px 3px; 108 | padding: 0; 109 | color: #bbb; 110 | } 111 | 112 | #header h2 a { 113 | display: inline; 114 | } 115 | 116 | .sidebar h3 { 117 | margin: 0; 118 | padding: 10px 10px 0 10px; 119 | font-size: 19px; 120 | font-weight: normal; 121 | } 122 | 123 | .sidebar ul { 124 | padding: 0.5em 0em; 125 | margin: 0; 126 | } 127 | 128 | .sidebar li { 129 | display: block; 130 | vertical-align: middle; 131 | } 132 | 133 | .sidebar li a, .sidebar li .no-link { 134 | border-left: 3px solid transparent; 135 | padding: 0 7px; 136 | white-space: nowrap; 137 | } 138 | 139 | .sidebar li .no-link { 140 | display: block; 141 | color: #777; 142 | font-style: italic; 143 | } 144 | 145 | .sidebar li .inner { 146 | display: inline-block; 147 | padding-top: 7px; 148 | height: 24px; 149 | } 150 | 151 | .sidebar li a, .sidebar li .tree { 152 | height: 31px; 153 | } 154 | 155 | .depth-1 .inner { padding-left: 2px; } 156 | .depth-2 .inner { padding-left: 6px; } 157 | .depth-3 .inner { padding-left: 20px; } 158 | .depth-4 .inner { padding-left: 34px; } 159 | .depth-5 .inner { padding-left: 48px; } 160 | .depth-6 .inner { padding-left: 62px; } 161 | 162 | .sidebar li .tree { 163 | display: block; 164 | float: left; 165 | position: relative; 166 | top: -10px; 167 | margin: 0 4px 0 0; 168 | padding: 0; 169 | } 170 | 171 | .sidebar li.depth-1 .tree { 172 | display: none; 173 | } 174 | 175 | .sidebar li .tree .top, .sidebar li .tree .bottom { 176 | display: block; 177 | margin: 0; 178 | padding: 0; 179 | width: 7px; 180 | } 181 | 182 | .sidebar li .tree .top { 183 | border-left: 1px solid #aaa; 184 | border-bottom: 1px solid #aaa; 185 | height: 19px; 186 | } 187 | 188 | .sidebar li .tree .bottom { 189 | height: 22px; 190 | } 191 | 192 | .sidebar li.branch .tree .bottom { 193 | border-left: 1px solid #aaa; 194 | } 195 | 196 | #namespaces li.current a { 197 | border-left: 3px solid #a33; 198 | color: #a33; 199 | } 200 | 201 | #vars li.current a { 202 | border-left: 3px solid #33a; 203 | color: #33a; 204 | } 205 | 206 | #content h3 { 207 | font-size: 13pt; 208 | font-weight: bold; 209 | } 210 | 211 | .public h3 { 212 | margin: 0; 213 | float: left; 214 | } 215 | 216 | .usage { 217 | clear: both; 218 | } 219 | 220 | .public { 221 | margin: 0; 222 | border-top: 1px solid #e0e0e0; 223 | padding-top: 14px; 224 | padding-bottom: 6px; 225 | } 226 | 227 | .public:last-child { 228 | margin-bottom: 20%; 229 | } 230 | 231 | .members .public:last-child { 232 | margin-bottom: 0; 233 | } 234 | 235 | .members { 236 | margin: 15px 0; 237 | } 238 | 239 | .members h4 { 240 | color: #555; 241 | font-weight: normal; 242 | font-variant: small-caps; 243 | margin: 0 0 5px 0; 244 | } 245 | 246 | .members .inner { 247 | padding-top: 5px; 248 | padding-left: 12px; 249 | margin-top: 2px; 250 | margin-left: 7px; 251 | border-left: 1px solid #bbb; 252 | } 253 | 254 | #content .members .inner h3 { 255 | font-size: 12pt; 256 | } 257 | 258 | .members .public { 259 | border-top: none; 260 | margin-top: 0; 261 | padding-top: 6px; 262 | padding-bottom: 0; 263 | } 264 | 265 | .members .public:first-child { 266 | padding-top: 0; 267 | } 268 | 269 | h4.type, 270 | h4.dynamic, 271 | h4.added, 272 | h4.deprecated { 273 | float: left; 274 | margin: 3px 10px 15px 0; 275 | font-size: 15px; 276 | font-weight: bold; 277 | font-variant: small-caps; 278 | } 279 | 280 | .public h4.type, 281 | .public h4.dynamic, 282 | .public h4.added, 283 | .public h4.deprecated { 284 | font-size: 13px; 285 | font-weight: bold; 286 | margin: 3px 0 0 10px; 287 | } 288 | 289 | .members h4.type, 290 | .members h4.added, 291 | .members h4.deprecated { 292 | margin-top: 1px; 293 | } 294 | 295 | h4.type { 296 | color: #717171; 297 | } 298 | 299 | h4.dynamic { 300 | color: #9933aa; 301 | } 302 | 303 | h4.added { 304 | color: #508820; 305 | } 306 | 307 | h4.deprecated { 308 | color: #880000; 309 | } 310 | 311 | .namespace { 312 | margin-bottom: 30px; 313 | } 314 | 315 | .namespace:last-child { 316 | margin-bottom: 10%; 317 | } 318 | 319 | .index { 320 | padding: 0; 321 | font-size: 80%; 322 | margin: 15px 0; 323 | line-height: 16px; 324 | } 325 | 326 | .index * { 327 | display: inline; 328 | } 329 | 330 | .index p { 331 | padding-right: 3px; 332 | } 333 | 334 | .index li { 335 | padding-right: 5px; 336 | } 337 | 338 | .index ul { 339 | padding-left: 0; 340 | } 341 | 342 | .usage code { 343 | display: block; 344 | color: #008; 345 | margin: 2px 0; 346 | } 347 | 348 | .usage code:first-child { 349 | padding-top: 10px; 350 | } 351 | 352 | p { 353 | margin: 15px 0; 354 | } 355 | 356 | .public p:first-child, .public pre.plaintext { 357 | margin-top: 12px; 358 | } 359 | 360 | .doc { 361 | margin: 0 0 26px 0; 362 | clear: both; 363 | } 364 | 365 | .public .doc { 366 | margin: 0; 367 | } 368 | 369 | .namespace-index .doc { 370 | margin-bottom: 20px; 371 | } 372 | 373 | .namespace-index .namespace .doc { 374 | margin-bottom: 10px; 375 | } 376 | 377 | .markdown { 378 | line-height: 18px; 379 | font-size: 14px; 380 | } 381 | 382 | .doc, .public, .namespace .index { 383 | max-width: 680px; 384 | overflow-x: visible; 385 | } 386 | 387 | .markdown code, .src-link a { 388 | background: #f6f6f6; 389 | border: 1px solid #e4e4e4; 390 | border-radius: 2px; 391 | } 392 | 393 | .markdown pre { 394 | background: #f4f4f4; 395 | border: 1px solid #e0e0e0; 396 | border-radius: 2px; 397 | padding: 5px 10px; 398 | margin: 0 10px; 399 | } 400 | 401 | .markdown pre code { 402 | background: transparent; 403 | border: none; 404 | } 405 | 406 | .doc ul, .doc ol { 407 | padding-left: 30px; 408 | } 409 | 410 | .doc table { 411 | border-collapse: collapse; 412 | margin: 0 10px; 413 | } 414 | 415 | .doc table td, .doc table th { 416 | border: 1px solid #dddddd; 417 | padding: 4px 6px; 418 | } 419 | 420 | .doc table th { 421 | background: #f2f2f2; 422 | } 423 | 424 | .doc dl { 425 | margin: 0 10px 20px 10px; 426 | } 427 | 428 | .doc dl dt { 429 | font-weight: bold; 430 | margin: 0; 431 | padding: 3px 0; 432 | border-bottom: 1px solid #ddd; 433 | } 434 | 435 | .doc dl dd { 436 | padding: 5px 0; 437 | margin: 0 0 5px 10px; 438 | } 439 | 440 | .doc abbr { 441 | border-bottom: 1px dotted #333; 442 | font-variant: none 443 | cursor: help; 444 | } 445 | 446 | .src-link { 447 | margin-bottom: 15px; 448 | } 449 | 450 | .src-link a { 451 | font-size: 70%; 452 | padding: 1px 4px; 453 | text-decoration: none; 454 | color: #5555bb; 455 | } -------------------------------------------------------------------------------- /doc/api/jubot.scheduler.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.scheduler documentation

jubot.scheduler

Jubot scheduler.
 3 | 

->Scheduler

(->Scheduler cj entries)
Positional factory function for class jubot.scheduler.Scheduler.
 4 | 

collect

(collect ns-regexp)
Return sequence of public schedules in specified namespaces.
 5 | 
 6 | Params
 7 |   ns-regexp - A regular expression which specifies namespaces for searching schedules.
 8 | Return
 9 |   Sequence of schedules.
10 | 

create-scheduler

(create-scheduler {:keys [entries], :as config-option})
Create the scheduler.
11 | 
12 | Params
13 |   :entries - Sequence of schedules.
14 | Return
15 |   Scheduler component.
16 | 

map->Scheduler

(map->Scheduler m__5869__auto__)
Factory function for class jubot.scheduler.Scheduler, taking a map of keywords to field values.
17 | 

public-schedules

(public-schedules ns-regexp)
Return sequence of public schedule vars which matched SCHEDULE_REGEXP in specified namespaces.
18 | 
19 | Params
20 |   ns-regexp - A regular expression which specifies namespaces for searching schedules.
21 | Return
22 |   Sequence of schedule vars.
23 | 

schedule

(schedule cron-expr f)
Generate a schedule from a pair of cronj format string and function.
24 | 
25 | Params
26 |   cron-expr - Cronj format string. http://docs.caudate.me/cronj/#crontab
27 |   f         - A function with zero parameter.
28 | Return
29 |   A schedule function.
30 | 

schedule->task

(schedule->task f)
Convert a schedule function to cronj task.
31 | 
32 | Params
33 |   f - A schedule function.
34 | Return
35 |   A cronj task.
36 | 

SCHEDULE_REGEXP

The regular expression for collecting schedules automatically.
37 | 

schedules

(schedules & args)
Generate sequence of schedules from pairs of cronj format string and function.
38 | 
39 | Params
40 |   args - Pairs of cronj format string and function.
41 | Return
42 |   Sequence of schedule functions.
43 | 
-------------------------------------------------------------------------------- /doc/api/jubot.adapter.slack.html: -------------------------------------------------------------------------------- 1 | 2 | jubot.adapter.slack documentation

jubot.adapter.slack

Jubot adapter for Slack.
 3 | https://slack.com/
 4 | 

->SlackAdapter

(->SlackAdapter name server handler)
Positional factory function for class jubot.adapter.slack.SlackAdapter.
 5 | 

map->SlackAdapter

(map->SlackAdapter m__5869__auto__)
Factory function for class jubot.adapter.slack.SlackAdapter, taking a map of keywords to field values.
 6 | 

not-from-slackbot

(not-from-slackbot username text)
If the user name of inputted message is "slackbot", return text as it is.
 7 | If that's not the case, return nil.
 8 | 
 9 | Params
10 |   username - The user name of inputted message.
11 |   text     - Message from user.
12 | Return
13 |   Text string or nil.
14 | 

process-input

(process-input this handler-fn params)
Process input from Slack.
15 | 
16 | Params
17 |   this       - REPL adapter.
18 |   handler-fn - A handler function.
19 |   params     - POST parameters.
20 |     {:channel_id   "xxxxxxxxx",
21 |      :token        "xxxxxxxxx",
22 |      :channel_name "test",
23 |      :user_id      "xxxxxxxxx",
24 |      :team_id      "xxxxxxxxx",
25 |      :service_id   "xxxxxxxxx",
26 |      :user_name    "slackbot",
27 |      :team_domain  "uochan",
28 |      :timestamp    "1422058599.000004",
29 |      :text         "foo bar"}
30 | Return
31 |   JSON string or empty string.
32 | 

process-output

(process-output this text & {:keys [as icon-url]})
Process output to Slack.
33 | 
34 | Params
35 |   this        - Slack adapter.
36 |     :name     - Bot's name.
37 |   text        - Output text to Slack.
38 |   option
39 |     :as       - Overwrite bot's name if you specify this option.
40 |     :icon-url - Customized icon URL.
41 | 

valid-outgoing-token

(valid-outgoing-token token text)
If the outgoing token is valid, return text as it is.
42 | If that's not the case, return nil.
43 | 
44 | Params
45 |   token - Outgoing token passed from slack.
46 |   text  - Message from user.
47 | Return
48 |   Text string or nil.
49 | 

wrap-adapter

(wrap-adapter handler adapter bot-handler)
Ring middleware to wrap jubot adapter on request.
50 | 
51 | Params
52 |   handler     - A ring handler.
53 |   adapter     - A jubot adapter.
54 |   bot-handler - A jubot handler function.
55 | Return
56 |   A ring handler.
57 | 
-------------------------------------------------------------------------------- /doc/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | Jubot 0.1.1 API documentation

Jubot 0.1.1

Chatbot framework in Clojure

jubot.adapter

Jubot adapter manager.

Public variables and functions:

jubot.adapter.repl

Jubot adapter for REPL.

jubot.adapter.util

Utilities for jubot adapter.

Public variables and functions:

jubot.brain

Jubot brain manager.

Public variables and functions:

jubot.brain.memory

Jubot brain for memory.

Public variables and functions:

jubot.brain.redis

Jubot brain for Redis.

Public variables and functions:

jubot.handler

Jubot handler utilities.

jubot.redef

Redefining function fot stubbing.

Public variables and functions:

jubot.require

Jubot utilities for requiring namespaces.

Public variables and functions:

jubot.scheduler.keep-awake

Built-in schedule to keep awake jubot system on PaaS. (such as Heroku)

Public variables and functions:

jubot.system

Jubot system manager.

Public variables and functions:

jubot.test

Jubot testing utilities

Public variables and functions:

-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------