<commit_before>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package workflow

import (
	"errors"
	"fmt"
	"sort"
	"time"

	uuidpkg "github.com/pborman/uuid"
	"github.com/venicegeo/pz-gocommon/elasticsearch"
)

type WorkflowIDResponse struct {
	ID Ident `json:"id"`
}

type Ident string

const NoIdent Ident = ""

func (id Ident) String() string {
	return string(id)
}

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

// expresses the idea of "this ES query returns an event"
// Query is specific to the event type
type Condition struct {
	EventTypeIds []Ident                `json:"eventTypeIds" binding:"required"`
	Query        map[string]interface{} `json:"query" binding:"required"`
}

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

// Job JSON struct
type Job struct {
	Username string                 `json:"userName" binding:"required"`
	JobType  map[string]interface{} `json:"jobType" binding:"required"`
}

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

// when the and'ed set of Conditions all are true, do Something
// Events are the results of the Conditions queries
// Job is the JobMessage to submit back to Pz
type Trigger struct {
	TriggerId     Ident     `json:"triggerId"`
	Title         string    `json:"title" binding:"required"`
	Condition     Condition `json:"condition" binding:"required"`
	Job           Job       `json:"job" binding:"required"`
	PercolationId Ident     `json:"percolationId"`
}

type TriggerList []Trigger

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

// posted by some source (service, user, etc) to indicate Something Happened
// Data is specific to the event type
type Event struct {
	EventId     Ident                  `json:"eventId"`
	EventTypeId Ident                  `json:"eventTypeId" binding:"required"`
	CreatedOn   time.Time              `json:"createdOn" binding:"required"`
	Data        map[string]interface{} `json:"data"`
}

type EventList []Event

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

type EventType struct {
	EventTypeId Ident                                           `json:"eventTypeId"`
	Name        string                                          `json:"name" binding:"required"`
	Mapping     map[string]elasticsearch.MappingElementTypeName `json:"mapping" binding:"required"`
}

type EventTypeList []EventType

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

// a notification, automatically created when an Trigger happens
type Alert struct {
	AlertId   Ident     `json:"alertId"`
	TriggerId Ident     `json:"triggerId"`
	EventId   Ident     `json:"eventId"`
	JobId     Ident     `json:"jobId"`
	CreatedOn time.Time `json:"createdOn"`
}

type AlertList []Alert

type AlertListById []Alert

func (a AlertListById) Len() int {
	return len(a)
}
func (a AlertListById) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}
func (a AlertListById) Less(i, j int) bool {
	return a[i].AlertId < a[j].AlertId
}

func (list AlertList) ToSortedArray() []Alert {
	array := make([]Alert, len(list))
	i := 0
	for _, v := range list {
		array[i] = v
		i++
	}
	sort.Sort(AlertListById(array))
	return array
}

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

type WorkflowAdminStats struct {
	CreatedOn     time.Time `json:"createdOn"`
	NumAlerts     int       `json:"numAlerts"`
	NumConditions int       `json:"numConditions"`
	NumEvents     int       `json:"numEvents"`
	NumTriggers   int       `json:"numTriggers"`
}

type WorkflowAdminSettings struct {
	Debug bool `json:"debug"`
}

func LoggedError(mssg string, args ...interface{}) error {
	str := fmt.Sprintf(mssg, args)
	//log.Printf(str)
	return errors.New(str)
}

// Checks to see if the Uuid is valid
func isUuid(uuid string) bool {
	check := uuidpkg.Parse(uuid)
	return check != nil
}
<commit_msg>CreatedOn not required, as supplied by system<commit_after>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package workflow

import (
	"errors"
	"fmt"
	"sort"
	"time"

	uuidpkg "github.com/pborman/uuid"
	"github.com/venicegeo/pz-gocommon/elasticsearch"
)

type WorkflowIDResponse struct {
	ID Ident `json:"id"`
}

type Ident string

const NoIdent Ident = ""

func (id Ident) String() string {
	return string(id)
}

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

// expresses the idea of "this ES query returns an event"
// Query is specific to the event type
type Condition struct {
	EventTypeIds []Ident                `json:"eventTypeIds" binding:"required"`
	Query        map[string]interface{} `json:"query" binding:"required"`
}

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

// Job JSON struct
type Job struct {
	Username string                 `json:"userName" binding:"required"`
	JobType  map[string]interface{} `json:"jobType" binding:"required"`
}

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

// when the and'ed set of Conditions all are true, do Something
// Events are the results of the Conditions queries
// Job is the JobMessage to submit back to Pz
type Trigger struct {
	TriggerId     Ident     `json:"triggerId"`
	Title         string    `json:"title" binding:"required"`
	Condition     Condition `json:"condition" binding:"required"`
	Job           Job       `json:"job" binding:"required"`
	PercolationId Ident     `json:"percolationId"`
}

type TriggerList []Trigger

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

// posted by some source (service, user, etc) to indicate Something Happened
// Data is specific to the event type
type Event struct {
	EventId     Ident                  `json:"eventId"`
	EventTypeId Ident                  `json:"eventTypeId" binding:"required"`
	CreatedOn   time.Time              `json:"createdOn"`
	Data        map[string]interface{} `json:"data"`
}

type EventList []Event

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

type EventType struct {
	EventTypeId Ident                                           `json:"eventTypeId"`
	Name        string                                          `json:"name" binding:"required"`
	Mapping     map[string]elasticsearch.MappingElementTypeName `json:"mapping" binding:"required"`
}

type EventTypeList []EventType

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

// a notification, automatically created when an Trigger happens
type Alert struct {
	AlertId   Ident     `json:"alertId"`
	TriggerId Ident     `json:"triggerId"`
	EventId   Ident     `json:"eventId"`
	JobId     Ident     `json:"jobId"`
	CreatedOn time.Time `json:"createdOn"`
}

type AlertList []Alert

type AlertListById []Alert

func (a AlertListById) Len() int {
	return len(a)
}
func (a AlertListById) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}
func (a AlertListById) Less(i, j int) bool {
	return a[i].AlertId < a[j].AlertId
}

func (list AlertList) ToSortedArray() []Alert {
	array := make([]Alert, len(list))
	i := 0
	for _, v := range list {
		array[i] = v
		i++
	}
	sort.Sort(AlertListById(array))
	return array
}

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

type WorkflowAdminStats struct {
	CreatedOn     time.Time `json:"createdOn"`
	NumAlerts     int       `json:"numAlerts"`
	NumConditions int       `json:"numConditions"`
	NumEvents     int       `json:"numEvents"`
	NumTriggers   int       `json:"numTriggers"`
}

type WorkflowAdminSettings struct {
	Debug bool `json:"debug"`
}

func LoggedError(mssg string, args ...interface{}) error {
	str := fmt.Sprintf(mssg, args)
	//log.Printf(str)
	return errors.New(str)
}

// Checks to see if the Uuid is valid
func isUuid(uuid string) bool {
	check := uuidpkg.Parse(uuid)
	return check != nil
}
<|endoftext|>


<commit_before>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package workflow

import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"

	uuidpkg "github.com/pborman/uuid"
	"github.com/venicegeo/pz-gocommon/elasticsearch"
	"github.com/venicegeo/pz-gocommon/gocommon"
)

//-TRIGGER----------------------------------------------------------------------

// TriggerDBMapping is the name of the Elasticsearch type to which Triggers are added
const TriggerDBMapping string = "Trigger"

