4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 20:09:24 +00:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Wim
e9105003b0 Release v0.9.2 2017-01-28 22:15:32 +01:00
Wim
587bb06558 Update changelog 2017-01-28 00:39:33 +01:00
Wim
53e9664cde Add support for private channels (slack). Closes #118 2017-01-28 00:36:53 +01:00
Wim
482fbac68f Update vendor (slack) 2017-01-28 00:36:22 +01:00
Wim
dcccd43427 Use unknown as username if unsigned channel (telegram) 2017-01-27 23:59:24 +01:00
Wim
397b8ff892 Update changelog 2017-01-27 23:40:20 +01:00
Wim
38a4cf315a Add telegram links about channel and tokens 2017-01-27 23:36:14 +01:00
Wim
5f8b24e32c Fix username (telegram) 2017-01-27 23:30:46 +01:00
Wim
678a7ceb4e Fix channel and group messages (telegram) 2017-01-27 23:26:06 +01:00
Wim
077d494c7b Update vendor (telegram) 2017-01-27 00:23:14 +01:00
Wim
09b243d8c2 Update vendor (irc) 2017-01-24 21:24:57 +01:00
Wim
991183e514 Fix IgnoreNicks (global). Closes #115 2017-01-21 21:00:40 +01:00
9bf10e4b58 Fix tls by setting ServerName (xmpp) (#114)
Fixes error message shown by tls: "either ServerName or InsecureSkipVerify must be specified in the tls.Config"
2017-01-18 21:01:42 +01:00
Wim
884599d27d Bump version 2017-01-18 20:06:52 +01:00
Wim
f8a6e65bfd Release v0.9.1 2017-01-18 00:02:37 +01:00
Wim
6df6c5d615 Add GetStatuses() 2017-01-17 22:47:59 +01:00
Wim
93114b7682 Sync with mattermost 3.6.0 2017-01-17 00:00:26 +01:00
Wim
9987ac3f13 Update changelog 2017-01-16 23:55:03 +01:00
01a32b2154 Handle SkipTLSVerify for XMPP client (#106). Closes #81
* Handle SkipTLSVerify for XMPP client

* Mention SkipTLSVerify for XMPP in sample config
2017-01-14 00:35:45 +01:00
Wim
b3c3142bb2 Do not use API functions in webhook (slack). Closes #110 2017-01-12 00:40:50 +01:00
Wim
77f1a959c3 Handle errors in initUser() 2017-01-06 23:51:44 +01:00
e3dda0e812 Telegram: add markdown (#103)
* Add support for markdown (telegram)

Close #98

* Telegram: add more Markdown Render blacklist
2017-01-06 23:32:17 +01:00
Wim
38103d36b4 Update changelog. Remove obsolete info 2017-01-04 14:20:24 +01:00
Wim
7685fe1724 Add channel key support (irc). Closes #27 2017-01-04 14:10:35 +01:00
Wim
01afe03a3f Merge pull request #104 from markusgraube/patch-2
Update matterbridge.toml.sample
2017-01-03 23:07:25 +01:00
7fbbf89c58 Update matterbridge.toml.sample
Add password entry to irc section matterbride.toml.sample
2016-12-26 10:12:59 +01:00
84d259d8b3 Merge pull request #102 from ishitatsuyuki/patch-1
Fix the missing username (telegram). Closes #93
2016-12-17 15:44:26 +01:00
8b47670a74 Telegram: Fix the missing username 2016-12-17 18:43:54 +09:00
Wim
7f5dc1d461 Update changelog 2016-12-08 00:16:59 +01:00
Wim
43e765f4f9 Exit when a bridge fails to start 2016-12-08 00:14:17 +01:00
Wim
adec73f542 Check errors only on first connect. Keep retrying after first connection succeeds. (mattermost) Closes #95 2016-12-08 00:07:24 +01:00
Wim
fee159541f Add initial Rocket.Chat support 2016-12-03 00:10:29 +01:00
64 changed files with 6939 additions and 551 deletions

View File

@ -1,9 +1,9 @@
# matterbridge
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram and Hipchat(via xmpp).
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat and Hipchat(via xmpp).
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram and Hipchat (via xmpp). Pick and mix.
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat and Hipchat (via xmpp). Pick and mix.
* Supports multiple channels.
* Matterbridge can also work with private groups on your mattermost.
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
@ -26,6 +26,7 @@ Accounts to one of the supported bridges
* [Discord] (https://discordapp.com)
* [Telegram] (https://telegram.org)
* [Hipchat] (https://www.hipchat.com)
* [Rocket.chat] (https://rocket.chat)
## Docker
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
@ -35,16 +36,13 @@ docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.5.0+ [v0.9.0](https://github.com/42wim/matterircd/releases/tag/v0.9.0)
* For use with mattermost 3.5.x - 3.6.0 [v0.9.2](https://github.com/42wim/matterircd/releases/tag/v0.9.2)
* For use with mattermost 3.3.0 - 3.4.0 [v0.7.1](https://github.com/42wim/matterircd/releases/tag/v0.7.1)
* For use with mattermost 3.0.0 - 3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0) (not maintained anymore)
## Compatibility
### Mattermost
* Matterbridge v0.9.0 works with mattermost 3.5.0+ [3.5.1 release](https://github.com/mattermost/platform/releases/tag/v3.5.1)
* Matterbridge v0.9.2 works with mattermost 3.5.x - 3.6.0 [3.6.0 release](https://github.com/mattermost/platform/releases/tag/v3.6.0)
* Matterbridge v0.7.1 works with mattermost 3.3.0 - 3.4.0 [3.4.0 release](https://github.com/mattermost/platform/releases/tag/v3.4.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.

View File

@ -6,6 +6,7 @@ import (
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/xmpp"
@ -59,6 +60,9 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
case "telegram":
b.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c)
case "rocketchat":
b.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c)
}
return b
}

View File

@ -52,9 +52,14 @@ type Protocol struct {
UseTLS bool // IRC
}
type ChannelOptions struct {
Key string // irc
}
type Bridge struct {
Account string
Channel string
Options ChannelOptions
}
type Gateway struct {
@ -80,6 +85,7 @@ type Config struct {
Xmpp map[string]Protocol
Discord map[string]Protocol
Telegram map[string]Protocol
Rocketchat map[string]Protocol
General Protocol
Gateway []Gateway
SameChannelGateway []SameChannelGateway

View File

@ -0,0 +1,82 @@
package brocketchat
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
)
type MMhook struct {
mh *matterhook.Client
rh *rockethook.Client
}
type Brocketchat struct {
MMhook
Config *config.Protocol
Remote chan config.Message
name string
Account string
}
var flog *log.Entry
var protocol = "rocketchat"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Brocketchat {
b := &Brocketchat{}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
}
func (b *Brocketchat) Command(cmd string) string {
return ""
}
func (b *Brocketchat) Connect() error {
flog.Info("Connecting webhooks")
b.mh = matterhook.New(b.Config.URL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true})
b.rh = rockethook.New(b.Config.URL, rockethook.Config{BindAddress: b.Config.BindAddress})
go b.handleRocketHook()
return nil
}
func (b *Brocketchat) JoinChannel(channel string) error {
return nil
}
func (b *Brocketchat) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username
matterMessage.Type = ""
matterMessage.Text = msg.Text
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return err
}
return nil
}
func (b *Brocketchat) handleRocketHook() {
for {
message := b.rh.Receive()
flog.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.Config.Nick {
continue
}
flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account}
}
}

View File

@ -159,7 +159,7 @@ func (b *Bslack) handleSlack() {
flog.Debug("Start listening for Slack messages")
for message := range mchan {
// do not send messages from ourself
if message.Username == b.si.User.Name {
if b.Config.UseAPI && message.Username == b.si.User.Name {
continue
}
texts := strings.Split(message.Text, "\n")
@ -204,6 +204,14 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
b.channels = ev.Info.Channels
b.si = ev.Info
b.Users, _ = b.sc.GetUsers()
// add private channels
groups, _ := b.sc.GetGroups(true)
for _, g := range groups {
channel := new(slack.Channel)
channel.ID = g.ID
channel.Name = g.Name
b.channels = append(b.channels, *channel)
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
default:

View File

@ -1,10 +1,14 @@
package btelegram
import (
"bytes"
"html"
"strconv"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api"
"strconv"
"github.com/russross/blackfriday"
)
type Btelegram struct {
@ -51,24 +55,107 @@ func (b *Btelegram) JoinChannel(channel string) error {
return nil
}
type customHtml struct {
blackfriday.Renderer
}
func (options *customHtml) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
if !text() {
out.Truncate(marker)
return
}
out.WriteString("\n")
}
func (options *customHtml) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text)))
out.WriteString("</pre>\n")
}
func (options *customHtml) Header(out *bytes.Buffer, text func() bool, level int, id string) {
options.Paragraph(out, text)
}
func (options *customHtml) HRule(out *bytes.Buffer) {
out.WriteByte('\n')
}
func (options *customHtml) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteString("> ")
out.Write(text)
out.WriteByte('\n')
}
func (options *customHtml) List(out *bytes.Buffer, text func() bool, flags int) {
options.Paragraph(out, text)
}
func (options *customHtml) ListItem(out *bytes.Buffer, text []byte, flags int) {
out.WriteString("- ")
out.Write(text)
out.WriteByte('\n')
}
func (b *Btelegram) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil {
return err
}
m := tgbotapi.NewMessage(chatid, msg.Text)
parsed := blackfriday.Markdown([]byte(msg.Text),
&customHtml{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
blackfriday.EXTENSION_FENCED_CODE|
blackfriday.EXTENSION_AUTOLINK|
blackfriday.EXTENSION_SPACE_HEADERS|
blackfriday.EXTENSION_HEADER_IDS|
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK|
blackfriday.EXTENSION_DEFINITION_LISTS)
m := tgbotapi.NewMessage(chatid, html.EscapeString(msg.Username)+string(parsed))
m.ParseMode = "HTML"
_, err = b.c.Send(m)
return err
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
username := ""
text := ""
channel := ""
for update := range updates {
if update.Message == nil {
continue
// handle channels
if update.ChannelPost != nil {
if update.ChannelPost.From != nil {
username = update.ChannelPost.From.FirstName
if username == "" {
username = update.ChannelPost.From.UserName
}
}
text = update.ChannelPost.Text
channel = strconv.FormatInt(update.ChannelPost.Chat.ID, 10)
}
// handle groups
if update.Message != nil {
if update.Message.From != nil {
username = update.Message.From.FirstName
if username == "" {
username = update.Message.From.UserName
}
}
text = update.Message.Text
channel = strconv.FormatInt(update.Message.Chat.ID, 10)
}
if username == "" {
username = "unknown"
}
if text != "" {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account}
}
flog.Debugf("Sending message from %s on %s to gateway", update.Message.From.UserName, b.Account)
b.Remote <- config.Message{Username: update.Message.From.UserName, Text: update.Message.Text, Channel: strconv.FormatInt(update.Message.Chat.ID, 10), Account: b.Account}
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/mattn/go-xmpp"
"crypto/tls"
"strings"
"time"
@ -58,12 +59,17 @@ func (b *Bxmpp) Send(msg config.Message) error {
}
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
tc := new(tls.Config)
tc.InsecureSkipVerify = b.Config.SkipTLSVerify
tc.ServerName = strings.Split(b.Config.Server, ":")[0]
options := xmpp.Options{
Host: b.Config.Server,
User: b.Config.Jid,
Password: b.Config.Password,
NoTLS: true,
StartTLS: true,
TLSConfig: tc,
//StartTLS: false,
Debug: true,
Session: true,

View File

@ -1,3 +1,27 @@
# v0.9.2
## Bugfix
* general: make ignorenicks work again #115
* telegram: fix receiving from channels and groups #112
* telegram: use html for username
* telegram: use ```unknown``` as username when username is not visible.
* irc: update vendor (fixes some crashes) #117
* xmpp: fix tls by setting ServerName #114
## New features
* slack: support private channels #118
# v0.9.1
## New features
* Rocket.Chat: New protocol support added (https://rocket.chat)
* irc: add channel key support #27 (see matterbrige.toml.sample for example)
* xmpp: add SkipTLSVerify #106
## Bugfix
* general: Exit when a bridge fails to start
* mattermost: Check errors only on first connect. Keep retrying after first connection succeeds. #95
* telegram: fix missing username #102
* slack: do not use API functions in webhook (slack) #110
# v0.9.0
## New features
* Telegram: New protocol support added (https://telegram.org)
@ -66,6 +90,7 @@ See matterbridge.toml.sample for an example
# v0.6.1
## New features
* Slack support added. See matterbridge.conf.sample for more information
## Bugfix
* Fix 100% CPU bug on incorrect closed connections

View File

@ -13,12 +13,12 @@ type Gateway struct {
*config.Config
MyConfig *config.Gateway
//Bridges []*bridge.Bridge
Bridges map[string]*bridge.Bridge
ChannelsOut map[string][]string
ChannelsIn map[string][]string
ignoreNicks map[string][]string
Name string
Message chan config.Message
Bridges map[string]*bridge.Bridge
ChannelsOut map[string][]string
ChannelsIn map[string][]string
ChannelOptions map[string]config.ChannelOptions
Name string
Message chan config.Message
}
func New(cfg *config.Config, gateway *config.Gateway) *Gateway {
@ -47,8 +47,13 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
exists := make(map[string]bool)
for _, channel := range append(gw.ChannelsOut[br.Account], gw.ChannelsIn[br.Account]...) {
if !exists[br.Account+channel] {
mychannel := channel
log.Infof("%s: joining %s", br.Account, channel)
br.JoinChannel(channel)
if br.Protocol == "irc" && gw.ChannelOptions[br.Account+channel].Key != "" {
log.Debugf("using key %s for channel %s", gw.ChannelOptions[br.Account+channel].Key, channel)
mychannel = mychannel + " " + gw.ChannelOptions[br.Account+channel].Key
}
br.JoinChannel(mychannel)
exists[br.Account+channel] = true
}
}
@ -63,8 +68,6 @@ func (gw *Gateway) Start() error {
return err
}
}
//TODO fix mapIgnores
//gw.mapIgnores()
go gw.handleReceive()
return nil
}
@ -73,41 +76,39 @@ func (gw *Gateway) handleReceive() {
for {
select {
case msg := <-gw.Message:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
if !gw.ignoreMessage(&msg) {
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
}
}
}
func (gw *Gateway) mapChannels() error {
options := make(map[string]config.ChannelOptions)
m := make(map[string][]string)
for _, br := range gw.MyConfig.Out {
m[br.Account] = append(m[br.Account], br.Channel)
options[br.Account+br.Channel] = br.Options
}
gw.ChannelsOut = m
m = nil
m = make(map[string][]string)
for _, br := range gw.MyConfig.In {
m[br.Account] = append(m[br.Account], br.Channel)
options[br.Account+br.Channel] = br.Options
}
gw.ChannelsIn = m
for _, br := range gw.MyConfig.InOut {
gw.ChannelsIn[br.Account] = append(gw.ChannelsIn[br.Account], br.Channel)
gw.ChannelsOut[br.Account] = append(gw.ChannelsOut[br.Account], br.Channel)
options[br.Account+br.Channel] = br.Options
}
gw.ChannelOptions = options
return nil
}
func (gw *Gateway) mapIgnores() {
m := make(map[string][]string)
for _, br := range gw.MyConfig.In {
accInfo := strings.Split(br.Account, ".")
m[br.Account] = strings.Fields(gw.Config.IRC[accInfo[1]].IgnoreNicks)
}
gw.ignoreNicks = m
}
func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
channels := gw.ChannelsIn[msg.Account]
// broadcast to every out channel (irc QUIT)
@ -123,9 +124,6 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
}
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
if gw.ignoreMessage(&msg) {
return
}
// only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return
@ -152,9 +150,9 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
}
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
for _, entry := range gw.ignoreNicks[msg.Account] {
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true
}
}

View File

@ -47,8 +47,10 @@ func (gw *SameChannelGateway) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
if !gw.ignoreMessage(&msg) {
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
}
}
@ -71,6 +73,16 @@ func (gw *SameChannelGateway) handleMessage(msg config.Message, dest *bridge.Bri
}
}
func (gw *SameChannelGateway) ignoreMessage(msg *config.Message) bool {
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true
}
}
return false
}
func (gw *SameChannelGateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
br := gw.Bridges[msg.Account]
nick := gw.Config.General.RemoteNickFormat

View File

@ -0,0 +1,108 @@
package rockethook
import (
"crypto/tls"
"encoding/json"
"io/ioutil"
"log"
"net"
"net/http"
)
// Message for rocketchat outgoing webhook.
type Message struct {
Token string `json:"token"`
ChannelID string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Timestamp string `json:"timestamp"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
Text string `json:"text"`
}
// Client for Rocketchat.
type Client struct {
In chan Message
httpclient *http.Client
Config
}
// Config for client.
type Config struct {
BindAddress string // Address to listen on
Token string // Only allow this token from Rocketchat. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
}
// New Rocketchat client.
func New(url string, config Config) *Client {
c := &Client{In: make(chan Message), Config: config}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
_, _, err := net.SplitHostPort(c.BindAddress)
if err != nil {
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
}
go c.StartServer()
return c
}
// StartServer starts a webserver listening for incoming mattermost POSTS.
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}
// ServeHTTP implementation.
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
log.Println("invalid " + r.Method + " connection from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
msg := Message{}
body, err := ioutil.ReadAll(r.Body)
log.Println(string(body))
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
defer r.Body.Close()
err = json.Unmarshal(body, &msg)
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
if msg.Token == "" {
log.Println("no token from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
msg.ChannelName = "#" + msg.ChannelName
if c.Token != "" {
if msg.Token != c.Token {
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
}
c.In <- msg
}
// Receive returns an incoming message from mattermost outgoing webhooks URL.
func (c *Client) Receive() Message {
for {
select {
case msg := <-c.In:
return msg
}
}
}

View File

@ -1,287 +0,0 @@
#This is configuration for matterbridge.
###################################################################
#IRC section
###################################################################
[IRC]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#irc server to connect to.
#REQUIRED
Server="irc.freenode.net:6667"
#Enable to use TLS connection to your irc server.
#OPTIONAL (default false)
UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Your nick on irc.
#REQUIRED
Nick="matterbot"
#If you registered your bot with a service like Nickserv on freenode.
#Also being used when UseSASL=true
#OPTIONAL
NickServNick="nickserv"
NickServPassword="secret"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
###################################################################
#XMPP section
###################################################################
[XMPP]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#xmpp server to connect to.
#REQUIRED
Server="jabber.example.com:5222"
#Jid
#REQUIRED
Jid="user@example.com"
#Password
#REQUIRED
Password="yourpass"
#MUC
#REQUIRED
Muc="conference.jabber.example.com"
#Your nick in the rooms
#REQUIRED
Nick="xmppbot"
###################################################################
#mattermost section
###################################################################
[mattermost]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when using -plus switch which doesn't use
#### webhooks.
#Url is your incoming webhook url as specified in mattermost.
#See account settings - integrations - incoming webhooks on mattermost.
#REQUIRED
URL="https://yourdomain/hooks/yourhookkey"
#Address to listen on for outgoing webhook requests from mattermost.
#See account settings - integrations - outgoing webhooks on mattermost.
#This setting will not be used when using -plus switch which doesn't use
#webhooks
#REQUIRED
BindAddress="0.0.0.0:9999"
#Icon that will be showed in mattermost.
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for matterbridge -plus
#### Thse settings will only be used when using the -plus switch.
#The mattermost hostname.
#REQUIRED
Server="yourmattermostserver.domain"
#Your team on mattermost.
#REQUIRED
Team="yourteam"
#login/pass of your bot.
#Use a dedicated user for this and not your own!
#REQUIRED
Login="yourlogin"
Password="yourpass"
#Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false)
NoTLS=false
#### Shared settings for matterbridge and -plus
#Enable to not verify the certificate on your mattermost server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Enable to show IRC joins/parts in mattermost.
#OPTIONAL (default false)
ShowJoinPart=false
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#mattermost server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Mattermost will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#Gitter section
#Best to make a dedicated gitter account for the bot.
###################################################################
[Gitter]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#Token to connect with Gitter API
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
#REQUIRED
Token="Yourtokenhere"
#Nicks you want to ignore. Messages of those users will not be bridged.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
###################################################################
#slack section
###################################################################
[slack]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when useAPI is enabled
#Url is your incoming webhook url as specified in slack
#See account settings - integrations - incoming webhooks on slack
#REQUIRED (unless useAPI=true)
URL="https://hooks.slack.com/services/yourhook"
#Address to listen on for outgoing webhook requests from slack
#See account settings - integrations - outgoing webhooks on slack
#This setting will not be used when useAPI is eanbled
#webhooks
#REQUIRED (unless useAPI=true)
BindAddress="0.0.0.0:9999"
#Icon that will be showed in slack
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for using slack API
#OPTIONAL
useAPI=false
#Token to connect with the Slack API
#REQUIRED (when useAPI=true)
Token="yourslacktoken"
#### Shared settings for webhooks and API
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#multiple channel config
###################################################################
#You can specify multiple channels.
#The name is just an identifier for you.
#REQUIRED (at least 1 channel)
[Channel "channel1"]
#Choose the IRC channel to send messages to.
IRC="#off-topic"
#Choose the mattermost channel to messages to.
mattermost="off-topic"
#Choose the xmpp channel to send messages to.
xmpp="off-topic"
#Choose the Gitter channel to send messages to.
#Gitter channels are named "user/repo"
gitter="42wim/matterbridge"
#Choose the slack channel to send messages to.
slack="general"
[Channel "testchannel"]
IRC="#testing"
mattermost="testing"
xmpp="testing"
gitter="user/repo"
slack="testing"
###################################################################
#general
###################################################################
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key.
#OPTIONAL
GiphyApiKey="dc6zaTOxFJmzC"
#Enabling plus means you'll use the API version instead of the webhooks one
Plus=false

View File

@ -9,7 +9,7 @@ import (
log "github.com/Sirupsen/logrus"
)
var version = "0.9.0"
var version = "0.9.2"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
@ -39,7 +39,7 @@ func main() {
go func(gw config.SameChannelGateway) {
err := samechannelgateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting gateway failed %#v", err)
log.Fatalf("starting gateway failed %#v", err)
}
}(gw)
}
@ -52,7 +52,7 @@ func main() {
g := gateway.New(cfg, &gw)
err := g.Start()
if err != nil {
log.Debugf("starting gateway failed %#v", err)
log.Fatalf("starting gateway failed %#v", err)
}
}
select {}

View File

@ -13,6 +13,10 @@
#REQUIRED
Server="irc.freenode.net:6667"
#Password for irc server (if necessary)
#OPTIONAL (default "")
Password=""
#Enable to use TLS connection to your irc server.
#OPTIONAL (default false)
UseTLS=false
@ -93,6 +97,11 @@ Muc="conference.jabber.example.com"
#REQUIRED
Nick="xmppbot"
#Enable to not verify the certificate on your xmpp server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
@ -396,6 +405,7 @@ ShowJoinPart=false
#REQUIRED
[telegram.secure]
#Token to connect with telegram API
#See https://core.telegram.org/bots#6-botfather and https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
#REQUIRED
Token="Yourtokenhere"
@ -415,6 +425,64 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#rocketchat section
###################################################################
[rocketchat]
#You can configure multiple servers "[rocketchat.name]" or "[rocketchat.name2]"
#In this example we use [rocketchat.work]
#REQUIRED
[rocketchat.rockme]
#Url is your incoming webhook url as specified in rocketchat
#Read #https://rocket.chat/docs/administrator-guides/integrations/#how-to-create-a-new-incoming-webhook
#See administration - integrations - new integration - incoming webhook
#REQUIRED
URL="https://yourdomain/hooks/yourhookkey"
#Address to listen on for outgoing webhook requests from rocketchat.
#See administration - integrations - new integration - outgoing webhook
#REQUIRED
BindAddress="0.0.0.0:9999"
#Your nick/username as specified in your incoming webhook "Post as" setting
#REQUIRED
Nick="matterbot"
#Enable this to make a http connection (instead of https) to your rocketchat
#OPTIONAL (default false)
NoTLS=false
#Enable to not verify the certificate on your rocketchat server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Whether to prefix messages from other bridges to rocketchat with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment)
#OPTIONAL (default false)
ShowJoinPart=false
###################################################################
#General configuration
###################################################################
@ -427,8 +495,6 @@ ShowJoinPart=false
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
###################################################################
#Gateway configuration
###################################################################
@ -469,22 +535,38 @@ enable=true
# - ID:123456789 (where 123456789 is the channel ID)
# (https://github.com/42wim/matterbridge/issues/57)
#telegram - chatid (a large negative number, eg -123456789)
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
#rocketchat - #channel (# is required)
#REQUIRED
channel="#testing"
#OPTIONAL - only used for IRC protocol at the moment
[gateway.in.options]
#OPTIONAL - your irc channel key
key="yourkey"
#[[gateway.out]] specifies the account and channels we will sent messages to.
[[gateway.out]]
account="irc.freenode"
channel="#testing"
#OPTIONAL - only used for IRC protocol at the moment
[gateway.out.options]
#OPTIONAL - your irc channel key
key="yourkey"
#[[gateway.inout]] can be used when then channel will be used to receive from
#and send messages to
[[gateway.inout]]
account="mattermost.work"
channel="off-topic"
#OPTIONAL - only used for IRC protocol at the moment
[gateway.inout.options]
#OPTIONAL - your irc channel key
key="yourkey"
#If you want to do a 1:1 mapping between protocols where the channelnames are the same
#e.g. slack and mattermost you can use the samechannelgateway configuration

View File

@ -80,6 +80,11 @@ func (m *MMClient) SetLogLevel(level string) {
}
func (m *MMClient) Login() error {
// check if this is a first connect or a reconnection
firstConnection := true
if m.WsConnected == true {
firstConnection = false
}
m.WsConnected = false
if m.WsQuit {
return nil
@ -125,11 +130,7 @@ func (m *MMClient) Login() error {
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
//TODO more generic fix needed
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") &&
!strings.Contains(appErr.DetailedError, "connection reset by peer") &&
!strings.Contains(appErr.DetailedError, "connection timed out") {
if firstConnection {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
@ -581,6 +582,27 @@ func (m *MMClient) GetStatus(userId string) string {
return "offline"
}
func (m *MMClient) GetStatuses() map[string]string {
var ok bool
statuses := make(map[string]string)
res, err := m.Client.GetStatuses()
if err != nil {
return statuses
}
if statuses, ok = res.Data.(map[string]string); ok {
for userId, status := range statuses {
statuses[userId] = "offline"
if status == model.STATUS_AWAY {
statuses[userId] = "away"
}
if status == model.STATUS_ONLINE {
statuses[userId] = "online"
}
}
}
return statuses
}
func (m *MMClient) GetTeamId() string {
return m.Team.Id
}
@ -621,11 +643,20 @@ func (m *MMClient) initUser() error {
//m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(0, 50000, "")
mmusers, err := m.Client.GetProfiles(0, 50000, "")
if err != nil {
return errors.New(err.DetailedError)
}
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
mmchannels, err := m.Client.GetChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
mmchannels, err = m.Client.GetMoreChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
t.MoreChannels = mmchannels.Data.(*model.ChannelList)
m.OtherTeams = append(m.OtherTeams, t)
if v.Name == m.Credentials.Team {

View File

@ -7,7 +7,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/technoweenie/multipartstreamer"
"io/ioutil"
"log"
"net/http"
@ -16,6 +15,8 @@ import (
"strconv"
"strings"
"time"
"github.com/technoweenie/multipartstreamer"
)
// BotAPI allows you to interact with the Telegram Bot API.
@ -80,7 +81,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse,
json.Unmarshal(bytes, &apiResp)
if !apiResp.Ok {
return APIResponse{}, errors.New(apiResp.Description)
return apiResp, errors.New(apiResp.Description)
}
return apiResp, nil
@ -105,16 +106,17 @@ func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Messa
//
// Requires the parameter to hold the file not be in the params.
// File should be a string to a file path, a FileBytes struct,
// or a FileReader struct.
// a FileReader struct, or a url.URL.
//
// Note that if your FileReader has a size set to -1, it will read
// the file into memory to calculate a size.
func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
ms := multipartstreamer.New()
ms.WriteFields(params)
switch f := file.(type) {
case string:
ms.WriteFields(params)
fileHandle, err := os.Open(f)
if err != nil {
return APIResponse{}, err
@ -128,9 +130,13 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna
ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
case FileBytes:
ms.WriteFields(params)
buf := bytes.NewBuffer(f.Bytes)
ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
case FileReader:
ms.WriteFields(params)
if f.Size != -1 {
ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
@ -145,6 +151,10 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna
buf := bytes.NewBuffer(data)
ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
case url.URL:
params[fieldname] = f.String()
ms.WriteFields(params)
default:
return APIResponse{}, errors.New(ErrBadFileType)
}
@ -399,15 +409,22 @@ func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
// If you do not have a legitimate TLS certificate, you need to include
// your self signed certificate with the config.
func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
if config.Certificate == nil {
v := url.Values{}
v.Add("url", config.URL.String())
if config.MaxConnections != 0 {
v.Add("max_connections", strconv.Itoa(config.MaxConnections))
}
return bot.MakeRequest("setWebhook", v)
}
params := make(map[string]string)
params["url"] = config.URL.String()
if config.MaxConnections != 0 {
params["max_connections"] = strconv.Itoa(config.MaxConnections)
}
resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
if err != nil {
@ -424,9 +441,23 @@ func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
return apiResp, nil
}
// GetWebhookInfo allows you to fetch information about a webhook and if
// one currently is set, along with pending update count and error messages.
func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
resp, err := bot.MakeRequest("getWebhookInfo", url.Values{})
if err != nil {
return WebhookInfo{}, err
}
var info WebhookInfo
err = json.Unmarshal(resp.Result, &info)
return info, err
}
// GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
updatesChan := make(chan Update, 100)
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
ch := make(chan Update, 100)
go func() {
for {
@ -442,18 +473,18 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
for _, update := range updates {
if update.UpdateID >= config.Offset {
config.Offset = update.UpdateID + 1
updatesChan <- update
ch <- update
}
}
}
}()
return updatesChan, nil
return ch, nil
}
// ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
updatesChan := make(chan Update, 100)
func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
ch := make(chan Update, 100)
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
bytes, _ := ioutil.ReadAll(r.Body)
@ -461,10 +492,10 @@ func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
var update Update
json.Unmarshal(bytes, &update)
updatesChan <- update
ch <- update
})
return updatesChan
return ch
}
// AnswerInlineQuery sends a response to an inline query.
@ -495,8 +526,14 @@ func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, erro
v := url.Values{}
v.Add("callback_query_id", config.CallbackQueryID)
v.Add("text", config.Text)
if config.Text != "" {
v.Add("text", config.Text)
}
v.Add("show_alert", strconv.FormatBool(config.ShowAlert))
if config.URL != "" {
v.Add("url", config.URL)
}
v.Add("cache_time", strconv.Itoa(config.CacheTime))
bot.debugLog("answerCallbackQuery", v, nil)
@ -648,3 +685,18 @@ func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error)
return bot.MakeRequest("unbanChatMember", v)
}
// GetGameHighScores allows you to get the high scores for a game.
func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
v, _ := config.values()
resp, err := bot.MakeRequest(config.method(), v)
if err != nil {
return []GameHighScore{}, err
}
var highScores []GameHighScore
err = json.Unmarshal(resp.Result, &highScores)
return highScores, err
}

View File

@ -198,7 +198,10 @@ type MessageConfig struct {
// values returns a url.Values representation of MessageConfig.
func (config MessageConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("text", config.Text)
v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
if config.ParseMode != "" {
@ -223,7 +226,10 @@ type ForwardConfig struct {
// values returns a url.Values representation of ForwardConfig.
func (config ForwardConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("from_chat_id", strconv.FormatInt(config.FromChatID, 10))
v.Add("message_id", strconv.Itoa(config.MessageID))
return v, nil
@ -253,7 +259,10 @@ func (config PhotoConfig) params() (map[string]string, error) {
// Values returns a url.Values representation of PhotoConfig.
func (config PhotoConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
if config.Caption != "" {
@ -275,6 +284,7 @@ func (config PhotoConfig) method() string {
// AudioConfig contains information about a SendAudio request.
type AudioConfig struct {
BaseFile
Caption string
Duration int
Performer string
Title string
@ -282,7 +292,10 @@ type AudioConfig struct {
// values returns a url.Values representation of AudioConfig.
func (config AudioConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
@ -295,6 +308,9 @@ func (config AudioConfig) values() (url.Values, error) {
if config.Title != "" {
v.Add("title", config.Title)
}
if config.Caption != "" {
v.Add("caption", config.Caption)
}
return v, nil
}
@ -313,6 +329,9 @@ func (config AudioConfig) params() (map[string]string, error) {
if config.Title != "" {
params["title"] = config.Title
}
if config.Caption != "" {
params["caption"] = config.Caption
}
return params, nil
}
@ -334,7 +353,10 @@ type DocumentConfig struct {
// values returns a url.Values representation of DocumentConfig.
func (config DocumentConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
@ -365,7 +387,10 @@ type StickerConfig struct {
// values returns a url.Values representation of StickerConfig.
func (config StickerConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
@ -398,7 +423,10 @@ type VideoConfig struct {
// values returns a url.Values representation of VideoConfig.
func (config VideoConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
@ -431,12 +459,16 @@ func (config VideoConfig) method() string {
// VoiceConfig contains information about a SendVoice request.
type VoiceConfig struct {
BaseFile
Caption string
Duration int
}
// values returns a url.Values representation of VoiceConfig.
func (config VoiceConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add(config.name(), config.FileID)
if config.Duration != 0 {
@ -476,7 +508,10 @@ type LocationConfig struct {
// values returns a url.Values representation of LocationConfig.
func (config LocationConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
@ -500,7 +535,10 @@ type VenueConfig struct {
}
func (config VenueConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
@ -526,7 +564,10 @@ type ContactConfig struct {
}
func (config ContactConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("phone_number", config.PhoneNumber)
v.Add("first_name", config.FirstName)
@ -539,6 +580,94 @@ func (config ContactConfig) method() string {
return "sendContact"
}
// GameConfig allows you to send a game.
type GameConfig struct {
BaseChat
GameShortName string
}
func (config GameConfig) values() (url.Values, error) {
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("game_short_name", config.GameShortName)
return v, nil
}
func (config GameConfig) method() string {
return "sendGame"
}
// SetGameScoreConfig allows you to update the game score in a chat.
type SetGameScoreConfig struct {
UserID int
Score int
Force bool
DisableEditMessage bool
ChatID int
ChannelUsername string
MessageID int
InlineMessageID string
}
func (config SetGameScoreConfig) values() (url.Values, error) {
v := url.Values{}
v.Add("user_id", strconv.Itoa(config.UserID))
v.Add("score", strconv.Itoa(config.Score))
if config.InlineMessageID == "" {
if config.ChannelUsername == "" {
v.Add("chat_id", strconv.Itoa(config.ChatID))
} else {
v.Add("chat_id", config.ChannelUsername)
}
v.Add("message_id", strconv.Itoa(config.MessageID))
} else {
v.Add("inline_message_id", config.InlineMessageID)
}
v.Add("disable_edit_message", strconv.FormatBool(config.DisableEditMessage))
return v, nil
}
func (config SetGameScoreConfig) method() string {
return "setGameScore"
}
// GetGameHighScoresConfig allows you to fetch the high scores for a game.
type GetGameHighScoresConfig struct {
UserID int
ChatID int
ChannelUsername string
MessageID int
InlineMessageID string
}
func (config GetGameHighScoresConfig) values() (url.Values, error) {
v := url.Values{}
v.Add("user_id", strconv.Itoa(config.UserID))
if config.InlineMessageID == "" {
if config.ChannelUsername == "" {
v.Add("chat_id", strconv.Itoa(config.ChatID))
} else {
v.Add("chat_id", config.ChannelUsername)
}
v.Add("message_id", strconv.Itoa(config.MessageID))
} else {
v.Add("inline_message_id", config.InlineMessageID)
}
return v, nil
}
func (config GetGameHighScoresConfig) method() string {
return "getGameHighScores"
}
// ChatActionConfig contains information about a SendChatAction request.
type ChatActionConfig struct {
BaseChat
@ -547,7 +676,10 @@ type ChatActionConfig struct {
// values returns a url.Values representation of ChatActionConfig.
func (config ChatActionConfig) values() (url.Values, error) {
v, _ := config.BaseChat.values()
v, err := config.BaseChat.values()
if err != nil {
return v, err
}
v.Add("action", config.Action)
return v, nil
}
@ -566,7 +698,10 @@ type EditMessageTextConfig struct {
}
func (config EditMessageTextConfig) values() (url.Values, error) {
v, _ := config.BaseEdit.values()
v, err := config.BaseEdit.values()
if err != nil {
return v, err
}
v.Add("text", config.Text)
v.Add("parse_mode", config.ParseMode)
@ -635,6 +770,7 @@ type UpdateConfig struct {
type WebhookConfig struct {
URL *url.URL
Certificate interface{}
MaxConnections int
}
// FileBytes contains information about a set of bytes to upload
@ -669,6 +805,8 @@ type CallbackConfig struct {
CallbackQueryID string `json:"callback_query_id"`
Text string `json:"text"`
ShowAlert bool `json:"show_alert"`
URL string `json:"url"`
CacheTime int `json:"cache_time"`
}
// ChatMemberConfig contains information about a user in a chat for use

View File

@ -1,6 +1,7 @@
package tgbotapi
import (
"log"
"net/url"
)
@ -20,6 +21,7 @@ func NewMessage(chatID int64, text string) MessageConfig {
// NewMessageToChannel creates a new Message that is sent to a channel
// by username.
//
// username is the username of the channel, text is the message text.
func NewMessageToChannel(username string, text string) MessageConfig {
return MessageConfig{
@ -316,6 +318,21 @@ func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
}
}
// NewWebhookWithCert creates a new webhook with a certificate and max_connections.
//
// link is the url you wish to get webhooks,
// file contains a string to a file, FileReader, or FileBytes.
// maxConnections defines maximum number of connections from telegram to your server
func NewWebhookWithCertAndMaxConnections(link string, file interface{}, maxConnections int) WebhookConfig {
u, _ := url.Parse(link)
return WebhookConfig{
URL: u,
Certificate: file,
MaxConnections: maxConnections,
}
}
// NewInlineQueryResultArticle creates a new inline query article.
func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
@ -479,12 +496,23 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe
// NewHideKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone.
func NewHideKeyboard(selective bool) ReplyKeyboardHide {
log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard")
return ReplyKeyboardHide{
HideKeyboard: true,
Selective: selective,
}
}
// NewRemoveKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone.
func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {
return ReplyKeyboardRemove{
RemoveKeyboard: true,
Selective: selective,
}
}
// NewKeyboardButton creates a regular keyboard button.
func NewKeyboardButton(text string) KeyboardButton {
return KeyboardButton{

View File

@ -12,10 +12,17 @@ import (
// APIResponse is a response from the Telegram API with the result
// stored raw.
type APIResponse struct {
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
Parameters *ResponseParameters `json:"parameters"`
}
// ResponseParameters are various errors that can be returned in APIResponse.
type ResponseParameters struct {
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
RetryAfter int `json:"retry_after"` // optional
}
// Update is an update response, from GetUpdates.
@ -23,11 +30,23 @@ type Update struct {
UpdateID int `json:"update_id"`
Message *Message `json:"message"`
EditedMessage *Message `json:"edited_message"`
ChannelPost *Message `json:"channel_post"`
EditedChannelPost *Message `json:"edited_channel_post"`
InlineQuery *InlineQuery `json:"inline_query"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"`
CallbackQuery *CallbackQuery `json:"callback_query"`
}
// UpdatesChannel is the channel for getting updates.
type UpdatesChannel <-chan Update
// Clear discards all unprocessed incoming updates.
func (ch UpdatesChannel) Clear() {
for len(ch) != 0 {
<-ch
}
}
// User is a user on Telegram.
type User struct {
ID int `json:"id"`
@ -61,12 +80,13 @@ type GroupChat struct {
// Chat contains information about the place a message was sent.
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"` // optional
UserName string `json:"username"` // optional
FirstName string `json:"first_name"` // optional
LastName string `json:"last_name"` // optional
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"` // optional
UserName string `json:"username"` // optional
FirstName string `json:"first_name"` // optional
LastName string `json:"last_name"` // optional
AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional
}
// IsPrivate returns if the Chat is a private conversation.
@ -103,6 +123,7 @@ type Message struct {
Chat *Chat `json:"chat"`
ForwardFrom *User `json:"forward_from"` // optional
ForwardFromChat *Chat `json:"forward_from_chat"` // optional
ForwardFromMessageID int `json:"forward_from_message_id"` // optional
ForwardDate int `json:"forward_date"` // optional
ReplyToMessage *Message `json:"reply_to_message"` // optional
EditDate int `json:"edit_date"` // optional
@ -110,6 +131,7 @@ type Message struct {
Entities *[]MessageEntity `json:"entities"` // optional
Audio *Audio `json:"audio"` // optional
Document *Document `json:"document"` // optional
Game *Game `json:"game"` // optional
Photo *[]PhotoSize `json:"photo"` // optional
Sticker *Sticker `json:"sticker"` // optional
Video *Video `json:"video"` // optional
@ -314,6 +336,12 @@ type ReplyKeyboardHide struct {
Selective bool `json:"selective"` // optional
}
// ReplyKeyboardRemove allows the Bot to hide a custom keyboard.
type ReplyKeyboardRemove struct {
RemoveKeyboard bool `json:"remove_keyboard"`
Selective bool `json:"selective"`
}
// InlineKeyboardMarkup is a custom keyboard presented for an inline bot.
type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
@ -324,11 +352,15 @@ type InlineKeyboardMarkup struct {
//
// Note that some values are references as even an empty string
// will change behavior.
//
// CallbackGame, if set, MUST be first button in first row.
type InlineKeyboardButton struct {
Text string `json:"text"`
URL *string `json:"url,omitempty"` // optional
CallbackData *string `json:"callback_data,omitempty"` // optional
SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional
Text string `json:"text"`
URL *string `json:"url,omitempty"` // optional
CallbackData *string `json:"callback_data,omitempty"` // optional
SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional
SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional
CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional
}
// CallbackQuery is data sent when a keyboard button with callback data
@ -338,7 +370,9 @@ type CallbackQuery struct {
From *User `json:"from"`
Message *Message `json:"message"` // optional
InlineMessageID string `json:"inline_message_id"` // optional
Data string `json:"data"` // optional
ChatInstance string `json:"chat_instance"`
Data string `json:"data"` // optional
GameShortName string `json:"game_short_name"` // optional
}
// ForceReply allows the Bot to have users directly reply to it without
@ -369,6 +403,49 @@ func (chat ChatMember) HasLeft() bool { return chat.Status == "left" }
// WasKicked returns if the ChatMember was kicked from the chat.
func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" }
// Game is a game within Telegram.
type Game struct {
Title string `json:"title"`
Description string `json:"description"`
Photo []PhotoSize `json:"photo"`
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities"`
Animation Animation `json:"animation"`
}
// Animation is a GIF animation demonstrating the game.
type Animation struct {
FileID string `json:"file_id"`
Thumb PhotoSize `json:"thumb"`
FileName string `json:"file_name"`
MimeType string `json:"mime_type"`
FileSize int `json:"file_size"`
}
// GameHighScore is a user's score and position on the leaderboard.
type GameHighScore struct {
Position int `json:"position"`
User User `json:"user"`
Score int `json:"score"`
}
// CallbackGame is for starting a game in an inline keyboard button.
type CallbackGame struct{}
// WebhookInfo is information about a currently set webhook.
type WebhookInfo struct {
URL string `json:"url"`
HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
LastErrorDate int `json:"last_error_date"` // optional
LastErrorMessage string `json:"last_error_message"` // optional
}
// IsSet returns true if a webhook is currently set.
func (info WebhookInfo) IsSet() bool {
return info.URL != ""
}
// InlineQuery is a Query from Telegram for an inline request.
type InlineQuery struct {
ID string `json:"id"`
@ -460,6 +537,7 @@ type InlineQueryResultAudio struct {
ID string `json:"id"` // required
URL string `json:"audio_url"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
Performer string `json:"performer"`
Duration int `json:"audio_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
@ -472,6 +550,7 @@ type InlineQueryResultVoice struct {
ID string `json:"id"` // required
URL string `json:"voice_url"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
Duration int `json:"voice_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
@ -507,6 +586,14 @@ type InlineQueryResultLocation struct {
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultGame is an inline query response game.
type InlineQueryResultGame struct {
Type string `json:"type"`
ID string `json:"id"`
GameShortName string `json:"game_short_name"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup"`
}
// ChosenInlineResult is an inline query result chosen by a User
type ChosenInlineResult struct {
ResultID string `json:"result_id"`

View File

@ -11,13 +11,17 @@ type ClusterInterface interface {
StartInterNodeCommunication()
StopInterNodeCommunication()
GetClusterInfos() []*model.ClusterInfo
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
RemoveAllSessionsForUserId(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
InvalidateCacheForChannelPosts(channelId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)
GetClusterId() string
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
InvalidateAllCaches() *model.AppError
}
var theClusterInterface ClusterInterface

View File

@ -0,0 +1,41 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
type MetricsInterface interface {
StartServer()
StopServer()
IncrementPostCreate()
IncrementPostSentEmail()
IncrementPostSentPush()
IncrementPostBroadcast()
IncrementPostFileAttachment(count int)
IncrementHttpRequest()
IncrementHttpError()
ObserveHttpRequestDuration(elapsed float64)
IncrementLogin()
IncrementLoginFail()
IncrementEtagHitCounter(route string)
IncrementEtagMissCounter(route string)
IncrementMemCacheHitCounter(cacheName string)
IncrementMemCacheMissCounter(cacheName string)
AddMemCacheHitCounter(cacheName string, amount float64)
AddMemCacheMissCounter(cacheName string, amount float64)
}
var theMetricsInterface MetricsInterface
func RegisterMetricsInterface(newInterface MetricsInterface) {
theMetricsInterface = newInterface
}
func GetMetricsInterface() MetricsInterface {
return theMetricsInterface
}

View File

@ -18,6 +18,14 @@ const (
CHANNEL_MARK_UNREAD_MENTION = "mention"
)
type ChannelUnread struct {
TeamId string
TotalMsgCount int64
MsgCount int64
MentionCount int64
NotifyProps StringMap
}
type ChannelMember struct {
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`

View File

@ -0,0 +1,35 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelSearch struct {
Term string `json:"term"`
}
// ToJson convert a Channel to a json string
func (c *ChannelSearch) ToJson() string {
b, err := json.Marshal(c)
if err != nil {
return ""
} else {
return string(b)
}
}
// ChannelSearchFromJson will decode the input and return a Channel
func ChannelSearchFromJson(data io.Reader) *ChannelSearch {
decoder := json.NewDecoder(data)
var cs ChannelSearch
err := decoder.Decode(&cs)
if err == nil {
return &cs
} else {
return nil
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelView struct {
ChannelId string `json:"channel_id"`
PrevChannelId string `json:"prev_channel_id"`
}
func (o *ChannelView) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelViewFromJson(data io.Reader) *ChannelView {
decoder := json.NewDecoder(data)
var o ChannelView
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -6,7 +6,6 @@ package model
import (
"bytes"
"fmt"
l4g "github.com/alecthomas/log4go"
"io"
"io/ioutil"
"mime/multipart"
@ -15,6 +14,8 @@ import (
"strconv"
"strings"
"time"
l4g "github.com/alecthomas/log4go"
)
const (
@ -48,6 +49,13 @@ type Result struct {
Data interface{}
}
type ResponseMetadata struct {
StatusCode int
Error *AppError
RequestId string
Etag string
}
type Client struct {
Url string // The location of the server like "http://localhost:8065"
ApiUrl string // The api location of the server like "http://localhost:8065/api/v3"
@ -291,34 +299,6 @@ func (c *Client) GetPing() (map[string]string, *AppError) {
// Team Routes Section
// SignupTeam sends an email with a team sign-up link to the provided address if email
// verification is enabled, otherwise it returns a map with a "follow_link" entry
// containing the team sign-up link.
func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
m["display_name"] = displayName
if r, err := c.DoApiPost("/teams/signup", MapToJson(m)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
// CreateTeamFromSignup creates a team based on the provided TeamSignup struct. On success
// it returns the TeamSignup struct.
func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) {
if r, err := c.DoApiPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamSignupFromJson(r.Body)}, nil
}
}
// CreateTeam creates a team based on the provided Team struct. On success it returns
// the Team struct with the Id, CreateAt and other server-decided fields populated.
func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
@ -500,6 +480,32 @@ func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
}
}
// getByUsername returns a user based on a provided username string. Must be authenticated.
func (c *Client) GetByUsername(username string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/users/name/%v", username), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
}
}
// getByEmail returns a user based on a provided username string. Must be authenticated.
func (c *Client) GetByEmail(email string, etag string) (*User, *ResponseMetadata) {
if r, err := c.DoApiGet(fmt.Sprintf("/users/email/%v", email), "", etag); err != nil {
return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return UserFromJson(r.Body),
&ResponseMetadata{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HEADER_REQUEST_ID),
Etag: r.Header.Get(HEADER_ETAG_SERVER),
}
}
}
// GetMe returns the current user.
func (c *Client) GetMe(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/me", "", etag); err != nil {
@ -610,6 +616,19 @@ func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) {
}
}
// AutocompleteUsers returns a list for autocompletion of users on the system that match the provided term,
// matching against username, full name and nickname. Must be authenticated.
func (c *Client) AutocompleteUsers(term string) (*Result, *AppError) {
url := fmt.Sprintf("/users/autocomplete?term=%s", url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil
}
}
// LoginById authenticates a user by user id and password.
func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
m := make(map[string]string)
@ -802,12 +821,9 @@ func (c *Client) EmailToLDAP(m map[string]string) (*Result, *AppError) {
}
}
func (c *Client) Command(channelId string, command string, suggest bool) (*Result, *AppError) {
m := make(map[string]string)
m["command"] = command
m["channelId"] = channelId
m["suggest"] = strconv.FormatBool(suggest)
if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/execute", MapToJson(m)); err != nil {
func (c *Client) Command(channelId string, command string) (*Result, *AppError) {
args := &CommandArgs{ChannelId: channelId, Command: command}
if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/execute", args.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -846,6 +862,16 @@ func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) {
}
}
func (c *Client) UpdateCommand(cmd *Command) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/update", cmd.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil
}
}
func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/regen_token", MapToJson(data)); err != nil {
return nil, err
@ -940,6 +966,16 @@ func (c *Client) ReloadConfig() (bool, *AppError) {
}
}
func (c *Client) InvalidateAllCaches() (bool, *AppError) {
c.clearExtraProperties()
if r, err := c.DoApiGet("/admin/invalidate_all_caches", "", ""); err != nil {
return false, err
} else {
c.fillInExtraProperties(r)
return c.CheckStatusOK(r), nil
}
}
func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil {
return nil, err
@ -1143,6 +1179,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) {
}
}
// SCHEDULED FOR DEPRECATION IN 3.7 - use GetMoreChannelsPage instead
func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/more", "", etag); err != nil {
return nil, err
@ -1153,6 +1190,43 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
}
}
// GetMoreChannelsPage will return a page of open channels the user is not in based on
// the provided offset and limit. Must be authenticated.
func (c *Client) GetMoreChannelsPage(offset int, limit int) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetTeamRoute()+"/channels/more/%v/%v", offset, limit), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
// SearchMoreChannels will return a list of open channels the user is not in, that matches
// the search criteria provided. Must be authenticated.
func (c *Client) SearchMoreChannels(channelSearch ChannelSearch) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/more/search", channelSearch.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
// AutocompleteChannels will return a list of open channels that match the provided
// string. Must be authenticated.
func (c *Client) AutocompleteChannels(term string) (*Result, *AppError) {
url := fmt.Sprintf("%s/channels/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/counts", "", etag); err != nil {
return nil, err
@ -1173,6 +1247,16 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetChannelByName(channelName string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelNameRoute(channelName), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
}
}
func (c *Client) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil {
return nil, err
@ -1240,6 +1324,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
// UpdateLastViewedAt will mark a channel as read.
// The channelId indicates the channel to mark as read. If active is true, push notifications
// will be cleared if there are unread messages. The default for active is true.
// SCHEDULED FOR DEPRECATION IN 3.8 - use ViewChannel instead
func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *AppError) {
data := make(map[string]interface{})
data["active"] = active
@ -1252,6 +1337,23 @@ func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *Ap
}
}
// ViewChannel performs all the actions related to viewing a channel. This includes marking
// the channel and the previous one as read, and marking the channel as being actively viewed.
// ChannelId is required but may be blank to indicate no channel is being viewed.
// PrevChannelId is optional, populate to indicate a channel switch occurred.
func (c *Client) ViewChannel(params ChannelView) (bool, *ResponseMetadata) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/view", params.ToJson()); err != nil {
return false, &ResponseMetadata{StatusCode: r.StatusCode, Error: err}
} else {
return c.CheckStatusOK(r),
&ResponseMetadata{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HEADER_REQUEST_ID),
Etag: r.Header.Get(HEADER_ETAG_SERVER),
}
}
}
func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil {
return nil, err
@ -1272,6 +1374,18 @@ func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *Ap
}
}
// GetChannelMembersByIds will return channel member objects as an array based on the
// channel id and a list of user ids provided. Must be authenticated.
func (c *Client) GetChannelMembersByIds(channelId string, userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/members/ids", ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil
}
}
func (c *Client) CreatePost(post *Post) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(post.ChannelId)+"/posts/create", post.ToJson()); err != nil {
return nil, err
@ -1342,6 +1456,21 @@ func (c *Client) GetPost(channelId string, postId string, etag string) (*Result,
}
}
// GetPostById returns a post and any posts in the same thread by post id
func (c *Client) GetPostById(postId string, etag string) (*PostList, *ResponseMetadata) {
if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/posts/%v", postId), "", etag); err != nil {
return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return PostListFromJson(r.Body),
&ResponseMetadata{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HEADER_REQUEST_ID),
Etag: r.Header.Get(HEADER_ETAG_SERVER),
}
}
}
func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/delete", postId), ""); err != nil {
return nil, err
@ -1629,6 +1758,7 @@ func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) {
// SetActiveChannel sets the the channel id the user is currently viewing.
// The channelId key is required but the value can be blank. Returns standard
// response.
// SCHEDULED FOR DEPRECATION IN 3.8 - use ViewChannel instead
func (c *Client) SetActiveChannel(channelId string) (*Result, *AppError) {
data := map[string]string{}
data["channel_id"] = channelId
@ -1663,6 +1793,36 @@ func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result,
}
}
// GetMyTeamMembers will return an array with team member objects that the current user
// is a member of. Must be authenticated.
func (c *Client) GetMyTeamMembers() (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/members", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil
}
}
// GetMyTeamsUnread will return an array with TeamUnread objects that contain the amount of
// unread messages and mentions the current user has for the teams it belongs to.
// An optional team ID can be set to exclude that team from the results. Must be authenticated.
func (c *Client) GetMyTeamsUnread(teamId string) (*Result, *AppError) {
endpoint := "/teams/unread"
if teamId != "" {
endpoint += fmt.Sprintf("?id=%s", url.QueryEscape(teamId))
}
if r, err := c.DoApiGet(endpoint, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamsUnreadFromJson(r.Body)}, nil
}
}
// GetTeamMember will return a team member object based on the team id and user id provided.
// Must be authenticated.
func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) {
@ -1687,6 +1847,18 @@ func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) {
}
}
// GetTeamStats will return a team stats object containing the number of users on the team
// based on the team id provided. Must be authenticated.
func (c *Client) GetTeamByName(teamName string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/name/%v", teamName), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil
}
}
// GetTeamMembersByIds will return team member objects as an array based on the
// team id and a list of user ids provided. Must be authenticated.
func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) {
@ -2030,6 +2202,7 @@ func (c *Client) DeleteEmoji(id string) (bool, *AppError) {
if r, err := c.DoApiPost(c.GetEmojiRoute()+"/delete", MapToJson(data)); err != nil {
return false, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return c.CheckStatusOK(r), nil
}
@ -2060,6 +2233,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro
return AppErrorFromJson(rp.Body)
} else {
defer closeBody(rp)
c.fillInExtraProperties(rp)
return nil
}
}
@ -2071,6 +2245,7 @@ func (c *Client) RemoveCertificateFile(filename string) *AppError {
return err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return nil
}
}
@ -2082,6 +2257,7 @@ func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{},
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return StringInterfaceFromJson(r.Body), nil
}
}
@ -2110,3 +2286,36 @@ func (c *Client) GetFileInfosForPost(channelId string, postId string, etag strin
return FileInfosFromJson(r.Body), nil
}
}
// Saves an emoji reaction for a post in the given channel. Returns the saved reaction if successful, otherwise returns an AppError.
func (c *Client) SaveReaction(channelId string, reaction *Reaction) (*Reaction, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/save", reaction.PostId), reaction.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return ReactionFromJson(r.Body), nil
}
}
// Removes an emoji reaction for a post in the given channel. Returns nil if successful, otherwise returns an AppError.
func (c *Client) DeleteReaction(channelId string, reaction *Reaction) *AppError {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/delete", reaction.PostId), reaction.ToJson()); err != nil {
return err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return nil
}
}
// Lists all emoji reactions made for the given post in the given channel. Returns a list of Reactions if successful, otherwise returns an AppError.
func (c *Client) ListReactions(channelId string, postId string) ([]*Reaction, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions", postId), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return ReactionsFromJson(r.Body), nil
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ClusterStats struct {
Id string `json:"id"`
TotalWebsocketConnections int `json:"total_websocket_connections"`
TotalReadDbConnections int `json:"total_read_db_connections"`
TotalMasterDbConnections int `json:"total_master_db_connections"`
}
func (me *ClusterStats) ToJson() string {
b, err := json.Marshal(me)
if err != nil {
return ""
} else {
return string(b)
}
}
func ClusterStatsFromJson(data io.Reader) *ClusterStats {
decoder := json.NewDecoder(data)
var me ClusterStats
err := decoder.Decode(&me)
if err == nil {
return &me
} else {
return nil
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type CommandArgs struct {
ChannelId string `json:"channel_id"`
RootId string `json:"root_id"`
ParentId string `json:"parent_id"`
Command string `json:"command"`
}
func (o *CommandArgs) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func CommandArgsFromJson(data io.Reader) *CommandArgs {
decoder := json.NewDecoder(data)
var o CommandArgs
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -16,6 +16,8 @@ const (
type CommandResponse struct {
ResponseType string `json:"response_type"`
Text string `json:"text"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
GotoLocation string `json:"goto_location"`
Attachments interface{} `json:"attachments"`
}

View File

@ -38,9 +38,10 @@ const (
DIRECT_MESSAGE_ANY = "any"
DIRECT_MESSAGE_TEAM = "team"
PERMISSIONS_ALL = "all"
PERMISSIONS_TEAM_ADMIN = "team_admin"
PERMISSIONS_SYSTEM_ADMIN = "system_admin"
PERMISSIONS_ALL = "all"
PERMISSIONS_CHANNEL_ADMIN = "channel_admin"
PERMISSIONS_TEAM_ADMIN = "team_admin"
PERMISSIONS_SYSTEM_ADMIN = "system_admin"
FAKE_SETTING = "********************************"
@ -80,6 +81,7 @@ type ServiceSettings struct {
EnableSecurityFixAlert *bool
EnableInsecureOutgoingConnections *bool
EnableMultifactorAuthentication *bool
EnforceMultifactorAuthentication *bool
AllowCorsFrom *string
SessionLengthWebInDays *int
SessionLengthMobileInDays *int
@ -98,6 +100,16 @@ type ClusterSettings struct {
InterNodeUrls []string
}
type MetricsSettings struct {
Enable *bool
BlockProfileRate *int
ListenAddress *string
}
type AnalyticsSettings struct {
MaxUsersForStatistics *int
}
type SSOSettings struct {
Enable bool
Secret string
@ -219,8 +231,13 @@ type TeamSettings struct {
RestrictTeamInvite *string
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
RestrictPublicChannelCreation *string
RestrictPrivateChannelCreation *string
RestrictPublicChannelDeletion *string
RestrictPrivateChannelDeletion *string
UserStatusAwayTimeout *int64
MaxChannelsPerTeam *int64
MaxNotificationsPerChannel *int64
}
type LdapSettings struct {
@ -243,6 +260,7 @@ type LdapSettings struct {
UsernameAttribute *string
NicknameAttribute *string
IdAttribute *string
PositionAttribute *string
// Syncronization
SyncIntervalMinutes *int
@ -289,6 +307,7 @@ type SamlSettings struct {
UsernameAttribute *string
NicknameAttribute *string
LocaleAttribute *string
PositionAttribute *string
LoginButtonText *string
}
@ -330,6 +349,8 @@ type Config struct {
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
MetricsSettings MetricsSettings
AnalyticsSettings AnalyticsSettings
WebrtcSettings WebrtcSettings
}
@ -376,23 +397,32 @@ func (o *Config) SetDefaults() {
// Defaults to "s3.amazonaws.com"
o.FileSettings.AmazonS3Endpoint = "s3.amazonaws.com"
}
if o.FileSettings.AmazonS3Region == "" {
// Defaults to "us-east-1" region.
o.FileSettings.AmazonS3Region = "us-east-1"
}
if o.FileSettings.AmazonS3SSL == nil {
o.FileSettings.AmazonS3SSL = new(bool)
*o.FileSettings.AmazonS3SSL = true // Secure by default.
}
if o.FileSettings.MaxFileSize == nil {
o.FileSettings.MaxFileSize = new(int64)
*o.FileSettings.MaxFileSize = 52428800 // 50 MB
}
if len(*o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = new(string)
*o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
if o.FileSettings.InitialFont == "" {
// Defaults to "luximbi.ttf"
o.FileSettings.InitialFont = "luximbi.ttf"
}
if len(o.EmailSettings.InviteSalt) == 0 {
o.EmailSettings.InviteSalt = NewRandomString(32)
}
@ -426,6 +456,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnableMultifactorAuthentication = false
}
if o.ServiceSettings.EnforceMultifactorAuthentication == nil {
o.ServiceSettings.EnforceMultifactorAuthentication = new(bool)
*o.ServiceSettings.EnforceMultifactorAuthentication = false
}
if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
@ -491,6 +526,30 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.RestrictPublicChannelCreation == nil {
o.TeamSettings.RestrictPublicChannelCreation = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPublicChannelCreation = *o.TeamSettings.RestrictPublicChannelManagement
}
if o.TeamSettings.RestrictPrivateChannelCreation == nil {
o.TeamSettings.RestrictPrivateChannelCreation = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPrivateChannelCreation = *o.TeamSettings.RestrictPrivateChannelManagement
}
if o.TeamSettings.RestrictPublicChannelDeletion == nil {
o.TeamSettings.RestrictPublicChannelDeletion = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPublicChannelDeletion = *o.TeamSettings.RestrictPublicChannelManagement
}
if o.TeamSettings.RestrictPrivateChannelDeletion == nil {
o.TeamSettings.RestrictPrivateChannelDeletion = new(string)
// If this setting does not exist, assume migration from <3.6, so use management setting as default.
*o.TeamSettings.RestrictPrivateChannelDeletion = *o.TeamSettings.RestrictPrivateChannelManagement
}
if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = 300
@ -501,6 +560,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.MaxChannelsPerTeam = 2000
}
if o.TeamSettings.MaxNotificationsPerChannel == nil {
o.TeamSettings.MaxNotificationsPerChannel = new(int64)
*o.TeamSettings.MaxNotificationsPerChannel = 1000
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@ -671,6 +735,11 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.IdAttribute = ""
}
if o.LdapSettings.PositionAttribute == nil {
o.LdapSettings.PositionAttribute = new(string)
*o.LdapSettings.PositionAttribute = ""
}
if o.LdapSettings.SyncIntervalMinutes == nil {
o.LdapSettings.SyncIntervalMinutes = new(int)
*o.LdapSettings.SyncIntervalMinutes = 60
@ -772,6 +841,21 @@ func (o *Config) SetDefaults() {
o.ClusterSettings.InterNodeUrls = []string{}
}
if o.MetricsSettings.ListenAddress == nil {
o.MetricsSettings.ListenAddress = new(string)
*o.MetricsSettings.ListenAddress = ":8067"
}
if o.MetricsSettings.Enable == nil {
o.MetricsSettings.Enable = new(bool)
*o.MetricsSettings.Enable = false
}
if o.AnalyticsSettings.MaxUsersForStatistics == nil {
o.AnalyticsSettings.MaxUsersForStatistics = new(int)
*o.AnalyticsSettings.MaxUsersForStatistics = 2500
}
if o.ComplianceSettings.Enable == nil {
o.ComplianceSettings.Enable = new(bool)
*o.ComplianceSettings.Enable = false
@ -882,6 +966,11 @@ func (o *Config) SetDefaults() {
*o.SamlSettings.NicknameAttribute = ""
}
if o.SamlSettings.PositionAttribute == nil {
o.SamlSettings.PositionAttribute = new(string)
*o.SamlSettings.PositionAttribute = ""
}
if o.SamlSettings.LocaleAttribute == nil {
o.SamlSettings.LocaleAttribute = new(string)
*o.SamlSettings.LocaleAttribute = ""
@ -952,6 +1041,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.Forward80To443 = false
}
if o.MetricsSettings.BlockProfileRate == nil {
o.MetricsSettings.BlockProfileRate = new(int)
*o.MetricsSettings.BlockProfileRate = 0
}
o.defaultWebrtcSettings()
}
@ -987,6 +1081,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "")
}
if *o.TeamSettings.MaxNotificationsPerChannel <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_notify_per_channel.app_error", nil, "")
}
if !(*o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_ANY || *o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_TEAM) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "")
}

View File

@ -6,6 +6,7 @@ package model
import (
"bytes"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
@ -233,7 +234,7 @@ func expandAnnouncements(i *IncomingWebhookRequest) {
for _, field := range fields {
f := field.(map[string]interface{})
if f["value"] != nil {
f["value"] = expandAnnouncement(f["value"].(string))
f["value"] = expandAnnouncement(fmt.Sprintf("%v", f["value"]))
}
}
}

View File

@ -39,6 +39,7 @@ type Features struct {
Office365OAuth *bool `json:"office365_oauth"`
Compliance *bool `json:"compliance"`
Cluster *bool `json:"cluster"`
Metrics *bool `json:"metrics"`
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
@ -55,6 +56,7 @@ func (f *Features) ToMap() map[string]interface{} {
"office365": *f.Office365OAuth,
"compliance": *f.Compliance,
"cluster": *f.Cluster,
"metrics": *f.Metrics,
"custom_brand": *f.CustomBrand,
"mhpns": *f.MHPNS,
"saml": *f.SAML,
@ -104,6 +106,11 @@ func (f *Features) SetDefaults() {
*f.Cluster = *f.FutureFeatures
}
if f.Metrics == nil {
f.Metrics = new(bool)
*f.Metrics = *f.FutureFeatures
}
if f.CustomBrand == nil {
f.CustomBrand = new(bool)
*f.CustomBrand = *f.FutureFeatures

View File

@ -17,8 +17,14 @@ const (
POST_JOIN_LEAVE = "system_join_leave"
POST_ADD_REMOVE = "system_add_remove"
POST_HEADER_CHANGE = "system_header_change"
POST_DISPLAYNAME_CHANGE = "system_displayname_change"
POST_CHANNEL_DELETED = "system_channel_deleted"
POST_EPHEMERAL = "system_ephemeral"
POST_FILEIDS_MAX_RUNES = 150
POST_FILENAMES_MAX_RUNES = 4000
POST_HASHTAGS_MAX_RUNES = 1000
POST_MESSAGE_MAX_RUNES = 4000
POST_PROPS_MAX_RUNES = 8000
)
type Post struct {
@ -38,6 +44,7 @@ type Post struct {
Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more
FileIds StringArray `json:"file_ids,omitempty"`
PendingPostId string `json:"pending_post_id" db:"-"`
HasReactions bool `json:"has_reactions,omitempty"`
}
func (o *Post) ToJson() string {
@ -102,28 +109,30 @@ func (o *Post) IsValid() *AppError {
return NewLocAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "")
}
if utf8.RuneCountInString(o.Message) > 4000 {
if utf8.RuneCountInString(o.Message) > POST_MESSAGE_MAX_RUNES {
return NewLocAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.Hashtags) > 1000 {
if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES {
return NewLocAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id)
}
// should be removed once more message types are supported
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_ADD_REMOVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) {
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_ADD_REMOVE ||
o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE ||
o.Type == POST_DISPLAYNAME_CHANGE || o.Type == POST_CHANNEL_DELETED) {
return NewLocAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type)
}
if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > 4000 {
if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES {
return NewLocAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > 150 {
if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES {
return NewLocAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > 8000 {
if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES {
return NewLocAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id)
}

View File

@ -33,6 +33,7 @@ const (
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
PREFERENCE_NAME_LAST_TEAM = "team"
PREFERENCE_CATEGORY_NOTIFICATIONS = "notifications"
PREFERENCE_NAME_EMAIL_INTERVAL = "email_interval"

View File

@ -30,6 +30,7 @@ type PushNotification struct {
Message string `json:"message"`
Badge int `json:"badge"`
ContentAvailable int `json:"cont_ava"`
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Type string `json:"type"`

View File

@ -0,0 +1,78 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Reaction struct {
UserId string `json:"user_id"`
PostId string `json:"post_id"`
EmojiName string `json:"emoji_name"`
CreateAt int64 `json:"create_at"`
}
func (o *Reaction) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return ""
} else {
return string(b)
}
}
func ReactionFromJson(data io.Reader) *Reaction {
var o Reaction
if err := json.NewDecoder(data).Decode(&o); err != nil {
return nil
} else {
return &o
}
}
func ReactionsToJson(o []*Reaction) string {
if b, err := json.Marshal(o); err != nil {
return ""
} else {
return string(b)
}
}
func ReactionsFromJson(data io.Reader) []*Reaction {
var o []*Reaction
if err := json.NewDecoder(data).Decode(&o); err != nil {
return nil
} else {
return o
}
}
func (o *Reaction) IsValid() *AppError {
if len(o.UserId) != 26 {
return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.user_id.app_error", nil, "user_id="+o.UserId)
}
if len(o.PostId) != 26 {
return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.post_id.app_error", nil, "post_id="+o.PostId)
}
if len(o.EmojiName) == 0 || len(o.EmojiName) > 64 {
return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName)
}
if o.CreateAt == 0 {
return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.create_at.app_error", nil, "")
}
return nil
}
func (o *Reaction) PreSave() {
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
}

View File

@ -22,7 +22,7 @@ type Status struct {
Status string `json:"status"`
Manual bool `json:"manual"`
LastActivityAt int64 `json:"last_activity_at"`
ActiveChannel string `json:"active_channel"`
ActiveChannel string `json:"active_channel" db:"-"`
}
func (o *Status) ToJson() string {
@ -44,3 +44,14 @@ func StatusFromJson(data io.Reader) *Status {
return nil
}
}
func StatusMapToInterfaceMap(statusMap map[string]*Status) map[string]interface{} {
interfaceMap := map[string]interface{}{}
for _, s := range statusMap {
// Omitted statues mean offline
if s.Status != STATUS_OFFLINE {
interfaceMap[s.UserId] = s.Status
}
}
return interfaceMap
}

View File

@ -24,6 +24,7 @@ type Team struct {
DeleteAt int64 `json:"delete_at"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Description string `json:"description"`
Email string `json:"email"`
Type string `json:"type"`
CompanyName string `json:"company_name"`
@ -130,6 +131,10 @@ func (o *Team) IsValid() *AppError {
return NewLocAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id)
}
if len(o.Description) > 255 {
return NewLocAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id)
}
if IsReservedTeamName(o.Name) {
return NewLocAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id)
}

View File

@ -16,6 +16,12 @@ type TeamMember struct {
DeleteAt int64 `json:"delete_at"`
}
type TeamUnread struct {
TeamId string `json:"team_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
}
func (o *TeamMember) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@ -55,6 +61,25 @@ func TeamMembersFromJson(data io.Reader) []*TeamMember {
}
}
func TeamsUnreadToJson(o []*TeamUnread) string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func TeamsUnreadFromJson(data io.Reader) []*TeamUnread {
decoder := json.NewDecoder(data)
var o []*TeamUnread
err := decoder.Decode(&o)
if err == nil {
return o
} else {
return nil
}
}
func (o *TeamMember) IsValid() *AppError {
if len(o.TeamId) != 26 {

View File

@ -1,40 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
)
type TeamSignup struct {
Team Team `json:"team"`
User User `json:"user"`
Invites []string `json:"invites"`
Data string `json:"data"`
Hash string `json:"hash"`
}
func TeamSignupFromJson(data io.Reader) *TeamSignup {
decoder := json.NewDecoder(data)
var o TeamSignup
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
fmt.Println(err)
return nil
}
}
func (o *TeamSignup) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}

View File

@ -37,6 +37,7 @@ type User struct {
Nickname string `json:"nickname"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Roles string `json:"roles"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
@ -78,6 +79,10 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
}
if utf8.RuneCountInString(u.Position) > 35 {
return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id)
}
if utf8.RuneCountInString(u.FirstName) > 64 {
return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
}

View File

@ -304,7 +304,7 @@ func Etag(parts ...interface{}) string {
return etag
}
var validHashtag = regexp.MustCompile(`^(#[A-Za-zäöüÄÖÜß]+[A-Za-z0-9äöüÄÖÜß_\-]*[A-Za-z0-9äöüÄÖÜß])$`)
var validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`)
var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`)
var hashtagStart = regexp.MustCompile(`^#{2,}`)
var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`)

View File

@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
"3.6.0",
"3.5.0",
"3.4.0",
"3.3.0",

View File

@ -18,6 +18,7 @@ const (
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_UPDATED = "user_updated"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
@ -27,6 +28,8 @@ const (
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added"
WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed"
)
type WebSocketMessage interface {
@ -34,6 +37,7 @@ type WebSocketMessage interface {
IsValid() bool
DoPreComputeJson()
GetPreComputeJson() []byte
EventType() string
}
type WebsocketBroadcast struct {
@ -63,6 +67,10 @@ func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
func (o *WebSocketEvent) EventType() string {
return o.Event
}
func (o *WebSocketEvent) DoPreComputeJson() {
b, err := json.Marshal(o)
if err != nil {
@ -120,6 +128,10 @@ func (o *WebSocketResponse) IsValid() bool {
return o.Status != ""
}
func (o *WebSocketResponse) EventType() string {
return ""
}
func (o *WebSocketResponse) ToJson() string {
b, err := json.Marshal(o)
if err != nil {

View File

@ -30,7 +30,7 @@ func botRequest(path string, values url.Values, debug bool) (*botResponseFull, e
return response, nil
}
// GetBotInfo will retrive the complete bot information
// GetBotInfo will retrieve the complete bot information
func (api *Client) GetBotInfo(bot string) (*Bot, error) {
values := url.Values{
"token": {api.config.token},

View File

@ -29,18 +29,18 @@ type chatResponseFull struct {
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct {
Text string
Username string
AsUser bool
Parse string
LinkNames int
Attachments []Attachment
UnfurlLinks bool
UnfurlMedia bool
IconURL string
IconEmoji string
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool
Text string `json:"text"`
Username string `json:"user_name"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
}
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set

View File

@ -17,42 +17,38 @@ func main() {
rtm := api.NewRTM()
go rtm.ManageConnection()
Loop:
for {
select {
case msg := <-rtm.IncomingEvents:
fmt.Print("Event Received: ")
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello
for msg := range rtm.IncomingEvents {
fmt.Print("Event Received: ")
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello
case *slack.ConnectedEvent:
fmt.Println("Infos:", ev.Info)
fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace #general with your Channel ID
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general"))
case *slack.ConnectedEvent:
fmt.Println("Infos:", ev.Info)
fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace #general with your Channel ID
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general"))
case *slack.MessageEvent:
fmt.Printf("Message: %v\n", ev)
case *slack.MessageEvent:
fmt.Printf("Message: %v\n", ev)
case *slack.PresenceChangeEvent:
fmt.Printf("Presence Change: %v\n", ev)
case *slack.PresenceChangeEvent:
fmt.Printf("Presence Change: %v\n", ev)
case *slack.LatencyReport:
fmt.Printf("Current latency: %v\n", ev.Value)
case *slack.LatencyReport:
fmt.Printf("Current latency: %v\n", ev.Value)
case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())
case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())
case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
break Loop
case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
return
default:
default:
// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
}
}

View File

@ -198,3 +198,13 @@ func (info Info) GetGroupByID(groupID string) *Group {
}
return nil
}
// GetIMByID returns an IM given an IM id
func (info Info) GetIMByID(imID string) *IM {
for _, im := range info.IMs {
if im.ID == imID {
return &im
}
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
type OAuthResponseIncomingWebhook struct {
URL string `json:"url"`
Channel string `json:"channel"`
ChannelID string `json:"channel_id,omitempty"`
ConfigurationURL string `json:"configuration_url"`
}
@ -23,6 +24,7 @@ type OAuthResponse struct {
TeamID string `json:"team_id"`
IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"`
Bot OAuthResponseBot `json:"bot"`
UserID string `json:"user_id,omitempty"`
SlackResponse
}

View File

@ -44,6 +44,16 @@ type Login struct {
Region string `json:"region"`
}
type BillableInfoResponse struct {
BillableInfo map[string]BillingActive `json:"billable_info"`
SlackResponse
}
type BillingActive struct {
BillingActive bool `json:"billing_active"`
}
// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
type AccessLogParameters struct {
Count int
@ -73,6 +83,20 @@ func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, err
return response, nil
}
func billableInfoRequest(path string, values url.Values, debug bool) (map[string]BillingActive, error) {
response := &BillableInfoResponse{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.BillableInfo, nil
}
func accessLogsRequest(path string, values url.Values, debug bool) (*LoginResponse, error) {
response := &LoginResponse{}
err := post(path, values, response, debug)
@ -117,3 +141,20 @@ func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging,
return response.Logins, &response.Paging, nil
}
func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
return billableInfoRequest("team.billableInfo", values, api.debug)
}
// GetBillableInfoForTeam returns the billing_active status of all users on the team.
func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
values := url.Values{
"token": {api.config.token},
}
return billableInfoRequest("team.billableInfo", values, api.debug)
}

View File

@ -122,7 +122,7 @@ func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
return &response.UserPresence, nil
}
// GetUserInfo will retrive the complete user information
// GetUserInfo will retrieve the complete user information
func (api *Client) GetUserInfo(user string) (*User, error) {
values := url.Values{
"token": {api.config.token},

29
vendor/github.com/russross/blackfriday/LICENSE.txt generated vendored Normal file
View File

@ -0,0 +1,29 @@
Blackfriday is distributed under the Simplified BSD License:
> Copyright © 2011 Russ Ross
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions
> are met:
>
> 1. Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
>
> 2. Redistributions in binary form must reproduce the above
> copyright notice, this list of conditions and the following
> disclaimer in the documentation and/or other materials provided with
> the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
> POSSIBILITY OF SUCH DAMAGE.

1430
vendor/github.com/russross/blackfriday/block.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

949
vendor/github.com/russross/blackfriday/html.go generated vendored Normal file
View File

@ -0,0 +1,949 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// HTML rendering backend
//
//
package blackfriday
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
)
// Html renderer configuration options.
const (
HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks
HTML_SKIP_STYLE // skip embedded <style> elements
HTML_SKIP_IMAGES // skip embedded images
HTML_SKIP_LINKS // skip all links
HTML_SAFELINK // only link to trusted protocols
HTML_NOFOLLOW_LINKS // only link with rel="nofollow"
HTML_NOREFERRER_LINKS // only link with rel="noreferrer"
HTML_HREF_TARGET_BLANK // add a blank target
HTML_TOC // generate a table of contents
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents)
HTML_COMPLETE_PAGE // generate a complete HTML page
HTML_USE_XHTML // generate XHTML output instead of HTML
HTML_USE_SMARTYPANTS // enable smart punctuation substitutions
HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS)
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS)
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES)
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source
)
var (
alignments = []string{
"left",
"right",
"center",
}
// TODO: improve this regexp to catch all possible entities:
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
)
type HtmlRendererParameters struct {
// Prepend this text to each relative URL.
AbsolutePrefix string
// Add this text to each footnote anchor, to ensure uniqueness.
FootnoteAnchorPrefix string
// Show this text inside the <a> tag for a footnote return link, if the
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
// <sup>[return]</sup> is used.
FootnoteReturnLinkContents string
// If set, add this text to the front of each Header ID, to ensure
// uniqueness.
HeaderIDPrefix string
// If set, add this text to the back of each Header ID, to ensure uniqueness.
HeaderIDSuffix string
}
// Html is a type that implements the Renderer interface for HTML output.
//
// Do not create this directly, instead use the HtmlRenderer function.
type Html struct {
flags int // HTML_* options
closeTag string // how to end singleton tags: either " />" or ">"
title string // document title
css string // optional css file url (used with HTML_COMPLETE_PAGE)
parameters HtmlRendererParameters
// table of contents data
tocMarker int
headerCount int
currentLevel int
toc *bytes.Buffer
// Track header IDs to prevent ID collision in a single generation.
headerIDs map[string]int
smartypants *smartypantsRenderer
}
const (
xhtmlClose = " />"
htmlClose = ">"
)
// HtmlRenderer creates and configures an Html object, which
// satisfies the Renderer interface.
//
// flags is a set of HTML_* options ORed together.
// title is the title of the document, and css is a URL for the document's
// stylesheet.
// title and css are only used when HTML_COMPLETE_PAGE is selected.
func HtmlRenderer(flags int, title string, css string) Renderer {
return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
}
func HtmlRendererWithParameters(flags int, title string,
css string, renderParameters HtmlRendererParameters) Renderer {
// configure the rendering engine
closeTag := htmlClose
if flags&HTML_USE_XHTML != 0 {
closeTag = xhtmlClose
}
if renderParameters.FootnoteReturnLinkContents == "" {
renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
}
return &Html{
flags: flags,
closeTag: closeTag,
title: title,
css: css,
parameters: renderParameters,
headerCount: 0,
currentLevel: 0,
toc: new(bytes.Buffer),
headerIDs: make(map[string]int),
smartypants: smartypants(flags),
}
}
// Using if statements is a bit faster than a switch statement. As the compiler
// improves, this should be unnecessary this is only worthwhile because
// attrEscape is the single largest CPU user in normal use.
// Also tried using map, but that gave a ~3x slowdown.
func escapeSingleChar(char byte) (string, bool) {
if char == '"' {
return "&quot;", true
}
if char == '&' {
return "&amp;", true
}
if char == '<' {
return "&lt;", true
}
if char == '>' {
return "&gt;", true
}
return "", false
}
func attrEscape(out *bytes.Buffer, src []byte) {
org := 0
for i, ch := range src {
if entity, ok := escapeSingleChar(ch); ok {
if i > org {
// copy all the normal characters since the last escape
out.Write(src[org:i])
}
org = i + 1
out.WriteString(entity)
}
}
if org < len(src) {
out.Write(src[org:])
}
}
func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) {
end := 0
for _, rang := range skipRanges {
attrEscape(out, src[end:rang[0]])
out.Write(src[rang[0]:rang[1]])
end = rang[1]
}
attrEscape(out, src[end:])
}
func (options *Html) GetFlags() int {
return options.flags
}
func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) {
text = bytes.TrimPrefix(text, []byte("% "))
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
out.WriteString("<h1 class=\"title\">")
out.Write(text)
out.WriteString("\n</h1>")
}
func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) {
marker := out.Len()
doubleSpace(out)
if id == "" && options.flags&HTML_TOC != 0 {
id = fmt.Sprintf("toc_%d", options.headerCount)
}
if id != "" {
id = options.ensureUniqueHeaderID(id)
if options.parameters.HeaderIDPrefix != "" {
id = options.parameters.HeaderIDPrefix + id
}
if options.parameters.HeaderIDSuffix != "" {
id = id + options.parameters.HeaderIDSuffix
}
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
} else {
out.WriteString(fmt.Sprintf("<h%d>", level))
}
tocMarker := out.Len()
if !text() {
out.Truncate(marker)
return
}
// are we building a table of contents?
if options.flags&HTML_TOC != 0 {
options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id)
}
out.WriteString(fmt.Sprintf("</h%d>\n", level))
}
func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) {
if options.flags&HTML_SKIP_HTML != 0 {
return
}
doubleSpace(out)
out.Write(text)
out.WriteByte('\n')
}
func (options *Html) HRule(out *bytes.Buffer) {
doubleSpace(out)
out.WriteString("<hr")
out.WriteString(options.closeTag)
out.WriteByte('\n')
}
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) {
doubleSpace(out)
// parse out the language names/classes
count := 0
for _, elt := range strings.Fields(lang) {
if elt[0] == '.' {
elt = elt[1:]
}
if len(elt) == 0 {
continue
}
if count == 0 {
out.WriteString("<pre><code class=\"language-")
} else {
out.WriteByte(' ')
}
attrEscape(out, []byte(elt))
count++
}
if count == 0 {
out.WriteString("<pre><code>")
} else {
out.WriteString("\">")
}
attrEscape(out, text)
out.WriteString("</code></pre>\n")
}
func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) {
doubleSpace(out)
out.WriteString("<blockquote>\n")
out.Write(text)
out.WriteString("</blockquote>\n")
}
func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
doubleSpace(out)
out.WriteString("<table>\n<thead>\n")
out.Write(header)
out.WriteString("</thead>\n\n<tbody>\n")
out.Write(body)
out.WriteString("</tbody>\n</table>\n")
}
func (options *Html) TableRow(out *bytes.Buffer, text []byte) {
doubleSpace(out)
out.WriteString("<tr>\n")
out.Write(text)
out.WriteString("\n</tr>\n")
}
func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
doubleSpace(out)
switch align {
case TABLE_ALIGNMENT_LEFT:
out.WriteString("<th align=\"left\">")
case TABLE_ALIGNMENT_RIGHT:
out.WriteString("<th align=\"right\">")
case TABLE_ALIGNMENT_CENTER:
out.WriteString("<th align=\"center\">")
default:
out.WriteString("<th>")
}
out.Write(text)
out.WriteString("</th>")
}
func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
doubleSpace(out)
switch align {
case TABLE_ALIGNMENT_LEFT:
out.WriteString("<td align=\"left\">")
case TABLE_ALIGNMENT_RIGHT:
out.WriteString("<td align=\"right\">")
case TABLE_ALIGNMENT_CENTER:
out.WriteString("<td align=\"center\">")
default:
out.WriteString("<td>")
}
out.Write(text)
out.WriteString("</td>")
}
func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) {
out.WriteString("<div class=\"footnotes\">\n")
options.HRule(out)
options.List(out, text, LIST_TYPE_ORDERED)
out.WriteString("</div>\n")
}
func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
doubleSpace(out)
}
slug := slugify(name)
out.WriteString(`<li id="`)
out.WriteString(`fn:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)
out.WriteString(`">`)
out.Write(text)
if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 {
out.WriteString(` <a class="footnote-return" href="#`)
out.WriteString(`fnref:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)
out.WriteString(`">`)
out.WriteString(options.parameters.FootnoteReturnLinkContents)
out.WriteString(`</a>`)
}
out.WriteString("</li>\n")
}
func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) {
marker := out.Len()
doubleSpace(out)
if flags&LIST_TYPE_DEFINITION != 0 {
out.WriteString("<dl>")
} else if flags&LIST_TYPE_ORDERED != 0 {
out.WriteString("<ol>")
} else {
out.WriteString("<ul>")
}
if !text() {
out.Truncate(marker)
return
}
if flags&LIST_TYPE_DEFINITION != 0 {
out.WriteString("</dl>\n")
} else if flags&LIST_TYPE_ORDERED != 0 {
out.WriteString("</ol>\n")
} else {
out.WriteString("</ul>\n")
}
}
func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) {
if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) ||
flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
doubleSpace(out)
}
if flags&LIST_TYPE_TERM != 0 {
out.WriteString("<dt>")
} else if flags&LIST_TYPE_DEFINITION != 0 {
out.WriteString("<dd>")
} else {
out.WriteString("<li>")
}
out.Write(text)
if flags&LIST_TYPE_TERM != 0 {
out.WriteString("</dt>\n")
} else if flags&LIST_TYPE_DEFINITION != 0 {
out.WriteString("</dd>\n")
} else {
out.WriteString("</li>\n")
}
}
func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
doubleSpace(out)
out.WriteString("<p>")
if !text() {
out.Truncate(marker)
return
}
out.WriteString("</p>\n")
}
func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) {
skipRanges := htmlEntity.FindAllIndex(link, -1)
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL {
// mark it but don't link it if it is not a safe link: no smartypants
out.WriteString("<tt>")
entityEscapeWithSkip(out, link, skipRanges)
out.WriteString("</tt>")
return
}
out.WriteString("<a href=\"")
if kind == LINK_TYPE_EMAIL {
out.WriteString("mailto:")
} else {
options.maybeWriteAbsolutePrefix(out, link)
}
entityEscapeWithSkip(out, link, skipRanges)
var relAttrs []string
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
relAttrs = append(relAttrs, "nofollow")
}
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
relAttrs = append(relAttrs, "noreferrer")
}
if len(relAttrs) > 0 {
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
}
// blank target only add to external link
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
out.WriteString("\" target=\"_blank")
}
out.WriteString("\">")
// Pretty print: if we get an email address as
// an actual URI, e.g. `mailto:foo@bar.com`, we don't
// want to print the `mailto:` prefix
switch {
case bytes.HasPrefix(link, []byte("mailto://")):
attrEscape(out, link[len("mailto://"):])
case bytes.HasPrefix(link, []byte("mailto:")):
attrEscape(out, link[len("mailto:"):])
default:
entityEscapeWithSkip(out, link, skipRanges)
}
out.WriteString("</a>")
}
func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString("<code>")
attrEscape(out, text)
out.WriteString("</code>")
}
func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString("<strong>")
out.Write(text)
out.WriteString("</strong>")
}
func (options *Html) Emphasis(out *bytes.Buffer, text []byte) {
if len(text) == 0 {
return
}
out.WriteString("<em>")
out.Write(text)
out.WriteString("</em>")
}
func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) {
if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
out.WriteString(options.parameters.AbsolutePrefix)
if link[0] != '/' {
out.WriteByte('/')
}
}
}
func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
if options.flags&HTML_SKIP_IMAGES != 0 {
return
}
out.WriteString("<img src=\"")
options.maybeWriteAbsolutePrefix(out, link)
attrEscape(out, link)
out.WriteString("\" alt=\"")
if len(alt) > 0 {
attrEscape(out, alt)
}
if len(title) > 0 {
out.WriteString("\" title=\"")
attrEscape(out, title)
}
out.WriteByte('"')
out.WriteString(options.closeTag)
}
func (options *Html) LineBreak(out *bytes.Buffer) {
out.WriteString("<br")
out.WriteString(options.closeTag)
out.WriteByte('\n')
}
func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
if options.flags&HTML_SKIP_LINKS != 0 {
// write the link text out but don't link it, just mark it with typewriter font
out.WriteString("<tt>")
attrEscape(out, content)
out.WriteString("</tt>")
return
}
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) {
// write the link text out but don't link it, just mark it with typewriter font
out.WriteString("<tt>")
attrEscape(out, content)
out.WriteString("</tt>")
return
}
out.WriteString("<a href=\"")
options.maybeWriteAbsolutePrefix(out, link)
attrEscape(out, link)
if len(title) > 0 {
out.WriteString("\" title=\"")
attrEscape(out, title)
}
var relAttrs []string
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
relAttrs = append(relAttrs, "nofollow")
}
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
relAttrs = append(relAttrs, "noreferrer")
}
if len(relAttrs) > 0 {
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
}
// blank target only add to external link
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
out.WriteString("\" target=\"_blank")
}
out.WriteString("\">")
out.Write(content)
out.WriteString("</a>")
return
}
func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) {
if options.flags&HTML_SKIP_HTML != 0 {
return
}
if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") {
return
}
if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") {
return
}
if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
return
}
out.Write(text)
}
func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString("<strong><em>")
out.Write(text)
out.WriteString("</em></strong>")
}
func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) {
out.WriteString("<del>")
out.Write(text)
out.WriteString("</del>")
}
func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
slug := slugify(ref)
out.WriteString(`<sup class="footnote-ref" id="`)
out.WriteString(`fnref:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)
out.WriteString(`"><a rel="footnote" href="#`)
out.WriteString(`fn:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)
out.WriteString(`">`)
out.WriteString(strconv.Itoa(id))
out.WriteString(`</a></sup>`)
}
func (options *Html) Entity(out *bytes.Buffer, entity []byte) {
out.Write(entity)
}
func (options *Html) NormalText(out *bytes.Buffer, text []byte) {
if options.flags&HTML_USE_SMARTYPANTS != 0 {
options.Smartypants(out, text)
} else {
attrEscape(out, text)
}
}
func (options *Html) Smartypants(out *bytes.Buffer, text []byte) {
smrt := smartypantsData{false, false}
// first do normal entity escaping
var escaped bytes.Buffer
attrEscape(&escaped, text)
text = escaped.Bytes()
mark := 0
for i := 0; i < len(text); i++ {
if action := options.smartypants[text[i]]; action != nil {
if i > mark {
out.Write(text[mark:i])
}
previousChar := byte(0)
if i > 0 {
previousChar = text[i-1]
}
i += action(out, &smrt, previousChar, text[i:])
mark = i + 1
}
}
if mark < len(text) {
out.Write(text[mark:])
}
}
func (options *Html) DocumentHeader(out *bytes.Buffer) {
if options.flags&HTML_COMPLETE_PAGE == 0 {
return
}
ending := ""
if options.flags&HTML_USE_XHTML != 0 {
out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
ending = " /"
} else {
out.WriteString("<!DOCTYPE html>\n")
out.WriteString("<html>\n")
}
out.WriteString("<head>\n")
out.WriteString(" <title>")
options.NormalText(out, []byte(options.title))
out.WriteString("</title>\n")
out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
out.WriteString(VERSION)
out.WriteString("\"")
out.WriteString(ending)
out.WriteString(">\n")
out.WriteString(" <meta charset=\"utf-8\"")
out.WriteString(ending)
out.WriteString(">\n")
if options.css != "" {
out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
attrEscape(out, []byte(options.css))
out.WriteString("\"")
out.WriteString(ending)
out.WriteString(">\n")
}
out.WriteString("</head>\n")
out.WriteString("<body>\n")
options.tocMarker = out.Len()
}
func (options *Html) DocumentFooter(out *bytes.Buffer) {
// finalize and insert the table of contents
if options.flags&HTML_TOC != 0 {
options.TocFinalize()
// now we have to insert the table of contents into the document
var temp bytes.Buffer
// start by making a copy of everything after the document header
temp.Write(out.Bytes()[options.tocMarker:])
// now clear the copied material from the main output buffer
out.Truncate(options.tocMarker)
// corner case spacing issue
if options.flags&HTML_COMPLETE_PAGE != 0 {
out.WriteByte('\n')
}
// insert the table of contents
out.WriteString("<nav>\n")
out.Write(options.toc.Bytes())
out.WriteString("</nav>\n")
// corner case spacing issue
if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 {
out.WriteByte('\n')
}
// write out everything that came after it
if options.flags&HTML_OMIT_CONTENTS == 0 {
out.Write(temp.Bytes())
}
}
if options.flags&HTML_COMPLETE_PAGE != 0 {
out.WriteString("\n</body>\n")
out.WriteString("</html>\n")
}
}
func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
for level > options.currentLevel {
switch {
case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")):
// this sublist can nest underneath a header
size := options.toc.Len()
options.toc.Truncate(size - len("</li>\n"))
case options.currentLevel > 0:
options.toc.WriteString("<li>")
}
if options.toc.Len() > 0 {
options.toc.WriteByte('\n')
}
options.toc.WriteString("<ul>\n")
options.currentLevel++
}
for level < options.currentLevel {
options.toc.WriteString("</ul>")
if options.currentLevel > 1 {
options.toc.WriteString("</li>\n")
}
options.currentLevel--
}
options.toc.WriteString("<li><a href=\"#")
if anchor != "" {
options.toc.WriteString(anchor)
} else {
options.toc.WriteString("toc_")
options.toc.WriteString(strconv.Itoa(options.headerCount))
}
options.toc.WriteString("\">")
options.headerCount++
options.toc.Write(text)
options.toc.WriteString("</a></li>\n")
}
func (options *Html) TocHeader(text []byte, level int) {
options.TocHeaderWithAnchor(text, level, "")
}
func (options *Html) TocFinalize() {
for options.currentLevel > 1 {
options.toc.WriteString("</ul></li>\n")
options.currentLevel--
}
if options.currentLevel > 0 {
options.toc.WriteString("</ul>\n")
}
}
func isHtmlTag(tag []byte, tagname string) bool {
found, _ := findHtmlTagPos(tag, tagname)
return found
}
// Look for a character, but ignore it when it's in any kind of quotes, it
// might be JavaScript
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
inSingleQuote := false
inDoubleQuote := false
inGraveQuote := false
i := start
for i < len(html) {
switch {
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
return i
case html[i] == '\'':
inSingleQuote = !inSingleQuote
case html[i] == '"':
inDoubleQuote = !inDoubleQuote
case html[i] == '`':
inGraveQuote = !inGraveQuote
}
i++
}
return start
}
func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
i := 0
if i < len(tag) && tag[0] != '<' {
return false, -1
}
i++
i = skipSpace(tag, i)
if i < len(tag) && tag[i] == '/' {
i++
}
i = skipSpace(tag, i)
j := 0
for ; i < len(tag); i, j = i+1, j+1 {
if j >= len(tagname) {
break
}
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
return false, -1
}
}
if i == len(tag) {
return false, -1
}
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
if rightAngle > i {
return true, rightAngle
}
return false, -1
}
func skipUntilChar(text []byte, start int, char byte) int {
i := start
for i < len(text) && text[i] != char {
i++
}
return i
}
func skipSpace(tag []byte, i int) int {
for i < len(tag) && isspace(tag[i]) {
i++
}
return i
}
func skipChar(data []byte, start int, char byte) int {
i := start
for i < len(data) && data[i] == char {
i++
}
return i
}
func doubleSpace(out *bytes.Buffer) {
if out.Len() > 0 {
out.WriteByte('\n')
}
}
func isRelativeLink(link []byte) (yes bool) {
// a tag begin with '#'
if link[0] == '#' {
return true
}
// link begin with '/' but not '//', the second maybe a protocol relative link
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
return true
}
// only the root '/'
if len(link) == 1 && link[0] == '/' {
return true
}
// current directory : begin with "./"
if bytes.HasPrefix(link, []byte("./")) {
return true
}
// parent directory : begin with "../"
if bytes.HasPrefix(link, []byte("../")) {
return true
}
return false
}
func (options *Html) ensureUniqueHeaderID(id string) string {
for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] {
tmp := fmt.Sprintf("%s-%d", id, count+1)
if _, tmpFound := options.headerIDs[tmp]; !tmpFound {
options.headerIDs[id] = count + 1
id = tmp
} else {
id = id + "-1"
}
}
if _, found := options.headerIDs[id]; !found {
options.headerIDs[id] = 0
}
return id
}

1148
vendor/github.com/russross/blackfriday/inline.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

332
vendor/github.com/russross/blackfriday/latex.go generated vendored Normal file
View File

@ -0,0 +1,332 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// LaTeX rendering backend
//
//
package blackfriday
import (
"bytes"
)
// Latex is a type that implements the Renderer interface for LaTeX output.
//
// Do not create this directly, instead use the LatexRenderer function.
type Latex struct {
}
// LatexRenderer creates and configures a Latex object, which
// satisfies the Renderer interface.
//
// flags is a set of LATEX_* options ORed together (currently no such options
// are defined).
func LatexRenderer(flags int) Renderer {
return &Latex{}
}
func (options *Latex) GetFlags() int {
return 0
}
// render code chunks using verbatim, or listings if we have a language
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) {
if lang == "" {
out.WriteString("\n\\begin{verbatim}\n")
} else {
out.WriteString("\n\\begin{lstlisting}[language=")
out.WriteString(lang)
out.WriteString("]\n")
}
out.Write(text)
if lang == "" {
out.WriteString("\n\\end{verbatim}\n")
} else {
out.WriteString("\n\\end{lstlisting}\n")
}
}
func (options *Latex) TitleBlock(out *bytes.Buffer, text []byte) {
}
func (options *Latex) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteString("\n\\begin{quotation}\n")
out.Write(text)
out.WriteString("\n\\end{quotation}\n")
}
func (options *Latex) BlockHtml(out *bytes.Buffer, text []byte) {
// a pretty lame thing to do...
out.WriteString("\n\\begin{verbatim}\n")
out.Write(text)
out.WriteString("\n\\end{verbatim}\n")
}
func (options *Latex) Header(out *bytes.Buffer, text func() bool, level int, id string) {
marker := out.Len()
switch level {
case 1:
out.WriteString("\n\\section{")
case 2:
out.WriteString("\n\\subsection{")
case 3:
out.WriteString("\n\\subsubsection{")
case 4:
out.WriteString("\n\\paragraph{")
case 5:
out.WriteString("\n\\subparagraph{")
case 6:
out.WriteString("\n\\textbf{")
}
if !text() {
out.Truncate(marker)
return
}
out.WriteString("}\n")
}
func (options *Latex) HRule(out *bytes.Buffer) {
out.WriteString("\n\\HRule\n")
}
func (options *Latex) List(out *bytes.Buffer, text func() bool, flags int) {
marker := out.Len()
if flags&LIST_TYPE_ORDERED != 0 {
out.WriteString("\n\\begin{enumerate}\n")
} else {
out.WriteString("\n\\begin{itemize}\n")
}
if !text() {
out.Truncate(marker)
return
}
if flags&LIST_TYPE_ORDERED != 0 {
out.WriteString("\n\\end{enumerate}\n")
} else {
out.WriteString("\n\\end{itemize}\n")
}
}
func (options *Latex) ListItem(out *bytes.Buffer, text []byte, flags int) {
out.WriteString("\n\\item ")
out.Write(text)
}
func (options *Latex) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
out.WriteString("\n")
if !text() {
out.Truncate(marker)
return
}
out.WriteString("\n")
}
func (options *Latex) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
out.WriteString("\n\\begin{tabular}{")
for _, elt := range columnData {
switch elt {
case TABLE_ALIGNMENT_LEFT:
out.WriteByte('l')
case TABLE_ALIGNMENT_RIGHT:
out.WriteByte('r')
default:
out.WriteByte('c')
}
}
out.WriteString("}\n")
out.Write(header)
out.WriteString(" \\\\\n\\hline\n")
out.Write(body)
out.WriteString("\n\\end{tabular}\n")
}
func (options *Latex) TableRow(out *bytes.Buffer, text []byte) {
if out.Len() > 0 {
out.WriteString(" \\\\\n")
}
out.Write(text)
}
func (options *Latex) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
if out.Len() > 0 {
out.WriteString(" & ")
}
out.Write(text)
}
func (options *Latex) TableCell(out *bytes.Buffer, text []byte, align int) {
if out.Len() > 0 {
out.WriteString(" & ")
}
out.Write(text)
}
// TODO: this
func (options *Latex) Footnotes(out *bytes.Buffer, text func() bool) {
}
func (options *Latex) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
}
func (options *Latex) AutoLink(out *bytes.Buffer, link []byte, kind int) {
out.WriteString("\\href{")
if kind == LINK_TYPE_EMAIL {
out.WriteString("mailto:")
}
out.Write(link)
out.WriteString("}{")
out.Write(link)
out.WriteString("}")
}
func (options *Latex) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString("\\texttt{")
escapeSpecialChars(out, text)
out.WriteString("}")
}
func (options *Latex) DoubleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString("\\textbf{")
out.Write(text)
out.WriteString("}")
}
func (options *Latex) Emphasis(out *bytes.Buffer, text []byte) {
out.WriteString("\\textit{")
out.Write(text)
out.WriteString("}")
}
func (options *Latex) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
if bytes.HasPrefix(link, []byte("http://")) || bytes.HasPrefix(link, []byte("https://")) {
// treat it like a link
out.WriteString("\\href{")
out.Write(link)
out.WriteString("}{")
out.Write(alt)
out.WriteString("}")
} else {
out.WriteString("\\includegraphics{")
out.Write(link)
out.WriteString("}")
}
}
func (options *Latex) LineBreak(out *bytes.Buffer) {
out.WriteString(" \\\\\n")
}
func (options *Latex) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
out.WriteString("\\href{")
out.Write(link)
out.WriteString("}{")
out.Write(content)
out.WriteString("}")
}
func (options *Latex) RawHtmlTag(out *bytes.Buffer, tag []byte) {
}
func (options *Latex) TripleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString("\\textbf{\\textit{")
out.Write(text)
out.WriteString("}}")
}
func (options *Latex) StrikeThrough(out *bytes.Buffer, text []byte) {
out.WriteString("\\sout{")
out.Write(text)
out.WriteString("}")
}
// TODO: this
func (options *Latex) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
}
func needsBackslash(c byte) bool {
for _, r := range []byte("_{}%$&\\~#") {
if c == r {
return true
}
}
return false
}
func escapeSpecialChars(out *bytes.Buffer, text []byte) {
for i := 0; i < len(text); i++ {
// directly copy normal characters
org := i
for i < len(text) && !needsBackslash(text[i]) {
i++
}
if i > org {
out.Write(text[org:i])
}
// escape a character
if i >= len(text) {
break
}
out.WriteByte('\\')
out.WriteByte(text[i])
}
}
func (options *Latex) Entity(out *bytes.Buffer, entity []byte) {
// TODO: convert this into a unicode character or something
out.Write(entity)
}
func (options *Latex) NormalText(out *bytes.Buffer, text []byte) {
escapeSpecialChars(out, text)
}
// header and footer
func (options *Latex) DocumentHeader(out *bytes.Buffer) {
out.WriteString("\\documentclass{article}\n")
out.WriteString("\n")
out.WriteString("\\usepackage{graphicx}\n")
out.WriteString("\\usepackage{listings}\n")
out.WriteString("\\usepackage[margin=1in]{geometry}\n")
out.WriteString("\\usepackage[utf8]{inputenc}\n")
out.WriteString("\\usepackage{verbatim}\n")
out.WriteString("\\usepackage[normalem]{ulem}\n")
out.WriteString("\\usepackage{hyperref}\n")
out.WriteString("\n")
out.WriteString("\\hypersetup{colorlinks,%\n")
out.WriteString(" citecolor=black,%\n")
out.WriteString(" filecolor=black,%\n")
out.WriteString(" linkcolor=black,%\n")
out.WriteString(" urlcolor=black,%\n")
out.WriteString(" pdfstartview=FitH,%\n")
out.WriteString(" breaklinks=true,%\n")
out.WriteString(" pdfauthor={Blackfriday Markdown Processor v")
out.WriteString(VERSION)
out.WriteString("}}\n")
out.WriteString("\n")
out.WriteString("\\newcommand{\\HRule}{\\rule{\\linewidth}{0.5mm}}\n")
out.WriteString("\\addtolength{\\parskip}{0.5\\baselineskip}\n")
out.WriteString("\\parindent=0pt\n")
out.WriteString("\n")
out.WriteString("\\begin{document}\n")
}
func (options *Latex) DocumentFooter(out *bytes.Buffer) {
out.WriteString("\n\\end{document}\n")
}

926
vendor/github.com/russross/blackfriday/markdown.go generated vendored Normal file
View File

@ -0,0 +1,926 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// Markdown parsing and processing
//
//
// Blackfriday markdown processor.
//
// Translates plain text with simple formatting rules into HTML or LaTeX.
package blackfriday
import (
"bytes"
"fmt"
"strings"
"unicode/utf8"
)
const VERSION = "1.5"
// These are the supported markdown parsing extensions.
// OR these values together to select multiple extensions.
const (
EXTENSION_NO_INTRA_EMPHASIS = 1 << iota // ignore emphasis markers inside words
EXTENSION_TABLES // render tables
EXTENSION_FENCED_CODE // render fenced code blocks
EXTENSION_AUTOLINK // detect embedded URLs that are not explicitly marked
EXTENSION_STRIKETHROUGH // strikethrough text using ~~test~~
EXTENSION_LAX_HTML_BLOCKS // loosen up HTML block parsing rules
EXTENSION_SPACE_HEADERS // be strict about prefix header rules
EXTENSION_HARD_LINE_BREAK // translate newlines into line breaks
EXTENSION_TAB_SIZE_EIGHT // expand tabs to eight spaces instead of four
EXTENSION_FOOTNOTES // Pandoc-style footnotes
EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
EXTENSION_HEADER_IDS // specify header IDs with {#id}
EXTENSION_TITLEBLOCK // Titleblock ala pandoc
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks
EXTENSION_DEFINITION_LISTS // render definition lists
commonHtmlFlags = 0 |
HTML_USE_XHTML |
HTML_USE_SMARTYPANTS |
HTML_SMARTYPANTS_FRACTIONS |
HTML_SMARTYPANTS_DASHES |
HTML_SMARTYPANTS_LATEX_DASHES
commonExtensions = 0 |
EXTENSION_NO_INTRA_EMPHASIS |
EXTENSION_TABLES |
EXTENSION_FENCED_CODE |
EXTENSION_AUTOLINK |
EXTENSION_STRIKETHROUGH |
EXTENSION_SPACE_HEADERS |
EXTENSION_HEADER_IDS |
EXTENSION_BACKSLASH_LINE_BREAK |
EXTENSION_DEFINITION_LISTS
)
// These are the possible flag values for the link renderer.
// Only a single one of these values will be used; they are not ORed together.
// These are mostly of interest if you are writing a new output format.
const (
LINK_TYPE_NOT_AUTOLINK = iota
LINK_TYPE_NORMAL
LINK_TYPE_EMAIL
)
// These are the possible flag values for the ListItem renderer.
// Multiple flag values may be ORed together.
// These are mostly of interest if you are writing a new output format.
const (
LIST_TYPE_ORDERED = 1 << iota
LIST_TYPE_DEFINITION
LIST_TYPE_TERM
LIST_ITEM_CONTAINS_BLOCK
LIST_ITEM_BEGINNING_OF_LIST
LIST_ITEM_END_OF_LIST
)
// These are the possible flag values for the table cell renderer.
// Only a single one of these values will be used; they are not ORed together.
// These are mostly of interest if you are writing a new output format.
const (
TABLE_ALIGNMENT_LEFT = 1 << iota
TABLE_ALIGNMENT_RIGHT
TABLE_ALIGNMENT_CENTER = (TABLE_ALIGNMENT_LEFT | TABLE_ALIGNMENT_RIGHT)
)
// The size of a tab stop.
const (
TAB_SIZE_DEFAULT = 4
TAB_SIZE_EIGHT = 8
)
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{
"blockquote": {},
"del": {},
"div": {},
"dl": {},
"fieldset": {},
"form": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"h6": {},
"iframe": {},
"ins": {},
"math": {},
"noscript": {},
"ol": {},
"pre": {},
"p": {},
"script": {},
"style": {},
"table": {},
"ul": {},
// HTML5
"address": {},
"article": {},
"aside": {},
"canvas": {},
"figcaption": {},
"figure": {},
"footer": {},
"header": {},
"hgroup": {},
"main": {},
"nav": {},
"output": {},
"progress": {},
"section": {},
"video": {},
}
// Renderer is the rendering interface.
// This is mostly of interest if you are implementing a new rendering format.
//
// When a byte slice is provided, it contains the (rendered) contents of the
// element.
//
// When a callback is provided instead, it will write the contents of the
// respective element directly to the output buffer and return true on success.
// If the callback returns false, the rendering function should reset the
// output buffer as though it had never been called.
//
// Currently Html and Latex implementations are provided
type Renderer interface {
// block-level callbacks
BlockCode(out *bytes.Buffer, text []byte, lang string)
BlockQuote(out *bytes.Buffer, text []byte)
BlockHtml(out *bytes.Buffer, text []byte)
Header(out *bytes.Buffer, text func() bool, level int, id string)
HRule(out *bytes.Buffer)
List(out *bytes.Buffer, text func() bool, flags int)
ListItem(out *bytes.Buffer, text []byte, flags int)
Paragraph(out *bytes.Buffer, text func() bool)
Table(out *bytes.Buffer, header []byte, body []byte, columnData []int)
TableRow(out *bytes.Buffer, text []byte)
TableHeaderCell(out *bytes.Buffer, text []byte, flags int)
TableCell(out *bytes.Buffer, text []byte, flags int)
Footnotes(out *bytes.Buffer, text func() bool)
FootnoteItem(out *bytes.Buffer, name, text []byte, flags int)
TitleBlock(out *bytes.Buffer, text []byte)
// Span-level callbacks
AutoLink(out *bytes.Buffer, link []byte, kind int)
CodeSpan(out *bytes.Buffer, text []byte)
DoubleEmphasis(out *bytes.Buffer, text []byte)
Emphasis(out *bytes.Buffer, text []byte)
Image(out *bytes.Buffer, link []byte, title []byte, alt []byte)
LineBreak(out *bytes.Buffer)
Link(out *bytes.Buffer, link []byte, title []byte, content []byte)
RawHtmlTag(out *bytes.Buffer, tag []byte)
TripleEmphasis(out *bytes.Buffer, text []byte)
StrikeThrough(out *bytes.Buffer, text []byte)
FootnoteRef(out *bytes.Buffer, ref []byte, id int)
// Low-level callbacks
Entity(out *bytes.Buffer, entity []byte)
NormalText(out *bytes.Buffer, text []byte)
// Header and footer
DocumentHeader(out *bytes.Buffer)
DocumentFooter(out *bytes.Buffer)
GetFlags() int
}
// Callback functions for inline parsing. One such function is defined
// for each character that triggers a response when parsing inline data.
type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int
// Parser holds runtime state used by the parser.
// This is constructed by the Markdown function.
type parser struct {
r Renderer
refOverride ReferenceOverrideFunc
refs map[string]*reference
inlineCallback [256]inlineParser
flags int
nesting int
maxNesting int
insideLink bool
// Footnotes need to be ordered as well as available to quickly check for
// presence. If a ref is also a footnote, it's stored both in refs and here
// in notes. Slice is nil if footnotes not enabled.
notes []*reference
}
func (p *parser) getRef(refid string) (ref *reference, found bool) {
if p.refOverride != nil {
r, overridden := p.refOverride(refid)
if overridden {
if r == nil {
return nil, false
}
return &reference{
link: []byte(r.Link),
title: []byte(r.Title),
noteId: 0,
hasBlock: false,
text: []byte(r.Text)}, true
}
}
// refs are case insensitive
ref, found = p.refs[strings.ToLower(refid)]
return ref, found
}
//
//
// Public interface
//
//
// Reference represents the details of a link.
// See the documentation in Options for more details on use-case.
type Reference struct {
// Link is usually the URL the reference points to.
Link string
// Title is the alternate text describing the link in more detail.
Title string
// Text is the optional text to override the ref with if the syntax used was
// [refid][]
Text string
}
// ReferenceOverrideFunc is expected to be called with a reference string and
// return either a valid Reference type that the reference string maps to or
// nil. If overridden is false, the default reference logic will be executed.
// See the documentation in Options for more details on use-case.
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool)
// Options represents configurable overrides and callbacks (in addition to the
// extension flag set) for configuring a Markdown parse.
type Options struct {
// Extensions is a flag set of bit-wise ORed extension bits. See the
// EXTENSION_* flags defined in this package.
Extensions int
// ReferenceOverride is an optional function callback that is called every
// time a reference is resolved.
//
// In Markdown, the link reference syntax can be made to resolve a link to
// a reference instead of an inline URL, in one of the following ways:
//
// * [link text][refid]
// * [refid][]
//
// Usually, the refid is defined at the bottom of the Markdown document. If
// this override function is provided, the refid is passed to the override
// function first, before consulting the defined refids at the bottom. If
// the override function indicates an override did not occur, the refids at
// the bottom will be used to fill in the link details.
ReferenceOverride ReferenceOverrideFunc
}
// MarkdownBasic is a convenience function for simple rendering.
// It processes markdown input with no extensions enabled.
func MarkdownBasic(input []byte) []byte {
// set up the HTML renderer
htmlFlags := HTML_USE_XHTML
renderer := HtmlRenderer(htmlFlags, "", "")
// set up the parser
return MarkdownOptions(input, renderer, Options{Extensions: 0})
}
// Call Markdown with most useful extensions enabled
// MarkdownCommon is a convenience function for simple rendering.
// It processes markdown input with common extensions enabled, including:
//
// * Smartypants processing with smart fractions and LaTeX dashes
//
// * Intra-word emphasis suppression
//
// * Tables
//
// * Fenced code blocks
//
// * Autolinking
//
// * Strikethrough support
//
// * Strict header parsing
//
// * Custom Header IDs
func MarkdownCommon(input []byte) []byte {
// set up the HTML renderer
renderer := HtmlRenderer(commonHtmlFlags, "", "")
return MarkdownOptions(input, renderer, Options{
Extensions: commonExtensions})
}
// Markdown is the main rendering function.
// It parses and renders a block of markdown-encoded text.
// The supplied Renderer is used to format the output, and extensions dictates
// which non-standard extensions are enabled.
//
// To use the supplied Html or LaTeX renderers, see HtmlRenderer and
// LatexRenderer, respectively.
func Markdown(input []byte, renderer Renderer, extensions int) []byte {
return MarkdownOptions(input, renderer, Options{
Extensions: extensions})
}
// MarkdownOptions is just like Markdown but takes additional options through
// the Options struct.
func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
// no point in parsing if we can't render
if renderer == nil {
return nil
}
extensions := opts.Extensions
// fill in the render structure
p := new(parser)
p.r = renderer
p.flags = extensions
p.refOverride = opts.ReferenceOverride
p.refs = make(map[string]*reference)
p.maxNesting = 16
p.insideLink = false
// register inline parsers
p.inlineCallback['*'] = emphasis
p.inlineCallback['_'] = emphasis
if extensions&EXTENSION_STRIKETHROUGH != 0 {
p.inlineCallback['~'] = emphasis
}
p.inlineCallback['`'] = codeSpan
p.inlineCallback['\n'] = lineBreak
p.inlineCallback['['] = link
p.inlineCallback['<'] = leftAngle
p.inlineCallback['\\'] = escape
p.inlineCallback['&'] = entity
if extensions&EXTENSION_AUTOLINK != 0 {
p.inlineCallback[':'] = autoLink
}
if extensions&EXTENSION_FOOTNOTES != 0 {
p.notes = make([]*reference, 0)
}
first := firstPass(p, input)
second := secondPass(p, first)
return second
}
// first pass:
// - normalize newlines
// - extract references (outside of fenced code blocks)
// - expand tabs (outside of fenced code blocks)
// - copy everything else
func firstPass(p *parser, input []byte) []byte {
var out bytes.Buffer
tabSize := TAB_SIZE_DEFAULT
if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 {
tabSize = TAB_SIZE_EIGHT
}
beg := 0
lastFencedCodeBlockEnd := 0
for beg < len(input) {
// Find end of this line, then process the line.
end := beg
for end < len(input) && input[end] != '\n' && input[end] != '\r' {
end++
}
if p.flags&EXTENSION_FENCED_CODE != 0 {
// track fenced code block boundaries to suppress tab expansion
// and reference extraction inside them:
if beg >= lastFencedCodeBlockEnd {
if i := p.fencedCodeBlock(&out, input[beg:], false); i > 0 {
lastFencedCodeBlockEnd = beg + i
}
}
}
// add the line body if present
if end > beg {
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks.
out.Write(input[beg:end])
} else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 {
beg += refEnd
continue
} else {
expandTabs(&out, input[beg:end], tabSize)
}
}
if end < len(input) && input[end] == '\r' {
end++
}
if end < len(input) && input[end] == '\n' {
end++
}
out.WriteByte('\n')
beg = end
}
// empty input?
if out.Len() == 0 {
out.WriteByte('\n')
}
return out.Bytes()
}
// second pass: actual rendering
func secondPass(p *parser, input []byte) []byte {
var output bytes.Buffer
p.r.DocumentHeader(&output)
p.block(&output, input)
if p.flags&EXTENSION_FOOTNOTES != 0 && len(p.notes) > 0 {
p.r.Footnotes(&output, func() bool {
flags := LIST_ITEM_BEGINNING_OF_LIST
for i := 0; i < len(p.notes); i += 1 {
ref := p.notes[i]
var buf bytes.Buffer
if ref.hasBlock {
flags |= LIST_ITEM_CONTAINS_BLOCK
p.block(&buf, ref.title)
} else {
p.inline(&buf, ref.title)
}
p.r.FootnoteItem(&output, ref.link, buf.Bytes(), flags)
flags &^= LIST_ITEM_BEGINNING_OF_LIST | LIST_ITEM_CONTAINS_BLOCK
}
return true
})
}
p.r.DocumentFooter(&output)
if p.nesting != 0 {
panic("Nesting level did not end at zero")
}
return output.Bytes()
}
//
// Link references
//
// This section implements support for references that (usually) appear
// as footnotes in a document, and can be referenced anywhere in the document.
// The basic format is:
//
// [1]: http://www.google.com/ "Google"
// [2]: http://www.github.com/ "Github"
//
// Anywhere in the document, the reference can be linked by referring to its
// label, i.e., 1 and 2 in this example, as in:
//
// This library is hosted on [Github][2], a git hosting site.
//
// Actual footnotes as specified in Pandoc and supported by some other Markdown
// libraries such as php-markdown are also taken care of. They look like this:
//
// This sentence needs a bit of further explanation.[^note]
//
// [^note]: This is the explanation.
//
// Footnotes should be placed at the end of the document in an ordered list.
// Inline footnotes such as:
//
// Inline footnotes^[Not supported.] also exist.
//
// are not yet supported.
// References are parsed and stored in this struct.
type reference struct {
link []byte
title []byte
noteId int // 0 if not a footnote ref
hasBlock bool
text []byte
}
func (r *reference) String() string {
return fmt.Sprintf("{link: %q, title: %q, text: %q, noteId: %d, hasBlock: %v}",
r.link, r.title, r.text, r.noteId, r.hasBlock)
}
// Check whether or not data starts with a reference link.
// If so, it is parsed and stored in the list of references
// (in the render struct).
// Returns the number of bytes to skip to move past it,
// or zero if the first line is not a reference.
func isReference(p *parser, data []byte, tabSize int) int {
// up to 3 optional leading spaces
if len(data) < 4 {
return 0
}
i := 0
for i < 3 && data[i] == ' ' {
i++
}
noteId := 0
// id part: anything but a newline between brackets
if data[i] != '[' {
return 0
}
i++
if p.flags&EXTENSION_FOOTNOTES != 0 {
if i < len(data) && data[i] == '^' {
// we can set it to anything here because the proper noteIds will
// be assigned later during the second pass. It just has to be != 0
noteId = 1
i++
}
}
idOffset := i
for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' {
i++
}
if i >= len(data) || data[i] != ']' {
return 0
}
idEnd := i
// spacer: colon (space | tab)* newline? (space | tab)*
i++
if i >= len(data) || data[i] != ':' {
return 0
}
i++
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i < len(data) && (data[i] == '\n' || data[i] == '\r') {
i++
if i < len(data) && data[i] == '\n' && data[i-1] == '\r' {
i++
}
}
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i >= len(data) {
return 0
}
var (
linkOffset, linkEnd int
titleOffset, titleEnd int
lineEnd int
raw []byte
hasBlock bool
)
if p.flags&EXTENSION_FOOTNOTES != 0 && noteId != 0 {
linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize)
lineEnd = linkEnd
} else {
linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i)
}
if lineEnd == 0 {
return 0
}
// a valid ref has been found
ref := &reference{
noteId: noteId,
hasBlock: hasBlock,
}
if noteId > 0 {
// reusing the link field for the id since footnotes don't have links
ref.link = data[idOffset:idEnd]
// if footnote, it's not really a title, it's the contained text
ref.title = raw
} else {
ref.link = data[linkOffset:linkEnd]
ref.title = data[titleOffset:titleEnd]
}
// id matches are case-insensitive
id := string(bytes.ToLower(data[idOffset:idEnd]))
p.refs[id] = ref
return lineEnd
}
func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) {
// link: whitespace-free sequence, optionally between angle brackets
if data[i] == '<' {
i++
}
linkOffset = i
if i == len(data) {
return
}
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
i++
}
linkEnd = i
if data[linkOffset] == '<' && data[linkEnd-1] == '>' {
linkOffset++
linkEnd--
}
// optional spacer: (space | tab)* (newline | '\'' | '"' | '(' )
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' {
return
}
// compute end-of-line
if i >= len(data) || data[i] == '\r' || data[i] == '\n' {
lineEnd = i
}
if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' {
lineEnd++
}
// optional (space|tab)* spacer after a newline
if lineEnd > 0 {
i = lineEnd + 1
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
}
// optional title: any non-newline sequence enclosed in '"() alone on its line
if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') {
i++
titleOffset = i
// look for EOL
for i < len(data) && data[i] != '\n' && data[i] != '\r' {
i++
}
if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' {
titleEnd = i + 1
} else {
titleEnd = i
}
// step back
i--
for i > titleOffset && (data[i] == ' ' || data[i] == '\t') {
i--
}
if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') {
lineEnd = titleEnd
titleEnd = i
}
}
return
}
// The first bit of this logic is the same as (*parser).listItem, but the rest
// is much simpler. This function simply finds the entire block and shifts it
// over by one tab if it is indeed a block (just returns the line if it's not).
// blockEnd is the end of the section in the input buffer, and contents is the
// extracted text that was shifted over one tab. It will need to be rendered at
// the end of the document.
func scanFootnote(p *parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) {
if i == 0 || len(data) == 0 {
return
}
// skip leading whitespace on first line
for i < len(data) && data[i] == ' ' {
i++
}
blockStart = i
// find the end of the line
blockEnd = i
for i < len(data) && data[i-1] != '\n' {
i++
}
// get working buffer
var raw bytes.Buffer
// put the first line into the working buffer
raw.Write(data[blockEnd:i])
blockEnd = i
// process the following lines
containsBlankLine := false
gatherLines:
for blockEnd < len(data) {
i++
// find the end of this line
for i < len(data) && data[i-1] != '\n' {
i++
}
// if it is an empty line, guess that it is part of this item
// and move on to the next line
if p.isEmpty(data[blockEnd:i]) > 0 {
containsBlankLine = true
blockEnd = i
continue
}
n := 0
if n = isIndented(data[blockEnd:i], indentSize); n == 0 {
// this is the end of the block.
// we don't want to include this last line in the index.
break gatherLines
}
// if there were blank lines before this one, insert a new one now
if containsBlankLine {
raw.WriteByte('\n')
containsBlankLine = false
}
// get rid of that first tab, write to buffer
raw.Write(data[blockEnd+n : i])
hasBlock = true
blockEnd = i
}
if data[blockEnd-1] != '\n' {
raw.WriteByte('\n')
}
contents = raw.Bytes()
return
}
//
//
// Miscellaneous helper functions
//
//
// Test if a character is a punctuation symbol.
// Taken from a private function in regexp in the stdlib.
func ispunct(c byte) bool {
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
if c == r {
return true
}
}
return false
}
// Test if a character is a whitespace character.
func isspace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
}
// Test if a character is letter.
func isletter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// Test if a character is a letter or a digit.
// TODO: check when this is looking for ASCII alnum and when it should use unicode
func isalnum(c byte) bool {
return (c >= '0' && c <= '9') || isletter(c)
}
// Replace tab characters with spaces, aligning to the next TAB_SIZE column.
// always ends output with a newline
func expandTabs(out *bytes.Buffer, line []byte, tabSize int) {
// first, check for common cases: no tabs, or only tabs at beginning of line
i, prefix := 0, 0
slowcase := false
for i = 0; i < len(line); i++ {
if line[i] == '\t' {
if prefix == i {
prefix++
} else {
slowcase = true
break
}
}
}
// no need to decode runes if all tabs are at the beginning of the line
if !slowcase {
for i = 0; i < prefix*tabSize; i++ {
out.WriteByte(' ')
}
out.Write(line[prefix:])
return
}
// the slow case: we need to count runes to figure out how
// many spaces to insert for each tab
column := 0
i = 0
for i < len(line) {
start := i
for i < len(line) && line[i] != '\t' {
_, size := utf8.DecodeRune(line[i:])
i += size
column++
}
if i > start {
out.Write(line[start:i])
}
if i >= len(line) {
break
}
for {
out.WriteByte(' ')
column++
if column%tabSize == 0 {
break
}
}
i++
}
}
// Find if a line counts as indented or not.
// Returns number of characters the indent is (0 = not indented).
func isIndented(data []byte, indentSize int) int {
if len(data) == 0 {
return 0
}
if data[0] == '\t' {
return 1
}
if len(data) < indentSize {
return 0
}
for i := 0; i < indentSize; i++ {
if data[i] != ' ' {
return 0
}
}
return indentSize
}
// Create a url-safe slug for fragments
func slugify(in []byte) []byte {
if len(in) == 0 {
return in
}
out := make([]byte, 0, len(in))
sym := false
for _, ch := range in {
if isalnum(ch) {
sym = false
out = append(out, ch)
} else if sym {
continue
} else {
out = append(out, '-')
sym = true
}
}
var a, b int
var ch byte
for a, ch = range out {
if ch != '-' {
break
}
}
for b = len(out) - 1; b > 0; b-- {
if out[b] != '-' {
break
}
}
return out[a : b+1]
}

400
vendor/github.com/russross/blackfriday/smartypants.go generated vendored Normal file
View File

@ -0,0 +1,400 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// SmartyPants rendering
//
//
package blackfriday
import (
"bytes"
)
type smartypantsData struct {
inSingleQuote bool
inDoubleQuote bool
}
func wordBoundary(c byte) bool {
return c == 0 || isspace(c) || ispunct(c)
}
func tolower(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c - 'A' + 'a'
}
return c
}
func isdigit(c byte) bool {
return c >= '0' && c <= '9'
}
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool {
// edge of the buffer is likely to be a tag that we don't get to see,
// so we treat it like text sometimes
// enumerate all sixteen possibilities for (previousChar, nextChar)
// each can be one of {0, space, punct, other}
switch {
case previousChar == 0 && nextChar == 0:
// context is not any help here, so toggle
*isOpen = !*isOpen
case isspace(previousChar) && nextChar == 0:
// [ "] might be [ "<code>foo...]
*isOpen = true
case ispunct(previousChar) && nextChar == 0:
// [!"] hmm... could be [Run!"] or [("<code>...]
*isOpen = false
case /* isnormal(previousChar) && */ nextChar == 0:
// [a"] is probably a close
*isOpen = false
case previousChar == 0 && isspace(nextChar):
// [" ] might be [...foo</code>" ]
*isOpen = false
case isspace(previousChar) && isspace(nextChar):
// [ " ] context is not any help here, so toggle
*isOpen = !*isOpen
case ispunct(previousChar) && isspace(nextChar):
// [!" ] is probably a close
*isOpen = false
case /* isnormal(previousChar) && */ isspace(nextChar):
// [a" ] this is one of the easy cases
*isOpen = false
case previousChar == 0 && ispunct(nextChar):
// ["!] hmm... could be ["$1.95] or [</code>"!...]
*isOpen = false
case isspace(previousChar) && ispunct(nextChar):
// [ "!] looks more like [ "$1.95]
*isOpen = true
case ispunct(previousChar) && ispunct(nextChar):
// [!"!] context is not any help here, so toggle
*isOpen = !*isOpen
case /* isnormal(previousChar) && */ ispunct(nextChar):
// [a"!] is probably a close
*isOpen = false
case previousChar == 0 /* && isnormal(nextChar) */ :
// ["a] is probably an open
*isOpen = true
case isspace(previousChar) /* && isnormal(nextChar) */ :
// [ "a] this is one of the easy cases
*isOpen = true
case ispunct(previousChar) /* && isnormal(nextChar) */ :
// [!"a] is probably an open
*isOpen = true
default:
// [a'b] maybe a contraction?
*isOpen = false
}
out.WriteByte('&')
if *isOpen {
out.WriteByte('l')
} else {
out.WriteByte('r')
}
out.WriteByte(quote)
out.WriteString("quo;")
return true
}
func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 2 {
t1 := tolower(text[1])
if t1 == '\'' {
nextChar := byte(0)
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
return 1
}
}
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
out.WriteString("&rsquo;")
return 0
}
if len(text) >= 3 {
t2 := tolower(text[2])
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
(len(text) < 4 || wordBoundary(text[3])) {
out.WriteString("&rsquo;")
return 0
}
}
}
nextChar := byte(0)
if len(text) > 1 {
nextChar = text[1]
}
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote) {
return 0
}
out.WriteByte(text[0])
return 0
}
func smartParens(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 3 {
t1 := tolower(text[1])
t2 := tolower(text[2])
if t1 == 'c' && t2 == ')' {
out.WriteString("&copy;")
return 2
}
if t1 == 'r' && t2 == ')' {
out.WriteString("&reg;")
return 2
}
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
out.WriteString("&trade;")
return 3
}
}
out.WriteByte(text[0])
return 0
}
func smartDash(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 2 {
if text[1] == '-' {
out.WriteString("&mdash;")
return 1
}
if wordBoundary(previousChar) && wordBoundary(text[1]) {
out.WriteString("&ndash;")
return 0
}
}
out.WriteByte(text[0])
return 0
}
func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
out.WriteString("&mdash;")
return 2
}
if len(text) >= 2 && text[1] == '-' {
out.WriteString("&ndash;")
return 1
}
out.WriteByte(text[0])
return 0
}
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
if bytes.HasPrefix(text, []byte("&quot;")) {
nextChar := byte(0)
if len(text) >= 7 {
nextChar = text[6]
}
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
return 5
}
}
if bytes.HasPrefix(text, []byte("&#0;")) {
return 3
}
out.WriteByte('&')
return 0
}
func smartAmp(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartAmpVariant(out, smrt, previousChar, text, 'd')
}
func smartAmpAngledQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartAmpVariant(out, smrt, previousChar, text, 'a')
}
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
out.WriteString("&hellip;")
return 2
}
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
out.WriteString("&hellip;")
return 4
}
out.WriteByte(text[0])
return 0
}
func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if len(text) >= 2 && text[1] == '`' {
nextChar := byte(0)
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
return 1
}
}
out.WriteByte(text[0])
return 0
}
func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
// note: check for regular slash (/) or fraction slash (, 0x2044, or 0xe2 81 84 in utf-8)
// and avoid changing dates like 1/23/2005 into fractions.
numEnd := 0
for len(text) > numEnd && isdigit(text[numEnd]) {
numEnd++
}
if numEnd == 0 {
out.WriteByte(text[0])
return 0
}
denStart := numEnd + 1
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
denStart = numEnd + 3
} else if len(text) < numEnd+2 || text[numEnd] != '/' {
out.WriteByte(text[0])
return 0
}
denEnd := denStart
for len(text) > denEnd && isdigit(text[denEnd]) {
denEnd++
}
if denEnd == denStart {
out.WriteByte(text[0])
return 0
}
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
out.WriteString("<sup>")
out.Write(text[:numEnd])
out.WriteString("</sup>&frasl;<sub>")
out.Write(text[denStart:denEnd])
out.WriteString("</sub>")
return denEnd - 1
}
}
out.WriteByte(text[0])
return 0
}
func smartNumber(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
if text[0] == '1' && text[1] == '/' && text[2] == '2' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
out.WriteString("&frac12;")
return 2
}
}
if text[0] == '1' && text[1] == '/' && text[2] == '4' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
out.WriteString("&frac14;")
return 2
}
}
if text[0] == '3' && text[1] == '/' && text[2] == '4' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
out.WriteString("&frac34;")
return 2
}
}
}
out.WriteByte(text[0])
return 0
}
func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
nextChar := byte(0)
if len(text) > 1 {
nextChar = text[1]
}
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
out.WriteString("&quot;")
}
return 0
}
func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd')
}
func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a')
}
func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
i := 0
for i < len(text) && text[i] != '>' {
i++
}
out.Write(text[:i+1])
return i
}
type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int
type smartypantsRenderer [256]smartCallback
func smartypants(flags int) *smartypantsRenderer {
r := new(smartypantsRenderer)
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
r['"'] = smartDoubleQuote
r['&'] = smartAmp
} else {
r['"'] = smartAngledDoubleQuote
r['&'] = smartAmpAngledQuote
}
r['\''] = smartSingleQuote
r['('] = smartParens
if flags&HTML_SMARTYPANTS_DASHES != 0 {
if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
r['-'] = smartDash
} else {
r['-'] = smartDashLatex
}
}
r['.'] = smartPeriod
if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
r['1'] = smartNumber
r['3'] = smartNumber
} else {
for ch := '1'; ch <= '9'; ch++ {
r[ch] = smartNumberGeneric
}
}
r['<'] = smartLeftAngle
r['`'] = smartBacktick
return r
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2015 Dmitri Shuralyov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,29 @@
// Package sanitized_anchor_name provides a func to create sanitized anchor names.
//
// Its logic can be reused by multiple packages to create interoperable anchor names
// and links to those anchors.
//
// At this time, it does not try to ensure that generated anchor names
// are unique, that responsibility falls on the caller.
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
import "unicode"
// Create returns a sanitized anchor name for the given text.
func Create(text string) string {
var anchorName []rune
var futureDash = false
for _, r := range []rune(text) {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
return string(anchorName)
}

View File

@ -74,7 +74,9 @@ func (irc *Connection) readLoop() {
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
}
irc.Lock()
irc.lastMessage = time.Now()
irc.Unlock()
event, err := parseToEvent(msg)
event.Connection = irc
if err == nil {
@ -171,10 +173,12 @@ func (irc *Connection) pingLoop() {
//Ping at the ping frequency
irc.SendRawf("PING %d", time.Now().UnixNano())
//Try to recapture nickname if it's not as configured.
irc.Lock()
if irc.nick != irc.nickcurrent {
irc.nickcurrent = irc.nick
irc.SendRawf("NICK %s", irc.nick)
}
irc.Unlock()
case <-irc.end:
ticker.Stop()
ticker2.Stop()
@ -183,13 +187,21 @@ func (irc *Connection) pingLoop() {
}
}
func (irc *Connection) isQuitting() bool {
irc.Lock()
defer irc.Unlock()
return irc.quit
}
// Main loop to control the connection.
func (irc *Connection) Loop() {
errChan := irc.ErrorChan()
for !irc.quit {
for !irc.isQuitting() {
err := <-errChan
irc.Log.Printf("Error, disconnected: %s\n", err)
for !irc.quit {
close(irc.end)
irc.Wait()
for !irc.isQuitting() {
irc.Log.Printf("Error, disconnected: %s\n", err)
if err = irc.Reconnect(); err != nil {
irc.Log.Printf("Error while reconnecting: %s\n", err)
time.Sleep(60 * time.Second)
@ -211,8 +223,10 @@ func (irc *Connection) Quit() {
}
irc.SendRaw(quit)
irc.Lock()
irc.stopped = true
irc.quit = true
irc.Unlock()
}
// Use the connection to join a given channel.
@ -341,37 +355,14 @@ func (irc *Connection) Connected() bool {
// A disconnect sends all buffered messages (if possible),
// stops all goroutines and then closes the socket.
func (irc *Connection) Disconnect() {
for event := range irc.events {
irc.ClearCallback(event)
}
if irc.end != nil {
close(irc.end)
}
irc.end = nil
if irc.pwrite != nil {
close(irc.pwrite)
}
irc.Wait()
if irc.socket != nil {
irc.socket.Close()
}
irc.socket = nil
irc.ErrorChan() <- ErrDisconnected
}
// Reconnect to a server using the current connection.
func (irc *Connection) Reconnect() error {
if irc.end != nil {
close(irc.end)
}
irc.end = nil
irc.Wait() //make sure that wait group is cleared ensuring that all spawned goroutines have completed
irc.end = make(chan struct{})
return irc.Connect(irc.Server)
}
@ -427,7 +418,7 @@ func (irc *Connection) Connect(server string) error {
}
irc.stopped = false
//irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
irc.pwrite = make(chan string, 10)
irc.Error = make(chan error, 2)

View File

@ -136,9 +136,8 @@ func (irc *Connection) RunCallbacks(event *Event) {
func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[int]func(*Event))
//Handle error events. This has to be called in a new thred to allow
//readLoop to exit
irc.AddCallback("ERROR", func(e *Event) { go irc.Disconnect() })
//Handle error events.
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
//Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
@ -201,7 +200,7 @@ func (irc *Connection) setupCallbacks() {
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
delta := time.Duration(time.Now().UnixNano() - ns)
if irc.Debug {
irc.Log.Printf("Lag: %vs\n", delta)
irc.Log.Printf("Lag: %.3f s\n", delta.Seconds())
}
})
@ -216,6 +215,8 @@ func (irc *Connection) setupCallbacks() {
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
// Set irc.nickcurrent to the actually used nick in this connection.
irc.AddCallback("001", func(e *Event) {
irc.Lock()
irc.nickcurrent = e.Arguments[0]
irc.Unlock()
})
}

View File

@ -13,6 +13,7 @@ import (
)
type Connection struct {
sync.Mutex
sync.WaitGroup
Debug bool
Error chan error
@ -46,7 +47,7 @@ type Connection struct {
Log *log.Logger
stopped bool
quit bool
quit bool //User called Quit, do not reconnect.
}
// A struct to represent an event.

14
vendor/manifest vendored
View File

@ -63,7 +63,7 @@
"importpath": "github.com/go-telegram-bot-api/telegram-bot-api",
"repository": "https://github.com/go-telegram-bot-api/telegram-bot-api",
"vcs": "git",
"revision": "a7f48eb2dd301356942677e65bebe0c9aef07013",
"revision": "3293f3ad8411de32d99e443cd82aec7c3bb01a5a",
"branch": "master",
"notests": true
},
@ -95,8 +95,8 @@
"importpath": "github.com/mattermost/platform/einterfaces",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "b55ec6148caa93d54b660afe55408c643d217108",
"branch": "release-3.5",
"revision": "976296cd52533ff565407e55e872339cc312a0cf",
"branch": "release-3.6",
"path": "/einterfaces",
"notests": true
},
@ -104,8 +104,8 @@
"importpath": "github.com/mattermost/platform/model",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "b55ec6148caa93d54b660afe55408c643d217108",
"branch": "release-3.5",
"revision": "976296cd52533ff565407e55e872339cc312a0cf",
"branch": "release-3.6",
"path": "/model",
"notests": true
},
@ -146,7 +146,7 @@
"importpath": "github.com/nlopes/slack",
"repository": "https://github.com/nlopes/slack",
"vcs": "git",
"revision": "e595e9d8590a04ff76407e4e7d1791d25b095c66",
"revision": "248e1d53d27901137764ae625890c127bf67e7aa",
"branch": "master",
"notests": true
},
@ -186,7 +186,7 @@
"importpath": "github.com/thoj/go-ircevent",
"repository": "https://github.com/thoj/go-ircevent",
"vcs": "git",
"revision": "98c1902dd2097f38142384167e60206ba26f1585",
"revision": "1b0acb5f2f1b615cfbd4b9f91abb14cb39a18769",
"branch": "master",
"notests": true
},