diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..0a5953a --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,18 @@ +version: 2.1 + +orbs: + node: circleci/node@6.3.0 + +jobs: + test: + executor: + name: node/default + tag: 22.0.0 + steps: + - checkout + - run: npm install && npm run lint + +workflows: + test: + jobs: + - test diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ef08538 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [davidchambers] diff --git a/.gitignore b/.gitignore index e69de29..2ccbe46 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4fd5edc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM nodesource/node:10 + +ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init + +RUN chmod +x /usr/local/bin/dumb-init && \ + groupadd --system -- nodejs && \ + useradd --system --gid nodejs --create-home -- nodejs + +USER nodejs + +RUN mkdir -p /home/nodejs/silly-goat + +WORKDIR /home/nodejs/silly-goat + +COPY package.json . + +RUN npm install --production + +COPY . . + +ENTRYPOINT ["dumb-init", "npm", "run", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ac7ee0 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# silly-goat + +silly-goat is a [Hubot][1] chat bot for the [Sanctuary][2] room on [Gitter][3]. + +### Commands + + - `/versions`: list Node version and dependency versions + +### JavaScript evaluation + +When [@silly-goat][4] is mentioned in a message containing a JavaScript code +block, silly-goat evaluates the code and bleats the result. + +In Markdown, a JavaScript code block looks like this: + + ```javascript + S.map(S.inc, [1, 2, 3]) + ``` + +`js` may be used in place of `javascript`, or the language may be left +unspecified. + + +[1]: https://hubot.github.com/ +[2]: https://gitter.im/sanctuary-js/sanctuary +[3]: https://gitter.im/ +[4]: https://github.com/silly-goat diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7c73ea --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "silly-goat", + "private": true, + "scripts": { + "lint": "eslint --config node_modules/sanctuary-style/eslint-es6.json --env es6 --env node --report-unused-disable-directives -- scripts", + "start": "PATH=\"node_modules/.bin:$PATH\" hubot --adapter gitter2 --alias / --name silly-goat" + }, + "dependencies": { + "fluture": "12.2.0", + "fluture-sanctuary-types": "7.0.0", + "hubot": "3.0.1", + "hubot-gitter2": "1.0.0", + "ramda": "*", + "sanctuary": "3.1.0", + "sanctuary-def": "0.22.0", + "sanctuary-descending": "2.1.0", + "sanctuary-either": "2.1.0", + "sanctuary-identity": "2.1.0", + "sanctuary-int": "*", + "sanctuary-maybe": "2.1.0", + "sanctuary-pair": "2.1.0", + "sanctuary-show": "2.0.0", + "sanctuary-type-classes": "12.1.0", + "sanctuary-type-identifiers": "3.0.0", + "sanctuary-useless": "2.0.1" + }, + "devDependencies": { + "eslint": "6.8.x", + "sanctuary-style": "4.0.x" + } +} diff --git a/scripts/eval.js b/scripts/eval.js new file mode 100644 index 0000000..91f9025 --- /dev/null +++ b/scripts/eval.js @@ -0,0 +1,86 @@ +'use strict'; + +const vm = require ('vm'); + +const Future = require ('fluture'); +const fst = require ('fluture-sanctuary-types'); +const R = require ('ramda'); +const {create} = require ('sanctuary'); +const $ = require ('sanctuary-def'); +const Descending = require ('sanctuary-descending'); +const Identity = require ('sanctuary-identity'); +const Int = require ('sanctuary-int'); +const Z = require ('sanctuary-type-classes'); +const type = require ('sanctuary-type-identifiers'); +const Useless = require ('sanctuary-useless'); +const Useless$pkg = require ('sanctuary-useless/package.json'); + + +// $Useless :: Type +const $Useless = $.NullaryType + ('Useless') + ('https://github.com/sanctuary-js/sanctuary-useless/tree/v' + + Useless$pkg.version) + ([]) + (x => type (x) === Useless['@@type']); + +// env :: Array Type +const env = $.env.concat (fst.env.concat ([$Useless])); + +// opts :: { checkTypes :: Boolean, env :: Array Type } +const opts = {checkTypes: true, env}; + +// S :: Module +const S = create (opts); + +// def :: String -> StrMap TypeClass -> Array Type -> Function -> Function +const def = $.create (opts); + +// evaluate :: String -> Either String String +const evaluate = +def ('evaluate') + ({}) + ([$.String, $.Either ($.String) ($.String)]) + (code => { + const logs = []; + const log = level => (...args) => { + logs.push (level + ': ' + S.joinWith (', ') (S.map (S.show) (args))); + }; + return S.bimap (S.prop ('message')) + (x => S.unlines (logs) + S.show (x)) + (S.encase (S.curry3 (vm.runInNewContext) + (code) + ({$, + Descending, + Future, + Identity, + Int, + R, + S, + Useless, + Z, + console: {error: log ('error'), + log: log ('log')}})) + ({timeout: 5000})); + }); + +// backticks :: String +const backticks = '```'; + +// formatCodeBlock :: String -> String -> String +const formatCodeBlock = +def ('formatCodeBlock') + ({}) + ([$.String, $.String, $.String]) + (lang => code => `${backticks}${lang}\n${code}\n${backticks}`); + + +module.exports = bot => { + + bot.respond (/```(?:javascript|js)?$([\s\S]*)```/m, res => { + res.send (S.either (formatCodeBlock ('text')) + (formatCodeBlock ('javascript')) + (evaluate (res.match[1]))); + }); + +}; diff --git a/scripts/versions.js b/scripts/versions.js new file mode 100644 index 0000000..77f7ff2 --- /dev/null +++ b/scripts/versions.js @@ -0,0 +1,41 @@ +'use strict'; + +const S = require ('sanctuary'); + +const pkg = require ('../package.json'); + + +// deps :: { hubot :: Array String, other :: Array String } +const deps = +S.reduce ($acc => name => { + $acc[/^hubot(-|$)/.test (name) ? 'hubot' : 'other'].push (name); + return $acc; + }) + ({hubot: [], other: []}) + (S.sort (S.keys (pkg.dependencies))); + +// version :: String -> String +const version = name => { + const v = pkg.dependencies[name]; + return `${name}@${v === '*' ? require (`${name}/package.json`).version : v}`; +}; + +// backticks :: String +const backticks = '```'; + +// versions :: String +const versions = +`${backticks}text +Node ${process.version} + +${S.joinWith ('\n') (S.map (version) (deps.hubot))} + +${S.joinWith ('\n') (S.map (version) (deps.other))} +${backticks}`; + + +module.exports = bot => { + + bot.respond (/versions/, res => { res.send (versions); }); + +};