Skip to content

Commit ee448a7

Browse files
committed
Extend webwatch codelab with Slack part. Make the codelabs 'timeless' (i.e., remove the year reference :D). Update webwatch codelab to reflect the current state of the code
1 parent 2403086 commit ee448a7

File tree

4 files changed

+161
-42
lines changed

4 files changed

+161
-42
lines changed

memegen/codelab/memegen_codelab.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#### ZuriHac 2016 Beginner Track Exercise
1+
#### ZuriHac Beginner Track Exercise
22
# Web meme generator
33

44
**Description:** A web application for creating [memes](https://en.wikipedia.org/wiki/Internet_meme) from pictures:
@@ -12,8 +12,7 @@
1212
* How to build a small, real-world web application in Haskell (no HTML or CSS
1313
knowledge (or work) required).
1414

15-
**NOTE:** Instructions are tested on Ubuntu Linux 16.04, OS X. For help with
16-
Microsoft Windows, please email [email protected].
15+
**NOTE:** Instructions are tested on Ubuntu Linux 16.04 and OS X.
1716

1817
Let's start!
1918

webwatch/README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,24 @@
22

33
## Introduction
44

5-
This is a demo beginners project for ZuriHac 2016. It watches a certain webpage
5+
This is a demo beginners project for ZuriHac 2016. It watches a certain webpage
66
for links with keywords (e.g. search http://news.ycombinator.com for "Haskell").
7-
When such a link is found, it sends you a slack message.
7+
When such a link is found, it sends you a slack message. See the
8+
[codelab](codelab/webwatch_codelab.md) for more details.
89

910
## Code layout
1011

1112
The code is organised in three modules.
1213

1314
- `WebWatch.Links`: Scrapes a web page and pulls out links matching given
1415
keywords
15-
- `WebWatch.Slack`: Performs a POST request to a Slack webhook
16+
- `WebWatch.Slack`: Performs a HTTP POST request to a Slack webhook
1617
- `Main.hs`: Main module, reads the configuration file and then runs the
1718
scraper/chatbot at a configurable interval
1819

1920
# Libraries
2021

21-
My implementation uses the following libraries:
22+
The implementation uses the following libraries:
2223

2324
- `http-client` for downloading a web page and sending a POST
2425
- `tagsoup` for parsing the web page and getting out the links

webwatch/codelab/webwatch_codelab.md

+153-34
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
#### ZuriHac 2017 Beginner Track Exercise
1+
#### ZuriHac Beginner Track Exercise
22

3-
**Description:** A command-line tool which watches a certain webpage
4-
for links with keywords (e.g. search http://news.ycombinator.com for "Haskell").
5-
When such a link is found, it sends you an email.
3+
**Description:** A command-line tool which watches a certain webpage for links
4+
with keywords (e.g. search http://news.ycombinator.com for "Haskell"). When
5+
such a link is found, it sends a message to the [Slack](https://slack.com/)
6+
channel.
67

78
**Learning goals:**
89
* How to write a basic command-line application
910
* How to load configuration from a config-file
1011
* How to fetch and parse a HTML page
11-
* How to use AWS SES to send emails
12+
* How to write HTTP client for REST API
1213
* How to use the Reader/State monad transformers
1314

1415
Let's start!
1516

1617

1718
## Development environment
1819

19-
The only tool you need to build this project is [Stack](http://haskellstack.org/) – The Haskell Tool Stack. Follow the install instructions on its homepage.
20+
The only tool you need to build this project is
21+
[Stack](http://haskellstack.org/) – The Haskell Tool Stack. Follow the install
22+
instructions on its homepage.
2023

2124
## New project
2225

@@ -94,20 +97,18 @@ import System.Environment (getArgs)
9497
import System.Exit (exitFailure)
9598

9699
data Config = Config
97-
{ cPatterns :: [T.Text]
98-
, cUrl :: !T.Text
99-
, cInterval :: !Int
100-
, cMailFrom :: !T.Text
101-
, cMailTo :: !T.Text
100+
{ cPatterns :: [T.Text]
101+
, cUrl :: !T.Text
102+
, cInterval :: !Int
103+
, cSlackWebhookUrl :: !T.Text
102104
} deriving (Show)
103105

104106
parseConfig :: C.Config -> IO Config
105107
parseConfig conf = do
106-
cPatterns <- C.require conf "patterns"
107-
cUrl <- C.require conf "url"
108-
cInterval <- C.require conf "interval"
109-
cMailFrom <- C.require conf "mail.from"
110-
cMailTo <- C.require conf "mail.to"
108+
cPatterns <- C.require conf "patterns"
109+
cUrl <- C.require conf "url"
110+
cInterval <- C.require conf "interval"
111+
cSlackWebhookUrl <- C.require conf "slack.webhook_url"
111112
return Config {..}
112113

113114

@@ -170,12 +171,11 @@ We need to give it one argument – the config file to load. Let's create `webw
170171
and fill it with some sensible thinks to watch.
171172

172173
```
173-
url = "http://news.ycombinator.com/newest"
174+
url = "http://news.ycombinator.com/"
174175
patterns = ["google", "haskell", "microsoft", "apple"]
175176
interval = 5
176-
mail {
177-
178-
177+
slack {
178+
webhook_url = "https://hooks.slack.com/services/TTTTTTTTT/CCCCCCCCC/XXXXXXXXXXXXXXXXXXXXXXXX"
179179
}
180180
```
181181

@@ -184,7 +184,7 @@ Now we can see if we can load this file:
184184
```
185185
$ stack exec webwatch -- webwatch.conf
186186
Got a config file:
187-
Config {cPatterns = ["google","haskell","microsoft","apple"], cUrl = "http://news.ycombinator.com/newest", cInterval = 5, cMailFrom = "[email protected]", cMailTo = "jaspervdj@gmail.com"}
187+
Config {cPatterns = ["google","haskell","microsoft","apple"], cUrl = "http://news.ycombinator.com/", cInterval = 5, cSlackWebhookUrl = "https://hooks.slack.com/services/TTTTTTTTT/CCCCCCCCC/XXXXXXXXXXXXXXXXXXXXXXXX"}
188188
```
189189

190190
## Keeping state across checks
@@ -267,10 +267,10 @@ You can exit ghci by pressing CTRL-D or writing `:q` followed by the enter key.
267267

268268
## The main watch function
269269

270-
We write the main watch function such that it fetches the links, sends
271-
the emails, then sleeps for some time and then it is done. It runs inside
272-
our `WebWatchM` monad, which gives it (read-only) access to the `Config`
273-
object and the ability to update the `LinkSet`.
270+
We write the main watch function such that it fetches the links, sends the
271+
message, then sleeps for some time and then it is done. It runs inside our
272+
`WebWatchM` monad, which gives it (read-only) access to the `Config` object and
273+
the ability to update the `LinkSet`.
274274

275275
The function is aptly named `watchOnce` because it runs the checks once
276276
and then is done.
@@ -287,7 +287,7 @@ watchOnce = do
287287

288288
-- TODO: Fetch the links
289289

290-
-- TODO: Send emails
290+
-- TODO: Send message
291291

292292
slog $ "Sleeping " ++ show cInterval ++ " minute(s)"
293293
liftIO $ threadDelay (cInterval * 60 * 1000 * 1000)
@@ -332,7 +332,7 @@ and then sleep for 5 minutes:
332332
```
333333
$ stack build
334334
$ stack exec webwatch -- webwatch.conf
335-
Getting links from http://news.ycombinator.com/newest
335+
Getting links from http://news.ycombinator.com/
336336
Sleeping 5 minute(s)
337337
```
338338

@@ -348,7 +348,7 @@ you need to use `threadDelay (5 * 1000 * 1000)`.
348348
Now that we have the main loop ready, there are two parts missing:
349349

350350
- fetching the links from the webpage
351-
- sending the emails
351+
- sending the message
352352

353353
Let's tackle the first one.
354354

@@ -385,10 +385,10 @@ getMatchingLinks patterns uri = do
385385
and register it in the cabal file:
386386

387387
```
388-
executabe
388+
Executable webwatch
389389
390390
391-
other-modules:
391+
Other-modules:
392392
WebWatch.GetLinks
393393
```
394394

@@ -491,7 +491,7 @@ see which links it has found:
491491
```
492492
$ stack build
493493
$ stack exec webwatch -- webwatch.conf
494-
Getting links from http://news.ycombinator.com/newest
494+
Getting links from http://news.ycombinator.com/
495495
[Link {lTitle = "Amazon reportedly working on proper Android \8216Ice\8217 smartphones with Google\8217s apps", lHref = "https://www.theverge.com/circuitbreaker/2017/6/5/15739540/amazon-android-ice-smartphones-google-apps-services-fire-phone-report"}]
496496
Sleeping 5 minute(s)
497497
```
@@ -563,15 +563,15 @@ watchOnce = do
563563
```
564564
$ stack build
565565
$ stack exec webwatch -- webwatch.conf
566-
Getting links from http://news.ycombinator.com/newest
566+
Getting links from http://news.ycombinator.com/
567567
All links: [Link {lTitle = "Apple, Amazon to back Foxconn on Toshiba chip bid, Gou says", lHref = "http://asia.nikkei.com/Business/Deals/Apple-Amazon-to-back-Foxconn-on-Toshiba-chip-bid-Gou-says"}]
568568
New links: [Link {lTitle = "Apple, Amazon to back Foxconn on Toshiba chip bid, Gou says", lHref = "http://asia.nikkei.com/Business/Deals/Apple-Amazon-to-back-Foxconn-on-Toshiba-chip-bid-Gou-says"}]
569569
570-
Getting links from http://news.ycombinator.com/newest
570+
Getting links from http://news.ycombinator.com/
571571
All links: [Link {lTitle = "Apple, Amazon to back Foxconn on Toshiba chip bid, Gou says", lHref = "http://asia.nikkei.com/Business/Deals/Apple-Amazon-to-back-Foxconn-on-Toshiba-chip-bid-Gou-says"}]
572572
New links: []
573573
574-
Getting links from http://news.ycombinator.com/newest
574+
Getting links from http://news.ycombinator.com/
575575
All links: [Link {lTitle = "Apple, Amazon to back Foxconn on Toshiba chip bid, Gou says", lHref = "http://asia.nikkei.com/Business/Deals/Apple-Amazon-to-back-Foxconn-on-Toshiba-chip-bid-Gou-says"}]
576576
New links: []
577577
```
@@ -581,3 +581,122 @@ That looks good, the list of all links remains the same, and the new link is con
581581
and thus ignored in the later iterations.
582582

583583

584+
## Sending the messages
585+
586+
Now that we have found the interesting articles, we should send them to the
587+
[Slack](https://slack.com) channel. We can do that through REST API provided by
588+
Slack. Consuming REST API means sending HTTP POST request. For that we'll use
589+
`http-client` and `http-client-tls` packages. Add them to the project's cabal
590+
file.
591+
592+
Create the file `src/WebWatch/Slack.hs`:
593+
594+
```haskell
595+
-- | This module contains code for sending messages to the Slack channel.
596+
{-# LANGUAGE OverloadedStrings #-}
597+
module WebWatch.Slack
598+
( sendLinks
599+
) where
600+
601+
import qualified Data.Text as T
602+
import WebWatch.GetLinks
603+
604+
sendLinks
605+
:: T.Text -- ^ Special access URL
606+
-> [Link] -- ^ Links
607+
-> IO ()
608+
sendLinks webhookUrl links = do
609+
return ()
610+
```
611+
612+
and register it in the cabal file:
613+
614+
```
615+
Executable webwatch
616+
617+
618+
Other-modules:
619+
WebWatch.GetLinks
620+
WebWatch.Slack
621+
```
622+
623+
Now you have all that's needed to finish the `watchOnce` function:
624+
625+
```haskell
626+
watchOnce = do
627+
628+
629+
unless (null newLinks) $ do
630+
slog $ "Sending slack message..."
631+
catchExceptions () $ Slack.sendLinks cSlackWebhookUrl newLinks
632+
```
633+
634+
We still have to finish the Slack client. To send data to the service, we'll
635+
use JSON and `aeson` library (remember to update cabal dependencies). Open
636+
`src/WebWatch/Slack.hs` and add: `Payload` data (message format for
637+
communication), its Aeson wrapper for automatic JSON serialization and
638+
deserialization, and the constructor `mkPayload`:
639+
640+
```haskell
641+
import Data.Aeson ((.=))
642+
import qualified Data.Aeson as Aeson
643+
import Data.Monoid ((<>))
644+
645+
data Payload = Payload
646+
{ payloadText :: !T.Text
647+
} deriving (Show)
648+
649+
instance Aeson.ToJSON Payload where
650+
toJSON p = Aeson.object
651+
[ "text" .= payloadText p
652+
]
653+
654+
mkPayload
655+
:: [Link]
656+
-> Payload
657+
mkPayload links = Payload
658+
{ payloadText = T.unlines $
659+
[ "We found a few links matching your query."
660+
, ""
661+
] ++
662+
[ "- " <> lTitle link <> "\n " <> lHref link
663+
| link <- links
664+
]
665+
}
666+
```
667+
668+
The core part of Stack client is `sendLinks` method. It creates the payload out
669+
of the given links, build the HTTP POST requests and sends it to the Slack
670+
service endpoint. Let's write it:
671+
672+
```haskell
673+
import qualified Network.HTTP.Client as Http
674+
import qualified Network.HTTP.Client.TLS as Http
675+
676+
sendLinks
677+
:: T.Text -- ^ Special access URL
678+
-> [Link] -- ^ Links
679+
-> IO ()
680+
sendLinks webhookUrl links = do
681+
-- Create payload
682+
let payload = mkPayload links
683+
684+
-- Initialize an HTTP manager
685+
manager <- Http.newManager Http.tlsManagerSettings
686+
687+
-- We create an initial request by parsing the URL
688+
request0 <- Http.parseRequest (T.unpack webhookUrl)
689+
690+
-- But we need to set a few more details:
691+
let request = request0
692+
{ Http.method = "POST"
693+
, Http.requestBody = Http.RequestBodyLBS (Aeson.encode payload)
694+
}
695+
696+
-- We perform the request (but we can ignore the result).
697+
_ <- Http.httpLbs request manager
698+
699+
return ()
700+
```
701+
702+
That's all!

webwatch/src/WebWatch/Slack.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- | This module contains code for sending notification mails.
1+
-- | This module contains code for sending messages to the Slack channel.
22
{-# LANGUAGE OverloadedStrings #-}
33
module WebWatch.Slack
44
( sendLinks

0 commit comments

Comments
 (0)