From a8c21fabf1f68ffe70315ebeb2ce570812eb3161 Mon Sep 17 00:00:00 2001 From: Marcus Noble Date: Thu, 18 Feb 2021 18:32:36 +0000 Subject: [PATCH] Initial release --- .gitignore | 170 +++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 22 +++++++ Makefile | 61 ++++++++++++++++++ README.md | 39 ++++++++++++ go.mod | 13 ++++ go.sum | 18 ++++++ main.go | 92 ++++++++++++++++++++++++++ tweet.svg.tmpl | 38 +++++++++++ 8 files changed, 453 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 tweet.svg.tmpl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad062f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# rollup.js default build output +dist/ + +# Storybook build outputs +.out +.storybook-out + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Temporary folders +tmp/ +temp/ + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# MacOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..af1903e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:alpine AS builder +RUN apk update && apk add --no-cache git && adduser -D -g '' gopher && apk add -U --no-cache ca-certificates +WORKDIR /app/ +ADD go.mod go.sum ./ +RUN go mod download +ADD . . +RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o tweetsvg main.go + +FROM scratch + +ENV ACCESS_TOKEN= +ENV ACCESS_TOKEN_SECRET= +ENV CONSUMER_KEY= +ENV CONSUMER_SECRET= + +WORKDIR /app/ +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /app/tweetsvg /app/tweetsvg +USER gopher + +ENTRYPOINT ["/app/tweetsvg"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f8a9ee8 --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +.DEFAULT_GOAL := default + +IMAGE ?= docker.cluster.fun/averagemarcus/tweetsvg:latest + +.PHONY: test # Run all tests, linting and format checks +test: lint check-format run-tests + +.PHONY: lint # Perform lint checks against code +lint: + @go vet && golint -set_exit_status ./... + +.PHONY: check-format # Checks code formatting and returns a non-zero exit code if formatting errors found +check-format: + @gofmt -e -l . + +.PHONY: format # Performs automatic format fixes on all code +format: + @gofmt -s -w . + +.PHONY: run-tests # Runs all tests +run-tests: + @echo "⚠️ 'run-tests' unimplemented" + +.PHONY: fetch-deps # Fetch all project dependencies +fetch-deps: + @go mod tidy + +.PHONY: build # Build the project +build: lint check-format fetch-deps + @go build -o tweetsvg main.go + +.PHONY: docker-build # Build the docker image +docker-build: + @docker build -t $(IMAGE) . + +.PHONY: docker-publish # Push the docker image to the remote registry +docker-publish: + @docker push $(IMAGE) + +.PHONY: run # Run the application +run: + @go run main.go@npm start + +.PHONY: ci # Perform CI specific tasks to perform on a pull request +ci: + @echo "⚠️ 'ci' unimplemented" + +.PHONY: release # Release the latest version of the application +release: + @echo "⚠️ 'release' unimplemented" + +.PHONY: help # Show this list of commands +help: + @echo "tweetsvg" + @echo "Usage: make [target]" + @echo "" + @echo "target description" | expand -t20 + @echo "-----------------------------------" + @grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1 \2/' | expand -t20 + +default: test build diff --git a/README.md b/README.md new file mode 100644 index 0000000..09bc8ae --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# TweetSVG + +Generate an SVG for a given Tweet ID + +## Features + +## Install + +```sh + +``` + +## Building from source + +With Docker: + +```sh +make docker-build +``` + +Standalone: + +```sh +make build +``` + +## Resources + +## Contributing + +If you find a bug or have an idea for a new feature please [raise an issue](issues/new) to discuss it. + +Pull requests are welcomed but please try and follow similar code style as the rest of the project and ensure all tests and code checkers are passing. + +Thank you 💛 + +## License + +See [LICENSE](LICENSE) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa70624 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module tweetsvg + +go 1.15 + +require ( + github.com/ChimeraCoder/anaconda v2.0.0+incompatible + github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 // indirect + github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect + github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect + github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect + github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e64ddde --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/ChimeraCoder/anaconda v2.0.0+incompatible h1:F0eD7CHXieZ+VLboCD5UAqCeAzJZxcr90zSCcuJopJs= +github.com/ChimeraCoder/anaconda v2.0.0+incompatible/go.mod h1:TCt3MijIq3Qqo9SBtuW/rrM4x7rDfWqYWHj8T7hLcLg= +github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 h1:r+EmXjfPosKO4wfiMLe1XQictsIlhErTufbWUsjOTZs= +github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7/go.mod h1:b2EuEMLSG9q3bZ95ql1+8oVqzzrTNSiOQqSXWFBzxeI= +github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78= +github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo= +github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI= +github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw= +github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA= +github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ= +github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= +github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3fc91f2 --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/ChimeraCoder/anaconda" +) + +var ( + api *anaconda.TwitterApi + + port = os.Getenv("PORT") + + tweetDateLayout = "Mon Jan 2 15:04:05 -0700 2006" + + accessToken = os.Getenv("ACCESS_TOKEN") + accessTokenSecret = os.Getenv("ACCESS_TOKEN_SECRET") + consumerKey = os.Getenv("CONSUMER_KEY") + consumerSecret = os.Getenv("CONSUMER_SECRET") +) + +func main() { + if accessToken == "" || accessTokenSecret == "" || consumerKey == "" || consumerSecret == "" { + panic("Missing Twitter credentials") + } + + api = anaconda.NewTwitterApiWithCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret) + + if port == "" { + port = "8080" + } + + http.HandleFunc("/", getTweet) + fmt.Println("Server started at port " + port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +func getTweet(w http.ResponseWriter, r *http.Request) { + id := strings.ReplaceAll(r.URL.Path, "/", "") + i, err := strconv.ParseInt(id, 10, 64) + if err != nil { + w.WriteHeader(400) + return + } + tweet, err := api.GetTweet(i, nil) + if err != nil { + w.WriteHeader(404) + return + } + + templateFuncs := template.FuncMap{ + "base64": func(url string) string { + res, err := http.Get(url) + if err != nil { + return "" + } + buf := new(bytes.Buffer) + buf.ReadFrom(res.Body) + return base64.StdEncoding.EncodeToString(buf.Bytes()) + }, + "isoDate": func(date string) string { + t, _ := time.Parse(tweetDateLayout, date) + return t.Format(time.RFC3339) + }, + "humanDate": func(date string) string { + t, _ := time.Parse(tweetDateLayout, date) + return t.Format("3:04 PM · Jan 2, 2006") + }, + } + + t := template.Must( + template.New("tweet.svg.tmpl"). + Funcs(templateFuncs). + ParseFiles("tweet.svg.tmpl")) + + w.Header().Set("Content-type", "image/svg+xml") + err = t.Execute(w, tweet) + if err != nil { + fmt.Println(err) + w.WriteHeader(500) + return + } +} diff --git a/tweet.svg.tmpl b/tweet.svg.tmpl new file mode 100644 index 0000000..bc4d092 --- /dev/null +++ b/tweet.svg.tmpl @@ -0,0 +1,38 @@ + + + +
+ + +

{{ .User.Name }}

+ +

@{{ .User.ScreenName }}

+ +

{{ .Text }}

+ + {{ if .ExtendedEntities }} + {{ range .ExtendedEntities.Media }} + + {{ .ExtAltText }} + + {{ end }} + {{ end }} + + + + +
+
+