Building a Slack Bot in PureScript

栏目: IT技术 · 发布时间: 4年前

内容简介:I recentlyI was excited about building this project in PureScript because it was complicated enough to be interesting but not so complicated as to be overwhelming. The key requirements for the bot were:Notably, one feature that was not required was a datab

I recently open sourced my first large PureScript project. It’s a slack bot that allows searching for cards from the Epic Card Game . In this post, I’ll discuss the process of writing the application, what went well and what went poorly.

An Overview of the Slack Bot

I was excited about building this project in PureScript because it was complicated enough to be interesting but not so complicated as to be overwhelming. The key requirements for the bot were:

  • Handle incoming HTTP requests and validate an authentication token
  • Scrape the Epic Card Game site for card names and images (no API exists)
  • Index the card names for in-memory full-text search
  • Parse JSON requests and generate JSON responses

Notably, one feature that was not required was a database.

Web Framework

I chose HTTPure for my web framework. The library has a simple design, good general documentation and good examples of using middleware.

I used middleware for two primary reasons: validating the slack token and running my application monad . The middleware design worked nicely for both of these use cases.

Routing was very straightforward with only two paths. The root path handles all commands and the /interactive path handles interactive input from the user.

Here’s a look at the router:

module Epicbot.Web.Router
  ( new
  ) where

import Epicbot.App (ResponseM)
import Epicbot.Web.Service.Command as CommandService
import Epicbot.Web.Service.Interactive as InteractiveService
import HTTPure as HTTPure

new :: HTTPure.Request -> ResponseM
new req = case req of
  { path: [] } ->
    CommandService.handle req

  { path: ["interactive"] } ->
    InteractiveService.handle req

  _  ->
    HTTPure.notFound

Web Scraping

To retrieve details about Epic Card Game’s cards, I used a combination of the PureScript Milkis library to make HTTP requests and JavaScript’s cheerio to extract data from the HTML responses.

Making HTTP requests with Milkis was a simple one-liner . When the application is in offline mode (for testing) I simply read a fixture from my filesystem rather than making an HTTP request.

module Epicbot.Scraper
  ( scrape
  ) where

import Prelude

import Effect.Aff (Aff)
import Epicbot.Card (Card)
import Epicbot.Http as Http
import Epicbot.Html.Parser as Parser
import Epicbot.OnlineStatus (OnlineStatus(..))
import Milkis as Milkis
import Node.Encoding (Encoding(UTF8))
import Node.FS.Aff as FS

testDocPath :: String
testDocPath = "./data/card-gallery.html"

prodUrl :: Milkis.URL
prodUrl = Milkis.URL "http://www.epiccardgame.com/card-gallery/"

getPage :: OnlineStatus -> Aff String
getPage Offline = FS.readTextFile UTF8 testDocPath
getPage Online  = Milkis.text =<< Http.get prodUrl

scrape :: OnlineStatus -> Aff (Array Card)
scrape onlineStatus = Parser.parseCards <$> getPage onlineStatus

My HTML parsing code was only 30 or so lines of JavaScript and some FFI in PureScript.

Full-Text Search

I decided to use JavaScript’s elasticlunr for full-text search. This is the totality of the JavaScript code for building the index, adding documents and searching:

const elasticlunr = require("elasticlunr");

exports._addDoc = function (doc, index) {
  index.addDoc(doc);

  return index;
};

exports._newDocIndex = elasticlunr(function () {
  this.addField("name");
  this.setRef("id");
});

exports._searchDoc = function (term, index) {
  return index.search(term, {});
};

On the PureScript side, I’m mostly using FFI and wrapping the JavaScript in a more idiomatic interface .

JSON Handling

I used Argonaut to handle JSON parsing and generation. Here’s an example of some custom JSON parsing and generation I’m doing to interact with the Slack API:

newtype Action = Action
  { name  :: Maybe String
  , text  :: Maybe String
  , type  :: Maybe String
  , value :: Maybe String
  }

derive instance eqAction :: Eq Action

derive instance ordAction :: Ord Action

derive newtype instance showAction :: Show Action

instance encodeJsonAction :: EncodeJson Action where
  encodeJson :: Action -> Json
  encodeJson (Action obj) = do
    "value" :=? obj.value
    ~>? "type" :=? obj.type
    ~>? "text" :=? obj.name
    ~>? "name" :=? obj.name
    ~>? jsonEmptyObject

