diff --git a/README.md b/README.md index b52b793c..9f56967e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ PBot is a pragmatic IRCv3 Bot written in Perl * [Factoids](#factoids) * [Code factoids](#code-factoids) * [Plugins](#plugins) - * [Modules](#modules) + * [Applets](#applets) * [Functions](#functions) * [Scripting interface](#scripting-interface) * [Virtual machine to safely execute user-submitted code](#virtual-machine-to-safely-execute-user-submitted-code) @@ -243,8 +243,8 @@ Plugin | Description [Connect4](lib/PBot/Plugin/Connect4.pm) | The classic Connect-4 game. [Spinach](lib/PBot/Plugin/Spinach.pm) | An advanced multiplayer Trivia game engine with a twist! A question is shown. Everybody privately submits a false answer. All false answers and the true answer is shown. Everybody tries to guess the true answer. Points are gained when people pick your false answer! -#### Modules -Modules are external command-line executable programs and scripts that can be +#### Applets +Applets are external command-line executable programs and scripts that can be loaded as PBot commands. Suppose you have the [Qalculate!](https://qalculate.github.io/) command-line @@ -254,7 +254,7 @@ shell script containing: #!/bin/sh qalc "$*" -And let's call it `qalc.sh` and put it in PBot's `modules/` directory. +And let's call it `qalc.sh` and put it in PBot's `applets/` directory. Then you can load it with the [`load`](doc/Admin.md#load) command. @@ -265,20 +265,20 @@ Now you have a [Qalculate!](https://qalculate.github.io/) calculator in PBot! !qalc 2 * 2 2 * 2 = 4 -These are just some of the modules PBot comes with; there are several more: +These are just some of the applets PBot comes with; there are several more: -Module | Description +Applet | Description --- | --- -[C-to-English translator](modules/c2english) | Translates C code to natural English sentences. -[C precedence analyzer](modules/paren) | Adds parentheses to C code to demonstrate precedence. -[C Jeopardy! game](modules/cjeopardy) | C programming trivia game based on the Jeopardy! TV game show. -[C Standard citations](modules/c11std.pl) | Cite specified sections/paragraphs from the C standard. -[Virtual machine](modules/compiler_vm) | Executes arbitrary code and commands within a virtual machine. -[dict.org Dictionary](modules/dict.org.pl) | Interface to dict.org for definitions, translations, acronyms, etc. -[Urban Dictionary](modules/urban) | Search Urban Dictionary for definitions. -[Manpages](modules/man.pl) | Display a concise formatting of manual pages (designed for C functions) +[C-to-English translator](applets/c2english) | Translates C code to natural English sentences. +[C precedence analyzer](applets/paren) | Adds parentheses to C code to demonstrate precedence. +[C Jeopardy! game](applets/cjeopardy) | C programming trivia game based on the Jeopardy! TV game show. +[C Standard citations](applets/c11std.pl) | Cite specified sections/paragraphs from the C standard. +[Virtual machine](applets/compiler_vm) | Executes arbitrary code and commands within a virtual machine. +[dict.org Dictionary](applets/dict.org.pl) | Interface to dict.org for definitions, translations, acronyms, etc. +[Urban Dictionary](applets/urban) | Search Urban Dictionary for definitions. +[Manpages](applets/man.pl) | Display a concise formatting of manual pages (designed for C functions) -For more information, see the [Modules documentation](doc/Modules.md). +For more information, see the [Applets documentation](doc/Applets.md). #### Functions Functions are commands that accept input, manipulate it and then output the result. They are extremely @@ -355,7 +355,7 @@ It can display the value of the most recent statement if there is no program out !cc sizeof (int) no output: sizeof(int) = 4 -For more information about the C programming language plugin, see [the `cc` command in the Modules documentation.](doc/Modules.md#cc) +For more information about the C programming language plugin, see [the `cc` command in the Applets documentation.](doc/Applets.md#cc) For more information about the virtual machine, see the [Virtual Machine documentation.](doc/VirtualMachine.md) diff --git a/modules/ago.pl b/applets/ago.pl similarity index 100% rename from modules/ago.pl rename to applets/ago.pl diff --git a/modules/bashfaq.pl b/applets/bashfaq.pl similarity index 100% rename from modules/bashfaq.pl rename to applets/bashfaq.pl diff --git a/modules/bashfaq.txt b/applets/bashfaq.txt similarity index 100% rename from modules/bashfaq.txt rename to applets/bashfaq.txt diff --git a/modules/bashpf.pl b/applets/bashpf.pl similarity index 100% rename from modules/bashpf.pl rename to applets/bashpf.pl diff --git a/modules/bashpf.txt b/applets/bashpf.txt similarity index 100% rename from modules/bashpf.txt rename to applets/bashpf.txt diff --git a/modules/c11std.pl b/applets/c11std.pl similarity index 100% rename from modules/c11std.pl rename to applets/c11std.pl diff --git a/modules/c2english.pl b/applets/c2english.pl similarity index 100% rename from modules/c2english.pl rename to applets/c2english.pl diff --git a/modules/c2english/CGrammar.pm b/applets/c2english/CGrammar.pm similarity index 100% rename from modules/c2english/CGrammar.pm rename to applets/c2english/CGrammar.pm diff --git a/modules/c2english/c2eng.pl b/applets/c2english/c2eng.pl similarity index 100% rename from modules/c2english/c2eng.pl rename to applets/c2english/c2eng.pl diff --git a/modules/c99std.pl b/applets/c99std.pl similarity index 100% rename from modules/c99std.pl rename to applets/c99std.pl diff --git a/modules/cdecl.pl b/applets/cdecl.pl similarity index 100% rename from modules/cdecl.pl rename to applets/cdecl.pl diff --git a/modules/cfacts.txt b/applets/cfacts.txt similarity index 100% rename from modules/cfacts.txt rename to applets/cfacts.txt diff --git a/modules/cfaq-questions.html b/applets/cfaq-questions.html similarity index 100% rename from modules/cfaq-questions.html rename to applets/cfaq-questions.html diff --git a/modules/cfaq.pl b/applets/cfaq.pl similarity index 100% rename from modules/cfaq.pl rename to applets/cfaq.pl diff --git a/modules/cjeopardy/.gitignore b/applets/cjeopardy/.gitignore similarity index 100% rename from modules/cjeopardy/.gitignore rename to applets/cjeopardy/.gitignore diff --git a/modules/cjeopardy/IRCColors.pm b/applets/cjeopardy/IRCColors.pm similarity index 100% rename from modules/cjeopardy/IRCColors.pm rename to applets/cjeopardy/IRCColors.pm diff --git a/modules/cjeopardy/QStatskeeper.pm b/applets/cjeopardy/QStatskeeper.pm similarity index 100% rename from modules/cjeopardy/QStatskeeper.pm rename to applets/cjeopardy/QStatskeeper.pm diff --git a/modules/cjeopardy/Scorekeeper.pm b/applets/cjeopardy/Scorekeeper.pm similarity index 100% rename from modules/cjeopardy/Scorekeeper.pm rename to applets/cjeopardy/Scorekeeper.pm diff --git a/modules/cjeopardy/cjeopardy.pl b/applets/cjeopardy/cjeopardy.pl similarity index 100% rename from modules/cjeopardy/cjeopardy.pl rename to applets/cjeopardy/cjeopardy.pl diff --git a/modules/cjeopardy/cjeopardy.txt b/applets/cjeopardy/cjeopardy.txt similarity index 100% rename from modules/cjeopardy/cjeopardy.txt rename to applets/cjeopardy/cjeopardy.txt diff --git a/modules/cjeopardy/cjeopardy_answer.pl b/applets/cjeopardy/cjeopardy_answer.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_answer.pl rename to applets/cjeopardy/cjeopardy_answer.pl diff --git a/modules/cjeopardy/cjeopardy_filter.pl b/applets/cjeopardy/cjeopardy_filter.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_filter.pl rename to applets/cjeopardy/cjeopardy_filter.pl diff --git a/modules/cjeopardy/cjeopardy_hint.pl b/applets/cjeopardy/cjeopardy_hint.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_hint.pl rename to applets/cjeopardy/cjeopardy_hint.pl diff --git a/modules/cjeopardy/cjeopardy_qstats.pl b/applets/cjeopardy/cjeopardy_qstats.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_qstats.pl rename to applets/cjeopardy/cjeopardy_qstats.pl diff --git a/modules/cjeopardy/cjeopardy_scores.pl b/applets/cjeopardy/cjeopardy_scores.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_scores.pl rename to applets/cjeopardy/cjeopardy_scores.pl diff --git a/modules/cjeopardy/cjeopardy_show.pl b/applets/cjeopardy/cjeopardy_show.pl similarity index 100% rename from modules/cjeopardy/cjeopardy_show.pl rename to applets/cjeopardy/cjeopardy_show.pl diff --git a/modules/cjeopardy/data/.gitignore b/applets/cjeopardy/data/.gitignore similarity index 100% rename from modules/cjeopardy/data/.gitignore rename to applets/cjeopardy/data/.gitignore diff --git a/modules/codepad.pl b/applets/codepad.pl similarity index 100% rename from modules/codepad.pl rename to applets/codepad.pl diff --git a/modules/compiler_block.pl b/applets/compiler_block.pl similarity index 100% rename from modules/compiler_block.pl rename to applets/compiler_block.pl diff --git a/modules/compiler_client.pl b/applets/compiler_client.pl similarity index 100% rename from modules/compiler_client.pl rename to applets/compiler_client.pl diff --git a/modules/compiler_vm/Diff.pm b/applets/compiler_vm/Diff.pm similarity index 100% rename from modules/compiler_vm/Diff.pm rename to applets/compiler_vm/Diff.pm diff --git a/modules/compiler_vm/README b/applets/compiler_vm/README similarity index 100% rename from modules/compiler_vm/README rename to applets/compiler_vm/README diff --git a/modules/compiler_vm/cc b/applets/compiler_vm/cc similarity index 100% rename from modules/compiler_vm/cc rename to applets/compiler_vm/cc diff --git a/modules/compiler_vm/compiler_client.pl b/applets/compiler_vm/compiler_client.pl similarity index 100% rename from modules/compiler_vm/compiler_client.pl rename to applets/compiler_vm/compiler_client.pl diff --git a/modules/compiler_vm/compiler_server.pl b/applets/compiler_vm/compiler_server.pl similarity index 100% rename from modules/compiler_vm/compiler_server.pl rename to applets/compiler_vm/compiler_server.pl diff --git a/modules/compiler_vm/compiler_server_watchdog.pl b/applets/compiler_vm/compiler_server_watchdog.pl similarity index 100% rename from modules/compiler_vm/compiler_server_watchdog.pl rename to applets/compiler_vm/compiler_server_watchdog.pl diff --git a/modules/compiler_vm/compiler_vm_client.pl b/applets/compiler_vm/compiler_vm_client.pl similarity index 100% rename from modules/compiler_vm/compiler_vm_client.pl rename to applets/compiler_vm/compiler_vm_client.pl diff --git a/modules/compiler_vm/compiler_vm_server.pl b/applets/compiler_vm/compiler_vm_server.pl similarity index 100% rename from modules/compiler_vm/compiler_vm_server.pl rename to applets/compiler_vm/compiler_vm_server.pl diff --git a/modules/compiler_vm/compiler_watchdog.pl b/applets/compiler_vm/compiler_watchdog.pl similarity index 100% rename from modules/compiler_vm/compiler_watchdog.pl rename to applets/compiler_vm/compiler_watchdog.pl diff --git a/modules/compiler_vm/history/.gitignore b/applets/compiler_vm/history/.gitignore similarity index 100% rename from modules/compiler_vm/history/.gitignore rename to applets/compiler_vm/history/.gitignore diff --git a/modules/compiler_vm/languages/_c_base.pm b/applets/compiler_vm/languages/_c_base.pm similarity index 100% rename from modules/compiler_vm/languages/_c_base.pm rename to applets/compiler_vm/languages/_c_base.pm diff --git a/modules/compiler_vm/languages/_default.pm b/applets/compiler_vm/languages/_default.pm similarity index 100% rename from modules/compiler_vm/languages/_default.pm rename to applets/compiler_vm/languages/_default.pm diff --git a/modules/compiler_vm/languages/bash.pm b/applets/compiler_vm/languages/bash.pm similarity index 100% rename from modules/compiler_vm/languages/bash.pm rename to applets/compiler_vm/languages/bash.pm diff --git a/modules/compiler_vm/languages/bc.pm b/applets/compiler_vm/languages/bc.pm similarity index 100% rename from modules/compiler_vm/languages/bc.pm rename to applets/compiler_vm/languages/bc.pm diff --git a/modules/compiler_vm/languages/bf.pm b/applets/compiler_vm/languages/bf.pm similarity index 100% rename from modules/compiler_vm/languages/bf.pm rename to applets/compiler_vm/languages/bf.pm diff --git a/modules/compiler_vm/languages/c11.pm b/applets/compiler_vm/languages/c11.pm similarity index 100% rename from modules/compiler_vm/languages/c11.pm rename to applets/compiler_vm/languages/c11.pm diff --git a/modules/compiler_vm/languages/c89.pm b/applets/compiler_vm/languages/c89.pm similarity index 100% rename from modules/compiler_vm/languages/c89.pm rename to applets/compiler_vm/languages/c89.pm diff --git a/modules/compiler_vm/languages/c99.pm b/applets/compiler_vm/languages/c99.pm similarity index 100% rename from modules/compiler_vm/languages/c99.pm rename to applets/compiler_vm/languages/c99.pm diff --git a/modules/compiler_vm/languages/clang.pm b/applets/compiler_vm/languages/clang.pm similarity index 100% rename from modules/compiler_vm/languages/clang.pm rename to applets/compiler_vm/languages/clang.pm diff --git a/modules/compiler_vm/languages/clang11.pm b/applets/compiler_vm/languages/clang11.pm similarity index 100% rename from modules/compiler_vm/languages/clang11.pm rename to applets/compiler_vm/languages/clang11.pm diff --git a/modules/compiler_vm/languages/clang89.pm b/applets/compiler_vm/languages/clang89.pm similarity index 100% rename from modules/compiler_vm/languages/clang89.pm rename to applets/compiler_vm/languages/clang89.pm diff --git a/modules/compiler_vm/languages/clang99.pm b/applets/compiler_vm/languages/clang99.pm similarity index 100% rename from modules/compiler_vm/languages/clang99.pm rename to applets/compiler_vm/languages/clang99.pm diff --git a/modules/compiler_vm/languages/clangpp.pm b/applets/compiler_vm/languages/clangpp.pm similarity index 100% rename from modules/compiler_vm/languages/clangpp.pm rename to applets/compiler_vm/languages/clangpp.pm diff --git a/modules/compiler_vm/languages/clisp.pm b/applets/compiler_vm/languages/clisp.pm similarity index 100% rename from modules/compiler_vm/languages/clisp.pm rename to applets/compiler_vm/languages/clisp.pm diff --git a/modules/compiler_vm/languages/cpp.pm b/applets/compiler_vm/languages/cpp.pm similarity index 100% rename from modules/compiler_vm/languages/cpp.pm rename to applets/compiler_vm/languages/cpp.pm diff --git a/modules/compiler_vm/languages/freebasic.pm b/applets/compiler_vm/languages/freebasic.pm similarity index 100% rename from modules/compiler_vm/languages/freebasic.pm rename to applets/compiler_vm/languages/freebasic.pm diff --git a/modules/compiler_vm/languages/go.pm b/applets/compiler_vm/languages/go.pm similarity index 100% rename from modules/compiler_vm/languages/go.pm rename to applets/compiler_vm/languages/go.pm diff --git a/modules/compiler_vm/languages/haskell.pm b/applets/compiler_vm/languages/haskell.pm similarity index 100% rename from modules/compiler_vm/languages/haskell.pm rename to applets/compiler_vm/languages/haskell.pm diff --git a/modules/compiler_vm/languages/java.pm b/applets/compiler_vm/languages/java.pm similarity index 100% rename from modules/compiler_vm/languages/java.pm rename to applets/compiler_vm/languages/java.pm diff --git a/modules/compiler_vm/languages/javascript.pm b/applets/compiler_vm/languages/javascript.pm similarity index 100% rename from modules/compiler_vm/languages/javascript.pm rename to applets/compiler_vm/languages/javascript.pm diff --git a/modules/compiler_vm/languages/ksh.pm b/applets/compiler_vm/languages/ksh.pm similarity index 100% rename from modules/compiler_vm/languages/ksh.pm rename to applets/compiler_vm/languages/ksh.pm diff --git a/modules/compiler_vm/languages/lua.pm b/applets/compiler_vm/languages/lua.pm similarity index 100% rename from modules/compiler_vm/languages/lua.pm rename to applets/compiler_vm/languages/lua.pm diff --git a/modules/compiler_vm/languages/perl.pm b/applets/compiler_vm/languages/perl.pm similarity index 100% rename from modules/compiler_vm/languages/perl.pm rename to applets/compiler_vm/languages/perl.pm diff --git a/modules/compiler_vm/languages/php.pm b/applets/compiler_vm/languages/php.pm similarity index 100% rename from modules/compiler_vm/languages/php.pm rename to applets/compiler_vm/languages/php.pm diff --git a/modules/compiler_vm/languages/python.pm b/applets/compiler_vm/languages/python.pm similarity index 100% rename from modules/compiler_vm/languages/python.pm rename to applets/compiler_vm/languages/python.pm diff --git a/modules/compiler_vm/languages/python3.pm b/applets/compiler_vm/languages/python3.pm similarity index 100% rename from modules/compiler_vm/languages/python3.pm rename to applets/compiler_vm/languages/python3.pm diff --git a/modules/compiler_vm/languages/qbasic.pm b/applets/compiler_vm/languages/qbasic.pm similarity index 100% rename from modules/compiler_vm/languages/qbasic.pm rename to applets/compiler_vm/languages/qbasic.pm diff --git a/modules/compiler_vm/languages/ruby.pm b/applets/compiler_vm/languages/ruby.pm similarity index 100% rename from modules/compiler_vm/languages/ruby.pm rename to applets/compiler_vm/languages/ruby.pm diff --git a/modules/compiler_vm/languages/scheme.pm b/applets/compiler_vm/languages/scheme.pm similarity index 100% rename from modules/compiler_vm/languages/scheme.pm rename to applets/compiler_vm/languages/scheme.pm diff --git a/modules/compiler_vm/languages/server/_c_base.pm b/applets/compiler_vm/languages/server/_c_base.pm similarity index 100% rename from modules/compiler_vm/languages/server/_c_base.pm rename to applets/compiler_vm/languages/server/_c_base.pm diff --git a/modules/compiler_vm/languages/server/_default.pm b/applets/compiler_vm/languages/server/_default.pm similarity index 100% rename from modules/compiler_vm/languages/server/_default.pm rename to applets/compiler_vm/languages/server/_default.pm diff --git a/modules/compiler_vm/languages/server/c11.pm b/applets/compiler_vm/languages/server/c11.pm similarity index 100% rename from modules/compiler_vm/languages/server/c11.pm rename to applets/compiler_vm/languages/server/c11.pm diff --git a/modules/compiler_vm/languages/server/c89.pm b/applets/compiler_vm/languages/server/c89.pm similarity index 100% rename from modules/compiler_vm/languages/server/c89.pm rename to applets/compiler_vm/languages/server/c89.pm diff --git a/modules/compiler_vm/languages/server/c99.pm b/applets/compiler_vm/languages/server/c99.pm similarity index 100% rename from modules/compiler_vm/languages/server/c99.pm rename to applets/compiler_vm/languages/server/c99.pm diff --git a/modules/compiler_vm/languages/server/clang.pm b/applets/compiler_vm/languages/server/clang.pm similarity index 100% rename from modules/compiler_vm/languages/server/clang.pm rename to applets/compiler_vm/languages/server/clang.pm diff --git a/modules/compiler_vm/languages/server/clang11.pm b/applets/compiler_vm/languages/server/clang11.pm similarity index 100% rename from modules/compiler_vm/languages/server/clang11.pm rename to applets/compiler_vm/languages/server/clang11.pm diff --git a/modules/compiler_vm/languages/server/clang89.pm b/applets/compiler_vm/languages/server/clang89.pm similarity index 100% rename from modules/compiler_vm/languages/server/clang89.pm rename to applets/compiler_vm/languages/server/clang89.pm diff --git a/modules/compiler_vm/languages/server/clang99.pm b/applets/compiler_vm/languages/server/clang99.pm similarity index 100% rename from modules/compiler_vm/languages/server/clang99.pm rename to applets/compiler_vm/languages/server/clang99.pm diff --git a/modules/compiler_vm/languages/server/clangpp.pm b/applets/compiler_vm/languages/server/clangpp.pm similarity index 100% rename from modules/compiler_vm/languages/server/clangpp.pm rename to applets/compiler_vm/languages/server/clangpp.pm diff --git a/modules/compiler_vm/languages/server/cpp.pm b/applets/compiler_vm/languages/server/cpp.pm similarity index 100% rename from modules/compiler_vm/languages/server/cpp.pm rename to applets/compiler_vm/languages/server/cpp.pm diff --git a/modules/compiler_vm/languages/server/freebasic.pm b/applets/compiler_vm/languages/server/freebasic.pm similarity index 100% rename from modules/compiler_vm/languages/server/freebasic.pm rename to applets/compiler_vm/languages/server/freebasic.pm diff --git a/modules/compiler_vm/languages/server/haskell.pm b/applets/compiler_vm/languages/server/haskell.pm similarity index 100% rename from modules/compiler_vm/languages/server/haskell.pm rename to applets/compiler_vm/languages/server/haskell.pm diff --git a/modules/compiler_vm/languages/server/java.pm b/applets/compiler_vm/languages/server/java.pm similarity index 100% rename from modules/compiler_vm/languages/server/java.pm rename to applets/compiler_vm/languages/server/java.pm diff --git a/modules/compiler_vm/languages/server/qbasic.pm b/applets/compiler_vm/languages/server/qbasic.pm similarity index 100% rename from modules/compiler_vm/languages/server/qbasic.pm rename to applets/compiler_vm/languages/server/qbasic.pm diff --git a/modules/compiler_vm/languages/server/tendra.pm b/applets/compiler_vm/languages/server/tendra.pm similarity index 100% rename from modules/compiler_vm/languages/server/tendra.pm rename to applets/compiler_vm/languages/server/tendra.pm diff --git a/modules/compiler_vm/languages/sh.pm b/applets/compiler_vm/languages/sh.pm similarity index 100% rename from modules/compiler_vm/languages/sh.pm rename to applets/compiler_vm/languages/sh.pm diff --git a/modules/compiler_vm/languages/tcl.pm b/applets/compiler_vm/languages/tcl.pm similarity index 100% rename from modules/compiler_vm/languages/tcl.pm rename to applets/compiler_vm/languages/tcl.pm diff --git a/modules/compiler_vm/languages/tendra.pm b/applets/compiler_vm/languages/tendra.pm similarity index 100% rename from modules/compiler_vm/languages/tendra.pm rename to applets/compiler_vm/languages/tendra.pm diff --git a/modules/compiler_vm/languages/zsh.pm b/applets/compiler_vm/languages/zsh.pm similarity index 100% rename from modules/compiler_vm/languages/zsh.pm rename to applets/compiler_vm/languages/zsh.pm diff --git a/modules/compiler_vm/misc/compiler.xml b/applets/compiler_vm/misc/compiler.xml similarity index 100% rename from modules/compiler_vm/misc/compiler.xml rename to applets/compiler_vm/misc/compiler.xml diff --git a/modules/compiler_vm/misc/compiler_server_qemu.pl b/applets/compiler_vm/misc/compiler_server_qemu.pl similarity index 100% rename from modules/compiler_vm/misc/compiler_server_qemu.pl rename to applets/compiler_vm/misc/compiler_server_qemu.pl diff --git a/modules/compiler_vm/misc/compiler_server_qemu_win32.pl b/applets/compiler_vm/misc/compiler_server_qemu_win32.pl similarity index 100% rename from modules/compiler_vm/misc/compiler_server_qemu_win32.pl rename to applets/compiler_vm/misc/compiler_server_qemu_win32.pl diff --git a/modules/compiler_vm/misc/compiler_server_vbox_win32.pl b/applets/compiler_vm/misc/compiler_server_vbox_win32.pl similarity index 100% rename from modules/compiler_vm/misc/compiler_server_vbox_win32.pl rename to applets/compiler_vm/misc/compiler_server_vbox_win32.pl diff --git a/modules/compiler_vm/misc/monitor b/applets/compiler_vm/misc/monitor similarity index 100% rename from modules/compiler_vm/misc/monitor rename to applets/compiler_vm/misc/monitor diff --git a/modules/compiler_vm/misc/mount_edit b/applets/compiler_vm/misc/mount_edit similarity index 100% rename from modules/compiler_vm/misc/mount_edit rename to applets/compiler_vm/misc/mount_edit diff --git a/modules/compiler_vm/misc/nptp_setup.zip b/applets/compiler_vm/misc/nptp_setup.zip similarity index 100% rename from modules/compiler_vm/misc/nptp_setup.zip rename to applets/compiler_vm/misc/nptp_setup.zip diff --git a/modules/compiler_vm/misc/prelude.h b/applets/compiler_vm/misc/prelude.h similarity index 100% rename from modules/compiler_vm/misc/prelude.h rename to applets/compiler_vm/misc/prelude.h diff --git a/modules/compiler_vm/misc/runqemu b/applets/compiler_vm/misc/runqemu similarity index 100% rename from modules/compiler_vm/misc/runqemu rename to applets/compiler_vm/misc/runqemu diff --git a/modules/compiler_vm/misc/runqemu.net b/applets/compiler_vm/misc/runqemu.net similarity index 100% rename from modules/compiler_vm/misc/runqemu.net rename to applets/compiler_vm/misc/runqemu.net diff --git a/modules/compiler_vm/misc/serial b/applets/compiler_vm/misc/serial similarity index 100% rename from modules/compiler_vm/misc/serial rename to applets/compiler_vm/misc/serial diff --git a/modules/compiler_vm/misc/umount_edit b/applets/compiler_vm/misc/umount_edit similarity index 100% rename from modules/compiler_vm/misc/umount_edit rename to applets/compiler_vm/misc/umount_edit diff --git a/modules/compliment b/applets/compliment similarity index 100% rename from modules/compliment rename to applets/compliment diff --git a/modules/compliments.txt b/applets/compliments.txt similarity index 100% rename from modules/compliments.txt rename to applets/compliments.txt diff --git a/modules/date.sh b/applets/date.sh similarity index 100% rename from modules/date.sh rename to applets/date.sh diff --git a/modules/define.pl b/applets/define.pl similarity index 100% rename from modules/define.pl rename to applets/define.pl diff --git a/modules/dice_roll.pl b/applets/dice_roll.pl similarity index 100% rename from modules/dice_roll.pl rename to applets/dice_roll.pl diff --git a/modules/dict.org.pl b/applets/dict.org.pl similarity index 100% rename from modules/dict.org.pl rename to applets/dict.org.pl diff --git a/modules/excuse.sh b/applets/excuse.sh similarity index 100% rename from modules/excuse.sh rename to applets/excuse.sh diff --git a/modules/excuses.txt b/applets/excuses.txt similarity index 100% rename from modules/excuses.txt rename to applets/excuses.txt diff --git a/modules/expand_macros.pl b/applets/expand_macros.pl similarity index 100% rename from modules/expand_macros.pl rename to applets/expand_macros.pl diff --git a/modules/fnord.pl b/applets/fnord.pl similarity index 100% rename from modules/fnord.pl rename to applets/fnord.pl diff --git a/modules/fnord.txt b/applets/fnord.txt similarity index 100% rename from modules/fnord.txt rename to applets/fnord.txt diff --git a/modules/funnyish_quote.pl b/applets/funnyish_quote.pl similarity index 100% rename from modules/funnyish_quote.pl rename to applets/funnyish_quote.pl diff --git a/modules/gdefine.pl b/applets/gdefine.pl similarity index 100% rename from modules/gdefine.pl rename to applets/gdefine.pl diff --git a/modules/gen_cfacts.pl b/applets/gen_cfacts.pl similarity index 100% rename from modules/gen_cfacts.pl rename to applets/gen_cfacts.pl diff --git a/modules/gencstd.pl b/applets/gencstd.pl similarity index 100% rename from modules/gencstd.pl rename to applets/gencstd.pl diff --git a/modules/get_title.pl b/applets/get_title.pl similarity index 100% rename from modules/get_title.pl rename to applets/get_title.pl diff --git a/modules/getcfact.pl b/applets/getcfact.pl similarity index 100% rename from modules/getcfact.pl rename to applets/getcfact.pl diff --git a/modules/headlines.pl b/applets/headlines.pl similarity index 100% rename from modules/headlines.pl rename to applets/headlines.pl diff --git a/applets/hoogle b/applets/hoogle new file mode 100755 index 00000000..0e6e7aba --- /dev/null +++ b/applets/hoogle @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +readarray -t lines < <(exec hoogle search -q --count=3 -- "$*"); IFS='|'; echo "${lines[*]}" diff --git a/modules/horoscope b/applets/horoscope similarity index 100% rename from modules/horoscope rename to applets/horoscope diff --git a/modules/horrorscope b/applets/horrorscope similarity index 100% rename from modules/horrorscope rename to applets/horrorscope diff --git a/modules/insult.pl b/applets/insult.pl similarity index 100% rename from modules/insult.pl rename to applets/insult.pl diff --git a/modules/insults.txt b/applets/insults.txt similarity index 100% rename from modules/insults.txt rename to applets/insults.txt diff --git a/modules/jisho.sh b/applets/jisho.sh similarity index 100% rename from modules/jisho.sh rename to applets/jisho.sh diff --git a/modules/lookupbot.pl b/applets/lookupbot.pl similarity index 100% rename from modules/lookupbot.pl rename to applets/lookupbot.pl diff --git a/modules/love_quote.pl b/applets/love_quote.pl similarity index 100% rename from modules/love_quote.pl rename to applets/love_quote.pl diff --git a/modules/man.pl b/applets/man.pl similarity index 100% rename from modules/man.pl rename to applets/man.pl diff --git a/modules/map.pl b/applets/map.pl similarity index 100% rename from modules/map.pl rename to applets/map.pl diff --git a/modules/math.pl b/applets/math.pl similarity index 100% rename from modules/math.pl rename to applets/math.pl diff --git a/modules/n1256.html b/applets/n1256.html similarity index 100% rename from modules/n1256.html rename to applets/n1256.html diff --git a/modules/n1256.out b/applets/n1256.out similarity index 100% rename from modules/n1256.out rename to applets/n1256.out diff --git a/modules/n1256.txt b/applets/n1256.txt similarity index 100% rename from modules/n1256.txt rename to applets/n1256.txt diff --git a/modules/n1570-cfact.txt b/applets/n1570-cfact.txt similarity index 100% rename from modules/n1570-cfact.txt rename to applets/n1570-cfact.txt diff --git a/modules/n1570.html b/applets/n1570.html similarity index 100% rename from modules/n1570.html rename to applets/n1570.html diff --git a/modules/n1570.out b/applets/n1570.out similarity index 100% rename from modules/n1570.out rename to applets/n1570.out diff --git a/modules/n1570.txt b/applets/n1570.txt similarity index 100% rename from modules/n1570.txt rename to applets/n1570.txt diff --git a/modules/nickometer.pl b/applets/nickometer.pl similarity index 100% rename from modules/nickometer.pl rename to applets/nickometer.pl diff --git a/modules/paren/INSTALL b/applets/paren/INSTALL similarity index 100% rename from modules/paren/INSTALL rename to applets/paren/INSTALL diff --git a/modules/paren/paren.py b/applets/paren/paren.py similarity index 100% rename from modules/paren/paren.py rename to applets/paren/paren.py diff --git a/modules/paren/stddef b/applets/paren/stddef similarity index 100% rename from modules/paren/stddef rename to applets/paren/stddef diff --git a/modules/paren/yacctab.py b/applets/paren/yacctab.py similarity index 100% rename from modules/paren/yacctab.py rename to applets/paren/yacctab.py diff --git a/modules/prototype.pl b/applets/prototype.pl similarity index 100% rename from modules/prototype.pl rename to applets/prototype.pl diff --git a/modules/qalc.pl b/applets/qalc.pl similarity index 100% rename from modules/qalc.pl rename to applets/qalc.pl diff --git a/modules/qrpn/qrpn.c b/applets/qrpn/qrpn.c similarity index 100% rename from modules/qrpn/qrpn.c rename to applets/qrpn/qrpn.c diff --git a/modules/qrpn/qrpn.h b/applets/qrpn/qrpn.h similarity index 100% rename from modules/qrpn/qrpn.h rename to applets/qrpn/qrpn.h diff --git a/modules/random_quote.pl b/applets/random_quote.pl similarity index 100% rename from modules/random_quote.pl rename to applets/random_quote.pl diff --git a/modules/rpn.pl b/applets/rpn.pl similarity index 100% rename from modules/rpn.pl rename to applets/rpn.pl diff --git a/modules/trans.pl b/applets/trans.pl similarity index 100% rename from modules/trans.pl rename to applets/trans.pl diff --git a/modules/urban b/applets/urban similarity index 100% rename from modules/urban rename to applets/urban diff --git a/modules/wikipedia.pl b/applets/wikipedia.pl similarity index 100% rename from modules/wikipedia.pl rename to applets/wikipedia.pl diff --git a/modules/wiktionary.pl b/applets/wiktionary.pl similarity index 100% rename from modules/wiktionary.pl rename to applets/wiktionary.pl diff --git a/modules/wiktionary.py b/applets/wiktionary.py similarity index 100% rename from modules/wiktionary.py rename to applets/wiktionary.py diff --git a/bin/pbot b/bin/pbot index 78f22922..10b80f6f 100755 --- a/bin/pbot +++ b/bin/pbot @@ -20,6 +20,6 @@ use PBot::Core; PBot::Core->new( data_dir => "$RealBin/../data", - module_dir => "$RealBin/../modules", + applet_dir => "$RealBin/../applets", update_dir => "$RealBin/../updates", )->start; diff --git a/data/commands b/data/commands index bf0396dc..56e56df4 100644 --- a/data/commands +++ b/data/commands @@ -1,6 +1,6 @@ { "$metadata$" : { - "name" : "Command metadata", + "name" : "Commands", "update_version" : 3503 }, "actiontrigger" : { @@ -301,7 +301,7 @@ "requires_cap" : 0 }, "load" : { - "help" : "This command loads a module as a PBot command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#load", + "help" : "This command loads an applet as a PBot command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#load", "requires_cap" : 1 }, "login" : { @@ -468,7 +468,7 @@ "requires_cap" : 1 }, "unload" : { - "help" : "Unloads a module and removes its associated command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#unload", + "help" : "Unloads an applet and removes its associated command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#unload", "requires_cap" : 1 }, "unmute" : { diff --git a/data/factoids.sqlite3 b/data/factoids.sqlite3 index 8f9291e1..3bc6053f 100644 Binary files a/data/factoids.sqlite3 and b/data/factoids.sqlite3 differ diff --git a/data/last_update b/data/last_update index 26148ad0..c4844ab2 100644 --- a/data/last_update +++ b/data/last_update @@ -1 +1 @@ -4315 +4422 diff --git a/data/registry b/data/registry index 2b656f60..f248ce68 100644 --- a/data/registry +++ b/data/registry @@ -169,6 +169,18 @@ } }, "general" : { + "applet_dir" : { + "type" : "text", + "value" : "./applets" + }, + "applet_repo" : { + "type" : "text", + "value" : "https://github.com/pragma-/pbot/blob/master/applets/" + }, + "applet_timeout" : { + "type" : "text", + "value" : "30" + }, "autojoin_wait_for_nickserv" : { "type" : "text", "value" : "0" @@ -205,18 +217,6 @@ "type" : "text", "value" : "nickserv" }, - "module_dir" : { - "type" : "text", - "value" : "./modules" - }, - "module_repo" : { - "type" : "text", - "value" : "https://github.com/pragma-/pbot/blob/master/modules/" - }, - "module_timeout" : { - "type" : "text", - "value" : "30" - }, "op_command" : { "type" : "text", "value" : "op $channel" diff --git a/doc/Admin.md b/doc/Admin.md index 044869bc..fdf24ff3 100644 --- a/doc/Admin.md +++ b/doc/Admin.md @@ -47,10 +47,10 @@ * [checkmute](#checkmute) * [invite](#invite) * [kick](#kick) -* [Module-management](#module-management) +* [Applet-management](#applet-management) * [load](#load) * [unload](#unload) - * [Listing modules](#listing-modules) + * [Listing applets](#listing-applets) * [Plugin-management](#plugin-management) * [plug](#plug) * [unplug](#unplug) @@ -541,30 +541,30 @@ Removes a user from the channel. `` can be a comma-separated list of multi Usage from channel: `kick [reason]` From private message: `kick [reason]` -## Module-management -Note that modules are "reloaded" each time they are executed. There is no need to `refresh` after editing a module. +## Applet-management +Note that applets are "reloaded" each time they are executed. There is no need to `refresh` after editing an applet. ### load -This command loads a module in `$data_dir/modules/` as a PBot command. It is -equivalent to `factadd`ing a new keyword and then setting its `type` to `module`. +This command loads an applet as a PBot command. It is equivalent to `factadd`ing a new keyword and then setting +its `type` to `applet`. -Usage: `load ` +Usage: `load ` -For example, to load `$data_dir/modules/qalc.sh` as the `qalc` command: +For example, to load `applets/qalc.sh` as the `qalc` command: !load qalc qalc.sh ### unload -This command unloads a module. It is equivalent to deleting the factoid keyword -the module was loaded as. +This command unloads an applet. It is equivalent to deleting the factoid keyword +the applet was loaded as. Usage: `unload ` -### Listing modules -To list the loaded modules, use the `list modules` command. This is not an admin command, but +### Listing applets +To list the loaded applets, use the `list applets` command. This is not an admin command, but it is included here for completeness. -Usage: `list modules` +Usage: `list applets` ## Plugin-management ### plug @@ -808,7 +808,7 @@ Exports specified list to HTML file in `$data_dir`. Usage: `export ` ### refresh -Refreshes/reloads PBot core modules and plugins (not the command-line modules since those are executed/loaded each time they are invoked). +Refreshes/reloads PBot core modules and plugins (not the command-line applets since those are executed/loaded each time they are invoked). For example, suppose you edit some PBot source file, be it a core file such as PBot/Factoids.pm or a Plugin such as Plugins/Wttr.pm. Rather than shut the bot down and restart it, you can simply use diff --git a/doc/Modules.md b/doc/Applets.md similarity index 98% rename from doc/Modules.md rename to doc/Applets.md index 8c0bf946..10ed5926 100644 --- a/doc/Modules.md +++ b/doc/Applets.md @@ -1,9 +1,9 @@ -# Modules +# Applets * [About](#about) -* [Creating modules](#creating-modules) -* [Documentation for built-in modules](#documentation-for-built-in-modules) +* [Creating applets](#creating-applets) +* [Documentation for built-in applets](#documentation-for-built-in-applets) * [cc](#cc) * [Usage](#usage) * [Supported Languages](#supported-languages) @@ -76,15 +76,15 @@ ## About -Modules are external command-line executable programs and scripts that can be +Applets are external command-line executable programs and scripts that can be loaded via PBot Factoids. -Command arguments are passed to Module scripts/programs as command-line arguments. The -standard output from the Module script/program is returned as the command result. The -standard error output is stored in a file named `-stderr` in the `modules/` +Command arguments are passed to Applet scripts/programs as command-line arguments. The +standard output from the Applet script/program is returned as the command result. The +standard error output is stored in a file named `-stderr` in the `applets/` directory. -## Creating modules +## Creating applets Suppose you have the [Qalculate!](https://qalculate.github.io/) command-line program and you want to provide a PBot command for it. You can create a _very_ simple shell script containing: @@ -92,24 +92,24 @@ shell script containing: #!/bin/sh qalc "$*" -And let's call it `qalc.sh` and put it in PBot's `modules/` directory. +And let's call it `qalc.sh` and put it in PBot's `applets/` directory. Then you can use the [`load`](Admin.md#load) command: !load qalc qalc.sh -Note: this is equivalent to creating a factoid and setting its `type` to `module`: +Note: this is equivalent to creating a factoid and setting its `type` to `applet`: !factadd global qalc qalc.sh - !factset global qalc type module + !factset global qalc type applet Now you have a `qalc` calculator in PBot! !qalc 2 * 2 2 * 2 = 4 -## Documentation for built-in modules -PBot comes with several Modules included. Here is the documentation for most of them. +## Documentation for built-in applets +PBot comes with several Applets included. Here is the documentation for most of them. ### cc Code compiler (and executor). This command will compile and execute user-provided code in a number of languages, and then display the compiler and/or program output. diff --git a/doc/Commands.md b/doc/Commands.md index e92701eb..2f69b15f 100644 --- a/doc/Commands.md +++ b/doc/Commands.md @@ -20,8 +20,8 @@ * [Functions](#functions) * [Factoids](#factoids) * [Code Factoids](#code-factoids) - * [Modules](#modules) - * [Listing all loaded modules](#listing-all-loaded-modules) + * [Applets](#applets) + * [Listing all loaded applets](#listing-all-loaded-applets) * [Commands documented here](#commands-documented-here) * [version](#version) * [help](#help) @@ -64,10 +64,10 @@ * [banlist](Admin.md#banlist) * [checkban](Admin.md#checkban) * [checkmute](Admin.md#checkmute) - * [Module-management](#module-management) + * [Applet-management](#applet-management) * [load](Admin.md#load) * [unload](Admin.md#unload) - * [list modules](Admin.md#listing-modules) + * [list applets](Admin.md#listing-applets) * [Plugin-management](#plugin-management) * [plug](Admin.md#plug) * [unplug](Admin.md#unplug) @@ -427,21 +427,21 @@ language specified by the argument following the `/code` command. For more information, see the [Code Factoid documentation.](Factoids.md#code) -#### Modules -Modules are simple stand-alone external command-line scripts and programs. Just +#### Applets +Applets are simple stand-alone external command-line scripts and programs. Just about any application that can be run in your command-line shell can be loaded as -a PBot module. +a PBot applet. -* only bot owner can install new command-line modules -* Modules do not have access to PBot internal API functions and data structures +* only bot owner can install new command-line applets +* Applets do not have access to PBot internal API functions and data structures -For more information, see the [Modules documentation.](Modules.md) +For more information, see the [Applets documentation.](Applets.md) -##### Listing all loaded modules -To list all of the currently loaded modules, use the `list modules` command. +##### Listing all loaded applets +To list all of the currently loaded applets, use the `list applets` command. - list modules - Loaded modules: ago bashfaq bashpf c11std c2english c99std cdecl cfact cfaq ... etc. + list applets + Loaded applets: ago bashfaq bashpf c11std c2english c99std cdecl cfact cfaq ... etc. ## Commands documented here These are the commands documented in this file. For commands documented in @@ -566,10 +566,10 @@ to have the command remember your location. ##### [checkban](Admin.md#checkban) ##### [checkmute](Admin.md#checkmute) -#### Module-management +#### Applet-management ##### [load](Admin.md#load) ##### [unload](Admin.md#unload) -##### [list modules](Admin.md#listing-modules) +##### [list applets](Admin.md#listing-applets) #### Plugin-management ##### [plug](Admin.md#plug) diff --git a/doc/FAQ.md b/doc/FAQ.md index 24572d58..3f26cbb1 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -95,7 +95,7 @@ If your command is registered by a plugin, use the [`cmdset`](Admin.md#cmdset) c to set the `preserve_whitespace` [command metadata](Admin.md#command-metadata-list) to control this behavior. -If your command is a command-line module, use the [`factset`](Factoids.md#factset) command +If your command is a command-line applet, use the [`factset`](Factoids.md#factset) command to set the `preserve_whitespace` [factoid metadata](Factoids.md#factoid-metadata-list) instead. ## How do I change my password? diff --git a/doc/Factoids.md b/doc/Factoids.md index 20af5bec..a9b9ead2 100644 --- a/doc/Factoids.md +++ b/doc/Factoids.md @@ -105,7 +105,7 @@ If a factoid begins with `/msg ` then PBot will privately message the fact ### /code Code Factoids are a special type of factoid whose text is treated as code and executed with a chosen programming language or interpreter. The output from code is then parsed and treated like any other factoid text. This allows anybody to add -new and unique commands to PBot without the need for installing Plugins or modules. +new and unique commands to PBot without the need for installing Plugins or applets. Code Factoids are executed within a virtual machine. You must install and set up a virtual machine with your operating system. See the [Virtual Machine](VirtualMachine.md) documentation for more information. @@ -123,35 +123,35 @@ languages or interpreters. Name | Description --- | --- -[bash](../modules/compiler_vm/languages/bash.pm) | Bourne-again Shell scripting language -[bc](../modules/compiler_vm/languages/bc.pm) | An arbitrary precision calculator language -[bf](../modules/compiler_vm/languages/bf.pm) | BrainFuck esoteric language -[c11](../modules/compiler_vm/languages/c11.pm) | C programming language using GCC -std=c11 -[c89](../modules/compiler_vm/languages/c89.pm) | C programming language using GCC -std=c89 -[c99](../modules/compiler_vm/languages/c99.pm) | C programming language using GCC -std=c99 -[clang11](../modules/compiler_vm/languages/clang11.pm) | C programming language using Clang -std=c11 -[clang89](../modules/compiler_vm/languages/clang89.pm) | C programming language using Clang -std=c89 -[clang99](../modules/compiler_vm/languages/clang99.pm) | C programming language using Clang -std=c99 -[clang](../modules/compiler_vm/languages/clang.pm) | Alias for `clang11` -[clangpp](../modules/compiler_vm/languages/clangpp.pm) | C++ programming language using Clang -[clisp](../modules/compiler_vm/languages/clisp.pm) | Common Lisp dialect of the Lisp programming language -[cpp](../modules/compiler_vm/languages/cpp.pm) | C++ using GCC -[freebasic](../modules/compiler_vm/languages/freebasic.pm) | FreeBasic BASIC compiler/interpreter -[go](../modules/compiler_vm/languages/go.pm) | Golang programming language -[haskell](../modules/compiler_vm/languages/haskell.pm) | Haskell programming language -[java](../modules/compiler_vm/languages/java.pm) | Java programming language -[javascript](../modules/compiler_vm/languages/javascript.pm) | JavaScript programming language -[ksh](../modules/compiler_vm/languages/ksh.pm) | Korn shell scripting language -[lua](../modules/compiler_vm/languages/lua.pm) | Lua programming language -[perl](../modules/compiler_vm/languages/perl.pm) | Perl programming language -[python3](../modules/compiler_vm/languages/python3.pm) | Python3 programming language -[python](../modules/compiler_vm/languages/python.pm) | Python programming language -[qbasic](../modules/compiler_vm/languages/qbasic.pm) | QuickBasic option using FreeBasic -[ruby](../modules/compiler_vm/languages/ruby.pm) | Ruby programming language -[scheme](../modules/compiler_vm/languages/scheme.pm) | Scheme dialect of the Lisp programming language -[sh](../modules/compiler_vm/languages/sh.pm) | Bourne Shell scripting language -[tcl](../modules/compiler_vm/languages/tcl.pm) | TCL scripting language -[zsh](../modules/compiler_vm/languages/zsh.pm) | Z Shell scripting language +[bash](../applets/compiler_vm/languages/bash.pm) | Bourne-again Shell scripting language +[bc](../applets/compiler_vm/languages/bc.pm) | An arbitrary precision calculator language +[bf](../applets/compiler_vm/languages/bf.pm) | BrainFuck esoteric language +[c11](../applets/compiler_vm/languages/c11.pm) | C programming language using GCC -std=c11 +[c89](../applets/compiler_vm/languages/c89.pm) | C programming language using GCC -std=c89 +[c99](../applets/compiler_vm/languages/c99.pm) | C programming language using GCC -std=c99 +[clang11](../applets/compiler_vm/languages/clang11.pm) | C programming language using Clang -std=c11 +[clang89](../applets/compiler_vm/languages/clang89.pm) | C programming language using Clang -std=c89 +[clang99](../applets/compiler_vm/languages/clang99.pm) | C programming language using Clang -std=c99 +[clang](../applets/compiler_vm/languages/clang.pm) | Alias for `clang11` +[clangpp](../applets/compiler_vm/languages/clangpp.pm) | C++ programming language using Clang +[clisp](../applets/compiler_vm/languages/clisp.pm) | Common Lisp dialect of the Lisp programming language +[cpp](../applets/compiler_vm/languages/cpp.pm) | C++ using GCC +[freebasic](../applets/compiler_vm/languages/freebasic.pm) | FreeBasic BASIC compiler/interpreter +[go](../applets/compiler_vm/languages/go.pm) | Golang programming language +[haskell](../applets/compiler_vm/languages/haskell.pm) | Haskell programming language +[java](../applets/compiler_vm/languages/java.pm) | Java programming language +[javascript](../applets/compiler_vm/languages/javascript.pm) | JavaScript programming language +[ksh](../applets/compiler_vm/languages/ksh.pm) | Korn shell scripting language +[lua](../applets/compiler_vm/languages/lua.pm) | Lua programming language +[perl](../applets/compiler_vm/languages/perl.pm) | Perl programming language +[python3](../applets/compiler_vm/languages/python3.pm) | Python3 programming language +[python](../applets/compiler_vm/languages/python.pm) | Python programming language +[qbasic](../applets/compiler_vm/languages/qbasic.pm) | QuickBasic option using FreeBasic +[ruby](../applets/compiler_vm/languages/ruby.pm) | Ruby programming language +[scheme](../applets/compiler_vm/languages/scheme.pm) | Scheme dialect of the Lisp programming language +[sh](../applets/compiler_vm/languages/sh.pm) | Bourne Shell scripting language +[tcl](../applets/compiler_vm/languages/tcl.pm) | TCL scripting language +[zsh](../applets/compiler_vm/languages/zsh.pm) | Z Shell scripting language #### Special variables @@ -453,7 +453,7 @@ You can use the [`factset`](#factset) command to set a special [factoid metadata * PBot gives orbitz a cookie. ## add_nick -You can use the [`factset`](#factset) command to set a special [factoid metadata](#factoid-metadata) key named `add_nick` to prepend the nick of the caller to the output. This is mostly useful for modules. +You can use the [`factset`](#factset) command to set a special [factoid metadata](#factoid-metadata) key named `add_nick` to prepend the nick of the caller to the output. This is mostly useful for applets. ## Channel namespaces Factoids added in one channel may be called/triggered in another channel or in private message, providing that the other channel doesn't already have a factoid of the same name (in which case that channel's factoid will be triggered). diff --git a/doc/QuickStart.md b/doc/QuickStart.md index 646b3b9b..32a9c51c 100644 --- a/doc/QuickStart.md +++ b/doc/QuickStart.md @@ -37,7 +37,7 @@ * [Commands](#commands) * [Factoids](#factoids) * [Plugins](#plugins) - * [Modules](#modules) + * [Applets](#applets) ## Installation @@ -86,7 +86,7 @@ Name | Description [`data/`](../data) | Default data-directory [`doc/`](../doc) | Helpful documentation [`lib/`](../lib) | PBot source tree -[`modules/`](../modules) | External command-line executables invokable as PBot commands +[`applets/`](../applets) | External command-line executables invokable as PBot commands [`updates/`](../updates) | Migration scripts run automatically by PBot after updates that modify data structures [`cpanfile`](../cpanfile) | CPAN dependencies file @@ -152,7 +152,7 @@ The CPAN modules may be installed with (assuming you do not need Windows support $ cpanm -n --installdeps . --with-all-features --without-feature=compiler_vm_win32 If you want to install the bare minimum CPAN modules required for PBot's core functionality, -you can use the following command. But be aware that several plugins and modules may not +you can use the following command. But be aware that several plugins and applets may not function. $ cpanm -n --installdeps . @@ -282,7 +282,7 @@ the PBot STDIN interface. #### Overriding directories You may override PBot's default directory locations via the command-line. - $ pbot data_dir=/path/to/data modules_dir=/path/to/modules + $ pbot data_dir=/path/to/data applets_dir=/path/to/applets #### Overriding registry You may override any of your Registry values via the command-line. Any overrides made will be @@ -480,8 +480,8 @@ Currently loaded plugins may be listed with the `pluglist` command. For more information, see the [Plugins documentation](Plugins.md). -### Modules -Modules are external command-line executable programs and scripts that can be +### Applets +Applets are external command-line executable programs and scripts that can be loaded as PBot commands. Suppose you have the [Qalculate!](https://qalculate.github.io/) command-line @@ -491,9 +491,9 @@ shell script containing: #!/bin/sh qalc "$*" -And let's call it `qalc.sh` and put it in PBot's `modules/` directory. +And let's call it `qalc.sh` and put it in PBot's `applets/` directory. -Then you can use the PBot [`load`](Admin.md#load) command to load the `modules/qalc.sh` script as the `qalc` command: +Then you can use the PBot [`load`](Admin.md#load) command to load the `applets/qalc.sh` script as the `qalc` command: !load qalc qalc.sh @@ -502,4 +502,4 @@ Now you have a [Qalculate!](https://qalculate.github.io/) calculator in PBot! !qalc 2 * 2 2 * 2 = 4 -For more information, see the [Modules documentation](Modules.md). +For more information, see the [Applets documentation](Applets.md). diff --git a/doc/README.md b/doc/README.md index 7abedc43..960cb0bd 100644 --- a/doc/README.md +++ b/doc/README.md @@ -35,7 +35,7 @@ * [Commands](QuickStart.md#commands) * [Factoids](QuickStart.md#factoids) * [Plugins](QuickStart.md#plugins) - * [Modules](QuickStart.md#modules) + * [Applets](QuickStart.md#applets) * [Plugins](Plugins.md#plugins) @@ -63,7 +63,7 @@ * [Functions](Commands.md#functions) * [Factoids](Commands.md#factoids) * [Code Factoids](Commands.md#code-factoids) - * [Modules](Commands.md#modules) + * [Applets](Commands.md#applets) * [Commands documented here](Commands.md#commands-documented-here) * [version](Commands.md#version) * [help](Commands.md#help) @@ -76,7 +76,7 @@ * [Logging in and out of PBot](Commands.md#logging-in-and-out-of-pbot) * [User-management](Commands.md#user-management) * [Channel-management](Commands.md#channel-management) - * [Module-management](Commands.md#module-management) + * [Applet-management](Commands.md#applet-management) * [Plugin-management](Commands.md#plugin-management) * [Command metadata](Commands.md#command-metadata) * [Event-queue management](Commands.md#event-queue-management) @@ -137,10 +137,10 @@ * [checkmute](Admin.md#checkmute) * [invite](Admin.md#invite) * [kick](Admin.md#kick) - * [Module-management](Admin.md#module-management) + * [Applet-management](Admin.md#applet-management) * [load](Admin.md#load) * [unload](Admin.md#unload) - * [Listing modules](Admin.md#listing-modules) + * [Listing applets](Admin.md#listing-applets) * [Plugin-management](Admin.md#plugin-management) * [plug](Admin.md#plug) * [unplug](Admin.md#unplug) @@ -248,60 +248,60 @@ * [Channel-specific Registry items](Registry.md#channel-specific-registry-items) -* [Modules](Modules.md#modules) - * [About](Modules.md#about) - * [Creating modules](Modules.md#creating-modules) - * [Documentation for built-in modules](Modules.md#documentation-for-built-in-modules) - * [cc](Modules.md#cc) - * [Usage](Modules.md#usage) - * [Supported Languages](Modules.md#supported-languages) - * [Default Language](Modules.md#default-language) - * [Disallowed system calls](Modules.md#disallowed-system-calls) - * [Program termination with no output](Modules.md#program-termination-with-no-output) - * [Abnormal program termination](Modules.md#abnormal-program-termination) - * [C and C++ Functionality](Modules.md#c-and-c-functionality) - * [Using the preprocessor](Modules.md#using-the-preprocessor) - * [main() Function Unnecessary](Modules.md#main-function-unnecessary) - * [Embedding Newlines](Modules.md#embedding-newlines) - * [Printing in binary/base2](Modules.md#printing-in-binarybase2) - * [Using the GDB debugger](Modules.md#using-the-gdb-debugger) - * [Interactive Editing](Modules.md#interactive-editing) - * [Some Examples](Modules.md#some-examples) - * [english](Modules.md#english) - * [expand](Modules.md#expand) - * [prec](Modules.md#prec) - * [paren](Modules.md#paren) - * [faq](Modules.md#faq) - * [cfact](Modules.md#cfact) - * [cjeopardy](Modules.md#cjeopardy) - * [hint](Modules.md#hint) - * [what](Modules.md#what) - * [w](Modules.md#w) - * [filter](Modules.md#filter) - * [score](Modules.md#score) - * [rank](Modules.md#rank) - * [reset](Modules.md#reset) - * [qstats](Modules.md#qstats) - * [qshow](Modules.md#qshow) - * [c99std](Modules.md#c99std) - * [c11std](Modules.md#c11std) - * [man](Modules.md#man) - * [google](Modules.md#google) - * [define](Modules.md#define) - * [dict](Modules.md#dict) - * [foldoc](Modules.md#foldoc) - * [vera](Modules.md#vera) - * [udict](Modules.md#udict) - * [wdict](Modules.md#wdict) - * [acronym](Modules.md#acronym) - * [math](Modules.md#math) - * [calc](Modules.md#calc) - * [qalc](Modules.md#qalc) - * [compliment](Modules.md#compliment) - * [insult](Modules.md#insult) - * [excuse](Modules.md#excuse) - * [horoscope](Modules.md#horoscope) - * [quote](Modules.md#quote) +* [Applets](Applets.md#applets) + * [About](Applets.md#about) + * [Creating applets](Applets.md#creating-applets) + * [Documentation for built-in applets](Applets.md#documentation-for-built-in-applets) + * [cc](Applets.md#cc) + * [Usage](Applets.md#usage) + * [Supported Languages](Applets.md#supported-languages) + * [Default Language](Applets.md#default-language) + * [Disallowed system calls](Applets.md#disallowed-system-calls) + * [Program termination with no output](Applets.md#program-termination-with-no-output) + * [Abnormal program termination](Applets.md#abnormal-program-termination) + * [C and C++ Functionality](Applets.md#c-and-c-functionality) + * [Using the preprocessor](Applets.md#using-the-preprocessor) + * [main() Function Unnecessary](Applets.md#main-function-unnecessary) + * [Embedding Newlines](Applets.md#embedding-newlines) + * [Printing in binary/base2](Applets.md#printing-in-binarybase2) + * [Using the GDB debugger](Applets.md#using-the-gdb-debugger) + * [Interactive Editing](Applets.md#interactive-editing) + * [Some Examples](Applets.md#some-examples) + * [english](Applets.md#english) + * [expand](Applets.md#expand) + * [prec](Applets.md#prec) + * [paren](Applets.md#paren) + * [faq](Applets.md#faq) + * [cfact](Applets.md#cfact) + * [cjeopardy](Applets.md#cjeopardy) + * [hint](Applets.md#hint) + * [what](Applets.md#what) + * [w](Applets.md#w) + * [filter](Applets.md#filter) + * [score](Applets.md#score) + * [rank](Applets.md#rank) + * [reset](Applets.md#reset) + * [qstats](Applets.md#qstats) + * [qshow](Applets.md#qshow) + * [c99std](Applets.md#c99std) + * [c11std](Applets.md#c11std) + * [man](Applets.md#man) + * [google](Applets.md#google) + * [define](Applets.md#define) + * [dict](Applets.md#dict) + * [foldoc](Applets.md#foldoc) + * [vera](Applets.md#vera) + * [udict](Applets.md#udict) + * [wdict](Applets.md#wdict) + * [acronym](Applets.md#acronym) + * [math](Applets.md#math) + * [calc](Applets.md#calc) + * [qalc](Applets.md#qalc) + * [compliment](Applets.md#compliment) + * [insult](Applets.md#insult) + * [excuse](Applets.md#excuse) + * [horoscope](Applets.md#horoscope) + * [quote](Applets.md#quote) * [Anti-Abuse](AntiAbuse.md#anti-abuse) diff --git a/doc/Registry.md b/doc/Registry.md index d4223b94..11cf124d 100644 --- a/doc/Registry.md +++ b/doc/Registry.md @@ -189,8 +189,9 @@ general.daemon | Run PBot in daemon mode. Closes stdin and stdout, writes only t general.deop_timeout | Time-out, in seconds, before PBot deops itself after being opped. | 300 general.default_ban_timeout | Default timeout for bans. | 24 hours general.default_mute_timeout | Default timeout for mutes. | 24 hours -general.module_dir | Path to PBot `modules/` directory. | -general.module_repo | URL to source code of PBot modules; used in `factinfo` | https://github.com/pragma-/pbot/tree/master/modules +general.applet_dir | Path to PBot `applets/` directory. | +general.applet_repo | URL to source code of PBot applets; used in `factinfo` | https://github.com/pragma-/pbot/tree/master/applets +general.applet_timeout | Duration, in seconds, of how long applets may run before being killed | 30 general.no_dehighlight_nicks | If set to at true value then when outputting text PBot will not convert nicks to text that avoids triggering IRC client nick-highlighting | not defined general.paste_ratelimit | How often, in seconds, between pastes to web paste-sites. | general.send_who_on_join | When joining a channel, send the `WHO` command to get detailed information about who is present, and to check for ban-evasions. | 1 diff --git a/lib/PBot/Core.pm b/lib/PBot/Core.pm index c9c1444c..9dd77f83 100644 --- a/lib/PBot/Core.pm +++ b/lib/PBot/Core.pm @@ -25,6 +25,7 @@ use Carp (); use PBot::Core::Logger; use PBot::Core::AntiFlood; use PBot::Core::AntiSpam; +use PBot::Core::Applets; use PBot::Core::BanList; use PBot::Core::BlackList; use PBot::Core::Capabilities; @@ -42,7 +43,6 @@ use PBot::Core::IRC; use PBot::Core::IRCHandlers; use PBot::Core::LagChecker; use PBot::Core::MessageHistory; -use PBot::Core::Modules; use PBot::Core::NickList; use PBot::Core::Plugins; use PBot::Core::ProcessManager; @@ -82,13 +82,13 @@ sub initialize { # process command-line arguments for path and registry overrides foreach my $arg (@ARGV) { - if ($arg =~ m/^-?(?:general\.)?((?:data|module|update)_dir)=(.*)$/) { + if ($arg =~ m/^-?(?:general\.)?((?:data|applet|update)_dir)=(.*)$/) { # check command-line arguments for directory overrides my $override = $1; my $value = $2; $value =~ s/[\\\/]$//; # strip trailing directory separator $conf{data_dir} = $value if $override eq 'data_dir'; - $conf{module_dir} = $value if $override eq 'module_dir'; + $conf{applet_dir} = $value if $override eq 'applet_dir'; $conf{update_dir} = $value if $override eq 'update_dir'; } else { # check command-line arguments for registry overrides @@ -112,7 +112,7 @@ sub initialize { } # make sure the paths exist - foreach my $path (qw/data_dir module_dir update_dir/) { + foreach my $path (qw/data_dir applet_dir update_dir/) { if (not -d $conf{$path}) { print STDERR "$path path ($conf{$path}) does not exist; aborting.\n"; exit; @@ -142,7 +142,7 @@ sub initialize { $self->{logger}->log("Args: @ARGV\n") if @ARGV; # log configured paths - $self->{logger}->log("module_dir: $conf{module_dir}\n"); + $self->{logger}->log("applet_dir: $conf{applet_dir}\n"); $self->{logger}->log(" data_dir: $conf{data_dir}\n"); $self->{logger}->log("update_dir: $conf{update_dir}\n"); @@ -180,6 +180,7 @@ sub initialize { $self->{users} = PBot::Core::Users->new(pbot => $self, filename => "$conf{data_dir}/users", %conf); $self->{antiflood} = PBot::Core::AntiFlood->new(pbot => $self, %conf); $self->{antispam} = PBot::Core::AntiSpam->new(pbot => $self, %conf); + $self->{applets} = PBot::Core::Applets->new(pbot => $self, %conf); $self->{banlist} = PBot::Core::BanList->new(pbot => $self, %conf); $self->{blacklist} = PBot::Core::BlackList->new(pbot => $self, filename => "$conf{data_dir}/blacklist", %conf); $self->{channels} = PBot::Core::Channels->new(pbot => $self, filename => "$conf{data_dir}/channels", %conf); @@ -193,7 +194,6 @@ sub initialize { $self->{interpreter} = PBot::Core::Interpreter->new(pbot => $self, %conf); $self->{lagchecker} = PBot::Core::LagChecker->new(pbot => $self, %conf); $self->{messagehistory} = PBot::Core::MessageHistory->new(pbot => $self, filename => "$conf{data_dir}/message_history.sqlite3", %conf); - $self->{modules} = PBot::Core::Modules->new(pbot => $self, %conf); $self->{nicklist} = PBot::Core::NickList->new(pbot => $self, %conf); $self->{parsedate} = PBot::Core::Utils::ParseDate->new(pbot => $self, %conf); $self->{plugins} = PBot::Core::Plugins->new(pbot => $self, %conf); diff --git a/lib/PBot/Core/Modules.pm b/lib/PBot/Core/Applets.pm similarity index 71% rename from lib/PBot/Core/Modules.pm rename to lib/PBot/Core/Applets.pm index 989d533a..975b7c17 100644 --- a/lib/PBot/Core/Modules.pm +++ b/lib/PBot/Core/Applets.pm @@ -1,15 +1,15 @@ -# File: Modules.pm +# File: Applets.pm # -# Purpose: Modules are command-line programs and scripts that can be loaded +# Purpose: Applets are command-line programs and scripts that can be loaded # via PBot factoids. Command arguments are passed as command-line arguments. # The standard output from the script is returned as the bot command result. -# The standard error output is stored in a file named -stderr in the -# modules/ directory. +# The standard error output is stored in a file named -stderr in the +# applets/ directory. # SPDX-FileCopyrightText: 2021 Pragmatic Software # SPDX-License-Identifier: MIT -package PBot::Core::Modules; +package PBot::Core::Applets; use parent 'PBot::Core::Class'; use PBot::Imports; @@ -21,20 +21,20 @@ sub initialize { # nothing to do here } -sub execute_module { +sub execute_applet { my ($self, $context) = @_; if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) { use Data::Dumper; $Data::Dumper::Sortkeys = 1; - $self->{pbot}->{logger}->log("execute_module\n"); + $self->{pbot}->{logger}->log("execute_applet\n"); $self->{pbot}->{logger}->log(Dumper $context); } - $self->{pbot}->{process_manager}->execute_process($context, sub { $self->launch_module(@_) }); + $self->{pbot}->{process_manager}->execute_process($context, sub { $self->launch_applet(@_) }); } -sub launch_module { +sub launch_applet { my ($self, $context) = @_; $context->{arguments} //= ''; @@ -43,7 +43,7 @@ sub launch_module { if (not @factoids or not $factoids[0]) { $context->{checkflood} = 1; - $self->{pbot}->{interpreter}->handle_result($context, "/msg $context->{nick} Failed to find module for '$context->{keyword}' in channel $context->{from}\n"); + $self->{pbot}->{interpreter}->handle_result($context, "/msg $context->{nick} Failed to find applet for '$context->{keyword}' in channel $context->{from}\n"); return; } @@ -53,27 +53,27 @@ sub launch_module { $context->{keyword} = $trigger; $context->{trigger} = $trigger; - my $module = $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $trigger, 'action'); + my $applet = $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $trigger, 'action'); $self->{pbot}->{logger}->log( '(' . (defined $context->{from} ? $context->{from} : "(undef)") . '): ' - . "$context->{hostmask}: Executing module [$context->{command}] $module $context->{arguments}\n" + . "$context->{hostmask}: Executing applet [$context->{command}] $applet $context->{arguments}\n" ); $context->{arguments} = $self->{pbot}->{factoids}->{variables}->expand_factoid_vars($context, $context->{arguments}); - my $module_dir = $self->{pbot}->{registry}->get_value('general', 'module_dir'); + my $applet_dir = $self->{pbot}->{registry}->get_value('general', 'applet_dir'); - if (not chdir $module_dir) { - $self->{pbot}->{logger}->log("Could not chdir to '$module_dir': $!\n"); - Carp::croak("Could not chdir to '$module_dir': $!"); + if (not chdir $applet_dir) { + $self->{pbot}->{logger}->log("Could not chdir to '$applet_dir': $!\n"); + Carp::croak("Could not chdir to '$applet_dir': $!"); } if ($self->{pbot}->{factoids}->{data}->{storage}->exists($channel, $trigger, 'workdir')) { chdir $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $trigger, 'workdir'); } - # FIXME -- add check to ensure $module exists + # FIXME -- add check to ensure $applet exists my ($exitval, $stdout, $stderr) = eval { my $args = $context->{arguments}; @@ -82,9 +82,9 @@ sub launch_module { $args = encode('UTF-8', $args); } - my @cmdline = ("./$module", $self->{pbot}->{interpreter}->split_line($args)); + my @cmdline = ("./$applet", $self->{pbot}->{interpreter}->split_line($args)); - my $timeout = $self->{pbot}->{registry}->get_value('general', 'module_timeout') // 30; + my $timeout = $self->{pbot}->{registry}->get_value('general', 'applet_timeout') // 30; my ($stdin, $stdout, $stderr); @@ -104,16 +104,16 @@ sub launch_module { ($exitval, $stdout, $stderr) = (-1, "$context->{trigger}: timed-out", ''); } else { ($exitval, $stdout, $stderr) = (-1, '', $error); - $self->{pbot}->{logger}->log("$context->{trigger}: error executing module: $error\n"); + $self->{pbot}->{logger}->log("$context->{trigger}: error executing applet: $error\n"); } } if (length $stderr) { - if (open(my $fh, '>>', "$module-stderr")) { + if (open(my $fh, '>>', "$applet-stderr")) { print $fh $stderr; close $fh; } else { - $self->{pbot}->{logger}->log("Failed to open $module-stderr: $!\n"); + $self->{pbot}->{logger}->log("Failed to open $applet-stderr: $!\n"); } } diff --git a/lib/PBot/Core/Commands/Modules.pm b/lib/PBot/Core/Commands/Applets.pm similarity index 51% rename from lib/PBot/Core/Commands/Modules.pm rename to lib/PBot/Core/Commands/Applets.pm index 01b969ee..bec46cd1 100644 --- a/lib/PBot/Core/Commands/Modules.pm +++ b/lib/PBot/Core/Commands/Applets.pm @@ -1,11 +1,11 @@ -# File: Modules.pm +# File: Applets.pm # -# Purpose: Registers commands to load and unload PBot modules. +# Purpose: Registers commands to load and unload PBot applets. # SPDX-FileCopyrightText: 2021 Pragmatic Software # SPDX-License-Identifier: MIT -package PBot::Core::Commands::Modules; +package PBot::Core::Commands::Applets; use parent 'PBot::Core::Class'; use PBot::Imports; @@ -16,7 +16,7 @@ use Encode; sub initialize { my ($self, %conf) = @_; - # bot commands to load and unload modules + # bot commands to load and unload applets $self->{pbot}->{commands}->register(sub { $self->cmd_load(@_) }, "load", 1); $self->{pbot}->{commands}->register(sub { $self->cmd_unload(@_) }, "unload", 1); } @@ -24,9 +24,9 @@ sub initialize { sub cmd_load { my ($self, $context) = @_; - my ($keyword, $module) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); + my ($keyword, $applet) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); - return "Usage: load " if not defined $module; + return "Usage: load " if not defined $applet; my $factoids = $self->{pbot}->{factoids}->{data}->{storage}; @@ -34,38 +34,38 @@ sub cmd_load { return 'There is already a keyword named ' . $factoids->get_data('.*', $keyword, '_name') . '.'; } - $self->{pbot}->{factoids}->{data}->add('module', '.*', $context->{hostmask}, $keyword, $module, 1); + $self->{pbot}->{factoids}->{data}->add('applet', '.*', $context->{hostmask}, $keyword, $applet, 1); $factoids->set('.*', $keyword, 'add_nick', 1, 1); $factoids->set('.*', $keyword, 'nooverride', 1); - $self->{pbot}->{logger}->log("$context->{hostmask} loaded module $keyword => $module\n"); + $self->{pbot}->{logger}->log("$context->{hostmask} loaded applet $keyword => $applet\n"); - return "Loaded module $keyword => $module"; + return "Loaded applet $keyword => $applet"; } sub cmd_unload { my ($self, $context) = @_; - my $module = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); + my $applet = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); - return "Usage: unload " if not defined $module; + return "Usage: unload " if not defined $applet; my $factoids = $self->{pbot}->{factoids}->{data}->{storage}; - if (not $factoids->exists('.*', $module)) { - return "/say $module not found."; + if (not $factoids->exists('.*', $applet)) { + return "/say $applet not found."; } - if ($factoids->get_data('.*', $module, 'type') ne 'module') { - return "/say " . $factoids->get_data('.*', $module, '_name') . ' is not a module.'; + if ($factoids->get_data('.*', $applet, 'type') ne 'applet') { + return "/say " . $factoids->get_data('.*', $applet, '_name') . ' is not an applet.'; } - my $name = $factoids->get_data('.*', $module, '_name'); + my $name = $factoids->get_data('.*', $applet, '_name'); - $factoids->remove('.*', $module); + $factoids->remove('.*', $applet); - $self->{pbot}->{logger}->log("$context->{hostmask} unloaded module $module\n"); + $self->{pbot}->{logger}->log("$context->{hostmask} unloaded applet $applet\n"); return "/say $name unloaded."; } diff --git a/lib/PBot/Core/Commands/ChanOp.pm b/lib/PBot/Core/Commands/ChanOp.pm index ffb14f07..1d519cd3 100644 --- a/lib/PBot/Core/Commands/ChanOp.pm +++ b/lib/PBot/Core/Commands/ChanOp.pm @@ -638,7 +638,7 @@ sub cmd_kick { my @insults; if (not length $reason) { - if (open my $fh, '<', $self->{pbot}->{registry}->get_value('general', 'module_dir') . '/insults.txt') { + if (open my $fh, '<', $self->{pbot}->{registry}->get_value('general', 'applet_dir') . '/insults.txt') { @insults = <$fh>; close $fh; $reason = $insults[rand @insults]; diff --git a/lib/PBot/Core/Commands/Factoids.pm b/lib/PBot/Core/Commands/Factoids.pm index bbee28e6..aa439d75 100644 --- a/lib/PBot/Core/Commands/Factoids.pm +++ b/lib/PBot/Core/Commands/Factoids.pm @@ -43,8 +43,8 @@ our %factoid_metadata_capabilities = ( sub initialize { my ($self, %conf) = @_; - $self->{pbot}->{registry}->add_default('text', 'general', 'module_repo', $conf{module_repo} - // 'https://github.com/pragma-/pbot/blob/master/modules/'); + $self->{pbot}->{registry}->add_default('text', 'general', 'applet_repo', $conf{applet_repo} + // 'https://github.com/pragma-/pbot/blob/master/applets/'); $self->{pbot}->{commands}->register(sub { $self->cmd_factadd(@_) }, "learn", 0); $self->{pbot}->{commands}->register(sub { $self->cmd_factadd(@_) }, "factadd", 0); @@ -833,7 +833,7 @@ sub cmd_factrem { $channel_name = 'global' if $channel_name eq '.*'; $trigger_name = "\"$trigger_name\"" if $trigger_name =~ / /; - if ($factoids->get_data($channel, $trigger, 'type') eq 'module') { return "/say $trigger_name is not a factoid."; } + if ($factoids->get_data($channel, $trigger, 'type') eq 'applet') { return "/say $trigger_name is not a factoid."; } if ($channel =~ /^#/ and $from_chan =~ /^#/ and lc $channel ne lc $from_chan) { return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to remove this factoid."; @@ -896,7 +896,7 @@ sub cmd_factshow { } $result .= $factoids->get_data($channel, $trigger, 'action'); - $result .= ' [module]' if $factoids->get_data($channel, $trigger, 'type') eq 'module'; + $result .= ' [applet]' if $factoids->get_data($channel, $trigger, 'type') eq 'applet'; $result = "[$channel_name] $result" if $channel ne lc $chan; return $result; } @@ -1030,16 +1030,16 @@ sub cmd_factinfo { . ')'; } - # module - if ($factoids->get_data($channel, $trigger, 'type') eq 'module') { - my $module_repo = $self->{pbot}->{registry}->get_value('general', 'module_repo'); - $module_repo .= $factoids->get_data($channel, $trigger, 'workdir') . '/' if $factoids->exists($channel, $trigger, 'workdir'); + # applet + if ($factoids->get_data($channel, $trigger, 'type') eq 'applet') { + my $applet_repo = $self->{pbot}->{registry}->get_value('general', 'applet_repo'); + $applet_repo .= $factoids->get_data($channel, $trigger, 'workdir') . '/' if $factoids->exists($channel, $trigger, 'workdir'); return "/say $trigger_name: Module loaded by " . $factoids->get_data($channel, $trigger, 'owner') . " for $channel_name on " . localtime($factoids->get_data($channel, $trigger, 'created_on')) - . " [$created_ago] -> $module_repo" + . " [$created_ago] -> $applet_repo" . $factoids->get_data($channel, $trigger, 'action') . ', used ' . $factoids->get_data($channel, $trigger, 'ref_count') @@ -1073,7 +1073,7 @@ sub cmd_factinfo { . ')'; } - return "/say $context->{arguments} is not a factoid or a module."; + return "/say $context->{arguments} is not a factoid or an applet."; } sub cmd_factfind { diff --git a/lib/PBot/Core/Commands/Misc.pm b/lib/PBot/Core/Commands/Misc.pm index 7f350fba..d7f1ca38 100644 --- a/lib/PBot/Core/Commands/Misc.pm +++ b/lib/PBot/Core/Commands/Misc.pm @@ -75,16 +75,16 @@ sub cmd_list { my ($self, $context) = @_; my $text; - my $usage = 'Usage: list '; + my $usage = 'Usage: list '; return $usage if not length $context->{arguments}; - if ($context->{arguments} =~ /^modules$/i) { - $text = 'Loaded modules: '; + if ($context->{arguments} =~ /^applets$/i) { + $text = 'Loaded applets: '; foreach my $channel (sort $self->{pbot}->{factoids}->{data}->{storage}->get_keys) { foreach my $command (sort $self->{pbot}->{factoids}->{data}->{storage}->get_keys($channel)) { next if $command eq '_name'; - if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $command, 'type') eq 'module') { + if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $command, 'type') eq 'applet') { $text .= $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $command, '_name') . ' '; } } diff --git a/lib/PBot/Core/Factoids/Code.pm b/lib/PBot/Core/Factoids/Code.pm index 9e23a270..24a936a7 100644 --- a/lib/PBot/Core/Factoids/Code.pm +++ b/lib/PBot/Core/Factoids/Code.pm @@ -1,7 +1,7 @@ # File: Code.pm # # Purpose: Launching pad for code factoids. Configures $context as a code -# factoid and executes the compiler-vm module. +# factoid and executes the compiler-vm applet. # SPDX-FileCopyrightText: 2021 Pragmatic Software # SPDX-License-Identifier: MIT @@ -50,7 +50,7 @@ sub execute { $context->{nickprefix_disabled} = 0; } - # set up `compiler` module arguments + # set up `compiler` applet arguments my %args = ( nick => $context->{nick}, channel => $context->{from}, @@ -78,10 +78,10 @@ sub execute { $context->{arguments} = $json; # set arguments to json string as `compiler` wants $context->{args_utf8} = 1; # arguments are utf8 encoded by encode_json - # launch the `compiler` module - $self->{pbot}->{modules}->execute_module($context); + # launch the `compiler` applet + $self->{pbot}->{applets}->execute_applet($context); - # return empty string since the module process reader will + # return empty string since the applet process reader will # pass the output along to the result handler return ''; } diff --git a/lib/PBot/Core/Factoids/Interpreter.pm b/lib/PBot/Core/Factoids/Interpreter.pm index 26bb1cb3..28b466b2 100644 --- a/lib/PBot/Core/Factoids/Interpreter.pm +++ b/lib/PBot/Core/Factoids/Interpreter.pm @@ -89,7 +89,7 @@ sub interpreter { unless ($strictnamespace) { # build list of which channels contain the keyword, keeping track of the last one and count foreach my $factoid ($self->{pbot}->{factoids}->{data}->{storage}->get_all("index2 = $original_keyword", 'index1', 'type')) { - next if $factoid->{type} ne 'text' and $factoid->{type} ne 'module'; + next if $factoid->{type} ne 'text' and $factoid->{type} ne 'applet'; push @chanlist, $self->{pbot}->{factoids}->{data}->{storage}->get_data($factoid->{index1}, '_name'); $fwd_chan = $factoid->{index1}; $fwd_trig = $original_keyword; @@ -444,11 +444,11 @@ sub handle_action { return $action if $context->{special} eq 'code-factoid'; - if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $keyword, 'type') eq 'module') { + if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $keyword, 'type') eq 'applet') { $context->{root_keyword} = $keyword unless defined $context->{root_keyword}; $context->{root_channel} = $channel; - my $result = $self->{pbot}->{modules}->execute_module($context); + my $result = $self->{pbot}->{applets}->execute_applet($context); if (defined $result && length $result) { return $ref_from . $result; diff --git a/lib/PBot/Core/ProcessManager.pm b/lib/PBot/Core/ProcessManager.pm index e7043f21..8cd8326a 100644 --- a/lib/PBot/Core/ProcessManager.pm +++ b/lib/PBot/Core/ProcessManager.pm @@ -1,6 +1,6 @@ # File: ProcessManager.pm # -# Purpose: Handles forking and execution of module/subroutine processes. +# Purpose: Handles forking and execution of applet/subroutine processes. # Provides commands to list running processes and to kill them. # SPDX-FileCopyrightText: 2021 Pragmatic Software diff --git a/lib/PBot/Core/Registry.pm b/lib/PBot/Core/Registry.pm index 0fcc1b79..48ca924e 100644 --- a/lib/PBot/Core/Registry.pm +++ b/lib/PBot/Core/Registry.pm @@ -39,7 +39,7 @@ sub initialize { # add default registry items $self->add_default('text', 'general', 'data_dir', $conf{data_dir}); - $self->add_default('text', 'general', 'module_dir', $conf{module_dir}); + $self->add_default('text', 'general', 'applet_dir', $conf{applet_dir}); $self->add_default('text', 'general', 'update_dir', $conf{update_dir}); # bot trigger @@ -74,7 +74,7 @@ sub initialize { # update important paths $self->set('general', 'data_dir', 'value', $conf{data_dir}, 0, 1); - $self->set('general', 'module_dir', 'value', $conf{module_dir}, 0, 1); + $self->set('general', 'applet_dir', 'value', $conf{applet_dir}, 0, 1); $self->set('general', 'update_dir', 'value', $conf{update_dir}, 0, 1); # override registry entries with command-line arguments, if any diff --git a/lib/PBot/Plugin/Date.pm b/lib/PBot/Plugin/Date.pm index 57989c95..55db2504 100644 --- a/lib/PBot/Plugin/Date.pm +++ b/lib/PBot/Plugin/Date.pm @@ -83,21 +83,21 @@ sub cmd_date { return "No timezone set or user account does not exist."; } - # execute `date_module` + # execute `date_applet` my $newcontext = { from => $context->{from}, nick => $context->{nick}, user => $context->{user}, host => $context->{host}, hostmask => $context->{hostmask}, - command => "date_module $timezone", + command => "date_applet $timezone", root_channel => $context->{from}, - root_keyword => "date_module", - keyword => "date_module", + root_keyword => "date_applet", + keyword => "date_applet", arguments => "$timezone" }; - $self->{pbot}->{modules}->execute_module($newcontext); + $self->{pbot}->{applets}->execute_applet($newcontext); } 1; diff --git a/lib/PBot/Plugin/UrlTitles.pm b/lib/PBot/Plugin/UrlTitles.pm index 39a96d5a..dde95f72 100644 --- a/lib/PBot/Plugin/UrlTitles.pm +++ b/lib/PBot/Plugin/UrlTitles.pm @@ -75,7 +75,7 @@ sub show_url_titles { suppress_no_output => 1, }; - $self->{pbot}->{modules}->execute_module($context); + $self->{pbot}->{applets}->execute_applet($context); } } return 0; diff --git a/lib/PBot/VERSION.pm b/lib/PBot/VERSION.pm index 697606fc..e88123d1 100644 --- a/lib/PBot/VERSION.pm +++ b/lib/PBot/VERSION.pm @@ -25,8 +25,8 @@ use PBot::Imports; # These are set by the /misc/update_version script use constant { BUILD_NAME => "PBot", - BUILD_REVISION => 4417, - BUILD_DATE => "2021-11-05", + BUILD_REVISION => 4422, + BUILD_DATE => "2021-11-19", }; sub initialize {} diff --git a/updates/4422_rename_modules_to_applets.pl b/updates/4422_rename_modules_to_applets.pl new file mode 100755 index 00000000..5827fff2 --- /dev/null +++ b/updates/4422_rename_modules_to_applets.pl @@ -0,0 +1,121 @@ +#!/usr/bin/env perl + +# Rename modules to applets + +use warnings; use strict; + +BEGIN { + use File::Basename; + my $location = -l __FILE__ ? dirname readlink __FILE__ : dirname __FILE__; + unshift @INC, $location; +} + +use lib4422::HashObject; +use lib4422::DualIndexHashObject; +use lib4422::DualIndexSQLiteObject; +use lib3503::PBot; + +my ($data_dir, $version, $last_update) = @ARGV; + +print "Adding version info... version: $version, last_update: $last_update, data_dir: $data_dir\n"; + +my $pbot = lib3503::PBot->new; +my $data; + +# update registry +my $registry = lib4422::DualIndexHashObject->new(name => 'Registry', filename => "$data_dir/registry", pbot => $pbot); +$registry->load; + +$data = $registry->get_data('general', 'module_dir'); +$registry->remove('general', 'module_dir', undef, 1); +$data->{value} =~ s/modules/applets/g; +$registry->add('general', 'applet_dir', $data, 1); + +$data = $registry->get_data('general', 'module_repo'); +$registry->remove('general', 'module_repo', undef, 1); +$data->{value} =~ s|/modules/|/applets/|; +$registry->add('general', 'applet_repo', $data, 1); + +$data = $registry->get_data('general', 'module_timeout'); +$registry->remove('general', 'module_timeout', undef, 1); +$registry->add('general', 'applet_timeout', $data, 1); +$registry->save; + +# update command help text +my $commands = lib4422::HashObject->new(name => 'Commands', filename => "$data_dir/commands", pbot => $pbot); +$commands->load; +$commands->set('load', 'help', 'This command loads an applet as a PBot command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#load', 1); +$commands->set('unload', 'help', 'Unloads an applet and removes its associated command. See https://github.com/pragma-/pbot/blob/master/doc/Admin.md#unload', 1); +$commands->save; + +# update factoids +my $factoids = lib4422::DualIndexSQLiteObject->new(name => 'Factoids', filename => "$data_dir/factoids.sqlite3", pbot => $pbot); +$factoids->load; +$factoids->load_metadata; + +$data = $factoids->get_data('.*', 'cjeopardy_answer_module'); +$factoids->remove('.*', 'cjeopardy_answer_module', undef, 1); +$factoids->add('.*', 'cjeopardy_answer_applet', $data, 1); + +$data = $factoids->get_data('.*', 'cjeopardy_filter_module'); +$factoids->remove('.*', 'cjeopardy_filter_module', undef, 1); +$factoids->add('.*', 'cjeopardy_filter_applet', $data, 1); + +$data = $factoids->get_data('.*', 'cjeopardy_hint_module'); +$factoids->remove('.*', 'cjeopardy_hint_module', undef, 1); +$factoids->add('.*', 'cjeopardy_hint_applet', $data, 1); + +$data = $factoids->get_data('.*', 'cjeopardy_scores_module'); +$factoids->remove('.*', 'cjeopardy_scores_module', undef, 1); +$factoids->add('.*', 'cjeopardy_scores_applet', $data, 1); + +$data = $factoids->get_data('.*', 'cjeopardy_module'); +$factoids->remove('.*', 'cjeopardy_module', undef, 1); +$factoids->add('.*', 'cjeopardy_applet', $data, 1); + +$data = $factoids->get_data('.*', 'date_module'); +$factoids->remove('.*', 'date_module', undef, 1); +$factoids->add('.*', 'date_applet', $data, 1); + +$data = $factoids->get_data('.*', 'rpn_module'); +$factoids->remove('.*', 'rpn_module', undef, 1); +$factoids->add('.*', 'rpn_applet', $data, 1); + +$data = $factoids->get_data('.*', 'modules'); +$factoids->remove('.*', 'modules', undef, 1); +$data->{action} = '/call list applets'; +$factoids->add('.*', 'applets', $data, 1); + +$factoids->save; + +my @keys; + +my $iter = $factoids->get_each('index1', 'index2', 'type = module'); + +while ($data = $factoids->get_next($iter), defined $data) { + push @keys, [ $data->{index1}, $data->{index2} ]; +} + +foreach my $pair (@keys) { + $factoids->set($pair->[0], $pair->[1], 'type', 'applet', 1); +} + +$iter = $factoids->get_each('index1', 'index2', 'action ~ /call%_module%'); + +@keys = (); + +while ($data = $factoids->get_next($iter), defined $data) { + push @keys, [ $data->{index1}, $data->{index2} ]; +} + +foreach my $pair (@keys) { + my ($index1, $index2) = ($pair->[0], $pair->[1]); + $data = $factoids->get_data($index1, $index2, 'action'); + $data =~ s/_module/_applet/; + $data = { action => $data }; + $factoids->add($index1, $index2, $data, 1); +} + +$factoids->end; + +exit 0; diff --git a/updates/lib4422/DualIndexHashObject.pm b/updates/lib4422/DualIndexHashObject.pm new file mode 100644 index 00000000..bf5f6d51 --- /dev/null +++ b/updates/lib4422/DualIndexHashObject.pm @@ -0,0 +1,457 @@ +# File: DualIndexHashObject.pm +# +# Purpose: Provides a hash-table object with an abstracted API that includes +# setting and deleting values, saving to and loading from files, etc. +# +# DualIndexHashObject extends the HashObject with an additional index key. +# Provides case-insensitive access to both index keys, while preserving +# original case when displaying the keys. +# +# Data is stored in working memory for lightning fast performance. If you have +# a huge amount of data, consider using DualIndexSQLiteObject instead. +# +# If a filename is provided, data is written to the file after any modifications. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package lib4422::DualIndexHashObject; + +use warnings; +use strict; +use feature ':5.16'; + +use Text::Levenshtein::XS qw(distance); +use JSON; + +sub new { + my ($class, %args) = @_; + my $self = bless {}, $class; + Carp::croak("Missing pbot reference to " . __FILE__) unless exists $args{pbot}; + $self->{pbot} = delete $args{pbot}; + $self->initialize(%args); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + $self->{name} = $conf{name} // 'unnamed'; + $self->{filename} = $conf{filename} // Carp::carp("Missing filename to DualIndexHashObject, will not be able to save to or load from file."); + $self->{save_queue_timeout} = $conf{save_queue_timeout} // 0; + $self->{hash} = {}; +} + +sub load { + my ($self, $filename) = @_; + $filename = $self->{filename} if not defined $filename; + + if (not defined $filename) { + Carp::carp "No $self->{name} filename specified -- skipping loading from file"; + return; + } + + $self->{pbot}->{logger}->log("Loading $self->{name} from $filename\n"); + + if (not open(FILE, "< $filename")) { + $self->{pbot}->{logger}->log("Skipping loading from file: Couldn't open $filename: $!\n"); + return; + } + + my $contents = do { + local $/; + ; + }; + + $self->{hash} = decode_json $contents if length $contents; + close FILE; + + # update existing entries to use _name to preserve case + # and lowercase any non-lowercased entries + foreach my $primary_index (keys %{$self->{hash}}) { + if (not exists $self->{hash}->{$primary_index}->{_name}) { + if ($primary_index ne lc $primary_index) { + if (exists $self->{hash}->{lc $primary_index}) { + Carp::croak "Cannot update $self->{name} primary index $primary_index; duplicate object found"; + } + + my $data = delete $self->{hash}->{$primary_index}; + $data->{_name} = $primary_index; + $primary_index = lc $primary_index; + $self->{hash}->{$primary_index} = $data; + } + } + + foreach my $secondary_index (grep { $_ ne '_name' } keys %{$self->{hash}->{$primary_index}}) { + if (not exists $self->{hash}->{$primary_index}->{$secondary_index}->{_name}) { + if ($secondary_index ne lc $secondary_index) { + if (exists $self->{hash}->{$primary_index}->{lc $secondary_index}) { + Carp::croak "Cannot update $self->{name} $primary_index sub-object $secondary_index; duplicate object found"; + } + + my $data = delete $self->{hash}->{$primary_index}->{$secondary_index}; + $data->{_name} = $secondary_index; + $secondary_index = lc $secondary_index; + $self->{hash}->{$primary_index}->{$secondary_index} = $data; + } + } + } + } +} + +sub save { + my $self = shift; + my $filename; + if (@_) { $filename = shift; } + else { $filename = $self->{filename}; } + + if (not defined $filename) { + Carp::carp "No $self->{name} filename specified -- skipping saving to file.\n"; + return; + } + + my $subref = sub { + $self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n"); + + if (not $self->get_data('$metadata$', '$metadata$', 'update_version')) { + $self->add('$metadata$', '$metadata$', { update_version => 4422 }); + } + + $self->set('$metadata$', '$metadata$', 'name', $self->{name}, 1); + + my $json = JSON->new; + my $json_text = $json->pretty->canonical->utf8->encode($self->{hash}); + + open(FILE, "> $filename") or die "Couldn't open $filename: $!\n"; + print FILE "$json_text\n"; + close FILE; + }; + + if ($self->{save_queue_timeout}) { + # enqueue the save to prevent save-thrashing + $self->{pbot}->{event_queue}->replace_subref_or_enqueue_event( + $subref, + $self->{save_queue_timeout}, + "save $self->{name}", + ); + } else { + # execute it right now + $subref->(); + } +} + +sub clear { + my $self = shift; + $self->{hash} = {}; +} + +sub levenshtein_matches { + my ($self, $primary_index, $secondary_index, $distance, $strictnamespace) = @_; + my $comma = ''; + my $result = ""; + + $distance = 0.60 if not defined $distance; + + $primary_index = '.*' if not defined $primary_index; + + if (not $secondary_index) { + foreach my $index (sort keys %{$self->{hash}}) { + my $distance_result = distance($primary_index, $index, 20); + next if not defined $distance_result; + + my $length = (length $primary_index > length $index) ? length $primary_index : length $index; + + if ($distance_result / $length < $distance) { + my $name = $self->get_key_name($index); + if ($name =~ / /) { $result .= $comma . "\"$name\""; } + else { $result .= $comma . $name; } + $comma = ", "; + } + } + } else { + my $lc_primary_index = lc $primary_index; + if (not exists $self->{hash}->{$lc_primary_index}) { return 'none'; } + + my $last_header = ""; + my $header = ""; + + foreach my $index1 (sort keys %{$self->{hash}}) { + $header = "[" . $self->get_key_name($index1) . "] "; + $header = '[global] ' if $header eq '[.*] '; + + if ($strictnamespace) { + next unless $index1 eq '.*' or $index1 eq $lc_primary_index; + $header = "" unless $header eq '[global] '; + } + + foreach my $index2 (sort keys %{$self->{hash}->{$index1}}) { + my $distance_result = distance($secondary_index, $index2, 20); + next if not defined $distance_result; + + my $length = (length $secondary_index > length $index2) ? length $secondary_index : length $index2; + + if ($distance_result / $length < $distance) { + my $name = $self->get_key_name($index1, $index2); + $header = "" if $last_header eq $header; + $last_header = $header; + $comma = '; ' if $comma ne '' and $header ne ''; + if ($name =~ / /) { $result .= $comma . $header . "\"$name\""; } + else { $result .= $comma . $header . $name; } + $comma = ", "; + } + } + } + } + + $result =~ s/(.*), /$1 or /; + $result = 'none' if $comma eq ''; + return $result; +} + +sub set { + my ($self, $primary_index, $secondary_index, $key, $value, $dont_save) = @_; + my $lc_primary_index = lc $primary_index; + my $lc_secondary_index = lc $secondary_index; + + if (not exists $self->{hash}->{$lc_primary_index}) { + my $result = "$self->{name}: $primary_index not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index); + return $result; + } + + if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) { + my $secondary_text = $secondary_index =~ / / ? "\"$secondary_index\"" : $secondary_index; + my $result = "$self->{name}: [" . $self->get_key_name($lc_primary_index) . "] $secondary_text not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index, $secondary_index); + return $result; + } + + my $name1 = $self->get_key_name($lc_primary_index); + my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index); + + $name1 = 'global' if $name1 eq '.*'; + $name2 = "\"$name2\"" if $name2 =~ / /; + + if (not defined $key) { + my $result = "[$name1] $name2 keys:\n"; + my $comma = ''; + foreach my $key (sort keys %{$self->{hash}->{$lc_primary_index}->{$lc_secondary_index}}) { + next if $key eq '_name'; + $result .= $comma . "$key: " . $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}; + $comma = ";\n"; + } + $result .= "none" if ($comma eq ''); + return $result; + } + + if (not defined $value) { $value = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}; } + else { + $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key} = $value; + $self->save unless $dont_save; + } + + return "[$name1] $name2: $key " . (defined $value ? "set to $value" : "is not set."); +} + +sub unset { + my ($self, $primary_index, $secondary_index, $key) = @_; + my $lc_primary_index = lc $primary_index; + my $lc_secondary_index = lc $secondary_index; + + if (not exists $self->{hash}->{$lc_primary_index}) { + my $result = "$self->{name}: $primary_index not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index); + return $result; + } + + my $name1 = $self->get_key_name($lc_primary_index); + $name1 = 'global' if $name1 eq '.*'; + + if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) { + my $result = "$self->{name}: [$name1] $secondary_index not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index, $secondary_index); + return $result; + } + + my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index); + $name2 = "\"$name2\"" if $name2 =~ / /; + + if (defined delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}) { + $self->save; + return "$self->{name}: [$name1] $name2: $key unset."; + } else { + return "$self->{name}: [$name1] $name2: $key does not exist."; + } + $self->save; +} + +sub exists { + my ($self, $primary_index, $secondary_index, $data_index) = @_; + return 0 if not defined $primary_index; + $primary_index = lc $primary_index; + return 0 if not exists $self->{hash}->{$primary_index}; + return 1 if not defined $secondary_index; + $secondary_index = lc $secondary_index; + return 0 if not exists $self->{hash}->{$primary_index}->{$secondary_index}; + return 1 if not defined $data_index; + return exists $self->{hash}->{$primary_index}->{$secondary_index}->{$data_index}; +} + +sub get_key_name { + my ($self, $primary_index, $secondary_index) = @_; + + my $lc_primary_index = lc $primary_index; + + return $lc_primary_index if not exists $self->{hash}->{$lc_primary_index}; + + if (not defined $secondary_index) { + if (exists $self->{hash}->{$lc_primary_index}->{_name}) { + return $self->{hash}->{$lc_primary_index}->{_name}; + } else { + return $lc_primary_index; + } + } + + my $lc_secondary_index = lc $secondary_index; + + return $lc_secondary_index if not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}; + + if (exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name}) { + return $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name}; + } else { + return $lc_secondary_index; + } +} + +sub get_keys { + my ($self, $primary_index, $secondary_index) = @_; + return grep { $_ ne '$metadata$' } keys %{$self->{hash}} if not defined $primary_index; + + my $lc_primary_index = lc $primary_index; + + if (not defined $secondary_index) { + return () if not exists $self->{hash}->{$lc_primary_index}; + return grep { $_ ne '_name' and $_ ne '$metadata$' } keys %{$self->{hash}->{$lc_primary_index}}; + } + + my $lc_secondary_index = lc $secondary_index; + + return () if not exists $self->{hash}->{$lc_primary_index} + or not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}; + + return grep { $_ ne '_name' } keys %{$self->{hash}->{lc $primary_index}->{lc $secondary_index}}; +} + +sub get_data { + my ($self, $primary_index, $secondary_index, $data_index) = @_; + $primary_index = lc $primary_index if defined $primary_index; + $secondary_index = lc $secondary_index if defined $secondary_index; + return undef if not exists $self->{hash}->{$primary_index}; + return $self->{hash}->{$primary_index} if not defined $secondary_index; + return $self->{hash}->{$primary_index}->{$secondary_index} if not defined $data_index; + return $self->{hash}->{$primary_index}->{$secondary_index}->{$data_index}; +} + +sub add { + my ($self, $primary_index, $secondary_index, $data, $dont_save, $quiet) = @_; + my $lc_primary_index = lc $primary_index; + my $lc_secondary_index = lc $secondary_index; + + if (not exists $self->{hash}->{$lc_primary_index}) { + # preserve case + if ($primary_index ne $lc_primary_index) { + $self->{hash}->{$lc_primary_index}->{_name} = $primary_index; + } + } + + if ($secondary_index ne $lc_secondary_index) { + # preserve case + $data->{_name} = $secondary_index; + } + + if (exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) { + foreach my $key (keys %{$data}) { + if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}) { + $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key} = $data->{$key}; + } + } + } else { + $self->{hash}->{$lc_primary_index}->{$lc_secondary_index} = $data; + } + + $self->save() unless $dont_save; + + my $name1 = $self->get_key_name($lc_primary_index); + my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index); + $name1 = 'global' if $name1 eq '.*'; + $name2 = "\"$name2\"" if $name2 =~ / /; + $self->{pbot}->{logger}->log("$self->{name}: [$name1]: $name2 added.\n") unless $dont_save or $quiet; + return "$self->{name}: [$name1]: $name2 added."; +} + +sub remove { + my ($self, $primary_index, $secondary_index, $data_index, $dont_save) = @_; + my $lc_primary_index = lc $primary_index; + my $lc_secondary_index = lc $secondary_index; + + if (not exists $self->{hash}->{$lc_primary_index}) { + my $result = "$self->{name}: $primary_index not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index); + return $result; + } + + if (not defined $secondary_index) { + my $data = delete $self->{hash}->{$lc_primary_index}; + if (defined $data) { + my $name = exists $data->{_name} ? $data->{_name} : $lc_primary_index; + $name = 'global' if $name eq '.*'; + $self->save unless $dont_save; + return "$self->{name}: $name removed."; + } else { + return "$self->{name}: $primary_index does not exist."; + } + } + + my $name1 = $self->get_key_name($lc_primary_index); + $name1 = 'global' if $name1 eq '.*'; + + if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) { + my $result = "$self->{name}: [$name1] $secondary_index not found; similiar matches: "; + $result .= $self->levenshtein_matches($primary_index, $secondary_index); + return $result; + } + + if (not defined $data_index) { + my $data = delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}; + if (defined $data) { + my $name2 = exists $data->{_name} ? $data->{_name} : $lc_secondary_index; + $name2 = "\"$name2\"" if $name2 =~ / /; + + # remove primary group if no more secondaries + if ((grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_primary_index}}) == 0) { + delete $self->{hash}->{$lc_primary_index}; + } + + $self->save unless $dont_save; + return "$self->{name}: [$name1] $name2 removed."; + } else { + return "$self->{name}: [$name1] $secondary_index does not exist."; + } + } + + my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index); + if (defined delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$data_index}) { + return "$self->{name}: [$name1] $name2.$data_index removed."; + } else { + return "$self->{name}: [$name1] $name2.$data_index does not exist."; + } +} + +# for compatibility with DualIndexSQLiteObject +sub create_metadata { } + +# todo: +sub get_each { } +sub get_next { } +sub get_all { } + +1; diff --git a/updates/lib4422/DualIndexSQLiteObject.pm b/updates/lib4422/DualIndexSQLiteObject.pm new file mode 100644 index 00000000..57c4ff76 --- /dev/null +++ b/updates/lib4422/DualIndexSQLiteObject.pm @@ -0,0 +1,945 @@ +# File: DualIndexSQLiteObject.pm +# +# Purpose: Provides a dual-indexed SQLite object with an abstracted API that includes +# setting and deleting values, caching, displaying nearest matches, etc. Designed to +# be as compatible as possible with DualIndexHashObject; e.g. get_keys, get_data, etc. +# +# This class is ideal if you don't want to store all of the data in working memory. +# Instead, the data is stored to and fetched from an SQLite database. To ensure +# lightning fast performance, data that is accessed will be temporarily cached +# in working memory. The caching TTL value can be adjusted via the +# `dualindexsqliteobject.cache_timeout` registry entry. +# +# Since this class does not store all the data in working memory, it provides +# iterator-based access via get_each, get_next and get_all. +# +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package lib4422::DualIndexSQLiteObject; + +use warnings; +use strict; +use feature ':5.16'; + +use DBI; +use Text::Levenshtein::XS qw(distance); + +sub new { + my ($class, %args) = @_; + Carp::croak("Missing pbot reference to " . __FILE__) unless exists $args{pbot}; + my $self = bless {}, $class; + $self->{pbot} = delete $args{pbot}; + $self->initialize(%args); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + + $self->{name} = $conf{name} // 'Dual Index SQLite object'; + $self->{filename} = $conf{filename} // Carp::croak("Missing filename in " . __FILE__); + + $self->{dbh} = undef; + $self->{cache} = {}; + + $self->begin; +} + +sub begin { + my ($self) = @_; + + $self->{pbot}->{logger}->log("Opening $self->{name} database ($self->{filename})\n"); + + $self->{dbh} = DBI->connect("dbi:SQLite:dbname=$self->{filename}", undef, undef, + { AutoCommit => 0, RaiseError => 1, PrintError => 0, AutoInactiveDestroy => 1, sqlite_unicode => 1 } + ) or die $DBI::errstr; +} + +sub end { + my ($self) = @_; + + $self->{pbot}->{logger}->log("Closing $self->{name} database ($self->{filename})\n"); + + if (defined $self->{dbh}) { + $self->{dbh}->commit; + $self->{dbh}->disconnect; + $self->{dbh} = undef; + } +} + +sub load { + my ($self) = @_; + $self->create_database; + $self->create_cache; +} + +sub save { + my ($self) = @_; + return if not $self->{dbh}; + + eval { $self->{dbh}->commit }; + + if ($@) { + $self->{pbot}->{logger}->log("Error saving $self->{name}: $@"); + } +} + +sub create_database { + my ($self) = @_; + + eval { + $self->{dbh}->do(<{dbh}->do('CREATE INDEX IF NOT EXISTS idx1 ON Stuff (index1, index2)'); + }; + + $self->{pbot}->{logger}->log("Error creating $self->{name} databse: $@") if $@; +} + +sub create_cache { + my ($self) = @_; + + $self->{cache} = {}; + + my ($index1_count, $index2_count) = (0, 0); + + foreach my $index1 ($self->get_keys(undef, undef, 1)) { + my $lc_index1 = lc $index1; + $index1_count++; + + foreach my $index2 ($self->get_keys($lc_index1, undef, 1)) { + my $lc_index2 = lc $index2; + $index2_count++; + + $self->{cache}->{$lc_index1}->{$lc_index2} = {}; + + # _name contains original typographical case + + if ($index1 ne $lc_index1) { + $self->{cache}->{$lc_index1}->{_name} = $index1; + } + + if ($index2 ne $lc_index2) { + $self->{cache}->{$lc_index1}->{$lc_index2}->{_name} = $index2; + } + } + } + + $self->{pbot}->{logger}->log("Cached $index2_count $self->{name} objects in $index1_count groups.\n"); +} + +sub cache_remove { + my ($self, $index1, $index2) = @_; + + if (not defined $index2) { + # remove index1 + delete $self->{cache}->{$index1}; + } else { + # remove index2 + delete $self->{cache}->{$index1}->{$index2}; + + # remove index1 if it has no more keys left (aside from _name) + if (not grep { $_ ne '_name' } keys %{$self->{cache}->{$index1}}) { + delete $self->{cache}->{$index1}; + } + } +} + +sub enqueue_decache { + my ($self, $index1, $index2) = @_; +} + +sub create_metadata { + my ($self, $columns) = @_; + + return if not $self->{dbh}; + + $self->{columns} = $columns; + + eval { + my %existing = (); + foreach my $col (@{$self->{dbh}->selectall_arrayref("PRAGMA TABLE_INFO(Stuff)")}) { + $existing{$col->[1]} = $col->[2]; + } + + foreach my $col (sort keys %$columns) { + unless (exists $existing{$col}) { + $self->{dbh}->do("ALTER TABLE Stuff ADD COLUMN \"$col\" $columns->{$col}"); + } + } + + $self->{dbh}->commit; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error creating metadata for $self->{name}: $@"); + $self->{dbh}->rollback; + } +} + +sub load_metadata { + my ($self) = @_; + + return if not $self->{dbh}; + + $self->{columns} = {}; + + eval { + my %existing = (); + foreach my $col (@{$self->{dbh}->selectall_arrayref("PRAGMA TABLE_INFO(Stuff)")}) { + $self->{columns}->{$col->[1]} = $col->[2]; + } + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error loading metadata for $self->{name}: $@"); + } +} + +sub levenshtein_matches { + my ($self, $index1, $index2, $distance, $strictnamespace) = @_; + + $index1 //= '.*'; + $distance //= 0.60; + + my $output = 'none'; + + if (not $index2) { + my @matches; + my $length_a = length $index1; + + foreach my $index (sort $self->get_keys) { + my $distance_result = distance($index1, $index, 20); + next if not defined $distance_result; + + my $length_b = length $index; + + my $length = $length_a > $length_b ? $length_a : $length_b; + + if ($distance_result / $length < $distance) { + my $name = $self->get_data($index, '_name'); + + if ($name =~ / /) { + push @matches, "\"$name\""; + } else { + push @matches, $name; + } + } + } + + $output = join ', ', @matches; + } else { + if (not $self->exists($index1)) { + return $output; + } + + my %sections; + my $section; + my $length_a = length $index2; + + foreach my $i1 (sort $self->get_keys) { + $section = $self->get_data($i1, '_name'); + $section = 'global' if $section eq '.*'; + + if ($strictnamespace) { + next unless $i1 eq '.*' or lc $i1 eq lc $index1; + } + + foreach my $i2 (sort $self->get_keys($i1)) { + my $distance_result = distance($index2, $i2, 20); + next if not defined $distance_result; + + my $length_b = length $i2; + + my $length = $length_a > $length_b ? $length_a : $length_b; + + if ($distance_result / $length < $distance) { + my $name = $self->get_data($i1, $i2, '_name'); + + if ($name =~ / /) { + push @{$sections{$section}}, "\"$name\""; + } else { + push @{$sections{$section}}, $name; + } + } + } + } + + $output = ''; + + foreach $section (sort keys %sections) { + $output .= "[$section] "; + $output .= join ', ', @{$sections{$section}}; + $output .= '; '; + } + + $output =~ s/; $//; + } + + $output =~ s/(.*), /$1 or /; + + $output = 'none' if not length $output; + + return $output; +} + +sub exists { + my ($self, $index1, $index2, $data_index) = @_; + return 0 if not defined $index1; + $index1 = lc $index1; + return 0 if not grep { $_ eq $index1 } $self->get_keys; + return 1 if not defined $index2; + $index2 = lc $index2; + return 0 if not grep { $_ eq $index2 } $self->get_keys($index1); + return 1 if not defined $data_index; + return defined $self->get_data($index1, $index2, $data_index); +} + +sub get_keys { + my ($self, $index1, $index2, $nocache) = @_; + + my @keys; + + if (not defined $index1) { + if (not $nocache) { + return keys %{$self->{cache}}; + } + + @keys = eval { + my $context = $self->{dbh}->selectall_arrayref('SELECT DISTINCT index1 FROM Stuff'); + + if (@$context) { + return map { $_->[0] } @$context; + } else { + return (); + } + }; + + if ($@) { + $self->{pbot}->{logger}->log($@); + return undef; + } + + return @keys; + } + + $index1 = lc $index1; + + if (not defined $index2) { + if (not $nocache) { + return grep { $_ ne '_name' } keys %{$self->{cache}->{$index1}}; + } + + @keys = eval { + my $sth = $self->{dbh}->prepare('SELECT index2 FROM Stuff WHERE index1 = ?'); + + $sth->execute($index1); + + my $context = $sth->fetchall_arrayref; + + if (@$context) { + return map { $_->[0] } @$context; + } else { + return (); + } + }; + + if ($@) { + $self->{pbot}->{logger}->log($@); + return (); + } + + return @keys; + } + + $index2 = lc $index2; + + if (not $nocache) { + @keys = grep { $_ ne '_name' } keys %{$self->{cache}->{$index1}->{$index2}}; + return @keys if @keys; + } + + @keys = eval { + my $sth = $self->{dbh}->prepare('SELECT * FROM Stuff WHERE index1 = ? AND index2 = ?'); + + $sth->execute($index1, $index2); + + my $context = $sth->fetchall_arrayref({}); + + my @k = (); + return @k if not @{$context}; + + my ($lc_index1, $lc_index2) = (lc $index1, lc $index2); + + foreach my $key (keys %{$context->[0]}) { + next if $key eq 'index1' or $key eq 'index2'; + push @k, $key if defined $context->[0]->{$key}; + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = $context->[0]->{$key}; + } + + $self->enqueue_decache($lc_index1, $lc_index2); + + return @k; + }; + + if ($@) { + $self->{pbot}->{logger}->log($@); + return (); + } + + return @keys; +} + +sub get_each { + my ($self, @opts) = @_; + + my $sth = eval { + my $sql = 'SELECT '; + my @keys = (); + my @values = (); + my @where = (); + my @sort = (); + + my $everything = 0; + + foreach my $expr (@opts) { + my ($key, $op, $value) = $expr =~ /(.*?)\s+([!~=<>]+)\s+(.*)/; + + if (not defined $key) { + $key = $expr; + } + + if ($key eq '_everything') { + $everything = 1; + push @keys, '*'; + next; + } + + if ($key eq '_sort') { + if ($value =~ s/^\-//) { + push @sort, "$value DESC"; + } else { + $value =~ s/^\+//; # optional + push @sort, "$value ASC"; + } + next; + } + + if (defined $op) { + my $prefix = 'AND'; + + if ($op eq '=' or $op eq '==') { + $op = '='; + } elsif ($op eq '!=' or $op eq '<>') { + $op = '!='; + } elsif ($op eq '~' or $op eq '~~') { + $op = 'LIKE'; + } + + if ($key =~ s/^(OR|AND)\s+//) { + $prefix = $1; + } + + $prefix = '' if not @where; + push @where, [ $prefix, $key, $op ]; + push @values, $value; + } + + push @keys, $key unless $everything or grep { $_ eq $key } @keys; + } + + $sql .= join ', ', @keys; + $sql .= ' FROM Stuff WHERE'; + + my $in_or = 0; + + for (my $i = 0; $i < @where; $i++) { + my ($prefix, $key, $op) = @{$where[$i]}; + + my ($next_prefix, $next_key) = ('', ''); + + if ($i < @where - 1) { + ($next_prefix, $next_key) = @{$where[$i + 1]}; + } + + if ($next_prefix eq 'OR' and $next_key eq $key) { + $sql .= "$prefix "; + $sql .= '(' if not $in_or; + $sql .= "\"$key\" $op ? "; + $in_or = 1; + } else { + $sql .= "$prefix \"$key\" $op ? "; + + if ($in_or) { + $sql .= ') '; + $in_or = 0; + } + } + } + + $sql .= ')' if $in_or; + + $sql .= ' ORDER BY ' . join(', ', @sort) if @sort; + + my $sth = $self->{dbh}->prepare($sql); + + my $param = 0; + foreach my $value (@values) { + $sth->bind_param(++$param, $value); + } + + $sth->execute; + return $sth; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error getting data: $@"); + return undef; + } + + return $sth; +} + +sub get_next { + my ($self, $sth) = @_; + + my $data = eval { + return $sth->fetchrow_hashref; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error getting next: $@"); + return undef; + } + + return $data; +} + +sub get_all { + my ($self, @opts) = @_; + + my $sth = $self->get_each(@opts); + + my $data = eval { + return $sth->fetchall_arrayref({}); + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error getting all data: $@\n"); + return undef; + } + + return @$data; +} + +sub get_key_name { + my ($self, $index1, $index2) = @_; + + my $lc_index1 = lc $index1; + + if (not exists $self->{cache}->{$lc_index1}) { + return $lc_index1; + } + + if (not defined $index2) { + if (exists $self->{cache}->{$lc_index1}->{_name}) { + return $self->{cache}->{$lc_index1}->{_name}; + } else { + return $lc_index1; + } + } + + my $lc_index2 = lc $index2; + + if (not exists $self->{cache}->{$lc_index1}->{$lc_index2}) { + return $lc_index2; + } + + if (exists $self->{cache}->{$lc_index1}->{$lc_index2}->{_name}) { + return $self->{cache}->{$lc_index1}->{$lc_index2}->{_name}; + } else { + return $lc_index2; + } +} + +sub get_data { + my ($self, $index1, $index2, $data_index) = @_; + + my $lc_index1 = lc $index1; + my $lc_index2 = lc $index2; + + if (not exists $self->{cache}->{$lc_index1}) { + return undef; + } + + if (not exists $self->{cache}->{$lc_index1}->{$lc_index2} and $lc_index2 ne '_name') { + return undef; + } + + if (not defined $data_index) { + # special case for compatibility with DualIndexHashObject + if ($lc_index2 eq '_name') { + if (exists $self->{cache}->{$lc_index1}->{_name}) { + return $self->{cache}->{$lc_index1}->{_name}; + } else { + return $lc_index1; + } + } + + my $data = eval { + my $sth = $self->{dbh}->prepare('SELECT * FROM Stuff WHERE index1 = ? AND index2 = ?'); + $sth->execute($index1, $index2); + my $context = $sth->fetchall_arrayref({}); + + my $d = {}; + + foreach my $key (keys %{$context->[0]}) { + next if $key eq 'index1' or $key eq 'index2'; + + if (defined $context->[0]->{$key}) { + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = $context->[0]->{$key}; + $d->{$key} = $context->[0]->{$key}; + } + } + + $self->enqueue_decache($lc_index1, $lc_index2); + + return $d; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error getting data for ($index1, $index2): $@\n"); + return undef; + } + + return $data; + } + + # special case for compatibility with DualIndexHashObject + if ($data_index eq '_name') { + if (exists $self->{cache}->{$lc_index1}->{$lc_index2}->{_name}) { + return $self->{cache}->{$lc_index1}->{$lc_index2}->{_name}; + } else { + return $lc_index2; + } + } + + if (exists $self->{cache}->{$lc_index1}->{$lc_index2}->{$data_index}) { + return $self->{cache}->{$lc_index1}->{$lc_index2}->{$data_index}; + } + + my $value = eval { + my $sth = $self->{dbh}->prepare('SELECT * FROM Stuff WHERE index1 = ? AND index2 = ?'); + $sth->execute($index1, $index2); + my $context = $sth->fetchall_arrayref({}); + + foreach my $key (keys %{$context->[0]}) { + next if $key eq 'index1' or $key eq 'index2'; + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = $context->[0]->{$key}; + } + + $self->enqueue_decache($lc_index1, $lc_index2); + + return $context->[0]->{$data_index}; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error getting data for ($index1, $index2, $data_index): $@\n"); + return undef; + } + + return $value; +} + +sub add { + my ($self, $index1, $index2, $data, $quiet) = @_; + + my $name1 = $self->get_data($index1, '_name') // $index1; + + eval { + my $sth; + + if (not $self->exists($index1, $index2)) { + $sth = $self->{dbh}->prepare('INSERT INTO Stuff (index1, index2) VALUES (?, ?)'); + $sth->execute($name1, $index2); + } + + my $sql = 'UPDATE Stuff SET '; + + my $comma = ''; + foreach my $key (sort keys %$data) { + if (not exists $self->{columns}->{$key}) { + next; + } + $sql .= "$comma\"$key\" = ?"; + $comma = ', '; + } + + $sql .= ' WHERE index1 == ? AND index2 == ?'; + + $sth = $self->{dbh}->prepare($sql); + + my $param = 1; + foreach my $key (sort keys %$data) { + next if not exists $self->{columns}->{$key}; + $sth->bind_param($param++, $data->{$key}); + } + + $sth->bind_param($param++, $index1); + $sth->bind_param($param++, $index2); + $sth->execute(); + + # no errors updating SQL -- now we update cache + my ($lc_index1, $lc_index2) = (lc $index1, lc $index2); + + if ($index1 ne $lc_index1 and not exists $self->{cache}->{$lc_index1}->{_name}) { + $self->{cache}->{$lc_index1}->{_name} = $index1; + } + + if (grep { $_ ne '_name' } keys %{$self->{cache}->{$lc_index1}->{$lc_index2}}) { + foreach my $key (sort keys %$data) { + next if not exists $self->{columns}->{$key}; + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = $data->{$key}; + } + } else { + $self->{cache}->{$lc_index1}->{lc $index2} = {} + } + + if (not exists $self->{cache}->{$lc_index1}->{$lc_index2}->{_name} and $index2 ne $lc_index2) { + $self->{cache}->{$lc_index1}->{$lc_index2}->{_name} = $index2; + } + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error adding $self->{name} $index2 to $index1: $@\n"); + $self->{dbh}->rollback; + return "Error adding $index2 to $name1: $@\n"; + } + + $index1 = 'global' if $index1 eq '.*'; + $index2 = "\"$index2\"" if $index2 =~ / /; + + $self->{pbot}->{logger}->log("$self->{name}: [$index1]: $index2 added.\n") unless $quiet; + + $self->save unless $quiet; + + return "$index2 added to $name1."; +} + +sub remove { + my ($self, $index1, $index2, $data_index, $dont_save) = @_; + + if (not $self->exists($index1)) { + my $result = "$self->{name}: $index1 not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1); + return $result; + } + + my $name1 = $self->get_data($index1, '_name'); + $name1 = 'global' if $name1 eq '.*'; + + my $lc_index1 = lc $index1; + + if (not defined $index2) { + eval { + my $sth = $self->{dbh}->prepare("DELETE FROM Stuff WHERE index1 = ?"); + $sth->execute($index1); + + $self->cache_remove($index1); + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error removing $index1 from $self->{name}: $@\n"); + return "Error removing $name1: $@"; + } + + $self->save unless $dont_save; + + return "$name1 removed."; + } + + if (not $self->exists($index1, $index2)) { + my $result = "$self->{name}: [$name1] $index2 not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1, $index2); + return $result; + } + + my $name2 = $self->get_data($index1, $index2, '_name'); + $name2 = "\"$name2\"" if $name2 =~ / /; + + my $lc_index2 = lc $index2; + + if (not defined $data_index) { + eval { + my $sth = $self->{dbh}->prepare("DELETE FROM Stuff WHERE index1 = ? AND index2 = ?"); + $sth->execute($index1, $index2); + + $self->cache_remove($index1, $index2); + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error removing $self->{name}: [$name1] $name2: $@\n"); + return "Error removing $name2 from $name1: $@"; + } + + $self->save unless $dont_save; + return "$name2 removed from $name1."; + } + + if (not exists $self->{columns}->{$data_index}) { + return "$self->{name} have no such metadata $data_index."; + } + + if (defined $self->get_data($lc_index1, $lc_index2, $data_index)) { + eval { + my $sth = $self->{dbh}->prepare("UPDATE Stuff SET '$data_index' = ? WHERE index1 = ? AND index2 = ?"); + $sth->execute(undef, $index1, $index2); + + $self->{cache}->{$index1}->{$index2}->{$data_index} = undef; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error unsetting $self->{name}: $name1.$name2: $@\n"); + return "Error unsetting $data_index from $name2: $@"; + } + + $self->save unless $dont_save; + return "$name2.$data_index unset."; + } + + return "$name2.$data_index is not set."; +} + +sub set { + my ($self, $index1, $index2, $key, $value, $dont_save) = @_; + + if (not $self->exists($index1)) { + my $result = "$self->{name}: $index1 not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1); + return $result; + } + + if (not $self->exists($index1, $index2)) { + my $secondary_text = $index2 =~ / / ? "\"$index2\"" : $index2; + my $result = "$self->{name}: [" . $self->get_data($index1, '_name') . "] $secondary_text not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1, $index2); + return $result; + } + + my $name1 = $self->get_data($index1, '_name'); + my $name2 = $self->get_data($index1, $index2, '_name'); + + $name1 = 'global' if $name1 eq '.*'; + $name2 = "\"$name2\"" if $name2 =~ / /; + + if (not defined $key) { + my $result = "[$name1] $name2 keys:\n"; + my @metadata = (); + + foreach my $key (sort $self->get_keys($index1, $index2)) { + my $value = $self->get_data($index1, $index2, $key); + push @metadata, "$key => $value" if defined $value; + } + + if (not @metadata) { + $result .= "none"; + } else { + $result .= join ";\n", @metadata; + } + + return $result; + } + + if (not exists $self->{columns}->{$key}) { + return "$self->{name} have no such metadata $key."; + } + + if (not defined $value) { + $value = $self->get_data($index1, $index2, $key); + } + else { + eval { + my $sth = $self->{dbh}->prepare("UPDATE Stuff SET '$key' = ? WHERE index1 = ? AND index2 = ?"); + + $sth->execute($value, $index1, $index2); + + my ($lc_index1, $lc_index2) = (lc $index1, lc $index2); + + if (exists $self->{cache}->{$lc_index1} + and exists $self->{cache}->{$lc_index1}->{$lc_index2} + and exists $self->{cache}->{$lc_index1}->{$lc_index2}->{$key}) { + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = $value; + } + + $self->save unless $dont_save; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error setting $self->{name} $index1 $index2.$key: $@\n"); + return "Error setting $name2.$key: $@"; + } + } + + return "[$name1] $name2.$key " . (defined $value ? "set to $value" : "is not set."); +} + +sub unset { + my ($self, $index1, $index2, $key) = @_; + + if (not $self->exists($index1)) { + my $result = "$self->{name}: $index1 not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1); + return $result; + } + + my $name1 = $self->get_data($index1, '_name'); + $name1 = 'global' if $name1 eq '.*'; + + if (not $self->exists($index1, $index2)) { + my $result = "$self->{name}: [$name1] $index2 not found; similiar matches: "; + $result .= $self->levenshtein_matches($index1, $index2); + return $result; + } + + my $name2 = $self->get_data($index1, $index2, '_name'); + $name2 = "\"$name2\"" if $name2 =~ / /; + + if (not exists $self->{columns}->{$key}) { + return "$self->{name} have no such metadata $key."; + } + + eval { + my $sth = $self->{dbh}->prepare("UPDATE Stuff SET '$key' = ? WHERE index1 = ? AND index2 = ?"); + + $sth->execute(undef, $index1, $index2); + + my ($lc_index1, $lc_index2) = (lc $index1, lc $index2); + + if (exists $self->{cache}->{$lc_index1} + and exists $self->{cache}->{$lc_index1}->{$lc_index2} + and exists $self->{cache}->{$lc_index1}->{$lc_index2}->{$key}) { + $self->{cache}->{$lc_index1}->{$lc_index2}->{$key} = undef; + } + + $self->save; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error unsetting key: $@\n"); + return "Error unsetting key: $@"; + } + + return "[$name1] $name2.$key unset."; +} + +# nothing to do here for SQLite +# kept for compatibility with DualIndexHashObject +sub clear { } + +1; diff --git a/updates/lib4422/HashObject.pm b/updates/lib4422/HashObject.pm new file mode 100644 index 00000000..92c08be0 --- /dev/null +++ b/updates/lib4422/HashObject.pm @@ -0,0 +1,301 @@ +# File: HashObject.pm +# +# Purpose: Provides a hash-table object with an abstracted API that includes +# setting and deleting values, saving to and loading from files, etc. Provides +# case-insensitive access to the index key while preserving original case when +# displaying index key. +# +# Data is stored in working memory for lightning fast performance. If a filename +# is provided, data is written to the file after any modifications. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package lib4422::HashObject; + +use warnings; +use strict; +use feature ':5.16'; + +use Text::Levenshtein::XS qw(distance); +use JSON; + +sub new { + my ($class, %args) = @_; + my $self = bless {}, $class; + Carp::croak("Missing pbot reference to " . __FILE__) unless exists $args{pbot}; + $self->{pbot} = delete $args{pbot}; + $self->initialize(%args); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + + $self->{name} = $conf{name} // 'unnammed'; + $self->{hash} = {}; + $self->{filename} = $conf{filename}; + + if (not defined $self->{filename}) { + Carp::carp("Missing filename for $self->{name} HashObject, will not be able to save to or load from file."); + } +} + +sub load { + my ($self, $filename) = @_; + + # allow overriding $self->{filename} with $filename parameter + $filename //= $self->{filename}; + + # no filename? nothing to load + if (not defined $filename) { + Carp::carp "No $self->{name} filename specified -- skipping loading from file"; + return; + } + + $self->{pbot}->{logger}->log("Loading $self->{name} from $filename\n"); + + if (not open(FILE, "< $filename")) { + $self->{pbot}->{logger}->log("Skipping loading from file: Couldn't open $filename: $!\n"); + return; + } + + # slurp file into $contents + my $contents = do { + local $/; + ; + }; + + close FILE; + + eval { + # first try to deocde json, throws exception on misparse/errors + my $newhash = decode_json $contents; + + # clear current hash only if decode succeeded + $self->clear; + + # update internal hash + $self->{hash} = $newhash; + + # update existing entries to use _name to preserve typographical casing + # e.g., when someone edits a config file by hand, they might add an + # entry with uppercase characters in its name. + foreach my $index (keys %{$self->{hash}}) { + if (not exists $self->{hash}->{$index}->{_name}) { + if ($index ne lc $index) { + if (exists $self->{hash}->{lc $index}) { + Carp::croak "Cannot update $self->{name} object $index; duplicate object found"; + } + + my $data = delete $self->{hash}->{$index}; + $data->{_name} = $index; # _name is original typographical case + $self->{hash}->{lc $index} = $data; # index key is lowercased + } + } + } + }; + + if ($@) { + # json parse error or such + $self->{pbot}->{logger}->log("Warning: failed to load $filename: $@\n"); + } +} + +sub save { + my ($self, $filename) = @_; + + # allow parameter overriding internal field + $filename //= $self->{filename}; + + # no filename? nothing to save + if (not defined $filename) { + Carp::carp "No $self->{name} filename specified -- skipping saving to file.\n"; + return; + } + + $self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n"); + + # add update_version to metadata + if (not $self->get_data('$metadata$', 'update_version')) { + $self->add('$metadata$', { update_version => 4422 }); + } + + # ensure `name` metadata is current + $self->set('$metadata$', 'name', $self->{name}, 1); + + # encode hash as JSON + my $json = JSON->new; + my $json_text = $json->pretty->canonical->utf8->encode($self->{hash}); + + # print JSON to file + open(FILE, "> $filename") or die "Couldn't open $filename: $!\n"; + print FILE "$json_text\n"; + close(FILE); +} + +sub clear { + my ($self) = @_; + $self->{hash} = {}; +} + +sub levenshtein_matches { + my ($self, $keyword) = @_; + + my @matches; + + foreach my $index (sort keys %{$self->{hash}}) { + my $distance = distance($keyword, $index, 20); + next if not defined $distance; + + my $length_a = length $keyword; + my $length_b = length $index; + my $length = $length_a > $length_b ? $length_a : $length_b; + + if ($length != 0 && $distance / $length < 0.50) { + push @matches, $index; + } + } + + return 'none' if not @matches; + + my $result = join ', ', @matches; + + # "a, b, c, d" -> "a, b, c or d" + $result =~ s/(.*), /$1 or /; + + return $result; +} + +sub set { + my ($self, $index, $key, $value, $dont_save) = @_; + my $lc_index = lc $index; + + # find similarly named keys + if (not exists $self->{hash}->{$lc_index}) { + my $result = "$self->{name}: $index not found; similar matches: "; + $result .= $self->levenshtein_matches($index); + return $result; + } + + if (not defined $key) { + # if no key provided, then list all keys and values + my $result = "[$self->{name}] " . $self->get_key_name($lc_index) . " keys: "; + + my @entries; + + foreach my $key (sort grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_index}}) { + push @entries, "$key: $self->{hash}->{$lc_index}->{$key}"; + } + + if (@entries) { + $result .= join ";\n", @entries; + } else { + $result .= 'none'; + } + + return $result; + } + + if (not defined $value) { + # if no value provided, then show this key's value + $value = $self->{hash}->{$lc_index}->{$key}; + } else { + # otherwise update the value belonging to key + $self->{hash}->{$lc_index}->{$key} = $value; + $self->save unless $dont_save; + } + + return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key " . (defined $value ? "set to $value" : "is not set."); +} + +sub unset { + my ($self, $index, $key) = @_; + my $lc_index = lc $index; + + if (not exists $self->{hash}->{$lc_index}) { + my $result = "$self->{name}: $index not found; similar matches: "; + $result .= $self->levenshtein_matches($index); + return $result; + } + + if (defined delete $self->{hash}->{$lc_index}->{$key}) { + $self->save; + return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key unset."; + } else { + return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key does not exist."; + } +} + +sub exists { + my ($self, $index, $data_index) = @_; + return exists $self->{hash}->{lc $index} if not defined $data_index; + return exists $self->{hash}->{lc $index}->{$data_index}; +} + +sub get_key_name { + my ($self, $index) = @_; + my $lc_index = lc $index; + return $lc_index if not exists $self->{hash}->{$lc_index}; + return exists $self->{hash}->{$lc_index}->{_name} ? $self->{hash}->{$lc_index}->{_name} : $lc_index; +} + +sub get_keys { + my ($self, $index) = @_; + return grep { $_ ne '$metadata$' } keys %{$self->{hash}} if not defined $index; + return grep { $_ ne '_name' } keys %{$self->{hash}->{lc $index}}; +} + +sub get_data { + my ($self, $index, $data_index) = @_; + my $lc_index = lc $index; + return undef if not exists $self->{hash}->{$lc_index}; + return $self->{hash}->{$lc_index} if not defined $data_index; + return $self->{hash}->{$lc_index}->{$data_index}; +} + +sub add { + my ($self, $index, $data, $dont_save) = @_; + my $lc_index = lc $index; + + # preserve case of index + if ($index ne $lc_index) { + $data->{_name} = $index; + } + + $self->{hash}->{$lc_index} = $data; + $self->save unless $dont_save; + return "$index added to $self->{name}."; +} + +sub remove { + my ($self, $index, $data_index, $dont_save) = @_; + my $lc_index = lc $index; + + if (not exists $self->{hash}->{$lc_index}) { + my $result = "$self->{name}: $index not found; similar matches: "; + $result .= $self->levenshtein_matches($lc_index); + return $result; + } + + if (defined $data_index) { + if (defined delete $self->{hash}->{$lc_index}->{$data_index}) { + delete $self->{hash}->{$lc_index} if keys(%{$self->{hash}->{$lc_index}}) == 1; + $self->save unless $dont_save; + return $self->get_key_name($lc_index) . ".$data_index removed from $self->{name}"; + } else { + return "$self->{name}: " . $self->get_key_name($lc_index) . ".$data_index does not exist."; + } + } + + my $data = delete $self->{hash}->{$lc_index}; + if (defined $data) { + $self->save unless $dont_save; + my $name = exists $data->{_name} ? $data->{_name} : $lc_index; + return "$name removed from $self->{name}."; + } else { + return "$self->{name}: $data_index does not exist."; + } +} + +1;