initial commit

master
Christopher Brown 2017-12-21 05:50:48 -06:00
commit 0caafd9f81
83 changed files with 1124 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/plugin.exe
/mattermost-memes-plugin.tar.gz
/memelibrary/assets.go
/dist
/vendor

33
Makefile Normal file
View File

@ -0,0 +1,33 @@
.PHONY: test
all: test dist
TAR_PLUGIN_EXE_TRANSFORM = --transform 'flags=r;s|dist/intermediate/plugin_.*|plugin.exe|'
ifneq (,$(findstring bsdtar,$(shell tar --version)))
TAR_PLUGIN_EXE_TRANSFORM = -s '|dist/intermediate/plugin_.*|plugin.exe|'
endif
dist: vendor memelibrary/assets.go $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}} {{end}}' ./...) plugin.yaml
rm -rf ./dist
go get github.com/mitchellh/gox
$(shell go env GOPATH)/bin/gox -osarch='darwin/amd64 linux/amd64 windows/amd64' -output 'dist/intermediate/plugin_{{.OS}}_{{.Arch}}'
tar -czvf dist/mattermost-meme-plugin-darwin-amd64.tar.gz $(TAR_PLUGIN_EXE_TRANSFORM) dist/intermediate/plugin_darwin_amd64 plugin.yaml
tar -czvf dist/mattermost-meme-plugin-linux-amd64.tar.gz $(TAR_PLUGIN_EXE_TRANSFORM) dist/intermediate/plugin_linux_amd64 plugin.yaml
tar -czvf dist/mattermost-meme-plugin-windows-amd64.tar.gz $(TAR_PLUGIN_EXE_TRANSFORM) dist/intermediate/plugin_windows_amd64.exe plugin.yaml
rm -rf dist/intermediate
memelibrary/assets.go: $(shell find memelibrary/assets)
go get github.com/jteeuwen/go-bindata/...
$(shell go env GOPATH)/bin/go-bindata -o $@ -pkg memelibrary -prefix memelibrary/assets/ -ignore '(^|/)\..*' memelibrary/assets/...
mattermost-memes-plugin.tar.gz: vendor memelibrary/assets.go $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}} {{end}}' ./...) plugin.yaml
go build -o plugin.exe
tar -czvf $@ plugin.exe plugin.yaml
rm plugin.exe
test: vendor
go test -v ./...
vendor: glide.lock
go get github.com/Masterminds/glide
$(shell go env GOPATH)/bin/glide install

1
README.md Normal file
View File

@ -0,0 +1 @@
# mattermost-plugin-memes

64
glide.lock generated Normal file
View File

@ -0,0 +1,64 @@
hash: 5c78b0337c128afa41779ef112bf41aea7a92c9094e13fab7223d831de386916
updated: 2017-12-10T01:37:04.279381-06:00
imports:
- name: github.com/alecthomas/log4go
version: 3fbce08846379ec7f4f6bc7fce6dd01ce28fae4c
repo: https://github.com/mattermost/log4go.git
- name: github.com/golang/freetype
version: e2365dfdc4a05e4b8299a783240d4a7d5a65d4e4
subpackages:
- raster
- truetype
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/mux
version: 7f08801859139f86dfafd1c296e2cba9a80d292e
- name: github.com/gorilla/websocket
version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b
- name: github.com/kballard/go-shellquote
version: cd60e84ee657ff3dc51de0b4f55dd299a3e136f2
- name: github.com/mattermost/mattermost-server
version: 4c17bdff1bb871fb31520b7b547f584c53ed854f
subpackages:
- model
- plugin
- plugin/rpcplugin
- name: github.com/nicksnyder/go-i18n
version: 0dc1626d56435e9d605a29875701721c54bc9bbd
subpackages:
- i18n
- i18n/bundle
- i18n/language
- i18n/translation
- name: github.com/pborman/uuid
version: e790cca94e6cc75c7064b1332e63811d4aae1a53
- name: github.com/pelletier/go-toml
version: 4e9e0ee19b60b13eb79915933f44d8ed5f268bdd
- name: github.com/pkg/errors
version: f15c970de5b76fac0b59abb32d62c17cc7bed265
- name: golang.org/x/crypto
version: 94eea52f7b742c7cbe0b03b22f0c4c8631ece122
subpackages:
- bcrypt
- blowfish
- name: golang.org/x/image
version: e5db4c466346ada62f27934dab20e4b6f6bda285
subpackages:
- font
- math/fixed
- name: gopkg.in/yaml.v2
version: 287cf08546ab5e7e37d55a84f7ed3fd1db036de5
testImports:
- name: github.com/davecgh/go-spew
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert
- require