// TriggerIndexSettings is the mapping for the "trigger" index in Elasticsearch
const TriggerIndexSettings = `
{
	"mappings": {
		"Trigger": {
			"properties": {
				"triggerId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"title": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"enabled": {
					"type": "boolean",
					"index": "not_analyzed"
				},
				"condition": {
					"properties": {
						"eventTypeIds": {
							"type": "string",
							"index": "not_analyzed"
						},
						"query": {
							"dynamic": true,
							"properties": {}
						}
					}
				},
				"job": {
					"properties": {
						"createdBy": {
							"type": "string",
							"index": "not_analyzed"
						},
						"jobType": {
							"dynamic": true,
							"properties": {}
						}
					}
				},
				"percolationId": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

// Condition expresses the idea of "this ES query returns an event"
// Query is specific to the event type
type Condition struct {
	EventTypeIds []piazza.Ident         `json:"eventTypeIds" binding:"required"`
	Query        map[string]interface{} `json:"query" binding:"required"`
}

type JobRequest struct {
	CreatedBy string  `json:"createdBy"`
	JobType   JobType `json:"jobType" binding:"required"`
}

type JobType struct {
	Data map[string]interface{} `json:"data" binding:"required"`
	Type string                 `json:"type" binding:"required"`
}

// Trigger does something when the and'ed set of Conditions all are true
// Events are the results of the Conditions queries
// Job is the JobMessage to submit back to Pz
type Trigger struct {
	TriggerId     piazza.Ident `json:"triggerId"`
	Name          string       `json:"name" binding:"required"`
	Condition     Condition    `json:"condition" binding:"required"`
	Job           JobRequest   `json:"job" binding:"required"`
	PercolationId piazza.Ident `json:"percolationId"`
	CreatedBy     string       `json:"createdBy"`
	CreatedOn     time.Time    `json:"createdOn"`
	Enabled       bool         `json:"enabled"`
}

// TriggerList is a list of triggers
type TriggerList []Trigger

//-EVENT------------------------------------------------------------------------

// EventIndexSettings is the mapping for the "events" index in Elasticsearch
const EventIndexSettings = `
{
	"settings": {
		"index.mapping.coerce": false
	},
	"mappings": {
		"_default_": {
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"data": {
					"properties": {}
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"cronSchedule": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

// An Event is posted by some source (service, user, etc) to indicate Something Happened
// Data is specific to the event type
type Event struct {
	EventId      piazza.Ident           `json:"eventId"`
	EventTypeId  piazza.Ident           `json:"eventTypeId" binding:"required"`
	Data         map[string]interface{} `json:"data"`
	CreatedBy    string                 `json:"createdBy"`
	CreatedOn    time.Time              `json:"createdOn"`
	CronSchedule string                 `json:"cronSchedule"`
}

// EventList is a list of events
type EventList []Event

//-EVENTTYPE--------------------------------------------------------------------

// EventTypeDBMapping is the name of the Elasticsearch type to which Events are added
const EventTypeDBMapping string = "EventType"

// EventTypeIndexSettings is the mapping for the "eventtypes" index in Elasticsearch
const EventTypeIndexSettings = `
{
	"mappings": {
		"EventType": {
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"name": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"mapping": {
					"dynamic": true,
					"properties": {}
				}
			}
		}
	}
}
`

// EventType describes an Event that is to be sent to workflow by a client or service
type EventType struct {
	EventTypeId piazza.Ident                                    `json:"eventTypeId"`
	Name        string                                          `json:"name" binding:"required"`
	Mapping     map[string]elasticsearch.MappingElementTypeName `json:"mapping" binding:"required"`
	CreatedBy   string                                          `json:"createdBy"`
	CreatedOn   time.Time                                       `json:"createdOn"`
}

// EventTypeList is a list of EventTypes
type EventTypeList []EventType

//-ALERT------------------------------------------------------------------------

// AlertDBMapping is the name of the Elasticsearch type to which Alerts are added
const AlertDBMapping string = "Alert"

// AlertIndexSettings are the default settings for our Elasticsearch alerts index
// Explanation:
//   "index": "not_analyzed"
//     This means that these properties are not analyzed by Elasticsearch.
//     Previously, these ids were analyzed by ES and thus broken up into chunks;
//     in the case of a UUID this would happen via break-up by the "-" character.
//     For example, the UUID "ab3142cd-1a8e-44f8-6a01-5ce8a9328fb2" would be broken
//     into "ab3142cd", "1a8e", "44f8", "6a01" and "5ce8a9328fb2", and queries would
//     match on all of these separate strings, which was undesired behavior.
const AlertIndexSettings = `
{
	"mappings": {
		"Alert": {
			"properties": {
				"alertId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"triggerId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"jobId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				}
			}
		}
	}
}
`

// Alert is a notification, automatically created when a Trigger happens
type Alert struct {
	AlertId   piazza.Ident `json:"alertId"`
	TriggerId piazza.Ident `json:"triggerId"`
	EventId   piazza.Ident `json:"eventId"`
	JobId     piazza.Ident `json:"jobId"`
	CreatedBy string       `json:"createdBy"`
	CreatedOn time.Time    `json:"createdOn"`
}

//-CRON-------------------------------------------------------------------------

const CronIndexSettings = `
{
	"settings": {
		"index.mapping.coerce": false
	},
	"mappings": {
		"Cron": {
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"data": {
					"properties": {}
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"cronSchedule": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

const cronDBMapping = "Cron"

//-- Stats ------------------------------------------------------------

type workflowStats struct {
	sync.Mutex
	CreatedOn        time.Time `json:"createdOn"`
	NumEventTypes    int       `json:"numEventTypes"`
	NumEvents        int       `json:"numEvents"`
	NumTriggers      int       `json:"numTriggers"`
	NumAlerts        int       `json:"numAlerts"`
	NumTriggeredJobs int       `json:"numTriggeredJobs"`
}

func (stats *workflowStats) incrCounter(counter *int) {
	stats.Lock()
	*counter++
	stats.Unlock()
}

func (stats *workflowStats) IncrEventTypes() {
	stats.incrCounter(&stats.NumEventTypes)
}

func (stats *workflowStats) IncrEvents() {
	stats.incrCounter(&stats.NumEvents)
}

func (stats *workflowStats) IncrTriggers() {
	stats.incrCounter(&stats.NumTriggers)
}

func (stats *workflowStats) IncrAlerts() {
	stats.incrCounter(&stats.NumAlerts)
}

func (stats *workflowStats) IncrTriggerJobs() {
	stats.incrCounter(&stats.NumTriggeredJobs)
}

//-UTILITY----------------------------------------------------------------------

// LoggedError logs the error's message and creates an error
func LoggedError(mssg string, args ...interface{}) error {
	str := fmt.Sprintf(mssg, args)
	log.Printf(str)
	return errors.New(str)
}

// isUUID checks to see if the UUID is valid
func isUUID(uuid string) bool {
	return uuidpkg.Parse(uuid) != nil
}

//-INIT-------------------------------------------------------------------------

func init() {
	piazza.JsonResponseDataTypes["*workflow.EventType"] = "eventtype"
	piazza.JsonResponseDataTypes["[]workflow.EventType"] = "eventtype-list"
	piazza.JsonResponseDataTypes["*workflow.Event"] = "event"
	piazza.JsonResponseDataTypes["[]workflow.Event"] = "event-list"
	piazza.JsonResponseDataTypes["*workflow.Trigger"] = "trigger"
	piazza.JsonResponseDataTypes["[]workflow.Trigger"] = "trigger-list"
	piazza.JsonResponseDataTypes["*workflow.Alert"] = "alert"
	piazza.JsonResponseDataTypes["[]workflow.Alert"] = "alert-list"
	piazza.JsonResponseDataTypes["workflow.workflowStats"] = "workflowstats"
}
<commit_msg>update<commit_after>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package workflow

import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"

	uuidpkg "github.com/pborman/uuid"
	"github.com/venicegeo/pz-gocommon/elasticsearch"
	"github.com/venicegeo/pz-gocommon/gocommon"
)

//-TRIGGER----------------------------------------------------------------------

// TriggerDBMapping is the name of the Elasticsearch type to which Triggers are added
const TriggerDBMapping string = "Trigger"

// TriggerIndexSettings is the mapping for the "trigger" index in Elasticsearch
const TriggerIndexSettings = `
{
	"mappings": {
		"Trigger": {
			"properties": {
				"triggerId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"title": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"enabled": {
					"type": "boolean",
					"index": "not_analyzed"
				},
				"condition": {
					"properties": {
						"eventTypeIds": {
							"type": "string",
							"index": "not_analyzed"
						},
						"query": {
							"dynamic": true,
							"properties": {}
						}
					}
				},
				"job": {
					"properties": {
						"createdBy": {
							"type": "string",
							"index": "not_analyzed"
						},
						"jobType": {
							"dynamic": true,
							"properties": {}
						}
					}
				},
				"percolationId": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

// Condition expresses the idea of "this ES query returns an event"
// Query is specific to the event type
type Condition struct {
	EventTypeIds []piazza.Ident         `json:"eventTypeIds" binding:"required"`
	Query        map[string]interface{} `json:"query" binding:"required"`
}

type JobRequest struct {
	CreatedBy string  `json:"createdBy"`
	JobType   JobType `json:"jobType" binding:"required"`
}

type JobType struct {
	Data map[string]interface{} `json:"data" binding:"required"`
	Type string                 `json:"type" binding:"required"`
}

// Trigger does something when the and'ed set of Conditions all are true
// Events are the results of the Conditions queries
// Job is the JobMessage to submit back to Pz
type Trigger struct {
	TriggerId     piazza.Ident `json:"triggerId"`
	Name          string       `json:"name" binding:"required"`
	Condition     Condition    `json:"condition" binding:"required"`
	Job           JobRequest   `json:"job" binding:"required"`
	PercolationId piazza.Ident `json:"percolationId"`
	CreatedBy     string       `json:"createdBy"`
	CreatedOn     time.Time    `json:"createdOn"`
	Enabled       bool         `json:"enabled"`
}

// TriggerList is a list of triggers
type TriggerList []Trigger

//-EVENT------------------------------------------------------------------------

// EventIndexSettings is the mapping for the "events" index in Elasticsearch
const EventIndexSettings = `
{
	"settings": {
		"index.mapping.coerce": false
	},
	"mappings": {
		"_default_": {
			"dynamic": "false",
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"data": {
					"type": "nested"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"cronSchedule": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

// An Event is posted by some source (service, user, etc) to indicate Something Happened
// Data is specific to the event type
type Event struct {
	EventId      piazza.Ident           `json:"eventId"`
	EventTypeId  piazza.Ident           `json:"eventTypeId" binding:"required"`
	Data         map[string]interface{} `json:"data"`
	CreatedBy    string                 `json:"createdBy"`
	CreatedOn    time.Time              `json:"createdOn"`
	CronSchedule string                 `json:"cronSchedule"`
}

// EventList is a list of events
type EventList []Event

//-EVENTTYPE--------------------------------------------------------------------

// EventTypeDBMapping is the name of the Elasticsearch type to which Events are added
const EventTypeDBMapping string = "EventType"

// EventTypeIndexSettings is the mapping for the "eventtypes" index in Elasticsearch
const EventTypeIndexSettings = `
{
	"mappings": {
		"EventType": {
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"name": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"mapping": {
					"dynamic": true,
					"properties": {}
				}
			}
		}
	}
}
`

// EventType describes an Event that is to be sent to workflow by a client or service
type EventType struct {
	EventTypeId piazza.Ident                                    `json:"eventTypeId"`
	Name        string                                          `json:"name" binding:"required"`
	Mapping     map[string]elasticsearch.MappingElementTypeName `json:"mapping" binding:"required"`
	CreatedBy   string                                          `json:"createdBy"`
	CreatedOn   time.Time                                       `json:"createdOn"`
}

// EventTypeList is a list of EventTypes
type EventTypeList []EventType

//-ALERT------------------------------------------------------------------------

// AlertDBMapping is the name of the Elasticsearch type to which Alerts are added
const AlertDBMapping string = "Alert"

// AlertIndexSettings are the default settings for our Elasticsearch alerts index
// Explanation:
//   "index": "not_analyzed"
//     This means that these properties are not analyzed by Elasticsearch.
//     Previously, these ids were analyzed by ES and thus broken up into chunks;
//     in the case of a UUID this would happen via break-up by the "-" character.
//     For example, the UUID "ab3142cd-1a8e-44f8-6a01-5ce8a9328fb2" would be broken
//     into "ab3142cd", "1a8e", "44f8", "6a01" and "5ce8a9328fb2", and queries would
//     match on all of these separate strings, which was undesired behavior.
const AlertIndexSettings = `
{
	"mappings": {
		"Alert": {
			"properties": {
				"alertId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"triggerId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"jobId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				}
			}
		}
	}
}
`

// Alert is a notification, automatically created when a Trigger happens
type Alert struct {
	AlertId   piazza.Ident `json:"alertId"`
	TriggerId piazza.Ident `json:"triggerId"`
	EventId   piazza.Ident `json:"eventId"`
	JobId     piazza.Ident `json:"jobId"`
	CreatedBy string       `json:"createdBy"`
	CreatedOn time.Time    `json:"createdOn"`
}

//-CRON-------------------------------------------------------------------------

const CronIndexSettings = `
{
	"settings": {
		"index.mapping.coerce": false
	},
	"mappings": {
		"Cron": {
			"properties": {
				"eventTypeId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"eventId": {
					"type": "string",
					"index": "not_analyzed"
				},
				"data": {
					"properties": {}
				},
				"createdBy": {
					"type": "string",
					"index": "not_analyzed"
				},
				"createdOn": {
					"type": "date"
				},
				"cronSchedule": {
					"type": "string",
					"index": "not_analyzed"
				}
			}
		}
	}
}
`

const cronDBMapping = "Cron"

//-- Stats ------------------------------------------------------------

type workflowStats struct {
	sync.Mutex
	CreatedOn        time.Time `json:"createdOn"`
	NumEventTypes    int       `json:"numEventTypes"`
	NumEvents        int       `json:"numEvents"`
	NumTriggers      int       `json:"numTriggers"`
	NumAlerts        int       `json:"numAlerts"`
	NumTriggeredJobs int       `json:"numTriggeredJobs"`
}

func (stats *workflowStats) incrCounter(counter *int) {
	stats.Lock()
	*counter++
	stats.Unlock()
}

func (stats *workflowStats) IncrEventTypes() {
	stats.incrCounter(&stats.NumEventTypes)
}

func (stats *workflowStats) IncrEvents() {
	stats.incrCounter(&stats.NumEvents)
}

func (stats *workflowStats) IncrTriggers() {
	stats.incrCounter(&stats.NumTriggers)
}

func (stats *workflowStats) IncrAlerts() {
	stats.incrCounter(&stats.NumAlerts)
}

func (stats *workflowStats) IncrTriggerJobs() {
	stats.incrCounter(&stats.NumTriggeredJobs)
}

//-UTILITY----------------------------------------------------------------------

// LoggedError logs the error's message and creates an error
func LoggedError(mssg string, args ...interface{}) error {
	str := fmt.Sprintf(mssg, args)
	log.Printf(str)
	return errors.New(str)
}

// isUUID checks to see if the UUID is valid
func isUUID(uuid string) bool {
	return uuidpkg.Parse(uuid) != nil
}

//-INIT-------------------------------------------------------------------------

func init() {
	piazza.JsonResponseDataTypes["*workflow.EventType"] = "eventtype"
	piazza.JsonResponseDataTypes["[]workflow.EventType"] = "eventtype-list"
	piazza.JsonResponseDataTypes["*workflow.Event"] = "event"
	piazza.JsonResponseDataTypes["[]workflow.Event"] = "event-list"
	piazza.JsonResponseDataTypes["*workflow.Trigger"] = "trigger"
	piazza.JsonResponseDataTypes["[]workflow.Trigger"] = "trigger-list"
	piazza.JsonResponseDataTypes["*workflow.Alert"] = "alert"
	piazza.JsonResponseDataTypes["[]workflow.Alert"] = "alert-list"
	piazza.JsonResponseDataTypes["workflow.workflowStats"] = "workflowstats"
}
<|endoftext|>


<commit_before>package backends

import (
	"github.com/3onyc/hipdate"
	"github.com/garyburd/redigo/redis"
	"log"
)

type HipacheBackend struct {
	r redis.Conn
}

func (hb *HipacheBackend) AddUpstream(
	h hipdate.Host,
	u hipdate.Upstream,
) error {
	exists, err := hb.hostExists(h)
	if err != nil {
		log.Println(err)
	}

	if !exists {
		if err := hb.hostCreate(h); err != nil {
			log.Println(err)
		}
	}

	if _, err := hb.r.Do("RPUSH", h.Key(), u); err != nil {
		return err
	}
	log.Println("Registered", h, u)

	return nil
}
func (hb *HipacheBackend) RemoveUpstream(
	h hipdate.Host,
	u hipdate.Upstream,
) error {
	if _, err := hb.r.Do("LREM", h.Key(), 0, u); err != nil {
		return err
	}

	log.Println("Unregistered", h, u)
	return nil
}
func (hb *HipacheBackend) hostExists(h hipdate.Host) (bool, error) {
	return redis.Bool(hb.r.Do("EXISTS", h.Key()))
}

func (hb *HipacheBackend) hostDelete(h hipdate.Host) error {
	if _, err := hb.r.Do("DEL", h.Key()); err != nil {
		return err
	}
	log.Println("Deleted", h)

	return nil
}

func (hb *HipacheBackend) hostCreate(h hipdate.Host) error {
	if _, err := hb.r.Do("RPUSH", h.Key(), h); err != nil {
		return err
	}
	log.Println("Created", h)

	return nil
}

func (hb *HipacheBackend) hostClear(h hipdate.Host) error {
	if err := hb.hostDelete(h); err != nil {
		return err
	}

	if err := hb.hostCreate(h); err != nil {
		return err
	}

	log.Println("Initialised", h)
	return nil
}

func NewHipacheBackend(r redis.Conn) *HipacheBackend {
	return &HipacheBackend{
		r: r,
	}
}
<commit_msg>Move NewHipacheBackend next to struct<commit_after>package backends

import (
	"github.com/3onyc/hipdate"
	"github.com/garyburd/redigo/redis"
	"log"
)

type HipacheBackend struct {
	r redis.Conn
}

func NewHipacheBackend(r redis.Conn) *HipacheBackend {
	return &HipacheBackend{
		r: r,
	}
}

func (hb *HipacheBackend) AddUpstream(
	h hipdate.Host,
	u hipdate.Upstream,
) error {
	exists, err := hb.hostExists(h)
	if err != nil {
		log.Println(err)
	}

	if !exists {
		if err := hb.hostCreate(h); err != nil {
			log.Println(err)
		}
	}

	if _, err := hb.r.Do("RPUSH", h.Key(), u); err != nil {
		return err
	}
	log.Println("Registered", h, u)

	return nil
}
func (hb *HipacheBackend) RemoveUpstream(
	h hipdate.Host,
	u hipdate.Upstream,
) error {
	if _, err := hb.r.Do("LREM", h.Key(), 0, u); err != nil {
		return err
	}

	log.Println("Unregistered", h, u)
	return nil
}
func (hb *HipacheBackend) hostExists(h hipdate.Host) (bool, error) {
	return redis.Bool(hb.r.Do("EXISTS", h.Key()))
}

func (hb *HipacheBackend) hostDelete(h hipdate.Host) error {
	if _, err := hb.r.Do("DEL", h.Key()); err != nil {
		return err
	}
	log.Println("Deleted", h)

	return nil
}

func (hb *HipacheBackend) hostCreate(h hipdate.Host) error {
	if _, err := hb.r.Do("RPUSH", h.Key(), h); err != nil {
		return err
	}
	log.Println("Created", h)

	return nil
}

func (hb *HipacheBackend) hostClear(h hipdate.Host) error {
	if err := hb.hostDelete(h); err != nil {
		return err
	}

	if err := hb.hostCreate(h); err != nil {
		return err
	}

	log.Println("Initialised", h)
	return nil
}
<|endoftext|>


<commit_before>package whiplash // github.com/sboyettedh/whiplash

import (
	"encoding/json"
)

const (
	Version = "0.3.0"
)

// Row represents a datacenter row. It contains Racks.
type Row struct {
	ID int
	Name string
	Children []int
}

// Rack represents a datacenter rack. It contains Hosts.
type Rack struct {
	ID int
	Name string
	Children []int
}

// Host represents a machine running Ceph services. Svcs belong to it.
type Host struct {
	ID int
	Name string
	Rack string
}

// ClientRequest is the struct used for interchange between whiplash clients
// and the aggregator. Each network request consists of the request
// name followed by whitespace followed by a JSON-encoded Request.
type ClientRequest struct {
	// Svc is the core identifying and status info about the service
	// making the request.
	Svc *SvcCore `json:"svc"`

	// Payload is the data accompanying the request. May be empty, as
	// in a ping request.
	Payload json.RawMessage `json:"payload"`
}

// QueryResponse is the standardized reply to a wlq query
type QueryResponse struct {
	// Code is like an HTTP code. 200 is success. 400 and up is error.
	Code int `json:"code"`
	// Cmd is the command which was dispatched
	Cmd string `json:"cmd"`
	// Subcmd is the dispatched subcommand
	Subcmd string `json:"subcmd"`
	// Args is the args which were passed in
	Args []string `json:"args"`
	// Data is the result of processing. It is generally the actual
	// response data, but if Code indicates an error, it will likely
	// be an error message.
	Data json.RawMessage `json:"data"`
}

// OsdSvc is the data we want to ship to the aggregator about an OSD
// service.
type OsdSvc struct {
	// Weight is the crush weight of the OSD.
	Weight float32
	// BytesUsed is the amount of data stored on the OSD
	BytesUsed int
	// BytesAvail is the space remaining on the OSD
	BytesAvail int
	// PgPrimary is the number of PGs for which the OSD is the primary
	PgPrimary int
	// PgReplica is the number of PGs for which the OSD is a replica
	PgReplica int
}

// CephOsdPerfDump represents the output of passing 'perf dump' to an
// OSD admin daemon.
type CephOsdPerfDump struct {
	WBThrottle json.RawMessage `json:"WBThrottle"`
	Filestore json.RawMessage `json:"filestore"`
	LevelDB json.RawMessage `json:"leveldb"`
	MutexFJCL json.RawMessage `json:"mutex-FileJournal::completions_lock"`
	Mutex json.RawMessage `json:"mutex-FileJournal::finisher_lock"`
	Mutex json.RawMessage `json:"mutex-FileJournal::write_lock"`
	Mutex json.RawMessage `json:"mutex-FileJournal::writeq_lock"`
	Mutex json.RawMessage `json:"mutex-JOS::ApplyManager::apply_lock"`
	Mutex json.RawMessage `json:"mutex-JOS::ApplyManager::com_lock"`
	Mutex json.RawMessage `json:"mutex-JOS::SubmitManager::lock"`
	Mutex json.RawMessage `json:"mutex-WBThrottle::lock"`
	Objecter json.RawMessage `json:"objecter"`
	// Osd is currently the only thing we care about in here. It holds
	// tons of data about how the OSD is currently looking.
	Osd CephOsdPerfDumpOsd `json:"osd"`
	RecoveryState json.RawMessage `json:"recoverystate_perf"`
	ThrottleFSBytes json.RawMessage `json:"throttle-filestore_bytes"`
	ThrottleFSOps json.RawMessage `json:"throttle-filestore_ops"`
	ThrottleMDTCli json.RawMessage `json:"throttle-msgr_dispatch_throttler-client"`
	ThrottleMDTClu json.RawMessage `json:"throttle-msgr_dispatch_throttler-cluster"`
	ThrottleMDTHBBS json.RawMessage `json:"throttle-msgr_dispatch_throttler-hb_back_server"`
	ThrottleMDTHBFS json.RawMessage `json:"throttle-msgr_dispatch_throttler-hb_front_server"`
	ThrottleMDTHBC json.RawMessage `json:"throttle-msgr_dispatch_throttler-hbclient"`
	ThrottleMDTMO json.RawMessage `json:"throttle-msgr_dispatch_throttler-ms_objecter"`
	ThrottleObjBytes json.RawMessage `json:"throttle-objecter_bytes"`
	ThrottleObjOps json.RawMessage `json:"throttle-objecter_ops"`
	ThrottleOsdCB json.RawMessage `json:"throttle-osd_client_bytes"`
	ThrottleOsdCM json.RawMessage `json:"throttle-osd_client_messages"`
}

// CephOsdPerfDumpOsd is the "osd" section of the output of "perf dump"
type CephOsdPerfDumpOsd struct {
	OpWip int `json:"op_wip"`
	Op int `json:"op"`
	OpInBytes int `json:"op_in_bytes"`
	OpOutBytes int `json:"op_out_bytes"`
	OpLatency json.RawMessage `json:"op_latency"`
	OpProcessLatency json.RawMessage `json:"op_process_latency"`
	OpR int `json:"op_r"`
	OpROutBytes int `json:"op_r_out_bytes"`
	OpRLatency json.RawMessage `json:"op_r_latency"`
	OpRProcessLatency json.RawMessage `json:"op_r_process_latency"`
	OpW int `json:"op_w"`
	OpWInBytes int `json:"op_w_in_bytes"`
	OpWRLatency json.RawMessage `json:"op_w_rlat"`
	OpWLatency json.RawMessage `json:"op_w_latency"`
	OpWProcessLatency json.RawMessage `json:"op_w_process_latency"`
	OpRW int `json:"op_rw"`
	OpRWInBytes int `json:"op_rw_in_bytes"`
	OpRWOutBytes int `json:"op_rw_out_bytes"`
	OpRWRLatency json.RawMessage `json:"op_rw_rlat"`
	OpRWLatency json.RawMessage `json:"op_rw_latency"`
	OpRWProcessLatency json.RawMessage `json:"op_rw_process_latency"`
	Subop int `json:"subop"`
	SubopInBytes int `json:"subop_in_bytes"`
	SubopLatency json.RawMessage `json:"subop_latency"`
	SubopW int `json:"subop_w"`
	SubopWInBytes int `json:"subop_w_in_bytes"`
	SubopWLatency json.RawMessage `json:"subop_w_latency"`
	SubopPull int `json:"subop_pull"`
	SubopPullLatency json.RawMessage `json:"subop_pull_latency"`
	SubopPush int `json:"subop_push"`
	SubopPushInBytes int `json:"subop_push_in_bytes"`
	SubopPushLatency json.RawMessage `json:"subop_push_latency"`
	Pull int `json:"pull"`
	Push int `json:"push"`
	PushOutBytes int `json:"push_out_bytes"`
	PushIn int `json:"push_in"`
	PushInBytes int `json:"push_in_bytes"`
	RecoveryOps int `json:"recovery_ops"`
	Loadavg int `json:"loadavg"`
	BufferBytes int `json:"buffer_bytes"`
	// NumPg is the count of PGs on this OSD
	NumPg int `json:"numpg"`
	// NumPgPrimary is the count of PGs for which this OSD is the primary
	NumPgPrimary int `json:"numpg_primary"`
	// NumPgReplica is the count of PGs for which this OSD is a replica
	NumPgReplica int `json:"numpg_replica"`
	NumPgStray int `json:"numpg_stray"`
	HeartbeatToPeers int `json:"heartbeat_to_peers"`
	HeartbeatFromPeers int `json:"heartbeat_from_peers"`
	MapMsgs int `json:"map_messages"`
	MapMsgEpochs int `json:"map_message_epochs"`
	MapMsgEpochDups int `json:"map_message_epoch_dups"`
	MsgsDelayedForMap int `json:"messages_delayed_for_map"`
	// StatBytes is the total size of the OSDs filesystem
	StatBytes int `json:"stat_bytes"`
	// StatBytesUsed is the amount of data stored by the OSD
	StatBytesUsed int `json:"stat_bytes_used"`
	// StatBytesAvail is the space remaining on the OSD
	StatBytesAvail int `json:"stat_bytes_avail"`
	Copyfrom int `json:"copyfrom"`
	TierPromote int `json:"tier_promote"`
	TierFlush int `json:"tier_flush"`
	TierFlushFail int `json:"tier_flush_fail"`
	TierTryFlush int `json:"tier_try_flush"`
	TierTryFlushFail int `json:"tier_try_flush_fail"`
	TierEvict int `json:"tier_evict"`
	TierWhiteout int `json:"tier_whiteout"`
	TierDirty int `json:"tier_dirty"`
	TierClean int `json:"tier_clean"`
	TierDelay int `json:"tier_delay"`
	TierProxyDead int `json:"tier_proxy_read"`
	AgentWake int `json:"agent_wake"`
	AgentSkip int `json:"agent_skip"`
	AgentFlush int `json:"agent_flush"`
	AgentEvict int `json:"agent_evict"`
	ObjCtxCacheHit int `json:"object_ctx_cache_hit"`
	ObjCtxCacheTotal int `json:"object_ctx_cache_total"`
}
<commit_msg>forgot to differentiate all the mutexes<commit_after>package whiplash // github.com/sboyettedh/whiplash

import (
	"encoding/json"
)

const (
	Version = "0.3.0"
)

// Row represents a datacenter row. It contains Racks.
type Row struct {
	ID int
	Name string
	Children []int
}

// Rack represents a datacenter rack. It contains Hosts.
type Rack struct {
	ID int
	Name string
	Children []int
}

// Host represents a machine running Ceph services. Svcs belong to it.
type Host struct {
	ID int
	Name string
	Rack string
}

// ClientRequest is the struct used for interchange between whiplash clients
// and the aggregator. Each network request consists of the request
// name followed by whitespace followed by a JSON-encoded Request.
type ClientRequest struct {
	// Svc is the core identifying and status info about the service
	// making the request.
	Svc *SvcCore `json:"svc"`

	// Payload is the data accompanying the request. May be empty, as
	// in a ping request.
	Payload json.RawMessage `json:"payload"`
}

// QueryResponse is the standardized reply to a wlq query
type QueryResponse struct {
	// Code is like an HTTP code. 200 is success. 400 and up is error.
	Code int `json:"code"`
	// Cmd is the command which was dispatched
	Cmd string `json:"cmd"`
	// Subcmd is the dispatched subcommand
	Subcmd string `json:"subcmd"`
	// Args is the args which were passed in
	Args []string `json:"args"`
	// Data is the result of processing. It is generally the actual
	// response data, but if Code indicates an error, it will likely
	// be an error message.
	Data json.RawMessage `json:"data"`
}

// OsdSvc is the data we want to ship to the aggregator about an OSD
// service.
type OsdSvc struct {
	// Weight is the crush weight of the OSD.
	Weight float32
	// BytesUsed is the amount of data stored on the OSD
	BytesUsed int
	// BytesAvail is the space remaining on the OSD
	BytesAvail int
	// PgPrimary is the number of PGs for which the OSD is the primary
	PgPrimary int
	// PgReplica is the number of PGs for which the OSD is a replica
	PgReplica int
}

// CephOsdPerfDump represents the output of passing 'perf dump' to an
// OSD admin daemon.
type CephOsdPerfDump struct {
	WBThrottle json.RawMessage `json:"WBThrottle"`
	Filestore json.RawMessage `json:"filestore"`
	LevelDB json.RawMessage `json:"leveldb"`
	MutexFJCL json.RawMessage `json:"mutex-FileJournal::completions_lock"`
	MutexFJFL json.RawMessage `json:"mutex-FileJournal::finisher_lock"`
	MutexFJWL json.RawMessage `json:"mutex-FileJournal::write_lock"`
	MutexFHWQL json.RawMessage `json:"mutex-FileJournal::writeq_lock"`
	MutexJAMAL json.RawMessage `json:"mutex-JOS::ApplyManager::apply_lock"`
	MutexJAMCL json.RawMessage `json:"mutex-JOS::ApplyManager::com_lock"`
	MutexJSML json.RawMessage `json:"mutex-JOS::SubmitManager::lock"`
	MutexWBTL json.RawMessage `json:"mutex-WBThrottle::lock"`
	Objecter json.RawMessage `json:"objecter"`
	// Osd is currently the only thing we care about in here. It holds
	// tons of data about how the OSD is currently looking.
	Osd CephOsdPerfDumpOsd `json:"osd"`
	RecoveryState json.RawMessage `json:"recoverystate_perf"`
	ThrottleFSBytes json.RawMessage `json:"throttle-filestore_bytes"`
	ThrottleFSOps json.RawMessage `json:"throttle-filestore_ops"`
	ThrottleMDTCli json.RawMessage `json:"throttle-msgr_dispatch_throttler-client"`
	ThrottleMDTClu json.RawMessage `json:"throttle-msgr_dispatch_throttler-cluster"`
	ThrottleMDTHBBS json.RawMessage `json:"throttle-msgr_dispatch_throttler-hb_back_server"`
	ThrottleMDTHBFS json.RawMessage `json:"throttle-msgr_dispatch_throttler-hb_front_server"`
	ThrottleMDTHBC json.RawMessage `json:"throttle-msgr_dispatch_throttler-hbclient"`
	ThrottleMDTMO json.RawMessage `json:"throttle-msgr_dispatch_throttler-ms_objecter"`
	ThrottleObjBytes json.RawMessage `json:"throttle-objecter_bytes"`
	ThrottleObjOps json.RawMessage `json:"throttle-objecter_ops"`
	ThrottleOsdCB json.RawMessage `json:"throttle-osd_client_bytes"`
	ThrottleOsdCM json.RawMessage `json:"throttle-osd_client_messages"`
}

// CephOsdPerfDumpOsd is the "osd" section of the output of "perf dump"
type CephOsdPerfDumpOsd struct {
	OpWip int `json:"op_wip"`
	Op int `json:"op"`
	OpInBytes int `json:"op_in_bytes"`
	OpOutBytes int `json:"op_out_bytes"`
	OpLatency json.RawMessage `json:"op_latency"`
	OpProcessLatency json.RawMessage `json:"op_process_latency"`
	OpR int `json:"op_r"`
	OpROutBytes int `json:"op_r_out_bytes"`
	OpRLatency json.RawMessage `json:"op_r_latency"`
	OpRProcessLatency json.RawMessage `json:"op_r_process_latency"`
	OpW int `json:"op_w"`
	OpWInBytes int `json:"op_w_in_bytes"`
	OpWRLatency json.RawMessage `json:"op_w_rlat"`
	OpWLatency json.RawMessage `json:"op_w_latency"`
	OpWProcessLatency json.RawMessage `json:"op_w_process_latency"`
	OpRW int `json:"op_rw"`
	OpRWInBytes int `json:"op_rw_in_bytes"`
	OpRWOutBytes int `json:"op_rw_out_bytes"`
	OpRWRLatency json.RawMessage `json:"op_rw_rlat"`
	OpRWLatency json.RawMessage `json:"op_rw_latency"`
	OpRWProcessLatency json.RawMessage `json:"op_rw_process_latency"`
	Subop int `json:"subop"`
	SubopInBytes int `json:"subop_in_bytes"`
	SubopLatency json.RawMessage `json:"subop_latency"`
	SubopW int `json:"subop_w"`
	SubopWInBytes int `json:"subop_w_in_bytes"`
	SubopWLatency json.RawMessage `json:"subop_w_latency"`
	SubopPull int `json:"subop_pull"`
	SubopPullLatency json.RawMessage `json:"subop_pull_latency"`
	SubopPush int `json:"subop_push"`
	SubopPushInBytes int `json:"subop_push_in_bytes"`
	SubopPushLatency json.RawMessage `json:"subop_push_latency"`
	Pull int `json:"pull"`
	Push int `json:"push"`
	PushOutBytes int `json:"push_out_bytes"`
	PushIn int `json:"push_in"`
	PushInBytes int `json:"push_in_bytes"`
	RecoveryOps int `json:"recovery_ops"`
	Loadavg int `json:"loadavg"`
	BufferBytes int `json:"buffer_bytes"`
	// NumPg is the count of PGs on this OSD
	NumPg int `json:"numpg"`
	// NumPgPrimary is the count of PGs for which this OSD is the primary
	NumPgPrimary int `json:"numpg_primary"`
	// NumPgReplica is the count of PGs for which this OSD is a replica
	NumPgReplica int `json:"numpg_replica"`
	NumPgStray int `json:"numpg_stray"`
	HeartbeatToPeers int `json:"heartbeat_to_peers"`
	HeartbeatFromPeers int `json:"heartbeat_from_peers"`
	MapMsgs int `json:"map_messages"`
	MapMsgEpochs int `json:"map_message_epochs"`
	MapMsgEpochDups int `json:"map_message_epoch_dups"`
	MsgsDelayedForMap int `json:"messages_delayed_for_map"`
	// StatBytes is the total size of the OSDs filesystem
	StatBytes int `json:"stat_bytes"`
	// StatBytesUsed is the amount of data stored by the OSD
	StatBytesUsed int `json:"stat_bytes_used"`
	// StatBytesAvail is the space remaining on the OSD
	StatBytesAvail int `json:"stat_bytes_avail"`
	Copyfrom int `json:"copyfrom"`
	TierPromote int `json:"tier_promote"`
	TierFlush int `json:"tier_flush"`
	TierFlushFail int `json:"tier_flush_fail"`
	TierTryFlush int `json:"tier_try_flush"`
	TierTryFlushFail int `json:"tier_try_flush_fail"`
	TierEvict int `json:"tier_evict"`
	TierWhiteout int `json:"tier_whiteout"`
	TierDirty int `json:"tier_dirty"`
	TierClean int `json:"tier_clean"`
	TierDelay int `json:"tier_delay"`
	TierProxyDead int `json:"tier_proxy_read"`
	AgentWake int `json:"agent_wake"`
	AgentSkip int `json:"agent_skip"`
	AgentFlush int `json:"agent_flush"`
	AgentEvict int `json:"agent_evict"`
	ObjCtxCacheHit int `json:"object_ctx_cache_hit"`
	ObjCtxCacheTotal int `json:"object_ctx_cache_total"`
}
<|endoftext|>


<commit_before>package wiki

import (
	"fmt"
	"math/rand"
	"regexp"
	"strings"
	"time"

	"cgt.name/pkg/go-mwclient"
	"github.com/erbridge/gotwit"
)

type (
	Client struct {
		bot    *gotwit.Bot
		wiki   *mwclient.Client
		ticker *time.Ticker
	}
)

func NewClient(bot *gotwit.Bot) (client Client, err error) {
	// TODO: Add contact details to the user agent.
	wiki, err := mwclient.New("https://en.wikipedia.org/w/api.php", "Wikipaedian")

	if err != nil {
		return
	}

	client = Client{
		bot:  bot,
		wiki: wiki,
	}

	return
}

func (c *Client) Start(d time.Duration) {
	c.ticker = time.NewTicker(d)
	defer c.ticker.Stop()

	c.post()

	for range c.ticker.C {
		c.post()
	}
}

func (c *Client) post() {
	last := c.lastPost()

	fmt.Println("Last post:", last)

	var content string

	if last == "" {
		content = "[[Hello, World!]]"
	} else {
		page := c.page(last)

		content = c.createPost(page)
	}

	fmt.Println("Posting:", content)

	// c.bot.Post(content, false)
}

func (c *Client) lastPost() string {
	if text, err := c.bot.LastTweetText(); err != nil {
		panic(err)
	} else {
		return text
	}

	return ""
}

func (c *Client) page(last string) (content string) {
	re := regexp.MustCompile("\\[{2}([^:]+?)\\]{2}")

	matches := re.FindAllStringSubmatch(last, -1)

	// FIXME: Handle the 0 match state.
	title := matches[rand.Intn(len(matches))][1]

	re = regexp.MustCompile("(.+?)\\|")

	match := re.FindStringSubmatch(title)

	if len(match) > 0 {
		title = match[1]
	}

	content, _, err := c.wiki.GetPageByName(title)

	if err != nil {
		panic(err)
	}

	re = regexp.MustCompile("^#REDIRECT")

	if re.MatchString(content) {
		fmt.Println("Redirecting:", content)

		return c.page(content)
	}

	return
}

func (c *Client) createPost(page string) (content string) {
	re := regexp.MustCompile("[.!?][[:space:]]+" +
		"(" +
		"(" +
		"([^{^}^\\[^\\]^<^>]+?[[:space:]]+?)+?" +
		")" +
		"(\\[{2}[^:]+?\\]{2})" +
		"(" +
		"([[:space:]]+?[^{^}^\\[^\\]^<^>]+?)+?" +
		")" +
		"([.!?])" +
		")" +
		"[[:space:]]+",
	)

	matches := re.FindAllStringSubmatch(page, -1)

	// FIXME: Handle the 0 match state.
	match := matches[rand.Intn(len(matches))]

	content = match[1]

	if len(content) <= 140 {
		return
	}

	content = match[4]

	if len(content) > 138 {
		re = regexp.MustCompile("\\[{2}[^:]+?\\]{2}")

		matches := re.FindAllString(content, -1)

		// FIXME: Handle the 0 match state.
		content = matches[rand.Intn(len(matches))]
	}

	before, after := strings.Split(match[2], " "), strings.Split(match[5], " ")

	newContent := content

	for len(newContent) < 138 {
		content = newContent

		word := ""

		if len(before) > 0 && rand.Float32() > 0.5 {
			word, before = before[len(before)-1], before[:len(before)-1]

			if word == "" {
				continue
			}

			newContent = word + " " + content
		} else if len(after) > 0 {
			word, after = after[0], after[1:]

			if word == "" {
				continue
			}

			newContent = content + " " + word
		} else {
			break
		}
	}

	if len(before) > 0 {
		content = "…" + content
	}

	if len(after) > 0 {
		content += "…"
	} else {
		content += match[7]
	}

	return
}
<commit_msg>Always use the first line of the redirect<commit_after>package wiki

import (
	"fmt"
	"math/rand"
	"regexp"
	"strings"
	"time"

	"cgt.name/pkg/go-mwclient"
	"github.com/erbridge/gotwit"
)

type (
	Client struct {
		bot    *gotwit.Bot
		wiki   *mwclient.Client
		ticker *time.Ticker
	}
)

func NewClient(bot *gotwit.Bot) (client Client, err error) {
	// TODO: Add contact details to the user agent.
	wiki, err := mwclient.New("https://en.wikipedia.org/w/api.php", "Wikipaedian")

	if err != nil {
		return
	}

	client = Client{
		bot:  bot,
		wiki: wiki,
	}

	return
}

func (c *Client) Start(d time.Duration) {
	c.ticker = time.NewTicker(d)
	defer c.ticker.Stop()

	c.post()

	for range c.ticker.C {
		c.post()
	}
}

func (c *Client) post() {
	last := c.lastPost()

	fmt.Println("Last post:", last)

	var content string

	if last == "" {
		content = "[[Hello, World!]]"
	} else {
		page := c.page(last)

		content = c.createPost(page)
	}

	fmt.Println("Posting:", content)

	// c.bot.Post(content, false)
}

func (c *Client) lastPost() string {
	if text, err := c.bot.LastTweetText(); err != nil {
		panic(err)
	} else {
		return text
	}

	return ""
}

func (c *Client) page(last string) (content string) {
	re := regexp.MustCompile("\\[{2}([^:]+?)\\]{2}")

	matches := re.FindAllStringSubmatch(last, -1)

	// FIXME: Handle the 0 match state.
	title := matches[rand.Intn(len(matches))][1]

	re = regexp.MustCompile("(.+?)\\|")

	match := re.FindStringSubmatch(title)

	if len(match) > 0 {
		title = match[1]
	}

	content, _, err := c.wiki.GetPageByName(title)

	if err != nil {
		panic(err)
	}

	re = regexp.MustCompile("^#REDIRECT[[:space:]]+\\[{2}[^:]+?\\]{2}.*?")

	if re.MatchString(content) {
		content = re.FindString(content)

		fmt.Println("Redirecting:", content)

		return c.page(content)
	}

	return
}

func (c *Client) createPost(page string) (content string) {
	re := regexp.MustCompile("[.!?][[:space:]]+" +
		"(" +
		"(" +
		"([^{^}^\\[^\\]^<^>]+?[[:space:]]+?)+?" +
		")" +
		"(\\[{2}[^:]+?\\]{2})" +
		"(" +
		"([[:space:]]+?[^{^}^\\[^\\]^<^>]+?)+?" +
		")" +
		"([.!?])" +
		")" +
		"[[:space:]]+",
	)

	matches := re.FindAllStringSubmatch(page, -1)

	// FIXME: Handle the 0 match state.
	match := matches[rand.Intn(len(matches))]

	content = match[1]

	if len(content) <= 140 {
		return
	}

	content = match[4]

	if len(content) > 138 {
		re = regexp.MustCompile("\\[{2}[^:]+?\\]{2}")

		matches := re.FindAllString(content, -1)

		// FIXME: Handle the 0 match state.
		content = matches[rand.Intn(len(matches))]
	}

	before, after := strings.Split(match[2], " "), strings.Split(match[5], " ")

	newContent := content

	for len(newContent) < 138 {
		content = newContent

		word := ""

		if len(before) > 0 && rand.Float32() > 0.5 {
			word, before = before[len(before)-1], before[:len(before)-1]

			if word == "" {
				continue
			}

			newContent = word + " " + content
		} else if len(after) > 0 {
			word, after = after[0], after[1:]

			if word == "" {
				continue
			}

			newContent = content + " " + word
		} else {
			break
		}
	}

	if len(before) > 0 {
		content = "…" + content
	}

	if len(after) > 0 {
		content += "…"
	} else {
		content += match[7]
	}

	return
}
<|endoftext|>


<commit_before>// Copyright © 2013 Steve Francia <spf@spf13.com>.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tpl

type Tmpl struct {
	Name string
	Data string
}

func (t *GoHTMLTemplate) EmbedShortcodes() {
	t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
	t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
	t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
	t.AddInternalShortcode("test.html", `This is a simple Test`)
	t.AddInternalShortcode("figure.html", `

    {{ with .Get "link"}}{{ end }}

    {{ if .Get "link"}}{{ end }}
    {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}

{{ if isset .Params "title" }}

{{ .Get "title" }}
{{ end }}
        {{ if or (.Get "caption") (.Get "attr")}}

        {{ .Get "caption" }}
        {{ with .Get "attrlink"}} {{ end }}
            {{ .Get "attr" }}
        {{ if .Get "attrlink"}} {{ end }}

 {{ end }}

    {{ end }}

`)
}

func (t *GoHTMLTemplate) EmbedTemplates() {

	t.AddInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}</title>
    <link>{{ .Permalink }}</link>
    <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description>
    <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
    <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
    <atom:link href="{{.URL}}" rel="self" type="application/rss+xml" />
    {{ range first 15 .Data.Pages }}
    <item>
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Content | html }}</description>
    </item>
    {{ end }}
  </channel>
</rss>`)

	t.AddInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{ range .Data.Pages }}
  <url>
    <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
    <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
    <priority>{{ .Sitemap.Priority }}</priority>{{ end }}
  </url>
  {{ end }}
</urlset>`)

	t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
    {{ if gt $pag.TotalPages 1 }}

        {{ with $pag.First }}

            ««

        {{ end }}

        «

        {{ range $pag.Pagers }}

{{ .PageNumber }}

        {{ end }}

        »

        {{ with $pag.Last }}

            »»

        {{ end }}

    {{ end }}`)

	t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}

Please enable JavaScript to view the comments powered by Disqus.
comments powered by Disqus{{end}}`)

	// Add SEO & Social metadata
	t.AddInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta property="og:url" content="{{ .Permalink }}" />
{{ with .Params.images }}{{ range first 6 . }}
  <meta property="og:image" content="{{ . | absURL }}" />
{{ end }}{{ end }}

{{ if not .Date.IsZero }}<meta property="og:updated_time" content="{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/>{{ end }}{{ with .Params.audio }}
<meta property="og:audio" content="{{ . }}" />{{ end }}{{ with .Params.locale }}
<meta property="og:locale" content="{{ . }}" />{{ end }}{{ with .Site.Params.title }}
<meta property="og:site_name" content="{{ . }}" />{{ end }}{{ with .Params.videos }}
{{ range .Params.videos }}
  <meta property="og:video" content="{{ . | absURL }}" />
{{ end }}{{ end }}

{{ $permalink := .Permalink }}
{{ $siteSeries := .Site.Taxonomies.series }}{{ with .Params.series }}
{{ range $name := . }}
  {{ $series := index $siteSeries $name }}
  {{ range $page := first 6 $series.Pages }}
    {{ if ne $page.Permalink $permalink }}<meta property="og:see_also" content="{{ $page.Permalink }}" />{{ end }}
  {{ end }}
{{ end }}{{ end }}

{{ if .IsPage }}
{{ range .Site.Authors }}{{ with .Social.facebook }}
<meta property="article:author" content="https://www.facebook.com/{{ . }}" />{{ end }}{{ with .Site.Social.facebook }}
<meta property="article:publisher" content="https://www.facebook.com/{{ . }}" />{{ end }}
<meta property="article:published_time" content="{{ .PublishDate }}" />
<meta property="article:modified_time" content="{{ .Date }}" />
<meta property="article:section" content="{{ .Section }}" />
{{ with .Params.tags }}{{ range first 6 . }}
  <meta property="article:tag" content="{{ . }}" />{{ end }}{{ end }}
{{ end }}{{ end }}

{{ with .Site.Social.facebook_admin }}<meta property="fb:admins" content="{{ . }}" />{{ end }}`)

	t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
{{ with .Params.images }}

  <meta name="twitter:card" content="summary_large_image"/>
  <meta name="twitter:image:src" content="{{ index . 0 | absURL }}"/>
{{ else }}
  <meta name="twitter:card" content="summary"/>
{{ end }}

