Initial release
This commit is contained in:
commit
a8c21fabf1
170
.gitignore
vendored
Normal file
170
.gitignore
vendored
Normal 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
22
Dockerfile
Normal 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
61
Makefile
Normal 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
39
README.md
Normal 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
13
go.mod
Normal 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
18
go.sum
Normal 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
92
main.go
Normal 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
38
tweet.svg.tmpl
Normal 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 |
Loading…
Reference in New Issue
Block a user