24
glide.yaml Normal file
View File

@ -0,0 +1,24 @@
package: github.com/mattermost/mattermost-plugin-memes
import:
- package: github.com/golang/freetype
subpackages:
- truetype
- package: github.com/gorilla/mux
version: ^1.6.0
- package: github.com/kballard/go-shellquote
- package: github.com/mattermost/mattermost-server
version: ^4.5.0-rc1
subpackages:
- plugin
- plugin/rpcplugin
- package: golang.org/x/image
subpackages:
- font
- math/fixed
- package: gopkg.in/yaml.v2
testImport:
- package: github.com/stretchr/testify
version: ^1.1.4
subpackages:
- assert
- require

27
meme/template.go Normal file
View File

@ -0,0 +1,27 @@
package meme
import (
"image"
"image/draw"
)
type Template struct {
Name string
Image image.Image
TextSlots []*TextSlot
}
func (t *Template) Render(text []string) (image.Image, error) {
b := t.Image.Bounds()
img := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(img, img.Bounds(), t.Image, b.Min, draw.Src)
for i, slot := range t.TextSlots {
if i >= len(text) {
break
}
slot.Render(img, text[i])
}
return img, nil
}

199
meme/text_slot.go Normal file
View File

@ -0,0 +1,199 @@
package meme
import (
"image"
"image/color"
"image/draw"
"strings"
"unicode"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
type HorizontalAlignment int
const (
Left HorizontalAlignment = -1
Center = 0
Right = 1
)
type VerticalAlignment int
const (
Top VerticalAlignment = -1
Middle = 0
Bottom = 1
)
type TextSlot struct {
Bounds image.Rectangle
Font *truetype.Font
MaxFontSize float64
HorizontalAlignment HorizontalAlignment
VerticalAlignment VerticalAlignment
TextColor color.Color
OutlineColor color.Color
AllUppercase bool
}
func (s *TextSlot) Render(img draw.Image, text string) {
if s.AllUppercase {
text = strings.ToUpper(text)
}
layout := s.TextLayout(text)
if layout == nil {
return
}
textColor := s.TextColor
if textColor == nil {
textColor = color.Black
}
for i, line := range layout.Lines {
if s.OutlineColor != nil {
// it's okay, memes aren't supposed to look good
offset := layout.Face.Metrics().Height / 16
for _, delta := range []fixed.Point26_6{
{X: offset, Y: offset},
{X: -offset, Y: offset},
{X: -offset, Y: -offset},
{X: offset, Y: -offset},
} {
drawer := font.Drawer{
Dst: img,
Src: image.NewUniform(s.OutlineColor),
Face: layout.Face,
Dot: layout.LinePositions[i].Add(delta),
}
drawer.DrawString(line)
}
}
drawer := font.Drawer{
Dst: img,
Src: image.NewUniform(textColor),
Face: layout.Face,
Dot: layout.LinePositions[i],
}
drawer.DrawString(line)
}
}
type TextLayout struct {
Face font.Face
Lines []string
LinePositions []fixed.Point26_6
}
func (s *TextSlot) TextLayout(text string) *TextLayout {
fontSize := s.MaxFontSize
if fontSize == 0.0 {
fontSize = 80.0
}
hlimit := fixed.Int26_6(s.Bounds.Dx() * 64)
vlimit := fixed.Int26_6(s.Bounds.Dy() * 64)
for fontSize >= 6.0 {
face := truetype.NewFace(s.Font, &truetype.Options{
Size: fontSize,
})
lineLimit := s.Bounds.Dy() / int(face.Metrics().Height/64)
lines, widths := lines(face, text, hlimit)
if len(lines) > lineLimit {
fontSize -= (fontSize + 9) / 10
continue
}
layout := &TextLayout{
Face: face,
Lines: lines,
LinePositions: make([]fixed.Point26_6, len(lines)),
}
y := fixed.Int26_6(s.Bounds.Min.Y * 64)
totalHeight := face.Metrics().Height.Mul(fixed.Int26_6(len(lines) * 64))
switch s.VerticalAlignment {
case Middle:
y += (vlimit - totalHeight) / 2
case Bottom:
y += (vlimit - totalHeight)
}
for i, width := range widths {
x := fixed.Int26_6(s.Bounds.Min.X * 64)
switch s.HorizontalAlignment {
case Center:
x += (hlimit - width) / 2
case Right:
x += (hlimit - width)
}
y += face.Metrics().Height
layout.LinePositions[i] = fixed.Point26_6{
X: x,
Y: y,
}
}
return layout
}
return nil
}
func lines(face font.Face, text string, limit fixed.Int26_6) (lines []string, widths []fixed.Int26_6) {
for text != "" {
line, width, remaining := firstLine(face, text, limit)
if line == "" {
return nil, nil
}
lines = append(lines, line)
widths = append(widths, width)
text = remaining
}
return
}
func firstLine(face font.Face, text string, limit fixed.Int26_6) (string, fixed.Int26_6, string) {
text = strings.TrimSpace(text)
pos := 0
lastBreak := 0
var width fixed.Int26_6
var lastBreakWidth fixed.Int26_6
var prev rune = -1
for _, r := range text {
advance, ok := face.GlyphAdvance(r)
if !ok {
continue
}
if prev >= 0 {
advance += face.Kern(prev, r)
}
if unicode.IsSpace(r) && !unicode.IsSpace(prev) {
lastBreak = pos
lastBreakWidth = width
}
if width+advance > limit {
if lastBreak == 0 {
return text[:pos], width, text[pos:]
}
return text[:lastBreak], lastBreakWidth, text[lastBreak:]
}
pos += 1
width += advance
prev = r
}
return text, width, ""
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,6 @@
patterns:
- pattern: ((?:\b\S+\b\W*){1,2}) all the ((?:\b\S+\b\W*){1,2})
text:
- $1
- all the $2
example: delete all the things!

View File

@ -0,0 +1,6 @@
patterns:
- pattern: am i the only one (here|around here)? (.*)
text:
- am i the only one $1
- $2
example: am i the only one here that knows how to meme

View File

@ -0,0 +1,2 @@
aliases:
- aliens

View File

@ -0,0 +1,3 @@
aliases:
- bad-luck
- brian

View File

@ -0,0 +1,16 @@
aliases:
- batman-robin
- batman-and-robin
- batman-slap
example:
- silence is golden
- and duct tape is silver
slots:
- x: 2
y: 2
width: 200
height: 90
- x: 212
y: 2
width: 180
height: 80

View File

@ -0,0 +1,29 @@
aliases:
- boardroom
- boardroom-meeting
example:
- Meow.
- Woof.
- Ribbet.
- I didn't know I worked in a mental hospital.
slots:
- x: 155
y: 6
width: 300
height: 40
font: ComicNeue-Regular
- x: 28
y: 249
width: 96
height: 21
font: ComicNeue-Regular
- x: 168
y: 258
width: 78
height: 21
font: ComicNeue-Regular
- x: 310
y: 260
width: 127
height: 41
font: ComicNeue-Regular

View File

@ -0,0 +1,9 @@
aliases:
- brace-yourself
- winter-is-coming
patterns:
- pattern: brace (yourselves|yourself)([^a-z]+)?\b([^,\.;]+.*)
text:
- brace $1$2
- $3
example: brace yourselves, winter is coming

View File

@ -0,0 +1,8 @@
aliases:
- kermit
patterns:
- pattern: (.*) (but that'?s none of [^ ]+ business.*)
text:
- $1
- $2
example: this amount of tea may be unhealthy. but that's none of your business

View File

@ -0,0 +1,3 @@
example:
- my reason to get out of bed in the morning
- is to earn enough money to have a bed to go back to

View File

@ -0,0 +1,3 @@
example:
- my house
- is lit

View File

@ -0,0 +1,40 @@
patterns:
- pattern: (wo+w|such|much|very|so+|many)\b(.*?)\b(wo+w|such|much|very|so+|many)\b(.*?)(?:\b(wo+w|such|much|very|so+|many)\b(.*?)(?:\b(wo+w|such|much|very|so+|many)\b(.*?)(?:\b(wo+w|such|much|very|so+|many)\b(.*?))?)?)?
text:
- $1$2
- $3$4
- $5$6
- $7$8
- $9$10
example: wooow such meme very dank so cool
slots:
- x: 30
y: 50
width: 280
height: 80
font: ComicNeue-Bold
text_color: [255, 0, 0]
- x: 367
y: 154
width: 235
height: 70
font: ComicNeue-Bold
text_color: [0, 0, 255]
- x: 231
y: 319
width: 300
height: 98
font: ComicNeue-Bold
text_color: [0, 255, 0]
- x: 76
y: 496
width: 240
height: 70
font: ComicNeue-Bold
text_color: [0, 255, 255]
- x: 394
y: 504
width: 190
height: 90
font: ComicNeue-Bold
text_color: [255, 255, 0]

View File

@ -0,0 +1,6 @@
patterns:
- pattern: (.+[\.,]) (.+) (everywhere[^a-z]*)
text:
- $1
- $2 $3
example: geese. geese everywhere

View File

@ -0,0 +1,5 @@
aliases:
- white-whine
example:
- someone on the internet
- disagrees with me

View File

@ -0,0 +1,5 @@
aliases:
- grandma-finds-the-internet
example:
- do they deliver emails
- on sunday?

View File

@ -0,0 +1,9 @@
aliases:
- gatsby
- old-sport
patterns:
- pattern: (.*)\b(old sport[^a-z]*)
text:
- $1
- $2
example: merry christmas, old sport

View File

@ -0,0 +1,5 @@
aliases:
- grumpy
example:
- i tried to sits.
- i didn't fits.

View File

@ -0,0 +1,3 @@
example:
- this does not
- compute

View File

@ -0,0 +1,9 @@
aliases:
- fry
- not-sure
patterns:
- pattern: (not sure if .+) (or .+)
text:
- $1
- $2
example: not sure if song is skipping or just a remix

View File

@ -0,0 +1,9 @@
aliases:
- not-simply
- one-does-not
patterns:
- pattern: (one does not simply)(.+)
text:
- $1
- $2
example: one does not simply walk into mordor

View File

@ -0,0 +1,29 @@
aliases:
- you-get
- everyone-gets
patterns:
- pattern: (you get|everyone gets) (.+)
text:
- you get $2
- and you get $2
- and you get $2
- everyone gets $2
example: you get a car!
slots:
- x: 20
y: 20
width: 380
height: 80
- x: 406
y: 124
width: 200
height: 80
- x: 36
y: 256
width: 206
height: 80
- x: 31
y: 348
width: 558
height: 86

View File

@ -0,0 +1,3 @@
example:
- if actions are stronger than words,
- why is the pen mightier than the sword?

View File

@ -0,0 +1,4 @@
aliases:
- captain-picard-facepalm
example:
- when you forget to study for your exam

View File

@ -0,0 +1,9 @@
aliases:
- annoyed-picard
- captain-picard-wtf
patterns:
- pattern: (why|what) the fuck\b(.+)
text:
- $1 the fuck
- $2
example: why the fuck does it always need to install directx

View File

@ -0,0 +1,5 @@
aliases:
- success
example:
- thought i only had one beer left
- two left

View File

@ -0,0 +1,8 @@
aliases:
- thatd-be-great
patterns:
- pattern: (.+)\b(that would|that'd|thatd)( be great[^a-z]*)
text:
- $1
- $2$3
example: yeah if you could post more memes, that would be great

View File

@ -0,0 +1,10 @@
aliases:
- most-interesting-man
- most-interesting-man-in-the-world
- i-dont-always
patterns:
- pattern: (i don'?t always) (.+) (but .+)
text:
- $1 $2
- $3
example: i don't always take out the recycling, but when i do, i look like a raging alcoholic

View File

@ -0,0 +1,14 @@
slots:
- x: 322
y: 36
width: 234
height: 110
font: ComicNeue-Regular
- x: 334
y: 264
width: 225
height: 104
font: ComicNeue-Regular
example:
- I finally caught all 151 Pokemon.
- There are 646 Pokemon now.

View File

@ -0,0 +1,8 @@
aliases:
- jimmy-mcmillan
patterns:
- pattern: (.+)(is|are) (too damn high[^a-z]*)
text:
- $1
- $2 $3
example: the mouse sensitivity is too damn high

View File

@ -0,0 +1,8 @@
aliases:
- deeper
patterns:
- pattern: (.*)\b(we need to go deeper[^a-z]*)
text:
- $1
- $2
example: we need to go deeper

View File

@ -0,0 +1,8 @@
aliases:
- morpheus
patterns:
- pattern: (what if i told you)\b(.+)
text:
- $1
- $2
example: what if i told you pressing a and b doesn't help you catch pokemon

View File

@ -0,0 +1,5 @@
aliases:
- condescending-wonka
example:
- i'm condescending
- that means i talk down to you

View File

@ -0,0 +1,6 @@
patterns:
- pattern: (.+) (y u .+)
text:
- $1
- $2
example: onions y u make eyes water

View File

@ -0,0 +1,6 @@
patterns:
- pattern: (ya'?ll got any more of them) (.+)
text:
- $1
- $2
example: ya'll got any more of them pixels?

View File

@ -0,0 +1,8 @@
aliases:
- xzibit
patterns:
- pattern: (yo dawg\b.+?)(so .+)
text:
- $1
- $2
example: yo dawg, i heard you like cars. so i put a car in your car so you can drive while you drive

101
memelibrary/memelibrary.go Normal file
View File

@ -0,0 +1,101 @@
package memelibrary
import (
"bytes"
"image"
"path/filepath"
"strings"
_ "image/jpeg"
_ "image/png"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-plugin-memes/meme"
)
var fonts map[string]*truetype.Font = make(map[string]*truetype.Font)
var images map[string]image.Image = make(map[string]image.Image)
var metadata map[string]*Metadata = make(map[string]*Metadata)
var templates map[string]*meme.Template = make(map[string]*meme.Template)
func isImageAsset(assetName string) bool {
ext := strings.ToLower(filepath.Ext(assetName))
return ext == ".jpg" || ext == ".jpeg" || ext == ".png"
}
func mustLoadImage(assetName string) image.Image {
img, _, err := image.Decode(bytes.NewReader(MustAsset(assetName)))
if err != nil {
panic(err)
}
return img
}
func init() {
fontAssets, _ := AssetDir("fonts")
for _, assetName := range fontAssets {
fontName := strings.TrimSuffix(assetName, filepath.Ext(assetName))
font, err := truetype.Parse(MustAsset(filepath.Join("fonts", assetName)))
if err != nil {
panic(err)
}
fonts[fontName] = font
}
imageAssets, _ := AssetDir("images")
for _, assetName := range imageAssets {
if !isImageAsset(assetName) {
continue
}
templateName := strings.TrimSuffix(assetName, filepath.Ext(assetName))
images[templateName] = mustLoadImage(filepath.Join("images", assetName))
}
metadataAssets, _ := AssetDir("metadata")
for _, assetName := range metadataAssets {
ext := filepath.Ext(assetName)
if ext != ".yaml" {
continue
}
templateName := strings.TrimSuffix(assetName, ext)
m, err := ParseMetadata(MustAsset(filepath.Join("metadata", assetName)))
if err != nil {
panic(err)
}
metadata[templateName] = m
}
for templateName, metadata := range metadata {
img := images[templateName]
template := &meme.Template{
Name: templateName,
Image: img,
TextSlots: metadata.TextSlots(img.Bounds()),
}
templates[templateName] = template
for _, alias := range metadata.Aliases {
templates[alias] = template
}
}
}
func Memes() map[string]*Metadata {
return metadata
}
func Template(name string) *meme.Template {
return templates[name]
}
func PatternMatch(input string) (*meme.Template, []string) {
for templateName, metadata := range metadata {
if text := metadata.PatternMatch(input); text != nil {
return templates[templateName], text
}
}
return nil, nil
}

View File

@ -0,0 +1,46 @@
package memelibrary
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsImageAsset(t *testing.T) {
assert.True(t, isImageAsset("all-the-things.jpg"))
assert.False(t, isImageAsset("all-the-things.json"))
}
func TestMustLoadImage(t *testing.T) {
assert.NotPanics(t, func() {
img := mustLoadImage("images/all-the-things.jpg")
assert.NotNil(t, img)
})
assert.Panics(t, func() {
mustLoadImage("this-asset-does-not-exist.jpg")
})
assert.Panics(t, func() {
mustLoadImage("metadata/all-the-things.yaml")
})
}
func TestTemplate(t *testing.T) {
assert.Nil(t, Template("not-a-template"))
template := Template("all-the-things")
require.NotNil(t, template)
assert.NotNil(t, template.Image)
}
func TestPatternMatch(t *testing.T) {
for name, metadata := range Memes() {
for _, pattern := range metadata.Patterns {
template, text := PatternMatch(pattern.Example)
assert.Equal(t, Template(name), template)
assert.NotNil(t, text)
}
}
}

124
memelibrary/metadata.go Normal file
View File

@ -0,0 +1,124 @@
package memelibrary
import (
"image"
"image/color"
"regexp"
"strings"
yaml "gopkg.in/yaml.v2"
"github.com/mattermost/mattermost-plugin-memes/meme"
)
type Pattern struct {
Pattern string
Text []string
Example string
pattern *regexp.Regexp
}
type Slot struct {
X int
Y int
Width int
Height int
Font string
TextColor []int `yaml:"text_color"`
}
type Metadata struct {
Aliases []string
Patterns []*Pattern
Slots []*Slot
Example []string
}
func ParseMetadata(in []byte) (*Metadata, error) {
var m Metadata
if err := yaml.Unmarshal(in, &m); err != nil {
return nil, err
}
for _, pattern := range m.Patterns {
r, err := regexp.Compile("(?i)^" + pattern.Pattern + "$")
if err != nil {
return nil, err
}
pattern.pattern = r
}
return &m, nil
}
func (m *Metadata) PatternMatch(input string) []string {
for _, pattern := range m.Patterns {
if text := pattern.Match(input); text != nil {
return text
}
}
return nil
}
func (m *Metadata) TextSlots(bounds image.Rectangle) (slots []*meme.TextSlot) {
if m.Slots != nil {
for _, slot := range m.Slots {
textSlot := &meme.TextSlot{
Bounds: image.Rect(slot.X, slot.Y, slot.X+slot.Width, slot.Y+slot.Height),
}
if slot.Font != "" {
textSlot.Font = fonts[slot.Font]
} else {
textSlot.Font = fonts["Anton-Regular"]
textSlot.TextColor = color.White
textSlot.OutlineColor = color.Black
textSlot.AllUppercase = true
}
if c := sliceToColor(slot.TextColor); c != nil {
textSlot.TextColor = c
}
slots = append(slots, textSlot)
}
return
}
padding := bounds.Dy() / 20
return []*meme.TextSlot{
{
Bounds: image.Rect(padding, padding, bounds.Dx()-padding, bounds.Dy()/4),
Font: fonts["Anton-Regular"],
TextColor: color.White,
OutlineColor: color.Black,
AllUppercase: true,
},
{
Bounds: image.Rect(padding, bounds.Dy()*3/4, bounds.Dx()-padding, bounds.Dy()-padding),
Font: fonts["Anton-Regular"],
TextColor: color.White,
OutlineColor: color.Black,
AllUppercase: true,
},
}
}
func sliceToColor(s []int) color.Color {
switch len(s) {
case 1:
return color.Gray16{uint16(s[0]) << 1}
case 2:
return color.RGBA{uint8(s[0]), uint8(s[0]), uint8(s[0]), uint8(s[1])}
case 3:
return color.RGBA{uint8(s[0]), uint8(s[1]), uint8(s[2]), 255}
case 4:
return color.RGBA{uint8(s[0]), uint8(s[1]), uint8(s[2]), uint8(s[3])}
}
return nil
}
func (p *Pattern) Match(input string) (text []string) {
if matches := p.pattern.FindStringSubmatchIndex(input); matches != nil {
for _, slotText := range p.Text {
text = append(text, strings.TrimSpace(string(p.pattern.ExpandString(nil, slotText, input, matches))))
}
}
return
}

187
plugin.go Normal file
View File

@ -0,0 +1,187 @@
package main
import (
"flag"
"fmt"
"image/jpeg"
"net/http"
"net/url"
"os"
"strings"
"github.com/gorilla/mux"
"github.com/kballard/go-shellquote"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
"github.com/mattermost/mattermost-plugin-memes/meme"
"github.com/mattermost/mattermost-plugin-memes/memelibrary"
)
func resolveMeme(input string) (*meme.Template, []string, error) {
if template, text := memelibrary.PatternMatch(input); template != nil {
return template, text, nil
}
parts, err := shellquote.Split(input)
if err != nil {
return nil, nil, err
}
if template := memelibrary.Template(parts[0]); template != nil {
return template, parts[1:], nil
}
return nil, nil, fmt.Errorf("i don't know this meme")
}
func demo(args []string) error {
fs := flag.NewFlagSet("demo", flag.ContinueOnError)
out := fs.String("out", "", "output path to write the meme to")
if err := fs.Parse(args); err != nil {
return err
}
input := fs.Args()
if len(input) != 1 {
return fmt.Errorf("specify an input string")
}
template, text, err := resolveMeme(input[0])
if err != nil {
return err
}
if *out != "" {
f, err := os.Create(*out)
if err != nil {
return err
}
defer f.Close()
img, err := template.Render(text)
if err != nil {
return err
}
if err := jpeg.Encode(f, img, &jpeg.Options{
Quality: 100,
}); err != nil {
return err
}
}
return nil
}
func serveTemplateJPEG(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
templateName := vars["name"]
template := memelibrary.Template(templateName)
if template == nil {
http.NotFound(w, r)
return
}
query := r.URL.Query()
text := query["text"]
img, err := template.Render(text)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Cache-Control", "public, max-age=604800")
if err := jpeg.Encode(w, img, &jpeg.Options{
Quality: 90,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type Plugin struct {
router *mux.Router
}
func (p *Plugin) OnActivate(api plugin.API) error {
p.router = mux.NewRouter()
p.router.HandleFunc("/templates/{name}.jpg", serveTemplateJPEG).Methods("GET")
return api.RegisterCommand(&model.Command{
Trigger: "meme",
AutoComplete: true,
AutoCompleteDesc: "Renders custom memes so you can express yourself with culture.",
})
}
func (p *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Mattermost-User-Id") == "" {
http.Error(w, "please log in", http.StatusForbidden)
return
}
p.router.ServeHTTP(w, r)
}
func (p *Plugin) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
input := strings.TrimSpace(strings.TrimPrefix(args.Command, "/meme"))
if input == "" {
var availableMemes []string
for name, metadata := range memelibrary.Memes() {
availableMemes = append(availableMemes, name)
for _, alias := range metadata.Aliases {
availableMemes = append(availableMemes, alias)
}
}
return &model.CommandResponse{
ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
Text: `You can get started meming in one of two ways:
If your meme has well-defined phrasing, you can just type it:
` + "`" + `/meme brace yourself. memes are coming.` + "`" + `
If your meme doesn't have well-defined phrasing or you want more control, you can name a meme, then follow it with text to fill the slots:
` + "`" + `/meme brace-yourself "brace yourself." "memes are coming."` + "`" + `
In either case...
![brace-yourself](/plugins/memes/templates/brace-yourselves.jpg?text=brace+yourself.&text=memes+are+coming.)
Available memes: ` + strings.Join(availableMemes, ", "),
}, nil
}
template, text, err := resolveMeme(input)
if err != nil {
return nil, model.NewAppError("ExecuteCommand", "error resolving meme", nil, err.Error(), http.StatusInternalServerError)
}
queryString := ""
for _, t := range text {
if queryString == "" {
queryString += "?"
} else {
queryString += "&"
}
queryString += "text=" + url.QueryEscape(t)
}
return &model.CommandResponse{
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Text: "![" + template.Name + "](/plugins/memes/templates/" + template.Name + ".jpg" + queryString + ")",
}, nil
}
func main() {
if len(os.Args) > 1 {
if err := demo(os.Args[1:]); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
} else {
rpcplugin.Main(&Plugin{})
}
}

6
plugin.yaml Normal file
View File

@ -0,0 +1,6 @@
id: memes
backend:
executable: plugin.exe
name: Memes
description: Gives you the ability to quickly create and post memes via a /meme slash command.
version: '0.0'