<meta name="twitter:title" content="{{ .Title }}"/>
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"/>
{{ with .Site.Social.twitter }}<meta name="twitter:site" content="@{{ . }}"/>{{ end }}
{{ with .Site.Social.twitter_domain }}<meta name="twitter:domain" content="{{ . }}"/>{{ end }}
{{ range .Site.Authors }}
  {{ with .twitter }}<meta name="twitter:creator" content="@{{ . }}"/>{{ end }}
{{ end }}{{ end }}`)

	t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
  <meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" />
{{ end }}{{ end }}`)

	t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
<meta itemprop="name" content="{{ .Title }}">
<meta itemprop="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}">

{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }}
<meta itemprop="datePublished" content="{{ .PublishDate.Format $ISO8601 | safeHTML }}" />{{ end }}
{{ if not .Date.IsZero }}<meta itemprop="dateModified" content="{{ .Date.Format $ISO8601 | safeHTML }}" />{{ end }}
<meta itemprop="wordCount" content="{{ .WordCount }}">
{{ with .Params.images }}{{ range first 6 . }}
  <meta itemprop="image" content="{{ . | absURL }}">
{{ end }}{{ end }}

<meta itemprop="keywords" content="{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}" />
{{ end }}`)

	t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}

{{ end }}`)

}
<commit_msg>Add shortcode for speakerdeck<commit_after>// Copyright © 2013 Steve Francia <spf@spf13.com>.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tpl

type Tmpl struct {
	Name string
	Data string
}

func (t *GoHTMLTemplate) EmbedShortcodes() {
	t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
	t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
	t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
	t.AddInternalShortcode("test.html", `This is a simple Test`)
	t.AddInternalShortcode("figure.html", `

    {{ with .Get "link"}}{{ end }}

    {{ if .Get "link"}}{{ end }}
    {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}

{{ if isset .Params "title" }}

{{ .Get "title" }}
{{ end }}
        {{ if or (.Get "caption") (.Get "attr")}}

        {{ .Get "caption" }}
        {{ with .Get "attrlink"}} {{ end }}
            {{ .Get "attr" }}
        {{ if .Get "attrlink"}} {{ end }}

 {{ end }}

    {{ end }}

`)
	t.AddInternalShortcode("speakerdeck.html", "")
}

func (t *GoHTMLTemplate) EmbedTemplates() {

	t.AddInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}</title>
    <link>{{ .Permalink }}</link>
    <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description>
    <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
    <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
    <atom:link href="{{.URL}}" rel="self" type="application/rss+xml" />
    {{ range first 15 .Data.Pages }}
    <item>
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Content | html }}</description>
    </item>
    {{ end }}
  </channel>
</rss>`)

	t.AddInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  {{ range .Data.Pages }}
  <url>
    <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
    <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
    <priority>{{ .Sitemap.Priority }}</priority>{{ end }}
  </url>
  {{ end }}
</urlset>`)

	t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
    {{ if gt $pag.TotalPages 1 }}

        {{ with $pag.First }}

            ««

        {{ end }}

        «

        {{ range $pag.Pagers }}

