Initial release

This commit is contained in:
Marcus Noble 2021-02-18 18:32:36 +00:00
commit a8c21fabf1
8 changed files with 453 additions and 0 deletions

170
.gitignore vendored Normal file
View File

@ -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

22
Dockerfile Normal file
View File

@ -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"]

61
Makefile Normal file
View File

@ -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

39
README.md Normal file
View File

@ -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)

13
go.mod Normal file
View File

@ -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
)

18
go.sum Normal file
View File

@ -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=

92
main.go Normal file
View File

@ -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
}
}

38
tweet.svg.tmpl Normal file
View File

@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32em">
<foreignObject x="0" y="0" width="32em" height="100%" fill="#eade52">
<style>
.tweetsvg{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
.tweetsvg.text{font-size: 23px;}
a.tweetsvg{color: rgb(27, 149, 224); text-decoration:none;}
blockquote.tweetsvg{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
.avatar-tweetsvg{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
h1.tweetsvg{margin:0;font-size:15px;text-decoration:none;color:#000;}
h2.tweetsvg{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
p.tweetsvg{font-size:1rem; clear:both;}
hr.tweetsvg{color:#ddd;}
.media-tweetsvg{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
time.tweetsvg{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
</style>
<blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml">
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="tweetsvg">{{ .User.Name }}</h1></a>
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="tweetsvg">@{{ .User.ScreenName }}</h2></a>
<p class="tweetsvg text">{{ .Text }}</p>
{{ if .ExtendedEntities }}
{{ range .ExtendedEntities.Media }}
<a href="{{ .Media_url_https }}" target="_blank"><img class="media-tweetsvg" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
{{ end }}
{{ end }}
<a class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
<time class="tweetsvg" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
</a>
</blockquote>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB