commit
31c6e7966e
11 changed files with 2732 additions and 169 deletions
3
.env.sample
Normal file
3
.env.sample
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export AWS_CERTIFICATE=arn:aws:acm:REGION:ACCOUNT:certificate/HASH
|
||||||
|
export DNS_ZONE=example.com.
|
||||||
|
export DOMAIN=puns.example.com
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules/
|
||||||
|
.serverless/
|
||||||
|
.env
|
||||||
|
.env.production
|
|
@ -1,25 +0,0 @@
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- docker build -t atomaka/punaday-api .
|
|
||||||
except:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
deploy:
|
|
||||||
stage: deploy
|
|
||||||
script:
|
|
||||||
- VERSION=$(git describe --tags)
|
|
||||||
- docker build -t atomaka/punaday-api .
|
|
||||||
- docker tag atomaka/punaday-api:latest docker.atomaka.com/atomaka/punaday-api:$VERSION
|
|
||||||
- docker tag atomaka/punaday-api:latest docker.atomaka.com/atomaka/punaday-api:latest
|
|
||||||
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com
|
|
||||||
- docker push docker.atomaka.com/atomaka/punaday-api
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- docker
|
|
12
Dockerfile
12
Dockerfile
|
@ -1,12 +0,0 @@
|
||||||
FROM alpine:latest
|
|
||||||
COPY . /go/src/github.com/atomaka/punaday-api
|
|
||||||
RUN apk update \
|
|
||||||
&& apk add --no-cache go git \
|
|
||||||
&& cd /go/src/github.com/atomaka/punaday-api \
|
|
||||||
&& export GOPATH=/go \
|
|
||||||
&& go get \
|
|
||||||
&& go build -o /bin/punaday-api \
|
|
||||||
&& rm -rf /go \
|
|
||||||
&& apk del --purge git go \
|
|
||||||
&& rm -rf /var/cache/apk*
|
|
||||||
ENTRYPOINT ["/bin/punaday-api"]
|
|
4
Makefile
Normal file
4
Makefile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
yarnbin = $(shell yarn bin)
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
sh -c "source .env.production; $(yarnbin)/serverless deploy --stage production"
|
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# punaday-api
|
||||||
|
|
||||||
|
JSON version of http://www.punoftheday.com/
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Server that takes Pun of the Day puns and converts it into JSON. Currently
|
||||||
|
living as a serverless application in AWS Lamda. It is frontended by AWS
|
||||||
|
Cloudfront because AWS Cloudformation does not yet have resources for AWS API
|
||||||
|
Gateway domains. This script will also create a DNS record to point at the
|
||||||
|
AWS Cloudfront Distribution.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
* `cp .env.sample .env.production`
|
||||||
|
* `AWS_CERTIFICATE`: arn to aws certificate that you generated for a domain
|
||||||
|
* `DNS_ZONE`: Route 53 zone name (ex. `example.com.`)
|
||||||
|
* `DOMAIN`: Route 53 domain (ex. `puns.example.com`)
|
||||||
|
* Set AWS credentials via environment or credential file
|
||||||
|
* `make`
|
132
main.go
132
main.go
|
@ -1,132 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
|
||||||
)
|
|
||||||
|
|
||||||
const PUN_BASE = "http://www.punoftheday.com"
|
|
||||||
const RANDOM_PUN = PUN_BASE + "/cgi-bin/randompun.pl"
|
|
||||||
const SELECT_PUN = PUN_BASE + "/pun"
|
|
||||||
|
|
||||||
type Pun struct {
|
|
||||||
Id int `json:"-"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Status int `json:"-"`
|
|
||||||
Detail string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
http.HandleFunc("/puns/today", TodayPun)
|
|
||||||
http.HandleFunc("/puns/random", RandomPun)
|
|
||||||
http.HandleFunc("/puns/", ShowPun)
|
|
||||||
http.HandleFunc("/", NotFound)
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("404 Not Found: %v\n", r.URL.Path)
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
err := &Error{404, "not_found"}
|
|
||||||
w.WriteHeader(err.Status)
|
|
||||||
json.NewEncoder(w).Encode(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WritePun(w http.ResponseWriter, r *http.Request, pun Pun) {
|
|
||||||
log.Printf("200 OK: %v\n", r.URL.Path)
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
json.NewEncoder(w).Encode(pun)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ShowPun(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id := r.URL.Path[len("/puns/"):]
|
|
||||||
|
|
||||||
pun := getPun(SELECT_PUN + "/" + id)
|
|
||||||
|
|
||||||
if id != strconv.Itoa(pun.Id) {
|
|
||||||
NotFound(w, r)
|
|
||||||
} else {
|
|
||||||
WritePun(w, r, pun)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandomPun(w http.ResponseWriter, r *http.Request) {
|
|
||||||
pun := getPun(RANDOM_PUN)
|
|
||||||
|
|
||||||
WritePun(w, r, pun)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TodayPun(w http.ResponseWriter, r *http.Request) {
|
|
||||||
pun := getPun(PUN_BASE)
|
|
||||||
|
|
||||||
pun.Text = stripQuotes(pun.Text)
|
|
||||||
|
|
||||||
WritePun(w, r, pun)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripQuotes(punText string) string {
|
|
||||||
punText = strings.Replace(punText, "“", "", 1)
|
|
||||||
punText = strings.Replace(punText, "”", "", 1)
|
|
||||||
|
|
||||||
return punText
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPun(url string) Pun {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("ERROR: Unable to access " + url)
|
|
||||||
}
|
|
||||||
|
|
||||||
b := resp.Body
|
|
||||||
defer b.Close()
|
|
||||||
|
|
||||||
pun := Pun{}
|
|
||||||
|
|
||||||
z := html.NewTokenizer(b)
|
|
||||||
|
|
||||||
for {
|
|
||||||
tt := z.Next()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tt == html.ErrorToken:
|
|
||||||
return pun
|
|
||||||
case tt == html.StartTagToken:
|
|
||||||
t := z.Token()
|
|
||||||
|
|
||||||
isParagraph := t.Data == "p"
|
|
||||||
isInput := t.Data == "input"
|
|
||||||
|
|
||||||
if isParagraph && pun.Text == "" {
|
|
||||||
z.Next()
|
|
||||||
t := z.Token()
|
|
||||||
pun.Text = t.Data
|
|
||||||
}
|
|
||||||
if isInput {
|
|
||||||
if getAttr("name", t) == "PunID" {
|
|
||||||
pun.Id, _ = strconv.Atoi(getAttr("value", t))
|
|
||||||
pun.Url = SELECT_PUN + "/" + strconv.Itoa(pun.Id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAttr(at string, t html.Token) string {
|
|
||||||
var val string
|
|
||||||
for _, a := range t.Attr {
|
|
||||||
if a.Key == at {
|
|
||||||
val = a.Val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return val
|
|
||||||
}
|
|
11
package.json
Normal file
11
package.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-preset-es2015": "^6.24.0",
|
||||||
|
"serverless": "^1.9.0",
|
||||||
|
"serverless-offline": "^3.10.3",
|
||||||
|
"serverless-plugin-optimize": "^1.0.0-rc.15"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.15.3"
|
||||||
|
}
|
||||||
|
}
|
82
serverless.yml
Normal file
82
serverless.yml
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
service: punaday-api
|
||||||
|
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
runtime: nodejs4.3
|
||||||
|
memorySize: 128
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- serverless-plugin-optimize
|
||||||
|
- serverless-offline
|
||||||
|
|
||||||
|
package:
|
||||||
|
individually: true
|
||||||
|
|
||||||
|
resources:
|
||||||
|
Resources:
|
||||||
|
CloudfrontDistribution:
|
||||||
|
Type: AWS::CloudFront::Distribution
|
||||||
|
Properties:
|
||||||
|
DistributionConfig:
|
||||||
|
Aliases:
|
||||||
|
- ${env:DOMAIN}
|
||||||
|
Origins:
|
||||||
|
- DomainName: { "Fn::Join": [".", [ { "Ref": "ApiGatewayRestApi" }, "execute-api", { "Ref": "AWS::Region" }, "amazonaws.com"] ] }
|
||||||
|
OriginPath: "/production"
|
||||||
|
Id: APIGW
|
||||||
|
CustomOriginConfig:
|
||||||
|
HTTPSPort: 443
|
||||||
|
OriginProtocolPolicy: https-only
|
||||||
|
DefaultCacheBehavior:
|
||||||
|
AllowedMethods:
|
||||||
|
- HEAD
|
||||||
|
- DELETE
|
||||||
|
- POST
|
||||||
|
- GET
|
||||||
|
- OPTIONS
|
||||||
|
- PUT
|
||||||
|
- PATCH
|
||||||
|
TargetOriginId: APIGW
|
||||||
|
ForwardedValues:
|
||||||
|
QueryString: true
|
||||||
|
Cookies:
|
||||||
|
Forward: none
|
||||||
|
ViewerProtocolPolicy: redirect-to-https
|
||||||
|
DefaultTTL: 0
|
||||||
|
Enabled: true
|
||||||
|
ViewerCertificate:
|
||||||
|
AcmCertificateArn: ${env:AWS_CERTIFICATE}
|
||||||
|
SslSupportMethod: sni-only
|
||||||
|
Route53RecordSet:
|
||||||
|
Type: AWS::Route53::RecordSetGroup
|
||||||
|
Properties:
|
||||||
|
HostedZoneName: ${env:DNS_ZONE}
|
||||||
|
RecordSets:
|
||||||
|
- Name: ${env:DOMAIN}
|
||||||
|
Type: A
|
||||||
|
AliasTarget:
|
||||||
|
DNSName: { "Fn::GetAtt" : [ "CloudfrontDistribution", "DomainName"] }
|
||||||
|
HostedZoneId: Z2FDTNDATAQYW2 # always use for cloudfront
|
||||||
|
|
||||||
|
functions:
|
||||||
|
punsShow:
|
||||||
|
handler: src/puns/show.show
|
||||||
|
events:
|
||||||
|
- http:
|
||||||
|
path: puns/{id}
|
||||||
|
method: get
|
||||||
|
cors: true
|
||||||
|
punsToday:
|
||||||
|
handler: src/puns/show.today
|
||||||
|
events:
|
||||||
|
- http:
|
||||||
|
path: puns/today
|
||||||
|
method: get
|
||||||
|
cors: true
|
||||||
|
punsRandom:
|
||||||
|
handler: src/puns/show.random
|
||||||
|
events:
|
||||||
|
- http:
|
||||||
|
path: puns/random
|
||||||
|
method: get
|
||||||
|
cors: true
|
53
src/puns/show.js
Normal file
53
src/puns/show.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
const PUN_BASE = 'http://www.punoftheday.com'
|
||||||
|
|
||||||
|
module.exports.today = ((event, context, callback) => {
|
||||||
|
respondWithPunFrom(PUN_BASE, callback)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports.random = ((event, context, callback) => {
|
||||||
|
respondWithPunFrom(`${PUN_BASE}/cgi-bin/randompun.pl`, callback)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports.show = ((event, context, callback) => {
|
||||||
|
const id = event.pathParameters.id
|
||||||
|
|
||||||
|
respondWithPunFrom(`${PUN_BASE}/pun/${id}`, callback)
|
||||||
|
})
|
||||||
|
|
||||||
|
function respondWithPunFrom(url, callback) {
|
||||||
|
return axios.get(url)
|
||||||
|
.then(response => {
|
||||||
|
const pun = parsePun(response.data)
|
||||||
|
|
||||||
|
callback(null, punResponse(pun))
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
callback(null, errorResponse())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePun(html) {
|
||||||
|
const punMatches = html.match(/<p>(.*)<\/p>/)
|
||||||
|
const text = punMatches[1].replace('“', '').replace('”', '')
|
||||||
|
const idMatches = html.match(/name="PunID" value="(\d+)"/)
|
||||||
|
const id = idMatches[1]
|
||||||
|
const urlMatches = html.match(/class="fb-share-button" data-href="(.*)" d/)
|
||||||
|
const url = urlMatches[1]
|
||||||
|
|
||||||
|
return { id, text, url }
|
||||||
|
}
|
||||||
|
|
||||||
|
function punResponse (pun) {
|
||||||
|
delete pun['id']
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: JSON.stringify(pun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorResponse () {
|
||||||
|
return { statusCode: 500 }
|
||||||
|
}
|
Loading…
Reference in a new issue