{{ .PageNumber }}

        {{ end }}

        »

        {{ with $pag.Last }}

            »»

        {{ end }}

    {{ end }}`)

	t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}

Please enable JavaScript to view the comments powered by Disqus.
comments powered by Disqus{{end}}`)

	// Add SEO & Social metadata
	t.AddInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta property="og:url" content="{{ .Permalink }}" />
{{ with .Params.images }}{{ range first 6 . }}
  <meta property="og:image" content="{{ . | absURL }}" />
{{ end }}{{ end }}

{{ if not .Date.IsZero }}<meta property="og:updated_time" content="{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/>{{ end }}{{ with .Params.audio }}
<meta property="og:audio" content="{{ . }}" />{{ end }}{{ with .Params.locale }}
<meta property="og:locale" content="{{ . }}" />{{ end }}{{ with .Site.Params.title }}
<meta property="og:site_name" content="{{ . }}" />{{ end }}{{ with .Params.videos }}
{{ range .Params.videos }}
  <meta property="og:video" content="{{ . | absURL }}" />
{{ end }}{{ end }}

{{ $permalink := .Permalink }}
{{ $siteSeries := .Site.Taxonomies.series }}{{ with .Params.series }}
{{ range $name := . }}
  {{ $series := index $siteSeries $name }}
  {{ range $page := first 6 $series.Pages }}
    {{ if ne $page.Permalink $permalink }}<meta property="og:see_also" content="{{ $page.Permalink }}" />{{ end }}
  {{ end }}
{{ end }}{{ end }}

{{ if .IsPage }}
{{ range .Site.Authors }}{{ with .Social.facebook }}
<meta property="article:author" content="https://www.facebook.com/{{ . }}" />{{ end }}{{ with .Site.Social.facebook }}
<meta property="article:publisher" content="https://www.facebook.com/{{ . }}" />{{ end }}
<meta property="article:published_time" content="{{ .PublishDate }}" />
<meta property="article:modified_time" content="{{ .Date }}" />
<meta property="article:section" content="{{ .Section }}" />
{{ with .Params.tags }}{{ range first 6 . }}
  <meta property="article:tag" content="{{ . }}" />{{ end }}{{ end }}
{{ end }}{{ end }}

{{ with .Site.Social.facebook_admin }}<meta property="fb:admins" content="{{ . }}" />{{ end }}`)

	t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
{{ with .Params.images }}

  <meta name="twitter:card" content="summary_large_image"/>
  <meta name="twitter:image:src" content="{{ index . 0 | absURL }}"/>
{{ else }}
  <meta name="twitter:card" content="summary"/>
{{ end }}

<meta name="twitter:title" content="{{ .Title }}"/>
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"/>
{{ with .Site.Social.twitter }}<meta name="twitter:site" content="@{{ . }}"/>{{ end }}
{{ with .Site.Social.twitter_domain }}<meta name="twitter:domain" content="{{ . }}"/>{{ end }}
{{ range .Site.Authors }}
  {{ with .twitter }}<meta name="twitter:creator" content="@{{ . }}"/>{{ end }}
{{ end }}{{ end }}`)

	t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
  <meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" />
{{ end }}{{ end }}`)

	t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
<meta itemprop="name" content="{{ .Title }}">
<meta itemprop="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}">

{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }}
<meta itemprop="datePublished" content="{{ .PublishDate.Format $ISO8601 | safeHTML }}" />{{ end }}
{{ if not .Date.IsZero }}<meta itemprop="dateModified" content="{{ .Date.Format $ISO8601 | safeHTML }}" />{{ end }}
<meta itemprop="wordCount" content="{{ .WordCount }}">
{{ with .Params.images }}{{ range first 6 . }}
  <meta itemprop="image" content="{{ . | absURL }}">
{{ end }}{{ end }}

<meta itemprop="keywords" content="{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}" />
{{ end }}`)

	t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}

{{ end }}`)
}
<|endoftext|>


<commit_before>package transceiver

import (
	"github.com/tgres/tgres/rrd"
	"time"
)

type ReadCache struct {
	serde rrd.SerDe
	dsns  *rrd.DataSourceNames
}

func (r *ReadCache) Reload() error {
	return r.dsns.Reload(r.serde)
}

// Satisfy DSGetter interface in tgres/dsl

func (r *ReadCache) GetDSById(id int64) *rrd.DataSource {
	ds, _ := r.serde.FetchDataSource(id)
	return ds
}

func (r *ReadCache) DsIdsFromIdent(ident string) map[string]int64 {
	return r.dsns.DsIdsFromIdent(ident)
}

func (r *ReadCache) SeriesQuery(ds *rrd.DataSource, from, to time.Time, maxPoints int64) (rrd.Series, error) {
	return r.serde.SeriesQuery(ds, from, to, maxPoints)
}

// End of DSGetter

func (r *ReadCache) FsFind(pattern string) []*rrd.FsFindNode {
	return r.dsns.FsFind(pattern)
}
<commit_msg>Reload dsns for every FsFind<commit_after>package transceiver

import (
	"github.com/tgres/tgres/rrd"
	"time"
)

type ReadCache struct {
	serde rrd.SerDe
	dsns  *rrd.DataSourceNames
}

func (r *ReadCache) Reload() error {
	return r.dsns.Reload(r.serde)
}

// Satisfy DSGetter interface in tgres/dsl

func (r *ReadCache) GetDSById(id int64) *rrd.DataSource {
	ds, _ := r.serde.FetchDataSource(id)
	return ds
}

func (r *ReadCache) DsIdsFromIdent(ident string) map[string]int64 {
	return r.dsns.DsIdsFromIdent(ident)
}

func (r *ReadCache) SeriesQuery(ds *rrd.DataSource, from, to time.Time, maxPoints int64) (rrd.Series, error) {
	return r.serde.SeriesQuery(ds, from, to, maxPoints)
}

// End of DSGetter

func (r *ReadCache) FsFind(pattern string) []*rrd.FsFindNode {
	r.dsns.Reload(r.serde)
	return r.dsns.FsFind(pattern)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"github.com/gorilla/websocket"
	"github.com/satori/go.uuid"
	"log"
	"net/http"
	"strings"
	"sync"
)

type CommandHandlerFunc func(conn *websocket.Conn, id string, message map[string]interface{})
type TreeNode struct {
	conn     *websocket.Conn
	id       string
	name     string
	children []*TreeNode
	parent   *TreeNode
}

// FIXME: this is pretty terrible. We should use custom JSON marshaling, but I couln't get it to work
func (t *TreeNode) json() map[string]interface{} {
	result := map[string]interface{}{}
	result["id"] = t.id
	result["name"] = t.name

	children := []interface{}{}
	for _, child := range t.children {
		children = append(children, child.json())
	}
	result["children"] = children
	return result
}

var (
	port         = flag.Int("port", 8081, "Port the server listens on")
	maxListeners = flag.Int("max-listeners", 3, "Max number of listeners (WebRTC peers) for a single client")
	iceServers   = flag.String("ice-servers", `[]`, "The ICE servers the peers should use")
	upgrader     = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
	commandHandlers = map[string]CommandHandlerFunc{
		"START_BROADCAST":             commandStartBroadcast,
		"END_BROADCAST":               commandEndBroadcast,
		"JOIN_BROADCAST":              commandJoinBroadcast,
		"RELAY_BROADCAST_RECEIVED":    commandRelayBroadCastReceived,
		"ICE_CANDIDATES":              commandIceCandidates,
		"ICE_CANDIDATES_RECEIVED":     commandIceCandidatesReceived,
		"SUBSCRIBE_TO_TREE_STATE":     commandSubscribeToTreeState,
		"UNSUBSCRIBE_FROM_TREE_STATE": commandUnsubscribeFromTreeState,
		"FETCH_CONFIG":                commandFetchConfig,
	}
	broadcasts         = map[string]*TreeNode{}
	connections        = map[string]*websocket.Conn{}
	treeStateListeners = map[string]*map[string]*websocket.Conn{}
	globalLock         = sync.Mutex{} // FIXME: yeah, I know it's horrible, but we'll fix it later
)

func main() {
	flag.Parse()
	fs := http.FileServer(http.Dir("static"))
	http.Handle("/", fs)
	http.HandleFunc("/api/ws", handleWebSocket)
	log.Println("Server starting on port", *port)
	log.Fatal("ListenAndServe:", http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	log.Println("Incoming", r.Method, "message")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Failed to upgrade:", err)
		return
	}

	id := uuid.NewV4().String()

	for {
		var rawMessage interface{}
		if err := conn.ReadJSON(&rawMessage); err != nil {
			log.Printf("Read error: %v\n", err)
			conn.Close()
			return
		}

		messageObject, ok := rawMessage.(map[string]interface{})
		if !ok {
			sendErrorMessage(conn, "Message is not a JSON object")
			continue
		}

		command, ok := messageObject["command"].(string)
		if !ok {
			sendErrorMessage(conn, "Message is lacking a command property")
			continue
		}

		log.Printf("Received command: %v\n", command)
		if commandHandler, ok := commandHandlers[command]; ok {
			commandHandler(conn, id, messageObject)
		} else {
			sendErrorMessage(conn, fmt.Sprintf("Unknown command: %v", command))
			continue
		}
	}
}

func commandStartBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in START_BROADCAST message")
		return
	}

	_, ok = broadcasts[name]
	if ok {
		sendErrorMessage(conn, fmt.Sprintf("Broadcast \"%v\" already exists", name))
		return
	}

	peerName, ok := stringProp(message, "peerName")
	if !ok {
		peerName = "Anonymous"
	}

	log.Printf("Peer %v starting broadcast: %v", id, name)

	globalLock.Lock()
	defer globalLock.Unlock()

	broadcasts[name] = &TreeNode{
		conn: conn,
		id:   id,
		name: peerName,
	}
	connections[id] = conn
	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"START_BROADCAST_RECEIVED",
	})

	notifyTreeListeners(name)
}

func commandEndBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in END_BROADCAST message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	broadcast, ok := broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Broadcast \"%v\" does not exist", name))
		return
	}

	if broadcast.id != id {
		sendErrorMessage(conn, fmt.Sprintf("Peer \"%v\" did not start broadcast \"%v\"", id, name))
		return
	}

	command := struct {
		Command string `json:"command"`
		Name    string `json:"name"`
	}{
		"BROADCAST_ENDED",
		name,
	}

	var destroyTree func(node *TreeNode)
	destroyTree = func(node *TreeNode) {
		node.conn.WriteJSON(command)
		delete(connections, node.id)

		for _, child := range node.children {
			destroyTree(child)
		}
	}

	destroyTree(broadcast)
	delete(broadcasts, name)

	conn.WriteJSON(struct {
		Command string `json:"command"`
		Name    string `json:"name"`
	}{
		"END_BROADCAST_RECEIVED",
		name,
	})
}

func commandJoinBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in JOIN_BROADCAST message")
	}

	offer, ok := objectProp(message, "offer")
	if !ok {
		sendErrorMessage(conn, "No \"offer\" property specified or not an object in JOIN_BROADCAST message")
	}

	peerName, ok := stringProp(message, "peerName")
	if !ok {
		peerName = "Anonymous"
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	if broadcast, ok := broadcasts[name]; ok {
		parent := findNodeWithSpareCapacity(broadcast)
		if parent == nil {
			log.Panic("Received a nil node when inserting: %+v", broadcast)
		}

		node := TreeNode{
			conn:   conn,
			id:     id,
			name:   peerName,
			parent: parent,
		}
		connections[id] = conn
		parent.children = append(node.parent.children, &node)

		log.Printf("Peer %v joining broadcast %v as a child of %v which now has %d child(ren)\n",
			id, name, parent.id, len(parent.children))

		parent.conn.WriteJSON(struct {
			Command string                 `json:"command"`
			Peer    string                 `json:"peer"`
			Offer   map[string]interface{} `json:"offer"`
		}{
			"RELAY_BROADCAST",
			id,
			offer,
		})

		notifyTreeListeners(name)
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
}

func commandRelayBroadCastReceived(conn *websocket.Conn, id string, message map[string]interface{}) {
	peer, ok := stringProp(message, "peer")
	if !ok {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in RELAY_BROADCAST_RECEIVED message")
	}

	answer, ok := objectProp(message, "answer")
	if !ok {
		sendErrorMessage(conn, "No \"answer\" property specified or not an object in RELAY_BROADCAST_RECEIVED message")
	}

	log.Printf("Peer %v responding to %v with answer: %+v\n", id, peer, answer)

	if peerConnection, ok := connections[peer]; ok {
		peerConnection.WriteJSON(struct {
			Command string                 `json:"command"`
			Peer    string                 `json:"peer"`
			Answer  map[string]interface{} `json:"answer"`
		}{
			"JOIN_BROADCAST_RECEIVED",
			id,
			answer,
		})
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
}

func commandIceCandidates(conn *websocket.Conn, id string, message map[string]interface{}) {
	peer, ok := stringProp(message, "peer")
	if !ok {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in ICE_CANDIDATES message")
	}

	candidates, ok := arrayProp(message, "candidates")
	if !ok {
		sendErrorMessage(conn, "No \"candidates\" property specified or not an array in ICE_CANDIDATES message")
	}

	log.Printf("Peer %v sending ICE candidates to peer %v: %+v", id, peer, candidates)

	if peerConnection, ok := connections[peer]; ok {
		peerConnection.WriteJSON(struct {
			Command    string        `json:"command"`
			Peer       string        `json:"peer"`
			Candidates []interface{} `json:"candidates"`
		}{
			"ICE_CANDIDATES",
			id,
			candidates,
		})
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
}

func commandIceCandidatesReceived(conn *websocket.Conn, id string, message map[string]interface{}) {
	if peer, ok := stringProp(message, "peer"); ok {
		if peerConnection, ok := connections[peer]; ok {

			log.Printf("Peer %v ack-ing ICE candidates from peer %v", id, peer)

			peerConnection.WriteJSON(struct {
				Command string `json:"command"`
				Peer    string `json:"peer"`
			}{
				"ICE_CANDIDATES_RECEIVED",
				id,
			})
			return
		}

		sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
	} else {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in ICE_CANDIDATES_RECEIVED message")
	}
}

func commandSubscribeToTreeState(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in SUBSCRIBE_TO_TREE_STATE message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	_, ok = broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
		return
	}

	listeners, ok := treeStateListeners[name]
	if !ok {
		listeners = &map[string]*websocket.Conn{}
		treeStateListeners[name] = listeners
	}

	(*listeners)[id] = conn
	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"SUBSCRIBE_TO_TREE_STATE_RECEIVED",
	})

	notifyTreeListeners(name)
}

func commandUnsubscribeFromTreeState(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in UNSUBSCRIBE_FROM_TREE_STATE message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	_, ok = broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
		return
	}

	listeners, ok := treeStateListeners[name]
	if !ok {
		return
	}

	delete(*listeners, id)

	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"UNSUBSCRIBE_FROM_TREE_STATE_RECEIVED",
	})
}

func commandFetchConfig(conn *websocket.Conn, id string, message map[string]interface{}) {
	var iceJSON []interface{}
	decoder := json.NewDecoder(strings.NewReader(*iceServers))
	if err := decoder.Decode(&iceJSON); err != nil {
		log.Println(err)
		sendErrorMessage(conn, "Invalid ICE server JSON")
		return
	}

	conn.WriteJSON(struct {
		Command    string        `json:"command"`
		IceServers []interface{} `json:"iceServers"`
	}{
		"FETCH_CONFIG_RECEIVED",
		iceJSON,
	})
}

func findNodeWithSpareCapacity(root *TreeNode) *TreeNode {
	queue := []*TreeNode{root}
	var node *TreeNode

	for len(queue) > 0 {
		node, queue = queue[0], queue[1:]

		// FIXME: we can be more clever here in order to spread the load between the children better
		if len(node.children) < *maxListeners {
			return node
		}

		queue = append(queue, node.children...)
	}

	return nil
}

func notifyTreeListeners(broadcastName string) {
	broadcast, ok := broadcasts[broadcastName]
	if !ok {
		log.Printf("Unknown broadcast in notifyTreeListeners: %v\n", broadcastName)
		return
	}
	listeners, ok := treeStateListeners[broadcastName]
	if !ok {
		log.Printf("No tree state listeners for broadcast: %v\n", broadcastName)
		return
	}

	for _, conn := range *listeners {
		conn.WriteJSON(struct {
			Command string                 `json:"command"`
			Tree    map[string]interface{} `json:"tree"`
		}{
			"TREE_STATE_CHANGED",
			broadcast.json(),
		})
	}
}

func sendErrorMessage(conn *websocket.Conn, message string) {
	log.Println(message)
	conn.WriteJSON(struct {
		Command string `json:"command"`
		Message string `json:"message"`
	}{"ERROR", message})
}

func sendErrorMessageAndCode(conn *websocket.Conn, message string, errorCode int) {
	log.Println(message)
	conn.WriteJSON(struct {
		Message string `json:"message"`
		Code    int    `json:"code"`
	}{
		message,
		errorCode,
	})
}
<commit_msg>Handle when the broadcast disconnects unexpectedly<commit_after>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"github.com/gorilla/websocket"
	"github.com/satori/go.uuid"
	"log"
	"net/http"
	"strings"
	"sync"
)

type CommandHandlerFunc func(conn *websocket.Conn, id string, message map[string]interface{})
type TreeNode struct {
	conn     *websocket.Conn
	id       string
	name     string
	children []*TreeNode
	parent   *TreeNode
}

// FIXME: this is pretty terrible. We should use custom JSON marshaling, but I couln't get it to work
func (t *TreeNode) json() map[string]interface{} {
	result := map[string]interface{}{}
	result["id"] = t.id
	result["name"] = t.name

	children := []interface{}{}
	for _, child := range t.children {
		children = append(children, child.json())
	}
	result["children"] = children
	return result
}

var (
	port         = flag.Int("port", 8081, "Port the server listens on")
	maxListeners = flag.Int("max-listeners", 3, "Max number of listeners (WebRTC peers) for a single client")
	iceServers   = flag.String("ice-servers", `[]`, "The ICE servers the peers should use")
	upgrader     = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
	commandHandlers = map[string]CommandHandlerFunc{
		"START_BROADCAST":             commandStartBroadcast,
		"END_BROADCAST":               commandEndBroadcast,
		"JOIN_BROADCAST":              commandJoinBroadcast,
		"RELAY_BROADCAST_RECEIVED":    commandRelayBroadCastReceived,
		"ICE_CANDIDATES":              commandIceCandidates,
		"ICE_CANDIDATES_RECEIVED":     commandIceCandidatesReceived,
		"SUBSCRIBE_TO_TREE_STATE":     commandSubscribeToTreeState,
		"UNSUBSCRIBE_FROM_TREE_STATE": commandUnsubscribeFromTreeState,
		"FETCH_CONFIG":                commandFetchConfig,
	}
	broadcasts         = map[string]*TreeNode{}
	connections        = map[string]*websocket.Conn{}
	treeStateListeners = map[string]*map[string]*websocket.Conn{}
	globalLock         = sync.Mutex{} // FIXME: yeah, I know it's horrible, but we'll fix it later
)

func main() {
	flag.Parse()
	fs := http.FileServer(http.Dir("static"))
	http.Handle("/", fs)
	http.HandleFunc("/api/ws", handleWebSocket)
	log.Println("Server starting on port", *port)
	log.Fatal("ListenAndServe:", http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	log.Println("Incoming", r.Method, "message")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Failed to upgrade:", err)
		return
	}

	id := uuid.NewV4().String()

	for {
		var rawMessage interface{}
		if err := conn.ReadJSON(&rawMessage); err != nil {
			log.Printf("Read error: %v\n", err)
			handleDisconnect(id)
			conn.Close()
			return
		}

		messageObject, ok := rawMessage.(map[string]interface{})
		if !ok {
			sendErrorMessage(conn, "Message is not a JSON object")
			continue
		}

		command, ok := messageObject["command"].(string)
		if !ok {
			sendErrorMessage(conn, "Message is lacking a command property")
			continue
		}

		log.Printf("Received command: %v\n", command)
		if commandHandler, ok := commandHandlers[command]; ok {
			commandHandler(conn, id, messageObject)
		} else {
			sendErrorMessage(conn, fmt.Sprintf("Unknown command: %v", command))
			continue
		}
	}
}

func commandStartBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in START_BROADCAST message")
		return
	}

	_, ok = broadcasts[name]
	if ok {
		sendErrorMessage(conn, fmt.Sprintf("Broadcast \"%v\" already exists", name))
		return
	}

	peerName, ok := stringProp(message, "peerName")
	if !ok {
		peerName = "Anonymous"
	}

	log.Printf("Peer %v starting broadcast: %v", id, name)

	globalLock.Lock()
	defer globalLock.Unlock()

	broadcasts[name] = &TreeNode{
		conn: conn,
		id:   id,
		name: peerName,
	}
	connections[id] = conn
	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"START_BROADCAST_RECEIVED",
	})

	notifyTreeListeners(name)
}

func commandEndBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in END_BROADCAST message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	broadcast, ok := broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Broadcast \"%v\" does not exist", name))
		return
	}

	if broadcast.id != id {
		sendErrorMessage(conn, fmt.Sprintf("Peer \"%v\" did not start broadcast \"%v\"", id, name))
		return
	}

	endBroadcast(broadcast)

	conn.WriteJSON(struct {
		Command string `json:"command"`
		Name    string `json:"name"`
	}{
		"END_BROADCAST_RECEIVED",
		name,
	})
}

func commandJoinBroadcast(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in JOIN_BROADCAST message")
	}

	offer, ok := objectProp(message, "offer")
	if !ok {
		sendErrorMessage(conn, "No \"offer\" property specified or not an object in JOIN_BROADCAST message")
	}

	peerName, ok := stringProp(message, "peerName")
	if !ok {
		peerName = "Anonymous"
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	if broadcast, ok := broadcasts[name]; ok {
		parent := findNodeWithSpareCapacity(broadcast)
		if parent == nil {
			log.Panic("Received a nil node when inserting: %+v", broadcast)
		}

		node := TreeNode{
			conn:   conn,
			id:     id,
			name:   peerName,
			parent: parent,
		}
		connections[id] = conn
		parent.children = append(node.parent.children, &node)

		log.Printf("Peer %v joining broadcast %v as a child of %v which now has %d child(ren)\n",
			id, name, parent.id, len(parent.children))

		parent.conn.WriteJSON(struct {
			Command string                 `json:"command"`
			Peer    string                 `json:"peer"`
			Offer   map[string]interface{} `json:"offer"`
		}{
			"RELAY_BROADCAST",
			id,
			offer,
		})

		notifyTreeListeners(name)
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
}

func commandRelayBroadCastReceived(conn *websocket.Conn, id string, message map[string]interface{}) {
	peer, ok := stringProp(message, "peer")
	if !ok {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in RELAY_BROADCAST_RECEIVED message")
	}

	answer, ok := objectProp(message, "answer")
	if !ok {
		sendErrorMessage(conn, "No \"answer\" property specified or not an object in RELAY_BROADCAST_RECEIVED message")
	}

	log.Printf("Peer %v responding to %v with answer: %+v\n", id, peer, answer)

	if peerConnection, ok := connections[peer]; ok {
		peerConnection.WriteJSON(struct {
			Command string                 `json:"command"`
			Peer    string                 `json:"peer"`
			Answer  map[string]interface{} `json:"answer"`
		}{
			"JOIN_BROADCAST_RECEIVED",
			id,
			answer,
		})
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
}

func commandIceCandidates(conn *websocket.Conn, id string, message map[string]interface{}) {
	peer, ok := stringProp(message, "peer")
	if !ok {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in ICE_CANDIDATES message")
	}

	candidates, ok := arrayProp(message, "candidates")
	if !ok {
		sendErrorMessage(conn, "No \"candidates\" property specified or not an array in ICE_CANDIDATES message")
	}

	log.Printf("Peer %v sending ICE candidates to peer %v: %+v", id, peer, candidates)

	if peerConnection, ok := connections[peer]; ok {
		peerConnection.WriteJSON(struct {
			Command    string        `json:"command"`
			Peer       string        `json:"peer"`
			Candidates []interface{} `json:"candidates"`
		}{
			"ICE_CANDIDATES",
			id,
			candidates,
		})
		return
	}

	sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
}

func commandIceCandidatesReceived(conn *websocket.Conn, id string, message map[string]interface{}) {
	if peer, ok := stringProp(message, "peer"); ok {
		if peerConnection, ok := connections[peer]; ok {

			log.Printf("Peer %v ack-ing ICE candidates from peer %v", id, peer)

			peerConnection.WriteJSON(struct {
				Command string `json:"command"`
				Peer    string `json:"peer"`
			}{
				"ICE_CANDIDATES_RECEIVED",
				id,
			})
			return
		}

		sendErrorMessage(conn, fmt.Sprintf("Unknown peer: %v", peer))
	} else {
		sendErrorMessage(conn, "No \"peer\" property specified or not a string in ICE_CANDIDATES_RECEIVED message")
	}
}

func commandSubscribeToTreeState(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in SUBSCRIBE_TO_TREE_STATE message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	_, ok = broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
		return
	}

	listeners, ok := treeStateListeners[name]
	if !ok {
		listeners = &map[string]*websocket.Conn{}
		treeStateListeners[name] = listeners
	}

	(*listeners)[id] = conn
	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"SUBSCRIBE_TO_TREE_STATE_RECEIVED",
	})

	notifyTreeListeners(name)
}

func commandUnsubscribeFromTreeState(conn *websocket.Conn, id string, message map[string]interface{}) {
	name, ok := stringProp(message, "name")
	if !ok {
		sendErrorMessage(conn, "No \"name\" property specified or not a string in UNSUBSCRIBE_FROM_TREE_STATE message")
		return
	}

	globalLock.Lock()
	defer globalLock.Unlock()

	_, ok = broadcasts[name]
	if !ok {
		sendErrorMessage(conn, fmt.Sprintf("Unknown broadcast: %v", name))
		return
	}

	listeners, ok := treeStateListeners[name]
	if !ok {
		return
	}

	delete(*listeners, id)

	conn.WriteJSON(struct {
		Command string `json:"command"`
	}{
		"UNSUBSCRIBE_FROM_TREE_STATE_RECEIVED",
	})
}

func commandFetchConfig(conn *websocket.Conn, id string, message map[string]interface{}) {
	var iceJSON []interface{}
	decoder := json.NewDecoder(strings.NewReader(*iceServers))
	if err := decoder.Decode(&iceJSON); err != nil {
		log.Println(err)
		sendErrorMessage(conn, "Invalid ICE server JSON")
		return
	}

	conn.WriteJSON(struct {
		Command    string        `json:"command"`
		IceServers []interface{} `json:"iceServers"`
	}{
		"FETCH_CONFIG_RECEIVED",
		iceJSON,
	})
}

func findNodeWithSpareCapacity(root *TreeNode) *TreeNode {
	queue := []*TreeNode{root}
	var node *TreeNode

	for len(queue) > 0 {
		node, queue = queue[0], queue[1:]

		// FIXME: we can be more clever here in order to spread the load between the children better
		if len(node.children) < *maxListeners {
			return node
		}

		queue = append(queue, node.children...)
	}

	return nil
}

func endBroadcast(broadcast *TreeNode) {

	command := struct {
		Command string `json:"command"`
		Name    string `json:"name"`
	}{
		"BROADCAST_ENDED",
		broadcast.name,
	}

	var destroyTree func(node *TreeNode)
	destroyTree = func(node *TreeNode) {
		node.conn.WriteJSON(command)
		delete(connections, node.id)

		for _, child := range node.children {
			destroyTree(child)
		}
	}

	destroyTree(broadcast)
	delete(broadcasts, broadcast.name)
}

func handleDisconnect(id string) {
	globalLock.Lock()
	defer globalLock.Unlock()

	// check first if this connection was broadcasting, if so, shut down the broadcast
	for _, broadcast := range broadcasts {
		if broadcast.id == id {
			log.Printf(`Peer "%v" disconnected, ending broadcast "%v"`, id, broadcast.name)
			endBroadcast(broadcast)
			return
		}
	}

	// FIXME: handle interior node disconnects
}

func notifyTreeListeners(broadcastName string) {
	broadcast, ok := broadcasts[broadcastName]
	if !ok {
		log.Printf("Unknown broadcast in notifyTreeListeners: %v\n", broadcastName)
		return
	}
	listeners, ok := treeStateListeners[broadcastName]
	if !ok {
		log.Printf("No tree state listeners for broadcast: %v\n", broadcastName)
		return
	}

	for _, conn := range *listeners {
		conn.WriteJSON(struct {
			Command string                 `json:"command"`
			Tree    map[string]interface{} `json:"tree"`
		}{
			"TREE_STATE_CHANGED",
			broadcast.json(),
		})
	}
}

func sendErrorMessage(conn *websocket.Conn, message string) {
	log.Println(message)
	conn.WriteJSON(struct {
		Command string `json:"command"`
		Message string `json:"message"`
	}{"ERROR", message})
}

func sendErrorMessageAndCode(conn *websocket.Conn, message string, errorCode int) {
	log.Println(message)
	conn.WriteJSON(struct {
		Message string `json:"message"`
		Code    int    `json:"code"`
	}{
		message,
		errorCode,
	})
}
<|endoftext|>