changeset 618:5afc0b3a9ad8

Plugin joke: brand new plugin, closes #609 @2h The new joke plugin offers a convenient registry of jokes that are displayed in a random and unique order. It keeps track of displayed jokes per channel/server pairs.
author David Demelier <markand@malikania.fr>
date Tue, 19 Dec 2017 22:02:12 +0100
parents 241583937af0
children a2ece4ed9f5d
files CHANGES.md plugins/joke/joke.js plugins/joke/joke.md tests/src/plugins/CMakeLists.txt tests/src/plugins/joke/CMakeLists.txt tests/src/plugins/joke/jokes-empty.json tests/src/plugins/joke/jokes-invalid.json tests/src/plugins/joke/jokes-not-array.json tests/src/plugins/joke/jokes-toobig.json tests/src/plugins/joke/jokes.json tests/src/plugins/joke/main.cpp
diffstat 11 files changed, 411 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.md	Tue Dec 19 20:22:31 2017 +0100
+++ b/CHANGES.md	Tue Dec 19 22:02:12 2017 +0100
@@ -33,6 +33,10 @@
     (#594), (#595), (#681), (#697),
   - The libircclient has been replaced by a simple homemade library (#581).
 
+Plugins:
+
+  - Introduce brand new joke plugin (#609).
+
 irccd 2.2.0 2017-09-26
 ----------------------
 
--- a/plugins/joke/joke.js	Tue Dec 19 20:22:31 2017 +0100
+++ b/plugins/joke/joke.js	Tue Dec 19 22:02:12 2017 +0100
@@ -95,36 +95,62 @@
 
     try {
         var file = new File(path, "r");
+        var data = JSON.parse(file.read());
     } catch (e) {
         throw Error(path + ": " + e.message);
     }
 
-    var data = JSON.parse(file.read());
-
     if (!data || !data.length)
         throw Error(path + ": no jokes found");
 
+    // Ensure that jokes only contain strings.
     var jokes = data.filter(function (joke) {
-        return joke && joke.length <= Plugin.config["max-list-lines"];
+        if (!joke || joke.length == 0 || joke.length > parseInt(Plugin.config["max-list-lines"]))
+            return false;
+
+        for (var i = 0; i < joke.length; ++i)
+            if (typeof (joke[i]) !== "string")
+                return false;
+
+        return true;
     });
 
-    if (!jokes)
+    if (!jokes || jokes.length === 0)
         throw Error(path + ": empty jokes");
 
     return jokes;
 }
 
+/**
+ * Convert a pair server/channel into a unique identifier.
+ *
+ * \return channel@server
+ */
 function id(server, channel)
 {
     return channel + "@" + server.toString();
 }
 
+/**
+ * Show the joke in the specified channel.
+ *
+ * \warning this function does not check for max-list-lines parameter
+ * \param server the server object
+ * \param channel the channel string
+ * \param joke the joke array (array of strings)
+ */
 function show(server, channel, joke)
 {
     for (var l = 0; l < joke.length; ++l)
         server.message(channel, joke[l]);
 }
 
+/**
+ * Remove the joke from the table.
+ *
+ * \param i the server/channel identifier
+ * \param index the joke index
+ */
 function remove(i, index)
 {
     table[i].splice(index, 1);
@@ -160,3 +186,9 @@
     show(server, channel, table[i][index]);
     remove(i, index);
 }
+
+function onReload()
+{
+    // This will force reload of jokes on next onCommand.
+    table = {};
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/joke/joke.md	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,85 @@
+---
+title: "Joke plugin"
+header: "Joke plugin"
+guide: yes
+---
+
+The plugin **joke** is a convenient command to display jokes in a random order
+without displaying always the same.
+
+It loads jokes per channel/server pair and display a unique joke each time it is
+invoked.
+
+# Installation
+
+The plugin **joke** is distributed with irccd. To enable it add the following to
+your `plugins` section:
+
+```ini
+[plugins]
+joke = ""
+```
+
+## Usage
+
+The plugin **joke** requires a database of jokes file, it consists of a plain
+JSON file of array of array of strings.
+
+Example of **jokes.json** file:
+
+```javascript
+[
+    [
+        "Tip to generate a good random password:",
+        "Ask a Windows user to quit vim."
+    ],
+    [
+        "Have you tried turning it off and on again?"
+    ]
+]
+```
+
+This file contains two jokes, the first one will be printed on two lines while
+the second only has one.
+
+Then, invoke the plugin:
+
+```nohighlight
+markand: !joke
+irccd: Have you tried turning it off and on again?
+markand: !joke
+irccd: Tip to generate a good random password:
+irccd: Ask a Windows user to quit vim.
+```
+
+## Configuration
+
+The following options are available under the `[plugin.history]` section:
+
+  - **file**: (string) path to the JSON jokes files (Optional: defaults to data
+              directory/jokes.json)
+
+### Keywords supported
+
+The following keywords are supported:
+
+| Parameter | Keywords        |
+|-----------|-----------------|
+| **file**  | channel, server |
+
+Warning: if you use keywords in the **file** parameter, you won't have a default
+         joke database anymore.
+
+## Formats
+
+The **joke** plugin supports the following formats in `[format.joke]` section:
+
+  - **error**: (string) format when an internal error occured.
+
+### Keywords supported
+
+The following keywords are supported:
+
+| Format    | Keywords                          |
+|-----------|-----------------------------------|
+| **error** | channel, nickname, origin, server |
--- a/tests/src/plugins/CMakeLists.txt	Tue Dec 19 20:22:31 2017 +0100
+++ b/tests/src/plugins/CMakeLists.txt	Tue Dec 19 22:02:12 2017 +0100
@@ -20,5 +20,6 @@
 add_subdirectory(auth)
 add_subdirectory(hangman)
 add_subdirectory(history)
+add_subdirectory(joke)
 add_subdirectory(logger)
 add_subdirectory(plugin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/CMakeLists.txt	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,32 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+    NAME plugin-joke
+    SOURCES
+        main.cpp
+        jokes-empty.json
+        jokes-invalid.json
+        jokes.json
+        jokes-not-array.json
+        jokes-toobig.json
+    LIBRARIES libirccd
+    FLAGS
+        PLUGIN_NAME="joke"
+        PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/joke/joke.js"
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/jokes-empty.json	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,9 @@
+[
+    [
+    ],
+    [
+    ],
+    [
+        false
+    ]
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/jokes-invalid.json	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,12 @@
+[
+    [
+    ],
+    [
+        1234,
+        true,
+        "still hav a string though"
+    ],
+    [
+        "a"
+    ]
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/jokes-not-array.json	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,3 @@
+{
+    "reason": "this is not a valid jokes database"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/jokes-toobig.json	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,15 @@
+[
+    [
+        "xxx",
+        "xxx",
+        "xxx"
+    ],
+    [
+        "a"
+    ],
+    [
+        "yyy",
+        "yyy",
+        "yyy"
+    ]
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/jokes.json	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,9 @@
+[
+    [
+        "aaa"
+    ],
+    [
+        "bbbb",
+        "bbbb"
+    ]
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/plugins/joke/main.cpp	Tue Dec 19 22:02:12 2017 +0100
@@ -0,0 +1,205 @@
+/*
+ * main.cpp -- test joke plugin
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "Joke plugin"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/plugin_test.hpp>
+
+namespace irccd {
+
+class joke_test : public plugin_test {
+public:
+    joke_test()
+        : plugin_test(PLUGIN_NAME, PLUGIN_PATH)
+    {
+        plugin_->set_formats({
+            { "error", "error=#{server}:#{channel}:#{origin}:#{nickname}" }
+        });
+    }
+
+    void load(plugin_config config = {})
+    {
+        // Add file if not there.
+        if (config.count("file") == 0)
+            config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/jokes.json");
+
+        plugin_->set_config(config);
+        plugin_->on_load(irccd_);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(joke_test_suite, joke_test)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    /*
+     * Jokes.json have two jokes.
+     *
+     * aaa
+     *
+     * And
+     *
+     * bbbb
+     * bbbb
+     */
+    std::unordered_map<std::string, int> said{
+        { "aaa", 0 },
+        { "bbbb", 0 }
+    };
+
+    load();
+
+    auto call = [&] () {
+        plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+        auto cmd = server_->cqueue().back();
+
+        // "bbbb" is two lines.
+        if (cmd["message"] == "bbbb") {
+            auto first = server_->cqueue().front();
+
+            BOOST_TEST(first["command"].template get<std::string>() == "message");
+            BOOST_TEST(first["target"].template get<std::string>() == "#joke");
+            BOOST_TEST(first["message"].template get<std::string>() == "bbbb");
+        } else
+            BOOST_TEST(cmd["message"].template get<std::string>() == "aaa");
+
+        said[cmd["message"].template get<std::string>()] += 1;
+        server_->cqueue().clear();
+    };
+
+    call();
+    call();
+
+    BOOST_TEST(said.size() == 2U);
+    BOOST_TEST(said["aaa"] == 1U);
+    BOOST_TEST(said["bbbb"] == 1U);
+}
+
+BOOST_AUTO_TEST_CASE(toobig)
+{
+    // xxx and yyy are both 3-lines which we disallow. only a must be said.
+    load({
+        { "file", CMAKE_CURRENT_SOURCE_DIR "/jokes-toobig.json" },
+        { "max-list-lines", "2" }
+    });
+
+    std::unordered_map<std::string, int> said{
+        { "a", 0 }
+    };
+
+    auto call = [&] () {
+        plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+        auto cmd = server_->cqueue().back();
+
+        BOOST_TEST(cmd["command"].template get<std::string>() == "message");
+        BOOST_TEST(cmd["target"].template get<std::string>() == "#joke");
+        BOOST_TEST(cmd["message"].template get<std::string>() == "a");
+
+        said[cmd["message"].template get<std::string>()] += 1;
+        server_->cqueue().clear();
+    };
+
+    call();
+    call();
+    call();
+
+    BOOST_TEST(said.size() == 1U);
+    BOOST_TEST(said["a"] == 3U);
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    // Only a is the valid joke in this file.
+    load({
+        { "file", CMAKE_CURRENT_SOURCE_DIR "/jokes-invalid.json" },
+    });
+
+    std::unordered_map<std::string, int> said{
+        { "a", 0 }
+    };
+
+    auto call = [&] () {
+        plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+        auto cmd = server_->cqueue().back();
+
+        BOOST_TEST(cmd["command"].template get<std::string>() == "message");
+        BOOST_TEST(cmd["target"].template get<std::string>() == "#joke");
+        BOOST_TEST(cmd["message"].template get<std::string>() == "a");
+
+        server_->cqueue().clear();
+        said[cmd["message"].template get<std::string>()] += 1;
+    };
+
+    call();
+    call();
+    call();
+
+    BOOST_TEST(said.size() == 1U);
+    BOOST_TEST(said["a"] == 3U);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+    load({{"file", "doesnotexist.json"}});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#joke");
+    BOOST_TEST(cmd["message"].get<std::string>() == "error=test:#joke:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_CASE(not_array)
+{
+    load({{"file", CMAKE_CURRENT_SOURCE_DIR "/jokes-not-array.json"}});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#joke");
+    BOOST_TEST(cmd["message"].get<std::string>() == "error=test:#joke:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+    load({{"file", CMAKE_CURRENT_SOURCE_DIR "/jokes-empty.json"}});
+
+    plugin_->on_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+
+    auto cmd = server_->cqueue().back();
+
+    BOOST_TEST(cmd["command"].get<std::string>() == "message");
+    BOOST_TEST(cmd["target"].get<std::string>() == "#joke");
+    BOOST_TEST(cmd["message"].get<std::string>() == "error=test:#joke:jean!jean@localhost:jean");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd