<commit_before>package xmmsclient

import (
	"bytes"
	"encoding/binary"
	"errors"
	"io"
	"net"
	"sync"
)

type context struct {
	result    chan reply
	broadcast bool
}

type header struct {
	objectID   uint32
	commandID  uint32
	sequenceNr uint32
	length     uint32
}

type message struct {
	header    header
	broadcast bool
	args      XmmsValue
	result    chan reply
}

type reply struct {
	sequenceNr uint32
	payload    []byte
	err        error
}

type Client struct {
	sync.RWMutex

	sequenceNr uint32
	clientName string

	shutdownRegistry chan bool
	shutdownIO       chan bool
	registry         chan message
}

func parseHeader(r io.Reader) (*header, error) {
	var hdr header

	err := binary.Read(r, binary.BigEndian, &hdr.objectID)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.commandID)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.sequenceNr)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.length)
	if err != nil {
		return nil, err
	}

	return &hdr, nil
}

func writeHeader(w io.Writer, hdr *header) error {
	err := binary.Write(w, binary.BigEndian, hdr.objectID)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.commandID)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.sequenceNr)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.length)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) nextSequenceNr() uint32 {
	c.sequenceNr++
	return c.sequenceNr
}

func (c *Client) reader(r io.Reader, inbound chan reply) {
	buffer := make([]byte, 16)

	for {
		_, err := io.ReadFull(r, buffer)
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		header, err := parseHeader(bytes.NewBuffer(buffer))
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		payload := make([]byte, header.length)

		_, err = io.ReadFull(r, payload)
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		inbound <- reply{sequenceNr: header.sequenceNr, payload: payload}
	}
}

func (c *Client) writer(w io.Writer, outbound chan message, errors chan error) {
writer:
	for {
		select {
		case msg := <-outbound:
			var payload bytes.Buffer

			err := serializeXmmsValue(&payload, msg.args)
			if err != nil {
				errors <- err
				break writer
			}

			msg.header.length = uint32(len(payload.Bytes()))

			err = writeHeader(w, &msg.header)
			if err != nil {
				errors <- err
				break writer
			}

			payload.WriteTo(w)
			if err != nil {
				errors <- err
				break writer
			}
		case <-c.shutdownIO:
			break writer
		}
	}
}

func (c *Client) shutdownRouter(registry map[uint32](context), err error) {
	// Reference command channel as it will be nullified
	channel := c.registry

	// Grab the RW-lock, close and nullify the command channel
	// reference which will allow the draining loop to exit.
	go func() {
		c.Lock()
		c.registry = nil
		close(channel)
		c.Unlock()
	}()

	// Terminate all active subscriptions
	for _, v := range registry {
		v.result <- reply{err: err}
	}

	// Drain trailing requests
	for msg := range channel {
		msg.result <- reply{err: io.EOF}
	}
}

func (c *Client) router(inbound chan reply, outbound chan message, errors chan error) {
	registry := make(map[uint32](context))

	for {
		select {
		case msg := <-c.registry:
			msg.header.sequenceNr = c.nextSequenceNr()
			registry[msg.header.sequenceNr] = context{
				msg.result,
				msg.broadcast,
			}
			outbound <- msg
		case reply := <-inbound:
			if reply.err != nil {
				c.shutdownRouter(registry, reply.err)
				return
			}

			ctx := registry[reply.sequenceNr]

			go func() {
				ctx.result <- reply
			}()

			if !ctx.broadcast {
				delete(registry, reply.sequenceNr)
			}
		case err := <-errors:
			c.shutdownRouter(registry, err)
			return
		case <-c.shutdownRegistry:
			c.shutdownRouter(registry, io.EOF)
			return
		}
	}
}

func (c *Client) dispatch(objectID uint32, commandID uint32, args XmmsValue) chan reply {
	c.RLock()
	defer c.RUnlock()

	result := make(chan reply, 1)
	if c.registry == nil {
		result <- reply{err: io.EOF}
	} else {
		c.registry <- message{
			header: header{
				objectID:  objectID,
				commandID: commandID,
			},
			broadcast: objectID == 0,
			args:      args,
			result:    result,
		}
	}
	return result
}

func (c *Client) sendHello() (int, error) {
	result := make(chan reply)

	c.registry <- message{
		header: header{
			objectID:  1,
			commandID: 32,
		},
		broadcast: false,
		args:      XmmsList{XmmsInt(IpcVersion), XmmsString(c.clientName)},
		result:    result,
	}

	reply := <-result
	if reply.err != nil {
		return -1, reply.err
	}

	buffer := bytes.NewBuffer(reply.payload)

	value, err := tryDeserialize(buffer)
	if err != nil {
		return -1, err
	}

	clientID, ok := value.(XmmsInt)
	if !ok {
		return -1, errors.New("Bad reply from server")
	}

	return int(clientID), nil
}

func (c *Client) Dial(url string) (int, error) {
	c.Lock()
	defer c.Unlock()

	addr, err := net.ResolveTCPAddr("tcp", url)
	if err != nil {
		return -1, err
	}

	conn, err := net.DialTCP("tcp", nil, addr)
	if err != nil {
		return -1, err
	}

	c.shutdownRegistry = make(chan bool)
	c.shutdownIO = make(chan bool)
	c.registry = make(chan message)

	errors := make(chan error)
	inbound := make(chan reply)
	outbound := make(chan message)

	go c.reader(conn, inbound)
	go c.writer(conn, outbound, errors)
	go c.router(inbound, outbound, errors)

	clientID, err := c.sendHello()
	if err != nil {
		c.Close()
		return -1, err
	}

	return clientID, nil
}

func (c *Client) Close() {
	c.shutdownRegistry <- true
	c.shutdownIO <- true
}

func NewClient(name string) *Client {
	client := Client{
		clientName: name,
	}

	return &client
}
<commit_msg>Don't throw away error message when writing payload.<commit_after>package xmmsclient

import (
	"bytes"
	"encoding/binary"
	"errors"
	"io"
	"net"
	"sync"
)

type context struct {
	result    chan reply
	broadcast bool
}

type header struct {
	objectID   uint32
	commandID  uint32
	sequenceNr uint32
	length     uint32
}

type message struct {
	header    header
	broadcast bool
	args      XmmsValue
	result    chan reply
}

type reply struct {
	sequenceNr uint32
	payload    []byte
	err        error
}

type Client struct {
	sync.RWMutex

	sequenceNr uint32
	clientName string

	shutdownRegistry chan bool
	shutdownIO       chan bool
	registry         chan message
}

func parseHeader(r io.Reader) (*header, error) {
	var hdr header

	err := binary.Read(r, binary.BigEndian, &hdr.objectID)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.commandID)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.sequenceNr)
	if err != nil {
		return nil, err
	}

	err = binary.Read(r, binary.BigEndian, &hdr.length)
	if err != nil {
		return nil, err
	}

	return &hdr, nil
}

func writeHeader(w io.Writer, hdr *header) error {
	err := binary.Write(w, binary.BigEndian, hdr.objectID)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.commandID)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.sequenceNr)
	if err != nil {
		return err
	}

	err = binary.Write(w, binary.BigEndian, hdr.length)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) nextSequenceNr() uint32 {
	c.sequenceNr++
	return c.sequenceNr
}

func (c *Client) reader(r io.Reader, inbound chan reply) {
	buffer := make([]byte, 16)

	for {
		_, err := io.ReadFull(r, buffer)
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		header, err := parseHeader(bytes.NewBuffer(buffer))
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		payload := make([]byte, header.length)

		_, err = io.ReadFull(r, payload)
		if err != nil {
			inbound <- reply{err: err}
			break
		}

		inbound <- reply{sequenceNr: header.sequenceNr, payload: payload}
	}
}

func (c *Client) writer(w io.Writer, outbound chan message, errors chan error) {
writer:
	for {
		select {
		case msg := <-outbound:
			var payload bytes.Buffer

			err := serializeXmmsValue(&payload, msg.args)
			if err != nil {
				errors <- err
				break writer
			}

			msg.header.length = uint32(len(payload.Bytes()))

			err = writeHeader(w, &msg.header)
			if err != nil {
				errors <- err
				break writer
			}

			_, err = payload.WriteTo(w)
			if err != nil {
				errors <- err
				break writer
			}
		case <-c.shutdownIO:
			break writer
		}
	}
}

func (c *Client) shutdownRouter(registry map[uint32](context), err error) {
	// Reference command channel as it will be nullified
	channel := c.registry

	// Grab the RW-lock, close and nullify the command channel
	// reference which will allow the draining loop to exit.
	go func() {
		c.Lock()
		c.registry = nil
		close(channel)
		c.Unlock()
	}()

	// Terminate all active subscriptions
	for _, v := range registry {
		v.result <- reply{err: err}
	}

	// Drain trailing requests
	for msg := range channel {
		msg.result <- reply{err: io.EOF}
	}
}

func (c *Client) router(inbound chan reply, outbound chan message, errors chan error) {
	registry := make(map[uint32](context))

	for {
		select {
		case msg := <-c.registry:
			msg.header.sequenceNr = c.nextSequenceNr()
			registry[msg.header.sequenceNr] = context{
				msg.result,
				msg.broadcast,
			}
			outbound <- msg
		case reply := <-inbound:
			if reply.err != nil {
				c.shutdownRouter(registry, reply.err)
				return
			}

			ctx := registry[reply.sequenceNr]

			go func() {
				ctx.result <- reply
			}()

			if !ctx.broadcast {
				delete(registry, reply.sequenceNr)
			}
		case err := <-errors:
			c.shutdownRouter(registry, err)
			return
		case <-c.shutdownRegistry:
			c.shutdownRouter(registry, io.EOF)
			return
		}
	}
}

func (c *Client) dispatch(objectID uint32, commandID uint32, args XmmsValue) chan reply {
	c.RLock()
	defer c.RUnlock()

	result := make(chan reply, 1)
	if c.registry == nil {
		result <- reply{err: io.EOF}
	} else {
		c.registry <- message{
			header: header{
				objectID:  objectID,
				commandID: commandID,
			},
			broadcast: objectID == 0,
			args:      args,
			result:    result,
		}
	}
	return result
}

func (c *Client) sendHello() (int, error) {
	result := make(chan reply)

	c.registry <- message{
		header: header{
			objectID:  1,
			commandID: 32,
		},
		broadcast: false,
		args:      XmmsList{XmmsInt(IpcVersion), XmmsString(c.clientName)},
		result:    result,
	}

	reply := <-result
	if reply.err != nil {
		return -1, reply.err
	}

	buffer := bytes.NewBuffer(reply.payload)

	value, err := tryDeserialize(buffer)
	if err != nil {
		return -1, err
	}

	clientID, ok := value.(XmmsInt)
	if !ok {
		return -1, errors.New("Bad reply from server")
	}

	return int(clientID), nil
}

func (c *Client) Dial(url string) (int, error) {
	c.Lock()
	defer c.Unlock()

	addr, err := net.ResolveTCPAddr("tcp", url)
	if err != nil {
		return -1, err
	}

	conn, err := net.DialTCP("tcp", nil, addr)
	if err != nil {
		return -1, err
	}

	c.shutdownRegistry = make(chan bool)
	c.shutdownIO = make(chan bool)
	c.registry = make(chan message)

	errors := make(chan error)
	inbound := make(chan reply)
	outbound := make(chan message)

	go c.reader(conn, inbound)
	go c.writer(conn, outbound, errors)
	go c.router(inbound, outbound, errors)

	clientID, err := c.sendHello()
	if err != nil {
		c.Close()
		return -1, err
	}

	return clientID, nil
}

func (c *Client) Close() {
	c.shutdownRegistry <- true
	c.shutdownIO <- true
}

func NewClient(name string) *Client {
	client := Client{
		clientName: name,
	}

	return &client
}
<|endoftext|>


<commit_before>package sharings

import (
	"context"
	"errors"
	"net/url"
	"time"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/jobs"
	"github.com/cozy/cozy-stack/pkg/permissions"
)

func init() {
	jobs.AddWorker("sharingupdates", &jobs.WorkerConfig{
		Concurrency:  4,
		MaxExecCount: 3,
		Timeout:      10 * time.Second,
		WorkerFunc:   SharingUpdates,
	})
}

var (
	// ErrSharingIDNotUnique is used when several occurences of the same sharing id are found
	ErrSharingIDNotUnique = errors.New("Several sharings with this id found")
	// ErrSharingDoesNotExist is used when the given sharing does not exist.
	ErrSharingDoesNotExist = errors.New("Sharing does not exist")
	// ErrDocumentNotLegitimate is used when a shared document is triggered but
	// not legitimate for this sharing
	ErrDocumentNotLegitimate = errors.New("Triggered illegitimate shared document")
	//ErrRecipientDoesNotExist is used when the given recipient does not exist
	ErrRecipientDoesNotExist = errors.New("Recipient with given ID does not exist")
	// ErrRecipientHasNoURL is used to signal that a recipient has no URL.
	ErrRecipientHasNoURL = errors.New("Recipient has no URL")
)

// TriggerEvent describes the fields retrieved after a triggered event
type TriggerEvent struct {
	Event   *EventDoc       `json:"event"`
	Message *SharingMessage `json:"message"`
}

// EventDoc describes the event returned by the trigger
type EventDoc struct {
	Type string `json:"type"`
	Doc  *couchdb.JSONDoc
}

// SharingMessage describes a sharing message
type SharingMessage struct {
	SharingID string `json:"sharing_id"`
	DocType   string `json:"doctype"`
}

// Sharing describes the sharing document structure
type Sharing struct {
	SharingType      string             `json:"sharing_type"`
	Permissions      permissions.Set    `json:"permissions,omitempty"`
	RecipientsStatus []*RecipientStatus `json:"recipients,omitempty"`
}

// RecipientStatus contains the information about a recipient for a sharing
type RecipientStatus struct {
	Status       string               `json:"status,omitempty"`
	RefRecipient couchdb.DocReference `json:"recipient,omitempty"`
	AccessToken  *auth.AccessToken
}

// SharingUpdates handles shared document updates
func SharingUpdates(ctx context.Context, m *jobs.Message) error {
	domain := ctx.Value(jobs.ContextDomainKey).(string)

	event := &TriggerEvent{}
	err := m.Unmarshal(&event)
	if err != nil {
		return err
	}
	sharingID := event.Message.SharingID
	docType := event.Message.DocType
	docID := event.Event.Doc.M["_id"].(string)

	// Get the sharing document
	i, err := instance.Get(domain)
	if err != nil {
		return err
	}
	var res []Sharing
	err = couchdb.FindDocs(i, consts.Sharings, &couchdb.FindRequest{
		UseIndex: "by-sharing-id",
		Selector: mango.Equal("sharing_id", sharingID),
	}, &res)
	if err != nil {
		return err
	}
	if len(res) < 1 {
		return ErrSharingDoesNotExist
	} else if len(res) > 1 {
		return ErrSharingIDNotUnique
	}
	sharing := &res[0]

	// Check the updated document is legitimate for this sharing
	if err = checkDocument(sharing, docID); err != nil {
		return err
	}

	return sendToRecipients(i, domain, sharing, docType, docID)
}

// checkDocument checks the legitimity of the updated document to be shared
func checkDocument(sharing *Sharing, docID string) error {
	// Check sharing type
	if sharing.SharingType == consts.OneShotSharing {
		return ErrDocumentNotLegitimate
	}
	// Check permissions
	for _, rule := range sharing.Permissions {
		for _, val := range rule.Values {
			if val == docID {
				return nil
			}
		}
	}
	return ErrDocumentNotLegitimate
}

// sendToRecipients retreives the recipients and send the document
func sendToRecipients(db couchdb.Database, domain string, sharing *Sharing, docType, docID string) error {

	recInfos := make([]*RecipientInfo, len(sharing.RecipientsStatus))
	for i, rec := range sharing.RecipientsStatus {
		recDoc, err := GetRecipient(db, rec.RefRecipient.ID)
		if err != nil {
			return err
		}
		u, err := ExtractHost(recDoc.M["url"].(string))
		if err != nil {
			return err
		}
		info := &RecipientInfo{
			URL:   u,
			Token: rec.AccessToken.AccessToken,
		}
		recInfos[i] = info
	}
	opts := &SendOptions{
		DocID:      docID,
		DocType:    docType,
		Update:     true,
		Recipients: recInfos,
	}
	// TODO: handle file sharing
	if opts.DocType != consts.Files {
		return SendDoc(domain, opts)
	}
	return nil
}

// GetRecipient returns the Recipient stored in database from a given ID
func GetRecipient(db couchdb.Database, recID string) (*couchdb.JSONDoc, error) {
	doc := &couchdb.JSONDoc{}
	err := couchdb.GetDoc(db, consts.Recipients, recID, doc)
	if couchdb.IsNotFoundError(err) {
		err = ErrRecipientDoesNotExist
	}
	return doc, err
}

// ExtractHost returns the recipient's host, without the scheme
func ExtractHost(fullURL string) (string, error) {
	if fullURL == "" {
		return "", ErrRecipientHasNoURL
	}
	u, err := url.Parse(fullURL)
	if err != nil {
		return "", err
	}
	return u.Host, nil
}
<commit_msg>Add dynamic sharing support<commit_after>package sharings

import (
	"context"
	"errors"
	"net/url"
	"time"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/jobs"
	"github.com/cozy/cozy-stack/pkg/permissions"
)

func init() {
	jobs.AddWorker("sharingupdates", &jobs.WorkerConfig{
		Concurrency:  4,
		MaxExecCount: 3,
		Timeout:      10 * time.Second,
		WorkerFunc:   SharingUpdates,
	})
}

var (
	// ErrSharingIDNotUnique is used when several occurences of the same sharing id are found
	ErrSharingIDNotUnique = errors.New("Several sharings with this id found")
	// ErrSharingDoesNotExist is used when the given sharing does not exist.
	ErrSharingDoesNotExist = errors.New("Sharing does not exist")
	// ErrDocumentNotLegitimate is used when a shared document is triggered but
	// not legitimate for this sharing
	ErrDocumentNotLegitimate = errors.New("Triggered illegitimate shared document")
	//ErrRecipientDoesNotExist is used when the given recipient does not exist
	ErrRecipientDoesNotExist = errors.New("Recipient with given ID does not exist")
	// ErrRecipientHasNoURL is used to signal that a recipient has no URL.
	ErrRecipientHasNoURL = errors.New("Recipient has no URL")
)

// TriggerEvent describes the fields retrieved after a triggered event
type TriggerEvent struct {
	Event   *EventDoc       `json:"event"`
	Message *SharingMessage `json:"message"`
}

// EventDoc describes the event returned by the trigger
type EventDoc struct {
	Type string `json:"type"`
	Doc  *couchdb.JSONDoc
}

// SharingMessage describes a sharing message
type SharingMessage struct {
	SharingID string `json:"sharing_id"`
	DocType   string `json:"doctype"`
}

// Sharing describes the sharing document structure
type Sharing struct {
	SharingType      string             `json:"sharing_type"`
	Permissions      permissions.Set    `json:"permissions,omitempty"`
	RecipientsStatus []*RecipientStatus `json:"recipients,omitempty"`
}

// RecipientStatus contains the information about a recipient for a sharing
type RecipientStatus struct {
	Status       string               `json:"status,omitempty"`
	RefRecipient couchdb.DocReference `json:"recipient,omitempty"`
	AccessToken  *auth.AccessToken
}

// SharingUpdates handles shared document updates
func SharingUpdates(ctx context.Context, m *jobs.Message) error {
	domain := ctx.Value(jobs.ContextDomainKey).(string)

	event := &TriggerEvent{}
	err := m.Unmarshal(&event)
	if err != nil {
		return err
	}
	sharingID := event.Message.SharingID
	docType := event.Message.DocType
	docID := event.Event.Doc.M["_id"].(string)

	// Get the sharing document
	i, err := instance.Get(domain)
	if err != nil {
		return err
	}
	var res []Sharing
	err = couchdb.FindDocs(i, consts.Sharings, &couchdb.FindRequest{
		UseIndex: "by-sharing-id",
		Selector: mango.Equal("sharing_id", sharingID),
	}, &res)
	if err != nil {
		return err
	}
	if len(res) < 1 {
		return ErrSharingDoesNotExist
	} else if len(res) > 1 {
		return ErrSharingIDNotUnique
	}
	sharing := &res[0]

	// Check the updated document is legitimate for this sharing
	if err = checkDocument(sharing, docID); err != nil {
		return err
	}
	return sendToRecipients(i, domain, sharing, docType, docID, event.Event.Type)
}

// checkDocument checks the legitimity of the updated document to be shared
func checkDocument(sharing *Sharing, docID string) error {
	// Check sharing type
	if sharing.SharingType == consts.OneShotSharing {
		return ErrDocumentNotLegitimate
	}
	return nil
}

// sendToRecipients retreives the recipients and send the document
func sendToRecipients(db couchdb.Database, domain string, sharing *Sharing, docType, docID, eventType string) error {

	recInfos := make([]*RecipientInfo, len(sharing.RecipientsStatus))
	for i, rec := range sharing.RecipientsStatus {
		recDoc, err := GetRecipient(db, rec.RefRecipient.ID)
		if err != nil {
			return err
		}
		u, err := ExtractHost(recDoc.M["url"].(string))
		if err != nil {
			return err
		}
		info := &RecipientInfo{
			URL:   u,
			Token: rec.AccessToken.AccessToken,
		}
		recInfos[i] = info
	}
	isUpdate := (eventType == "UPDATED")
	opts := &SendOptions{
		DocID:      docID,
		DocType:    docType,
		Update:     isUpdate,
		Recipients: recInfos,
	}
	// TODO: handle file sharing
	if opts.DocType != consts.Files {
		return SendDoc(domain, opts)
	}
	return nil
}

// GetRecipient returns the Recipient stored in database from a given ID
func GetRecipient(db couchdb.Database, recID string) (*couchdb.JSONDoc, error) {
	doc := &couchdb.JSONDoc{}
	err := couchdb.GetDoc(db, consts.Recipients, recID, doc)
	if couchdb.IsNotFoundError(err) {
		err = ErrRecipientDoesNotExist
	}
	return doc, err
}

// ExtractHost returns the recipient's host, without the scheme
func ExtractHost(fullURL string) (string, error) {
	if fullURL == "" {
		return "", ErrRecipientHasNoURL
	}
	u, err := url.Parse(fullURL)
	if err != nil {
		return "", err
	}
	return u.Host, nil
}
<|endoftext|>


<commit_before>package dashboards

import (
	"strings"
	"time"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/guardian"
	"github.com/grafana/grafana/pkg/util"
)

// DashboardService service for operating on dashboards
type DashboardService interface {
	SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
	ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
}

// DashboardProvisioningService service for operating on provisioned dashboards
type DashboardProvisioningService interface {
	SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
	SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
	GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}

// NewService factory for creating a new dashboard service
var NewService = func() DashboardService {
	return &dashboardServiceImpl{
		log: log.New("dashboard-service"),
	}
}

// NewProvisioningService factory for creating a new dashboard provisioning service
var NewProvisioningService = func() DashboardProvisioningService {
	return &dashboardServiceImpl{}
}

type SaveDashboardDTO struct {
	OrgId     int64
	UpdatedAt time.Time
	User      *models.SignedInUser
	Message   string
	Overwrite bool
	Dashboard *models.Dashboard
}

type dashboardServiceImpl struct {
	orgId int64
	user  *models.SignedInUser
	log   log.Logger
}

func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
	cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
	err := bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
	dash := dto.Dashboard

	dash.Title = strings.TrimSpace(dash.Title)
	dash.Data.Set("title", dash.Title)
	dash.SetUid(strings.TrimSpace(dash.Uid))

	if dash.Title == "" {
		return nil, models.ErrDashboardTitleEmpty
	}

	if dash.IsFolder && dash.FolderId > 0 {
		return nil, models.ErrDashboardFolderCannotHaveParent
	}

	if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) {
		return nil, models.ErrDashboardFolderNameExists
	}

	if !util.IsValidShortUID(dash.Uid) {
		return nil, models.ErrDashboardInvalidUid
	} else if len(dash.Uid) > 40 {
		return nil, models.ErrDashboardUidToLong
	}

	if validateAlerts {
		validateAlertsCmd := models.ValidateDashboardAlertsCommand{
			OrgId:     dto.OrgId,
			Dashboard: dash,
			User:      dto.User,
		}

		if err := bus.Dispatch(&validateAlertsCmd); err != nil {
			return nil, err
		}
	}

	validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
		OrgId:     dto.OrgId,
		Dashboard: dash,
		Overwrite: dto.Overwrite,
	}

	if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
		return nil, err
	}

	if validateBeforeSaveCmd.Result.IsParentFolderChanged {
		folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
		if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
			if err != nil {
				return nil, err
			}
			return nil, models.ErrDashboardUpdateAccessDenied
		}
	}

	if validateProvisionedDashboard {
		isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
		err := bus.Dispatch(isDashboardProvisioned)

		if err != nil {
			return nil, err
		}

		if isDashboardProvisioned.Result {
			return nil, models.ErrDashboardCannotSaveProvisionedDashboard
		}
	}

	guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
	if canSave, err := guard.CanSave(); err != nil || !canSave {
		if err != nil {
			return nil, err
		}
		return nil, models.ErrDashboardUpdateAccessDenied
	}

	cmd := &models.SaveDashboardCommand{
		Dashboard: dash.Data,
		Message:   dto.Message,
		OrgId:     dto.OrgId,
		Overwrite: dto.Overwrite,
		UserId:    dto.User.UserId,
		FolderId:  dash.FolderId,
		IsFolder:  dash.IsFolder,
		PluginId:  dash.PluginId,
	}

	if !dto.UpdatedAt.IsZero() {
		cmd.UpdatedAt = dto.UpdatedAt
	}

	return cmd, nil
}

func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
	alertCmd := models.UpdateDashboardAlertsCommand{
		OrgId:     dto.OrgId,
		Dashboard: cmd.Result,
		User:      dto.User,
	}

	return bus.Dispatch(&alertCmd)
}

func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
	dto.User = &models.SignedInUser{
		UserId:  0,
		OrgRole: models.ROLE_ADMIN,
		OrgId:   dto.OrgId,
	}

	cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
	if err != nil {
		return nil, err
	}

	saveCmd := &models.SaveProvisionedDashboardCommand{
		DashboardCmd:          cmd,
		DashboardProvisioning: provisioning,
	}

	// dashboard
	err = bus.Dispatch(saveCmd)
	if err != nil {
		return nil, err
	}

	//alerts
	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	dto.User = &models.SignedInUser{
		UserId:  0,
		OrgRole: models.ROLE_ADMIN,
	}
	cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

type FakeDashboardService struct {
	SaveDashboardResult *models.Dashboard
	SaveDashboardError  error
	SavedDashboards     []*SaveDashboardDTO
}

func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	s.SavedDashboards = append(s.SavedDashboards, dto)

	if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
		s.SaveDashboardResult = dto.Dashboard
	}

	return s.SaveDashboardResult, s.SaveDashboardError
}

func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	return s.SaveDashboard(dto)
}

func MockDashboardService(mock *FakeDashboardService) {
	NewService = func() DashboardService {
		return mock
	}
}
<commit_msg>dashboards: user automatically becomes admin for created dashboards<commit_after>package dashboards

import (
	"strings"
	"time"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/guardian"
	"github.com/grafana/grafana/pkg/util"
)

// DashboardService service for operating on dashboards
type DashboardService interface {
	SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
	ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
}

// DashboardProvisioningService service for operating on provisioned dashboards
type DashboardProvisioningService interface {
	SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
	SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
	GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}

// NewService factory for creating a new dashboard service
var NewService = func() DashboardService {
	return &dashboardServiceImpl{
		log: log.New("dashboard-service"),
	}
}

// NewProvisioningService factory for creating a new dashboard provisioning service
var NewProvisioningService = func() DashboardProvisioningService {
	return &dashboardServiceImpl{}
}

type SaveDashboardDTO struct {
	OrgId     int64
	UpdatedAt time.Time
	User      *models.SignedInUser
	Message   string
	Overwrite bool
	Dashboard *models.Dashboard
}

type dashboardServiceImpl struct {
	orgId int64
	user  *models.SignedInUser
	log   log.Logger
}

func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
	cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
	err := bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
	dash := dto.Dashboard

	dash.Title = strings.TrimSpace(dash.Title)
	dash.Data.Set("title", dash.Title)
	dash.SetUid(strings.TrimSpace(dash.Uid))

	if dash.Title == "" {
		return nil, models.ErrDashboardTitleEmpty
	}

	if dash.IsFolder && dash.FolderId > 0 {
		return nil, models.ErrDashboardFolderCannotHaveParent
	}

	if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) {
		return nil, models.ErrDashboardFolderNameExists
	}

	if !util.IsValidShortUID(dash.Uid) {
		return nil, models.ErrDashboardInvalidUid
	} else if len(dash.Uid) > 40 {
		return nil, models.ErrDashboardUidToLong
	}

	if validateAlerts {
		validateAlertsCmd := models.ValidateDashboardAlertsCommand{
			OrgId:     dto.OrgId,
			Dashboard: dash,
			User:      dto.User,
		}

		if err := bus.Dispatch(&validateAlertsCmd); err != nil {
			return nil, err
		}
	}

	validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
		OrgId:     dto.OrgId,
		Dashboard: dash,
		Overwrite: dto.Overwrite,
	}

	if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
		return nil, err
	}

	if validateBeforeSaveCmd.Result.IsParentFolderChanged {
		folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
		if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
			if err != nil {
				return nil, err
			}
			return nil, models.ErrDashboardUpdateAccessDenied
		}
	}

	if validateProvisionedDashboard {
		isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
		err := bus.Dispatch(isDashboardProvisioned)

		if err != nil {
			return nil, err
		}

		if isDashboardProvisioned.Result {
			return nil, models.ErrDashboardCannotSaveProvisionedDashboard
		}
	}

	guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
	if canSave, err := guard.CanSave(); err != nil || !canSave {
		if err != nil {
			return nil, err
		}
		return nil, models.ErrDashboardUpdateAccessDenied
	}

	cmd := &models.SaveDashboardCommand{
		Dashboard: dash.Data,
		Message:   dto.Message,
		OrgId:     dto.OrgId,
		Overwrite: dto.Overwrite,
		UserId:    dto.User.UserId,
		FolderId:  dash.FolderId,
		IsFolder:  dash.IsFolder,
		PluginId:  dash.PluginId,
	}

	if !dto.UpdatedAt.IsZero() {
		cmd.UpdatedAt = dto.UpdatedAt
	}

	return cmd, nil
}

func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
	alertCmd := models.UpdateDashboardAlertsCommand{
		OrgId:     dto.OrgId,
		Dashboard: cmd.Result,
		User:      dto.User,
	}

	return bus.Dispatch(&alertCmd)
}

func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
	dto.User = &models.SignedInUser{
		UserId:  0,
		OrgRole: models.ROLE_ADMIN,
		OrgId:   dto.OrgId,
	}

	cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
	if err != nil {
		return nil, err
	}

	saveCmd := &models.SaveProvisionedDashboardCommand{
		DashboardCmd:          cmd,
		DashboardProvisioning: provisioning,
	}

	// dashboard
	err = bus.Dispatch(saveCmd)
	if err != nil {
		return nil, err
	}

	//alerts
	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	dto.User = &models.SignedInUser{
		UserId:  0,
		OrgRole: models.ROLE_ADMIN,
	}
	cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	err = dr.updateAlerting(cmd, dto)
	if err != nil {
		return nil, err
	}

	// TODO: check if dashboard exists already. could have id set but not exist
	if dto.Dashboard.Id == 0 && dto.Dashboard.Uid == "" {
		rtEditor := models.ROLE_EDITOR
		rtViewer := models.ROLE_VIEWER

		items := []*models.DashboardAcl{
			{
				OrgId:       dr.orgId,
				DashboardId: cmd.Result.Id,
				UserId:      cmd.Result.CreatedBy,
				Permission:  models.PERMISSION_ADMIN,
				Created:     time.Now(),
				Updated:     time.Now(),
			},
			{
				OrgId:       dr.orgId,
				DashboardId: cmd.Result.Id,
				Role:        &rtEditor,
				Permission:  models.PERMISSION_EDIT,
				Created:     time.Now(),
				Updated:     time.Now(),
			},
			{
				OrgId:       dr.orgId,
				DashboardId: cmd.Result.Id,
				Role:        &rtViewer,
				Permission:  models.PERMISSION_VIEW,
				Created:     time.Now(),
				Updated:     time.Now(),
			},
		}

		aclCmd := &models.UpdateDashboardAclCommand{
			DashboardId: cmd.Result.Id,
			Items:       items,
		}

		if err = bus.Dispatch(aclCmd); err != nil {
			return cmd.Result, err
		}

	}

	return cmd.Result, nil
}

func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
	if err != nil {
		return nil, err
	}

	err = bus.Dispatch(cmd)
	if err != nil {
		return nil, err
	}

	return cmd.Result, nil
}

type FakeDashboardService struct {
	SaveDashboardResult *models.Dashboard
	SaveDashboardError  error
	SavedDashboards     []*SaveDashboardDTO
}

func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	s.SavedDashboards = append(s.SavedDashboards, dto)

	if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
		s.SaveDashboardResult = dto.Dashboard
	}

	return s.SaveDashboardResult, s.SaveDashboardError
}

func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
	return s.SaveDashboard(dto)
}

func MockDashboardService(mock *FakeDashboardService) {
	NewService = func() DashboardService {
		return mock
	}
}
<|endoftext|>


<commit_before>// Copyright 2021 The Zombie Zen Log Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// SPDX-License-Identifier: BSD-3-Clause

// Package zstdlog provides functions to support interoperation between the
// standard library logger and zombiezen.com/go/log.
package zstdlog

import (
	"context"
	"fmt"
	stdlog "log"
	"strconv"
	"strings"
	"time"

	"zombiezen.com/go/log"
)

// Options is the set of optional arguments to New, SetOutput, and SetDefaultOutput.
type Options struct {
	// Context is used if non-nil when logging entries. Otherwise,
	// context.Background() is used.
	Context context.Context
	// Level is used for all created entries. Defaults to log.Info (the zero value).
	Level log.Level
}

// New returns a new standard library logger that writes to the given
// zombiezen.com/go/log logger.
func New(dst log.Logger, opts *Options) *stdlog.Logger {
	w := newWriter(dst, opts)
	return stdlog.New(w, "", stdlogFlags)
}

const stdlogFlags = stdlog.Ldate |
	stdlog.Ltime |
	stdlog.Lmicroseconds |
	stdlog.LUTC |
	stdlog.Llongfile

// SetOutput configures the standard library logger src to write to the given
// zombiezen.com/go/log.Logger dst. opts may be nil, in which case it is treated
// the same as if new(Options) were passed.
func SetOutput(dst log.Logger, src *stdlog.Logger, opts *Options) {
	src.SetFlags(stdlogFlags)
	src.SetPrefix("")
	src.SetOutput(newWriter(dst, opts))
}

// SetDefaultOutput configures the default standard library logger to write to
// the given zombiezen.com/go/log.Logger dst. opts may be nil, in which case it
// is treated the same as if new(Options) were passed.
func SetDefaultOutput(dst log.Logger, opts *Options) {
	setDefaultOutput(dst, opts)
}

type writer struct {
	ctx   context.Context
	level log.Level
	dst   log.Logger
}

func newWriter(dst log.Logger, opts *Options) *writer {
	w := &writer{
		ctx: context.Background(),
		dst: dst,
	}
	if opts != nil {
		if opts.Context != nil {
			w.ctx = opts.Context
		}
		w.level = opts.Level
	}
	return w
}

func (w *writer) Write(p []byte) (int, error) {
	const layout = "2006/01/02 15:04:05.999999 "
	ps := string(p)
	var ent log.Entry
	var err error
	ent.Time, err = time.Parse(layout, ps[:len(layout)])
	if err != nil {
		return 0, fmt.Errorf("log entry %q: invalid format: %v", p, err)
	}
	ent.Time = ent.Time.Local()

	const msgSeparator = ": "
	fileLineEnd := strings.Index(ps[len(layout):], msgSeparator)
	if fileLineEnd == -1 {
		return 0, fmt.Errorf("log entry %q: invalid format", p)
	}
	fileLineEnd += len(layout)
	fileLine := ps[len(layout):fileLineEnd]
	if fileEnd := strings.LastIndex(fileLine, ":"); fileEnd == -1 {
		ent.File = fileLine
	} else {
		ent.File = fileLine[:fileEnd]
		ent.Line, err = strconv.Atoi(fileLine[fileEnd+len(":"):])
		if err != nil {
			return 0, fmt.Errorf("log entry %q: invalid format: %v", p, err)
		}
	}

	ent.Level = w.level
	ent.Msg = strings.TrimSuffix(ps[fileLineEnd+len(msgSeparator):], "\n")
	w.dst.Log(w.ctx, ent)
	return len(p), nil
}
<commit_msg>zstdlog: handle unknown files<commit_after>// Copyright 2021 The Zombie Zen Log Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// SPDX-License-Identifier: BSD-3-Clause

// Package zstdlog provides functions to support interoperation between the
// standard library logger and zombiezen.com/go/log.
package zstdlog

import (
	"context"
	"fmt"
	stdlog "log"
	"strconv"
	"strings"
	"time"

	"zombiezen.com/go/log"
)

// Options is the set of optional arguments to New, SetOutput, and SetDefaultOutput.
type Options struct {
	// Context is used if non-nil when logging entries. Otherwise,
	// context.Background() is used.
	Context context.Context
	// Level is used for all created entries. Defaults to log.Info (the zero value).
	Level log.Level
}

// New returns a new standard library logger that writes to the given
// zombiezen.com/go/log logger.
func New(dst log.Logger, opts *Options) *stdlog.Logger {
	w := newWriter(dst, opts)
	return stdlog.New(w, "", stdlogFlags)
}

const stdlogFlags = stdlog.Ldate |
	stdlog.Ltime |
	stdlog.Lmicroseconds |
	stdlog.LUTC |
	stdlog.Llongfile

// SetOutput configures the standard library logger src to write to the given
// zombiezen.com/go/log.Logger dst. opts may be nil, in which case it is treated
// the same as if new(Options) were passed.
func SetOutput(dst log.Logger, src *stdlog.Logger, opts *Options) {
	src.SetFlags(stdlogFlags)
	src.SetPrefix("")
	src.SetOutput(newWriter(dst, opts))
}

// SetDefaultOutput configures the default standard library logger to write to
// the given zombiezen.com/go/log.Logger dst. opts may be nil, in which case it
// is treated the same as if new(Options) were passed.
func SetDefaultOutput(dst log.Logger, opts *Options) {
	setDefaultOutput(dst, opts)
}

type writer struct {
	ctx   context.Context
	level log.Level
	dst   log.Logger
}

func newWriter(dst log.Logger, opts *Options) *writer {
	w := &writer{
		ctx: context.Background(),
		dst: dst,
	}
	if opts != nil {
		if opts.Context != nil {
			w.ctx = opts.Context
		}
		w.level = opts.Level
	}
	return w
}

func (w *writer) Write(p []byte) (int, error) {
	const layout = "2006/01/02 15:04:05.999999 "
	ps := string(p)
	var ent log.Entry
	var err error
	ent.Time, err = time.Parse(layout, ps[:len(layout)])
	if err != nil {
		return 0, fmt.Errorf("log entry %q: invalid format: %v", p, err)
	}
	ent.Time = ent.Time.Local()

	const msgSeparator = ": "
	fileLineEnd := strings.Index(ps[len(layout):], msgSeparator)
	if fileLineEnd == -1 {
		return 0, fmt.Errorf("log entry %q: invalid format", p)
	}
	fileLineEnd += len(layout)
	fileLine := ps[len(layout):fileLineEnd]
	if fileEnd := strings.LastIndex(fileLine, ":"); fileEnd == -1 {
		ent.File = fileLine
	} else {
		ent.File = fileLine[:fileEnd]
		ent.Line, err = strconv.Atoi(fileLine[fileEnd+len(":"):])
		if err != nil {
			return 0, fmt.Errorf("log entry %q: invalid format: %v", p, err)
		}
	}
	if ent.File == "???" {
		ent.File = ""
		ent.Line = 0
	}

	ent.Level = w.level
	ent.Msg = strings.TrimSuffix(ps[fileLineEnd+len(msgSeparator):], "\n")
	w.dst.Log(w.ctx, ent)
	return len(p), nil
}
<|endoftext|>


<commit_before>// +build !wasm

package reader

import (
	"context"
	"fmt"
	"time"

	"github.com/Jeffail/benthos/v3/lib/log"
	"github.com/Jeffail/benthos/v3/lib/message"
	"github.com/Jeffail/benthos/v3/lib/message/batch"
	"github.com/Jeffail/benthos/v3/lib/metrics"
	"github.com/Jeffail/benthos/v3/lib/types"
	sess "github.com/Jeffail/benthos/v3/lib/util/aws/session"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/patrobinson/gokini"
)

//------------------------------------------------------------------------------

// KinesisBalancedConfig is configuration values for the input type.
type KinesisBalancedConfig struct {
	sess.Config           `json:",inline" yaml:",inline"`
	Stream                string `json:"stream" yaml:"stream"`
	DynamoDBTable         string `json:"dynamodb_table" yaml:"dynamodb_table"`
	DynamoDBBillingMode   string `json:"dynamodb_billing_mode" yaml:"dynamodb_billing_mode"`
	DynamoDBReadCapacity  int64  `json:"dynamodb_read_provision" yaml:"dynamodb_read_provision"`
	DynamoDBWriteCapacity int64  `json:"dynamodb_write_provision" yaml:"dynamodb_write_provision"`
	// TODO: V4 Remove this.
	MaxBatchCount   int                `json:"max_batch_count" yaml:"max_batch_count"`
	Batching        batch.PolicyConfig `json:"batching" yaml:"batching"`
	StartFromOldest bool               `json:"start_from_oldest" yaml:"start_from_oldest"`
}

// NewKinesisBalancedConfig creates a new Config with default values.
func NewKinesisBalancedConfig() KinesisBalancedConfig {
	batchConf := batch.NewPolicyConfig()
	batchConf.Count = 1
	s := sess.NewConfig()
	return KinesisBalancedConfig{
		Config:                s,
		Stream:                "",
		DynamoDBTable:         "",
		DynamoDBBillingMode:   "",
		DynamoDBReadCapacity:  0,
		DynamoDBWriteCapacity: 0,
		MaxBatchCount:         1,
		Batching:              batchConf,
		StartFromOldest:       true,
	}
}

//------------------------------------------------------------------------------

// KinesisBalanced is a benthos reader.Type implementation that reads messages
// from an Amazon Kinesis stream.
type KinesisBalanced struct {
	conf KinesisBalancedConfig

	session *session.Session

	lastSequence *string
	namespace    string

	log     log.Modular
	stats   metrics.Type
	kc      *gokini.KinesisConsumer
	records chan *gokini.Records
	shardID string
}

// NewKinesisBalanced creates a new Amazon Kinesis stream reader.Type.
func NewKinesisBalanced(
	conf KinesisBalancedConfig,
	log log.Modular,
	stats metrics.Type,
) (*KinesisBalanced, error) {
	records := make(chan *gokini.Records)
	consumer := &KinesisBalanced{
		conf:    conf,
		log:     log,
		stats:   stats,
		records: records,
	}
	sess, err := conf.GetSession()
	if err != nil {
		return nil, err
	}
	kc := &gokini.KinesisConsumer{
		StreamName:                  conf.Stream,
		ShardIteratorType:           "TRIM_HORIZON",
		RecordConsumer:              consumer,
		TableName:                   conf.DynamoDBTable,
		EmptyRecordBackoffMs:        1000,
		DisableAutomaticCheckpoints: true,
		Session:                     sess,
	}
	if !consumer.conf.StartFromOldest {
		kc.ShardIteratorType = "LATEST"
	}
	if consumer.conf.DynamoDBBillingMode != "" {
		kc.DynamoBillingMode = &consumer.conf.DynamoDBBillingMode
	}
	if consumer.conf.DynamoDBReadCapacity != 0 && consumer.conf.DynamoDBWriteCapacity != 0 {
		kc.DynamoReadCapacityUnits = &consumer.conf.DynamoDBReadCapacity
		kc.DynamoWriteCapacityUnits = &consumer.conf.DynamoDBWriteCapacity
	}

	consumer.kc = kc
	return consumer, nil
}

// Connect attempts to establish a connection to the target Kinesis stream.
func (k *KinesisBalanced) Connect() error {
	return k.ConnectWithContext(context.Background())
}

// ConnectWithContext attempts to establish a connection to the target Kinesis
// stream.
func (k *KinesisBalanced) ConnectWithContext(ctx context.Context) error {
	err := k.kc.StartConsumer()

	k.log.Infof("Receiving Amazon Kinesis messages from stream: %v\n", k.conf.Stream)
	return err
}

func (k *KinesisBalanced) setMetadata(record *gokini.Records, p types.Part) {
	met := p.Metadata()
	met.Set("kinesis_shard", k.shardID)
	met.Set("kinesis_partition_key", record.PartitionKey)
	met.Set("kinesis_sequence_number", record.SequenceNumber)
}

// ReadWithContext attempts to read a new message from the target Kinesis
// stream.
func (k *KinesisBalanced) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) {
	var record *gokini.Records
	select {
	case record = <-k.records:
	case <-ctx.Done():
		return nil, nil, types.ErrTimeout
	}
	if record == nil {
		return nil, nil, fmt.Errorf("shard '%s' has closed", k.shardID)
	}

	part := message.NewPart(record.Data)
	k.setMetadata(record, part)

	msg := message.New(nil)
	msg.Append(part)

	return msg, func(rctx context.Context, res types.Response) error {
		return k.kc.Checkpoint(k.shardID, record.SequenceNumber)
	}, nil
}

// Read attempts to read a new message from the target Kinesis stream.
func (k *KinesisBalanced) Read() (types.Message, error) {
	msg := message.New(nil)

	record := <-k.records
	if record == nil {
		return nil, fmt.Errorf("shard '%s' has closed", k.shardID)
	}
	k.lastSequence = &record.SequenceNumber
	{
		part := message.NewPart(record.Data)
		k.setMetadata(record, part)
		msg.Append(part)
	}

batchLoop:
	for i := 1; i < k.conf.MaxBatchCount; i++ {
		select {
		case record := <-k.records:
			if record != nil {
				k.lastSequence = &record.SequenceNumber
				part := message.NewPart(record.Data)
				k.setMetadata(record, part)
				msg.Append(part)
			} else {
				break batchLoop
			}
		default:
			// Drained the buffer
			break batchLoop
		}
	}

	return msg, nil
}

// Acknowledge confirms whether or not our unacknowledged messages have been
// successfully propagated or not.
func (k *KinesisBalanced) Acknowledge(err error) error {
	if err == nil && k.lastSequence != nil {
		return k.kc.Checkpoint(k.shardID, *k.lastSequence)
	}
	return nil
}

// CloseAsync begins cleaning up resources used by this reader asynchronously.
func (k *KinesisBalanced) CloseAsync() {
	go k.kc.Shutdown()
}

// WaitForClose will block until either the reader is closed or a specified
// timeout occurs.
func (k *KinesisBalanced) WaitForClose(time.Duration) error {
	return nil
}

// Init is required by the KinesisConsumer interface
func (k *KinesisBalanced) Init(shardID string) error {
	k.shardID = shardID
	return nil
}

// ProcessRecords implements the KinesisConsumer interface
func (k *KinesisBalanced) ProcessRecords(records []*gokini.Records, _ *gokini.KinesisConsumer) {
	for _, record := range records {
		k.records <- record
	}
}

// Shutdown implements the KinesisConsumer interface
func (k *KinesisBalanced) Shutdown() {
	k.log.Infof("Stopping processing of Stream %s Shard %s", k.conf.Stream, k.shardID)
	close(k.records)
}

//------------------------------------------------------------------------------
<commit_msg>Ensure we checkpoint the right shard (#425)<commit_after>// +build !wasm

package reader

import (
	"context"
	"time"

	"github.com/Jeffail/benthos/v3/lib/log"
	"github.com/Jeffail/benthos/v3/lib/message"
	"github.com/Jeffail/benthos/v3/lib/message/batch"
	"github.com/Jeffail/benthos/v3/lib/metrics"
	"github.com/Jeffail/benthos/v3/lib/types"
	sess "github.com/Jeffail/benthos/v3/lib/util/aws/session"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/patrobinson/gokini"
)

//------------------------------------------------------------------------------

// KinesisBalancedConfig is configuration values for the input type.
type KinesisBalancedConfig struct {
	sess.Config           `json:",inline" yaml:",inline"`
	Stream                string `json:"stream" yaml:"stream"`
	DynamoDBTable         string `json:"dynamodb_table" yaml:"dynamodb_table"`
	DynamoDBBillingMode   string `json:"dynamodb_billing_mode" yaml:"dynamodb_billing_mode"`
	DynamoDBReadCapacity  int64  `json:"dynamodb_read_provision" yaml:"dynamodb_read_provision"`
	DynamoDBWriteCapacity int64  `json:"dynamodb_write_provision" yaml:"dynamodb_write_provision"`
	// TODO: V4 Remove this.
	MaxBatchCount   int                `json:"max_batch_count" yaml:"max_batch_count"`
	Batching        batch.PolicyConfig `json:"batching" yaml:"batching"`
	StartFromOldest bool               `json:"start_from_oldest" yaml:"start_from_oldest"`
}

// NewKinesisBalancedConfig creates a new Config with default values.
func NewKinesisBalancedConfig() KinesisBalancedConfig {
	batchConf := batch.NewPolicyConfig()
	batchConf.Count = 1
	s := sess.NewConfig()
	return KinesisBalancedConfig{
		Config:                s,
		Stream:                "",
		DynamoDBTable:         "",
		DynamoDBBillingMode:   "",
		DynamoDBReadCapacity:  0,
		DynamoDBWriteCapacity: 0,
		MaxBatchCount:         1,
		Batching:              batchConf,
		StartFromOldest:       true,
	}
}

//------------------------------------------------------------------------------

// KinesisBalanced is a benthos reader.Type implementation that reads messages
// from an Amazon Kinesis stream.
type KinesisBalanced struct {
	conf KinesisBalancedConfig

	session *session.Session

	lastSequences map[string]*string
	namespace     string

	log     log.Modular
	stats   metrics.Type
	kc      *gokini.KinesisConsumer
	records chan *gokini.Records
	shardID string
}

// NewKinesisBalanced creates a new Amazon Kinesis stream reader.Type.
func NewKinesisBalanced(
	conf KinesisBalancedConfig,
	log log.Modular,
	stats metrics.Type,
) (*KinesisBalanced, error) {
	records := make(chan *gokini.Records)
	consumer := &KinesisBalanced{
		conf:    conf,
		log:     log,
		stats:   stats,
		records: records,
	}
	sess, err := conf.GetSession()
	if err != nil {
		return nil, err
	}
	kc := &gokini.KinesisConsumer{
		StreamName:                  conf.Stream,
		ShardIteratorType:           "TRIM_HORIZON",
		RecordConsumer:              consumer,
		TableName:                   conf.DynamoDBTable,
		EmptyRecordBackoffMs:        1000,
		DisableAutomaticCheckpoints: true,
		Session:                     sess,
	}
	if !consumer.conf.StartFromOldest {
		kc.ShardIteratorType = "LATEST"
	}
	if consumer.conf.DynamoDBBillingMode != "" {
		kc.DynamoBillingMode = &consumer.conf.DynamoDBBillingMode
	}
	if consumer.conf.DynamoDBReadCapacity != 0 && consumer.conf.DynamoDBWriteCapacity != 0 {
		kc.DynamoReadCapacityUnits = &consumer.conf.DynamoDBReadCapacity
		kc.DynamoWriteCapacityUnits = &consumer.conf.DynamoDBWriteCapacity
	}

	consumer.kc = kc
	return consumer, nil
}

// Connect attempts to establish a connection to the target Kinesis stream.
func (k *KinesisBalanced) Connect() error {
	return k.ConnectWithContext(context.Background())
}

// ConnectWithContext attempts to establish a connection to the target Kinesis
// stream.
func (k *KinesisBalanced) ConnectWithContext(ctx context.Context) error {
	err := k.kc.StartConsumer()

	k.log.Infof("Receiving Amazon Kinesis messages from stream: %v\n", k.conf.Stream)
	return err
}

func (k *KinesisBalanced) setMetadata(record *gokini.Records, p types.Part) {
	met := p.Metadata()
	met.Set("kinesis_shard", k.shardID)
	met.Set("kinesis_partition_key", record.PartitionKey)
	met.Set("kinesis_sequence_number", record.SequenceNumber)
}

// ReadWithContext attempts to read a new message from the target Kinesis
// stream.
func (k *KinesisBalanced) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) {
	var record *gokini.Records
	select {
	case record = <-k.records:
	case <-ctx.Done():
		return nil, nil, types.ErrTimeout
	}
	if record == nil {
		return nil, nil, types.ErrTimeout
	}

	part := message.NewPart(record.Data)
	k.setMetadata(record, part)

	msg := message.New(nil)
	msg.Append(part)

	return msg, func(rctx context.Context, res types.Response) error {
		return k.kc.Checkpoint(record.ShardID, record.SequenceNumber)
	}, nil
}

// Read attempts to read a new message from the target Kinesis stream.
func (k *KinesisBalanced) Read() (types.Message, error) {
	msg := message.New(nil)

	record := <-k.records
	if record == nil {
		return nil, types.ErrTimeout
	}
	k.lastSequences[record.ShardID] = &record.SequenceNumber
	{
		part := message.NewPart(record.Data)
		k.setMetadata(record, part)
		msg.Append(part)
	}

batchLoop:
	for i := 1; i < k.conf.MaxBatchCount; i++ {
		select {
		case record := <-k.records:
			if record != nil {
				k.lastSequences[record.ShardID] = &record.SequenceNumber
				part := message.NewPart(record.Data)
				k.setMetadata(record, part)
				msg.Append(part)
			} else {
				break batchLoop
			}
		default:
			// Drained the buffer
			break batchLoop
		}
	}

	return msg, nil
}

// Acknowledge confirms whether or not our unacknowledged messages have been
// successfully propagated or not.
func (k *KinesisBalanced) Acknowledge(err error) error {
	if err == nil && k.lastSequences != nil {
		for shard, sequence := range k.lastSequences {
			err := k.kc.Checkpoint(shard, *sequence)
			if err != nil {
				return err
			}
			delete(k.lastSequences, shard)
		}
	}
	return nil
}

// CloseAsync begins cleaning up resources used by this reader asynchronously.
func (k *KinesisBalanced) CloseAsync() {
	go k.kc.Shutdown()
}

// WaitForClose will block until either the reader is closed or a specified
// timeout occurs.
func (k *KinesisBalanced) WaitForClose(time.Duration) error {
	return nil
}

// Init is required by the KinesisConsumer interface
func (k *KinesisBalanced) Init(shardID string) error {
	return nil
}

// ProcessRecords implements the KinesisConsumer interface
func (k *KinesisBalanced) ProcessRecords(records []*gokini.Records, _ *gokini.KinesisConsumer) {
	for _, record := range records {
		k.records <- record
	}
}

// Shutdown implements the KinesisConsumer interface
func (k *KinesisBalanced) Shutdown() {
	k.log.Infof("Stopping processing of Stream %s Shard %s", k.conf.Stream, k.shardID)
	close(k.records)
}

//------------------------------------------------------------------------------
<|endoftext|>


<commit_before>package models

import (
	"fmt"
	"github.com/coopernurse/gorp"
	"github.com/revel/revel"
	"strings"
	"regexp"
)

type Profile struct {
	ProfileId          int
	UserId             int
	UserName           string
	Name               string
	Summary            string
	Description        string
	PhotoUrl           string
	AggregateFollowers int
	AggregateFollowing int

	// Transient
	User               *User
}

var UserNameRegex = regexp.MustCompile("^[a-zA-Z0-9]+$")

var UserNameBlacklistRegex = regexp.MustCompile("^(.*)?(account|contact|about|public|log(in|out)|sign(in|up|out)|register|home|index|default|post(s)?)$")

func (p *Profile) String() string {
	return fmt.Sprintf("Profile(%s)", p.Summary)
}

func (profile *Profile) Validate(v *revel.Validation) {
	ValidateProfileName(v, profile.Name)
	ValidateProfileSummary(v, profile.Summary)
	ValidateProfileDescription(v, profile.Description)
	ValidateProfilePhotoUrl(v, profile.PhotoUrl)
}

func ValidateProfileUserName(v *revel.Validation, username string) *revel.ValidationResult {
	result := v.Required(username).Message("User name required")
	if !result.Ok {
		return result
	}

	result = v.MaxSize(username, 64).Message("User name can not exceed 64 characters")
	if !result.Ok {
		return result
	}

	result = v.Match(username, UserNameRegex).Message("Invalid User name. Alphanumerics allowed only")
	if !result.Ok {
		return result
	}

	// Inverse regexp matcher
	if blacklistMatcher := UserNameBlacklistRegex.FindString(username); blacklistMatcher != "" {
		result = v.Error("Invalid User name. Reserved keywords not allowed")
	}

	return result
}

func ValidateProfileName(v *revel.Validation, name string) *revel.ValidationResult {
	result := v.Required(name).Message("Name required")
	if !result.Ok {
		return result
	}

	result = v.MinSize(name, 6).Message("Name must be at least 6 characters")
	if !result.Ok {
		return result
	}

	result = v.MaxSize(name, 100).Message("Name must be at most 100 characters")

	return result
}

func ValidateProfileSummary(v *revel.Validation, summary string) *revel.ValidationResult {
	result := v.MaxSize(summary, 140).Message("Profile summary cannot exceed 140 characters")

	return result
}

func ValidateProfileDescription(v *revel.Validation, description string) *revel.ValidationResult {
	result := v.MaxSize(description, 400).Message("Profile description cannot exceed 400 characters")

	return result
}

func ValidateProfilePhotoUrl(v *revel.Validation, photoUrl string) *revel.ValidationResult {
	result := v.MaxSize(photoUrl, 200).Message("Photo URL cannot exceed 200 characters")

	return result
}

func (p *Profile) PreInsert(_ gorp.SqlExecutor) error {
	p.UserName = strings.ToLower(p.UserName)
	return nil
}

func (p *Profile) PreUpdate(_ gorp.SqlExecutor) error {
	p.UserName = strings.ToLower(p.UserName)
	return nil
}

func (p *Profile) PostGet(exe gorp.SqlExecutor) error {
	var (
		obj interface{}
		err error
	)

	obj, err = exe.Get(User{}, p.UserId)
	if err != nil {
		return fmt.Errorf("Error loading a profile's user (%d): %s", p.UserId, err)
	}
	p.User = obj.(*User)

	/*obj, err = exe.Get(Post{}, p.ProfileId)
	if err != nil {
		return fmt.Errorf("Error loading a profile's posts (%d): %s", p.ProfileId, err)
	}
	var posts []*Post
	for _, post := range obj {
		posts = append(posts, post.(*Post))
	}
	p.Posts = posts*/

	return nil
}
<commit_msg>Add more usernames to blacklist<commit_after>package models

import (
	"fmt"
	"github.com/coopernurse/gorp"
	"github.com/revel/revel"
	"strings"
	"regexp"
)

type Profile struct {
	ProfileId          int
	UserId             int
	UserName           string
	Name               string
	Summary            string
	Description        string
	PhotoUrl           string
	AggregateFollowers int
	AggregateFollowing int

	// Transient
	User               *User
}

var UserNameRegex = regexp.MustCompile("^[a-zA-Z0-9]+$")

var UserNameBlacklistRegex = regexp.MustCompile("^(.*)?(account|contact|about|public|log(in|out)|sign(in|up|out)|register|home|index|default|post(s)?|user(name)?(s)?)$")

func (p *Profile) String() string {
	return fmt.Sprintf("Profile(%s)", p.Summary)
}

func (profile *Profile) Validate(v *revel.Validation) {
	ValidateProfileName(v, profile.Name)
	ValidateProfileSummary(v, profile.Summary)
	ValidateProfileDescription(v, profile.Description)
	ValidateProfilePhotoUrl(v, profile.PhotoUrl)
}

func ValidateProfileUserName(v *revel.Validation, username string) *revel.ValidationResult {
	result := v.Required(username).Message("User name required")
	if !result.Ok {
		return result
	}

	result = v.MaxSize(username, 64).Message("User name can not exceed 64 characters")
	if !result.Ok {
		return result
	}

	result = v.Match(username, UserNameRegex).Message("Invalid User name. Alphanumerics allowed only")
	if !result.Ok {
		return result
	}

	// Inverse regexp matcher
	if blacklistMatcher := UserNameBlacklistRegex.FindString(username); blacklistMatcher != "" {
		result = v.Error("Invalid User name. Reserved keywords not allowed")
	}

	return result
}

func ValidateProfileName(v *revel.Validation, name string) *revel.ValidationResult {
	result := v.Required(name).Message("Name required")
	if !result.Ok {
		return result
	}

	result = v.MinSize(name, 6).Message("Name must be at least 6 characters")
	if !result.Ok {
		return result
	}

	result = v.MaxSize(name, 100).Message("Name must be at most 100 characters")

	return result
}

func ValidateProfileSummary(v *revel.Validation, summary string) *revel.ValidationResult {
	result := v.MaxSize(summary, 140).Message("Profile summary cannot exceed 140 characters")

	return result
}

func ValidateProfileDescription(v *revel.Validation, description string) *revel.ValidationResult {
	result := v.MaxSize(description, 400).Message("Profile description cannot exceed 400 characters")

	return result
}

func ValidateProfilePhotoUrl(v *revel.Validation, photoUrl string) *revel.ValidationResult {
	result := v.MaxSize(photoUrl, 200).Message("Photo URL cannot exceed 200 characters")

	return result
}

func (p *Profile) PreInsert(_ gorp.SqlExecutor) error {
	p.UserName = strings.ToLower(p.UserName)
	return nil
}

func (p *Profile) PreUpdate(_ gorp.SqlExecutor) error {
	p.UserName = strings.ToLower(p.UserName)
	return nil
}

func (p *Profile) PostGet(exe gorp.SqlExecutor) error {
	var (
		obj interface{}
		err error
	)

	obj, err = exe.Get(User{}, p.UserId)
	if err != nil {
		return fmt.Errorf("Error loading a profile's user (%d): %s", p.UserId, err)
	}
	p.User = obj.(*User)

	/*obj, err = exe.Get(Post{}, p.ProfileId)
	if err != nil {
		return fmt.Errorf("Error loading a profile's posts (%d): %s", p.ProfileId, err)
	}
	var posts []*Post
	for _, post := range obj {
		posts = append(posts, post.(*Post))
	}
	p.Posts = posts*/

	return nil
}
<|endoftext|>


<commit_before>package btelegram

import (
	"html"
	"regexp"
	"strconv"
	"strings"

	"github.com/42wim/matterbridge/bridge/config"
	"github.com/42wim/matterbridge/bridge/helper"
	"github.com/go-telegram-bot-api/telegram-bot-api"
)

func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
	// handle channels
	if posted != nil {
		message = posted
		rmsg.Text = message.Text
	}

	// edited channel message
	if edited != nil && !b.GetBool("EditDisable") {
		message = edited
		rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
	}
	return message
}

// handleChannels checks if it's a channel message and if the message is a new or edited messages
func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
	return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
}

// handleGroups checks if it's a group message and if the message is a new or edited messages
func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
	return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
}

// handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
	if message.ForwardFrom != nil {
		usernameForward := ""
		if b.GetBool("UseFirstName") {
			usernameForward = message.ForwardFrom.FirstName
		}
		if usernameForward == "" {
			usernameForward = message.ForwardFrom.UserName
			if usernameForward == "" {
				usernameForward = message.ForwardFrom.FirstName
			}
		}
		if usernameForward == "" {
			usernameForward = unknownUser
		}
		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
	}
}

// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
	if message.ReplyToMessage != nil {
		usernameReply := ""
		if message.ReplyToMessage.From != nil {
			if b.GetBool("UseFirstName") {
				usernameReply = message.ReplyToMessage.From.FirstName
			}
			if usernameReply == "" {
				usernameReply = message.ReplyToMessage.From.UserName
				if usernameReply == "" {
					usernameReply = message.ReplyToMessage.From.FirstName
				}
			}
		}
		if usernameReply == "" {
			usernameReply = unknownUser
		}
		if !b.GetBool("QuoteDisable") {
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
		}
	}
}

// handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
	if message.From != nil {
		rmsg.UserID = strconv.Itoa(message.From.ID)
		if b.GetBool("UseFirstName") {
			rmsg.Username = message.From.FirstName
		}
		if rmsg.Username == "" {
			rmsg.Username = message.From.UserName
			if rmsg.Username == "" {
				rmsg.Username = message.From.FirstName
			}
		}
		// only download avatars if we have a place to upload them (configured mediaserver)
		if b.General.MediaServerUpload != "" {
			b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
		}
	}

	// if we really didn't find a username, set it to unknown
	if rmsg.Username == "" {
		rmsg.Username = unknownUser
	}
}

func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
	for update := range updates {
		b.Log.Debugf("== Receiving event: %#v", update.Message)

		if update.Message == nil && update.ChannelPost == nil &&
			update.EditedMessage == nil && update.EditedChannelPost == nil {
			b.Log.Error("Getting nil messages, this shouldn't happen.")
			continue
		}

		var message *tgbotapi.Message

		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}

		// handle channels
		message = b.handleChannels(&rmsg, message, update)

		// handle groups
		message = b.handleGroups(&rmsg, message, update)

		if message == nil {
			b.Log.Error("message is nil, this shouldn't happen.")
			continue
		}

		// set the ID's from the channel or group message
		rmsg.ID = strconv.Itoa(message.MessageID)
		rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)

		// handle username
		b.handleUsername(&rmsg, message)

		// handle any downloads
		err := b.handleDownload(&rmsg, message)
		if err != nil {
			b.Log.Errorf("download failed: %s", err)
		}

		// handle forwarded messages
		b.handleForwarded(&rmsg, message)

		// quote the previous message
		b.handleQuoting(&rmsg, message)

		// handle entities (adding URLs)
		b.handleEntities(&rmsg, message)

		if rmsg.Text != "" || len(rmsg.Extra) > 0 {
			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
			// channels don't have (always?) user information. see #410
			if message.From != nil {
				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
			}

			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
			b.Log.Debugf("<= Message is %#v", rmsg)
			b.Remote <- rmsg
		}
	}
}

// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
	rmsg := config.Message{Username: "system",
		Text:    "avatar",
		Channel: channel,
		Account: b.Account,
		UserID:  strconv.Itoa(userid),
		Event:   config.EventAvatarDownload,
		Extra:   make(map[string][]interface{})}

	if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
		if err != nil {
			b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
		}

		if len(photos.Photos) > 0 {
			photo := photos.Photos[0][0]
			url := b.getFileDirectURL(photo.FileID)
			name := strconv.Itoa(userid) + ".png"
			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)

			err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
			if err != nil {
				b.Log.Error(err)
				return
			}
			data, err := helper.DownloadFile(url)
			if err != nil {
				b.Log.Errorf("download %s failed %#v", url, err)
				return
			}
			helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
			b.Remote <- rmsg
		}
	}
}

// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
	size := 0
	var url, name, text string
	switch {
	case message.Sticker != nil:
		text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
		size = message.Sticker.FileSize
	case message.Voice != nil:
		text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
		size = message.Voice.FileSize
	case message.Video != nil:
		text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
		size = message.Video.FileSize
	case message.Audio != nil:
		text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
		size = message.Audio.FileSize
	case message.Document != nil:
		_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
		size = message.Document.FileSize
		name = message.Document.FileName
		text = " " + message.Document.FileName + " : " + url
	case message.Photo != nil:
		photos := *message.Photo
		size = photos[len(photos)-1].FileSize
		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
	}

	// if name is empty we didn't match a thing to download
	if name == "" {
		return nil
	}
	// use the URL instead of native upload
	if b.GetBool("UseInsecureURL") {
		b.Log.Debugf("Setting message text to :%s", text)
		rmsg.Text += text
		return nil
	}
	// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
	err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
	if err != nil {
		return err
	}
	data, err := helper.DownloadFile(url)
	if err != nil {
		return err
	}
	if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
		b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
		err := helper.ConvertWebPToPNG(data)
		if err != nil {
			b.Log.Errorf("conversion failed: %s", err)
		} else {
			name = strings.Replace(name, ".webp", ".png", 1)
		}
	}
	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
	return nil
}

func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
	url := b.getFileDirectURL(id)
	name := ""
	if urlpart {
		urlPart := strings.Split(url, "/")
		name = urlPart[len(urlPart)-1]
	}
	if suffix != "" && !strings.HasSuffix(name, suffix) {
		name += suffix
	}
	text := " " + url
	return text, name, url
}

// handleDelete handles message deleting
func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
	if msg.ID == "" {
		return "", nil
	}
	msgid, err := strconv.Atoi(msg.ID)
	if err != nil {
		return "", err
	}
	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
	return "", err
}

// handleEdit handles message editing.
func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
	msgid, err := strconv.Atoi(msg.ID)
	if err != nil {
		return "", err
	}
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
		b.Log.Debug("Using mode HTML - nick only")
		msg.Text = html.EscapeString(msg.Text)
	}
	m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
	switch b.GetString("MessageFormat") {
	case HTMLFormat:
		b.Log.Debug("Using mode HTML")
		m.ParseMode = tgbotapi.ModeHTML
	case "Markdown":
		b.Log.Debug("Using mode markdown")
		m.ParseMode = tgbotapi.ModeMarkdown
	}
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
		b.Log.Debug("Using mode HTML - nick only")
		m.ParseMode = tgbotapi.ModeHTML
	}
	_, err = b.c.Send(m)
	if err != nil {
		return "", err
	}
	return "", nil
}

// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
	var c tgbotapi.Chattable
	for _, f := range msg.Extra["file"] {
		fi := f.(config.FileInfo)
		file := tgbotapi.FileBytes{
			Name:  fi.Name,
			Bytes: *fi.Data,
		}
		re := regexp.MustCompile(".(jpg|png)$")
		if re.MatchString(fi.Name) {
			c = tgbotapi.NewPhotoUpload(chatid, file)
		} else {
			c = tgbotapi.NewDocumentUpload(chatid, file)
		}
		_, err := b.c.Send(c)
		if err != nil {
			b.Log.Errorf("file upload failed: %#v", err)
		}
		if fi.Comment != "" {
			if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
				b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
			}
		}
	}
	return ""
}

func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
	format := b.GetString("quoteformat")
	if format == "" {
		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
	}
	format = strings.Replace(format, "{MESSAGE}", message, -1)
	format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
	format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
	return format
}

// handleEntities handles messageEntities
func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
	if message.Entities == nil {
		return
	}
	// for now only do URL replacements
	for _, e := range *message.Entities {
		if e.Type == "text_link" {
			url, err := e.ParseURL()
			if err != nil {
				b.Log.Errorf("entity text_link url parse failed: %s", err)
				continue
			}
			link := rmsg.Text[e.Offset : e.Offset+e.Length]
			rmsg.Text = strings.Replace(rmsg.Text, link, url.String(), 1)
		}
	}
}
<commit_msg>Fix panic by checking slice bounds in handleEntities (telegram). Fixes #857 (#858)<commit_after>package btelegram

import (
	"html"
	"regexp"
	"strconv"
	"strings"
	"unicode/utf16"

	"github.com/42wim/matterbridge/bridge/config"
	"github.com/42wim/matterbridge/bridge/helper"
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
	// handle channels
	if posted != nil {
		message = posted
		rmsg.Text = message.Text
	}

	// edited channel message
	if edited != nil && !b.GetBool("EditDisable") {
		message = edited
		rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
	}
	return message
}

// handleChannels checks if it's a channel message and if the message is a new or edited messages
func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
	return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
}

// handleGroups checks if it's a group message and if the message is a new or edited messages
func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
	return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
}

// handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
	if message.ForwardFrom != nil {
		usernameForward := ""
		if b.GetBool("UseFirstName") {
			usernameForward = message.ForwardFrom.FirstName
		}
		if usernameForward == "" {
			usernameForward = message.ForwardFrom.UserName
			if usernameForward == "" {
				usernameForward = message.ForwardFrom.FirstName
			}
		}
		if usernameForward == "" {
			usernameForward = unknownUser
		}
		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
	}
}

// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
	if message.ReplyToMessage != nil {
		usernameReply := ""
		if message.ReplyToMessage.From != nil {
			if b.GetBool("UseFirstName") {
				usernameReply = message.ReplyToMessage.From.FirstName
			}
			if usernameReply == "" {
				usernameReply = message.ReplyToMessage.From.UserName
				if usernameReply == "" {
					usernameReply = message.ReplyToMessage.From.FirstName
				}
			}
		}
		if usernameReply == "" {
			usernameReply = unknownUser
		}
		if !b.GetBool("QuoteDisable") {
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
		}
	}
}

// handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
	if message.From != nil {
		rmsg.UserID = strconv.Itoa(message.From.ID)
		if b.GetBool("UseFirstName") {
			rmsg.Username = message.From.FirstName
		}
		if rmsg.Username == "" {
			rmsg.Username = message.From.UserName
			if rmsg.Username == "" {
				rmsg.Username = message.From.FirstName
			}
		}
		// only download avatars if we have a place to upload them (configured mediaserver)
		if b.General.MediaServerUpload != "" {
			b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
		}
	}

	// if we really didn't find a username, set it to unknown
	if rmsg.Username == "" {
		rmsg.Username = unknownUser
	}
}

func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
	for update := range updates {
		b.Log.Debugf("== Receiving event: %#v", update.Message)

		if update.Message == nil && update.ChannelPost == nil &&
			update.EditedMessage == nil && update.EditedChannelPost == nil {
			b.Log.Error("Getting nil messages, this shouldn't happen.")
			continue
		}

		var message *tgbotapi.Message

		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}

		// handle channels
		message = b.handleChannels(&rmsg, message, update)

		// handle groups
		message = b.handleGroups(&rmsg, message, update)

		if message == nil {
			b.Log.Error("message is nil, this shouldn't happen.")
			continue
		}

		// set the ID's from the channel or group message
		rmsg.ID = strconv.Itoa(message.MessageID)
		rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)

		// handle username
		b.handleUsername(&rmsg, message)

		// handle any downloads
		err := b.handleDownload(&rmsg, message)
		if err != nil {
			b.Log.Errorf("download failed: %s", err)
		}

		// handle forwarded messages
		b.handleForwarded(&rmsg, message)

		// quote the previous message
		b.handleQuoting(&rmsg, message)

		// handle entities (adding URLs)
		b.handleEntities(&rmsg, message)

		if rmsg.Text != "" || len(rmsg.Extra) > 0 {
			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
			// channels don't have (always?) user information. see #410
			if message.From != nil {
				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
			}

			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
			b.Log.Debugf("<= Message is %#v", rmsg)
			b.Remote <- rmsg
		}
	}
}

// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
	rmsg := config.Message{Username: "system",
		Text:    "avatar",
		Channel: channel,
		Account: b.Account,
		UserID:  strconv.Itoa(userid),
		Event:   config.EventAvatarDownload,
		Extra:   make(map[string][]interface{})}

	if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
		if err != nil {
			b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
		}

		if len(photos.Photos) > 0 {
			photo := photos.Photos[0][0]
			url := b.getFileDirectURL(photo.FileID)
			name := strconv.Itoa(userid) + ".png"
			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)

			err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
			if err != nil {
				b.Log.Error(err)
				return
			}
			data, err := helper.DownloadFile(url)
			if err != nil {
				b.Log.Errorf("download %s failed %#v", url, err)
				return
			}
			helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
			b.Remote <- rmsg
		}
	}
}

// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
	size := 0
	var url, name, text string
	switch {
	case message.Sticker != nil:
		text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
		size = message.Sticker.FileSize
	case message.Voice != nil:
		text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
		size = message.Voice.FileSize
	case message.Video != nil:
		text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
		size = message.Video.FileSize
	case message.Audio != nil:
		text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
		size = message.Audio.FileSize
	case message.Document != nil:
		_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
		size = message.Document.FileSize
		name = message.Document.FileName
		text = " " + message.Document.FileName + " : " + url
	case message.Photo != nil:
		photos := *message.Photo
		size = photos[len(photos)-1].FileSize
		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
	}

	// if name is empty we didn't match a thing to download
	if name == "" {
		return nil
	}
	// use the URL instead of native upload
	if b.GetBool("UseInsecureURL") {
		b.Log.Debugf("Setting message text to :%s", text)
		rmsg.Text += text
		return nil
	}
	// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
	err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
	if err != nil {
		return err
	}
	data, err := helper.DownloadFile(url)
	if err != nil {
		return err
	}
	if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
		b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
		err := helper.ConvertWebPToPNG(data)
		if err != nil {
			b.Log.Errorf("conversion failed: %s", err)
		} else {
			name = strings.Replace(name, ".webp", ".png", 1)
		}
	}
	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
	return nil
}

func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
	url := b.getFileDirectURL(id)
	name := ""
	if urlpart {
		urlPart := strings.Split(url, "/")
		name = urlPart[len(urlPart)-1]
	}
	if suffix != "" && !strings.HasSuffix(name, suffix) {
		name += suffix
	}
	text := " " + url
	return text, name, url
}

// handleDelete handles message deleting
func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
	if msg.ID == "" {
		return "", nil
	}
	msgid, err := strconv.Atoi(msg.ID)
	if err != nil {
		return "", err
	}
	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
	return "", err
}

// handleEdit handles message editing.
func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
	msgid, err := strconv.Atoi(msg.ID)
	if err != nil {
		return "", err
	}
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
		b.Log.Debug("Using mode HTML - nick only")
		msg.Text = html.EscapeString(msg.Text)
	}
	m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
	switch b.GetString("MessageFormat") {
	case HTMLFormat:
		b.Log.Debug("Using mode HTML")
		m.ParseMode = tgbotapi.ModeHTML
	case "Markdown":
		b.Log.Debug("Using mode markdown")
		m.ParseMode = tgbotapi.ModeMarkdown
	}
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
		b.Log.Debug("Using mode HTML - nick only")
		m.ParseMode = tgbotapi.ModeHTML
	}
	_, err = b.c.Send(m)
	if err != nil {
		return "", err
	}
	return "", nil
}

// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
	var c tgbotapi.Chattable
	for _, f := range msg.Extra["file"] {
		fi := f.(config.FileInfo)
		file := tgbotapi.FileBytes{
			Name:  fi.Name,
			Bytes: *fi.Data,
		}
		re := regexp.MustCompile(".(jpg|png)$")
		if re.MatchString(fi.Name) {
			c = tgbotapi.NewPhotoUpload(chatid, file)
		} else {
			c = tgbotapi.NewDocumentUpload(chatid, file)
		}
		_, err := b.c.Send(c)
		if err != nil {
			b.Log.Errorf("file upload failed: %#v", err)
		}
		if fi.Comment != "" {
			if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
				b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
			}
		}
	}
	return ""
}

func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
	format := b.GetString("quoteformat")
	if format == "" {
		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
	}
	format = strings.Replace(format, "{MESSAGE}", message, -1)
	format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
	format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
	return format
}

// handleEntities handles messageEntities
func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
	if message.Entities == nil {
		return
	}
	// for now only do URL replacements
	for _, e := range *message.Entities {
		if e.Type == "text_link" {
			url, err := e.ParseURL()
			if err != nil {
				b.Log.Errorf("entity text_link url parse failed: %s", err)
				continue
			}
			utfEncodedString := utf16.Encode([]rune(rmsg.Text))
			if e.Offset+e.Length > len(utfEncodedString) {
				b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
				continue
			}
			link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
			rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
		}
	}
}
<|endoftext|>