initial commit
|
@ -0,0 +1,5 @@
|
|||
/plugin.exe
|
||||
/mattermost-memes-plugin.tar.gz
|
||||
/memelibrary/assets.go
|
||||
/dist
|
||||
/vendor
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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, ""
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -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!
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
aliases:
|
||||
- aliens
|
|
@ -0,0 +1,3 @@
|
|||
aliases:
|
||||
- bad-luck
|
||||
- brian
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
example:
|
||||
- my house
|
||||
- is lit
|
|
@ -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]
|
|
@ -0,0 +1,6 @@
|
|||
patterns:
|
||||
- pattern: (.+[\.,]) (.+) (everywhere[^a-z]*)
|
||||
text:
|
||||
- $1
|
||||
- $2 $3
|
||||
example: geese. geese everywhere
|
|
@ -0,0 +1,5 @@
|
|||
aliases:
|
||||
- white-whine
|
||||
example:
|
||||
- someone on the internet
|
||||
- disagrees with me
|
|
@ -0,0 +1,5 @@
|
|||
aliases:
|
||||
- grandma-finds-the-internet
|
||||
example:
|
||||
- do they deliver emails
|
||||
- on sunday?
|
|
@ -0,0 +1,9 @@
|
|||
aliases:
|
||||
- gatsby
|
||||
- old-sport
|
||||
patterns:
|
||||
- pattern: (.*)\b(old sport[^a-z]*)
|
||||
text:
|
||||
- $1
|
||||
- $2
|
||||
example: merry christmas, old sport
|
|
@ -0,0 +1,5 @@
|
|||
aliases:
|
||||
- grumpy
|
||||
example:
|
||||
- i tried to sits.
|
||||
- i didn't fits.
|
|
@ -0,0 +1,3 @@
|
|||
example:
|
||||
- this does not
|
||||
- compute
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
example:
|
||||
- if actions are stronger than words,
|
||||
- why is the pen mightier than the sword?
|
|
@ -0,0 +1,4 @@
|
|||
aliases:
|
||||
- captain-picard-facepalm
|
||||
example:
|
||||
- when you forget to study for your exam
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
aliases:
|
||||
- success
|
||||
example:
|
||||
- thought i only had one beer left
|
||||
- two left
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
aliases:
|
||||
- condescending-wonka
|
||||
example:
|
||||
- i'm condescending
|
||||
- that means i talk down to you
|
|
@ -0,0 +1,6 @@
|
|||
patterns:
|
||||
- pattern: (.+) (y u .+)
|
||||
text:
|
||||
- $1
|
||||
- $2
|
||||
example: onions y u make eyes water
|
|
@ -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?
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{})
|
||||
}
|
||||
}
|
|
@ -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'
|