instance decodeJsonAction :: DecodeJson Action where
  decodeJson :: Json -> Either String Action
  decodeJson json = do
    obj <- decodeJson json
    name <- obj .:? "name"
    text <- obj .:? "text"
    t <- obj .:? "type"
    value <- obj .:? "value"

    pure $ Action { name, text, type: t, value }

Yes, it’s probably wrong to have a bunch of Maybe String s in a record. I’m still learning.

Application Monad

I decided to follow the ReaderT design pattern when architecting my application. Here’s the definition of my App type:

newtype App a = App (ReaderT RequestEnv Aff a)

It’s a simple newtype over a ReaderT . The RequestEnv type represents the application configuration (e.g. the full-text search index) as well as request-specific configuration (e.g. the unique request id). The base monand is Aff , PureScript’s asynchronous effect monad.

The Good Stuff

Other than the fact that I was actively learning the PureScript language while building the bot, most things went remarkably well. I’m very happy with the final application, though I might build some parts differently were I starting today.

HTTPure is an web framework. It’s both simple and powerful and its middleware implementation seems unrivaled in the PureScript ecosystem.

The ability to FFI into the JavaScript ecosystem is also a huge boon. Both elasticlunr and cheerio made quick work of what could have been very challenging problems. Even though I was relying on “unsafe” JS code for these portions of the application, once the FFI was in place and the JS code was written, I’ve never had a runtime issue with these seams. In a more robust production system, I may choose to use Foreign at my FFI boundaries, but avoiding that here worked fine in practice.

Spago , PureScript’s package manager and build tool, is an absolutely delight to use. It has spoiled me for other ecosystem’s package managers.

The editor tooling in PureScript is also fantastic thanks to the PureScript Language Server . Again, this makes it hard for me to go back to other languages.

Last, but certainly not least, the PureScript language itself is a pleasure to work with. When I began learning it, I had no experience with Haskell nor any other pure functional language. I’ve felt incredibly productive in PureScript once over the initial learning curve. It feels both light-weight and powerful. While the community is small, the people are fantastic and the available libraries are top notch. Much to my surprise, it even has excellent documentation .

The Less Good Stuff

Because I’m still new to purely functional languages, there are places where I can acutely feel the boilerplate. Here’s the minimum amount of code required to build a custom application monad:

newtype App a = App (ReaderT RequestEnv Aff a)

derive instance newtypeApp :: Newtype (App a) _

derive newtype instance functorApp :: Functor App

derive newtype instance applyApp :: Apply App

derive newtype instance applicativeApp :: Applicative App

derive newtype instance bindApp :: Bind App

derive newtype instance monadApp :: Monad App

derive newtype instance monadEffectApp :: MonadEffect App

derive newtype instance monadAffApp :: MonadAff App

instance monadAskApp :: TypeEquals e RequestEnv => MonadAsk e App where
  ask = App $ asks from

The Slack API itself is quite finicky, often times having different behavior if a JSON value is included with a null value or excluded completely. This led to lots of verbose JSON generation. This is more of a comment on the Slack API than any particular PureScript feature. It’s also entirely possible that I could structure this code differently to make it less verbose.

PureScript is a small language and ecosystem and this led to some DIY that I wasn’t expecting. For example, I couldn’t find an existing library for parsing an application/x-www-form-urlencoded body. I ended up writing one myself and learning parser combinators in the process, which was great, but this was a piece of work I wasn’t expecting. I also had to write my own implementation for shuffling an Array (note to reader: my implementation is horrible).

Overall Impression

This project has only increased my enthusiasm around PureScript. It was a joy to use and I’m happy with the resulting application. The language feels flexible, light-weight and very well designed.

I’m also watching the PureScript Native project excitedly. The ability to target the go ecosystem as an alternative backend would be fantastic.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance Python

High Performance Python

Andrew Lewis / O'Reilly Media, Inc. / 2010-09-15 / USD 34.99

Chapter 1. Introduction Section 1.1. The High Performance Buzz-word Chapter 2. The Theory of Computation Section 2.1. Introduction Section 2.2. Problems Section 2.3. Models of Computati......一起来看看 《High Performance Python》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换