mirror of https://github.com/veonik/squircy3
commit
1201a8dd50
@ -0,0 +1,3 @@
|
||||
out/*
|
||||
node_modules/*
|
||||
Dockerfile
|
@ -0,0 +1,7 @@
|
||||
.*
|
||||
!.gitignore
|
||||
!.dockerignore
|
||||
out/*
|
||||
node_modules/*
|
||||
yarn.lock
|
||||
package-lock.json
|
@ -0,0 +1,37 @@
|
||||
FROM golang:stretch AS build
|
||||
|
||||
WORKDIR /squircy
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go get -v ./...
|
||||
|
||||
RUN make clean all
|
||||
|
||||
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y ca-certificates curl gnupg
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y yarn
|
||||
|
||||
RUN useradd -d /home/squircy squircy
|
||||
|
||||
RUN mkdir -p /home/squircy && \
|
||||
chown -R squircy: /home/squircy
|
||||
|
||||
USER squircy
|
||||
|
||||
WORKDIR /squircy
|
||||
|
||||
COPY --from=build /squircy/out /squircy/out
|
||||
|
||||
RUN mkdir -p /home/squircy/.squircy/plugins && \
|
||||
ln -sfv /squircy/out/*.so /home/squircy/.squircy/plugins/
|
||||
|
||||
CMD out/squircy
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014, 2019 Tyler Sommer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,50 @@
|
||||
# Makefile for squircy, a proper IRC bot.
|
||||
# https://code.dopame.me/veonik/squircy3
|
||||
|
||||
SUBPACKAGES := config event irc plugin script vm $(wildcard plugins/*)
|
||||
|
||||
PLUGINS := $(patsubst plugins_shared/%,%,$(wildcard plugins_shared/*))
|
||||
|
||||
SOURCES := $(wildcard cmd/*/*.go) $(wildcard $(patsubst %,%/*.go,$(SUBPACKAGES)))
|
||||
GENERATOR_SOURCES := $(wildcard web/views/*.twig) $(wildcard web/views/*/*.twig) $(wildcard web/public/css/*.css)
|
||||
|
||||
OUTPUT_BASE := out
|
||||
|
||||
PLUGIN_TARGETS := $(patsubst %,$(OUTPUT_BASE)/%.so,$(PLUGINS))
|
||||
SQUIRCY_TARGET := $(OUTPUT_BASE)/squircy
|
||||
|
||||
.PHONY: all build generate run squircy plugins clean
|
||||
|
||||
all: build
|
||||
|
||||
clean:
|
||||
rm -rf $(OUTPUT_BASE)
|
||||
|
||||
build: plugins squircy
|
||||
|
||||
generate: $(OUTPUT_BASE)/.generated
|
||||
|
||||
squircy: $(SQUIRCY_TARGET)
|
||||
|
||||
plugins: $(PLUGIN_TARGETS)
|
||||
|
||||
run: build
|
||||
$(SQUIRCY_TARGET)
|
||||
|
||||
.SECONDEXPANSION:
|
||||
$(PLUGIN_TARGETS): $(OUTPUT_BASE)/%.so: $$(wildcard plugins_shared/%/*) $(SOURCES)
|
||||
go build -race -o $@ -buildmode=plugin plugins_shared/$*/*.go
|
||||
|
||||
$(SQUIRCY_TARGET): $(SOURCES)
|
||||
go build -race -o $@ cmd/squircy/*.go
|
||||
|
||||
$(OUTPUT_BASE)/.generated: $(GENERATOR_SOURCES)
|
||||
go generate
|
||||
touch $@
|
||||
|
||||
$(OUTPUT_BASE):
|
||||
mkdir -p $(OUTPUT_BASE)
|
||||
|
||||
$(SOURCES): $(OUTPUT_BASE)
|
||||
|
||||
$(GENERATOR_SOURCES): $(OUTPUT_BASE)
|
@ -0,0 +1,34 @@
|
||||
# squircy3
|
||||
|
||||
A proper IRC bot.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
squircy3 is a cross-platform application written in Go and should work just
|
||||
about anywhere. Using a plugin architecture, the bot's capabilities and
|
||||
functionality are expandable to support pretty much anything.
|
||||
|
||||
Core plugins provide IRC client functionality, central configuration, and
|
||||
an embedded JavaScript runtime with support for ES6 and beyond.
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
Clone this repository, then build using `make`.
|
||||
|
||||
```bash
|
||||
git clone git@code.dopame.me:veonik/squircy3
|
||||
cd squircy3
|
||||
make all
|
||||
```
|
||||
|
||||
The main `squircy` executable and all built plugins will be in `out/` after
|
||||
a successful build.
|
||||
|
||||
Run `squircy`.
|
||||
|
||||
```bash
|
||||
out/squircy
|
||||
```
|
||||
|
@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
tilde "gopkg.in/mattes/go-expand-tilde.v1"
|
||||
)
|
||||
|
||||
type stringsFlag []string
|
||||
|
||||
func (s stringsFlag) String() string {
|
||||
return strings.Join(s, "")
|
||||
}
|
||||
func (s *stringsFlag) Set(str string) error {
|
||||
*s = append(*s, str)
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringLevel logrus.Level
|
||||
|
||||
func (s stringLevel) String() string {
|
||||
return logrus.Level(s).String()
|
||||
}
|
||||
func (s *stringLevel) Set(str string) error {
|
||||
l, err := logrus.ParseLevel(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = stringLevel(l)
|
||||
return nil
|
||||
}
|
||||
|
||||
var rootDir string
|
||||
var extraPlugins stringsFlag
|
||||
var logLevel = stringLevel(logrus.DebugLevel)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&rootDir, "root", "~/.squircy", "path to folder containing squircy data")
|
||||
flag.Var(&logLevel, "log-level", "controls verbosity of logging output")
|
||||
flag.Var(&extraPlugins, "plugin", "path to shared plugin .so file, multiple plugins may be given")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: ", os.Args[0], "[options]")
|
||||
fmt.Println()
|
||||
fmt.Println("squircy is a proper IRC bot.")
|
||||
fmt.Println()
|
||||
fmt.Println("Options:")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
bp, err := tilde.Expand(rootDir)
|
||||
if err != nil {
|
||||
logrus.Fatalln(err)
|
||||
}
|
||||
err = os.MkdirAll(bp, os.FileMode(0644))
|
||||
if err != nil {
|
||||
logrus.Fatalln(err)
|
||||
}
|
||||
rootDir = bp
|
||||
}
|
||||
|
||||
func main() {
|
||||
logrus.SetLevel(logrus.Level(logLevel))
|
||||
m, err := NewManager(rootDir, extraPlugins...)
|
||||
if err != nil {
|
||||
logrus.Fatalln("error initializing squircy:", err)
|
||||
}
|
||||
if err = m.Loop(); err != nil {
|
||||
logrus.Fatalln("exiting main loop with error:", err)
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/plugins/babel"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"code.dopame.me/veonik/squircy3/config"
|
||||
"code.dopame.me/veonik/squircy3/event"
|
||||
"code.dopame.me/veonik/squircy3/irc"
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
"code.dopame.me/veonik/squircy3/script"
|
||||
"code.dopame.me/veonik/squircy3/vm"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
plugins *plugin.Manager
|
||||
rawConf map[string]interface{}
|
||||
|
||||
RootDir string `toml:"root_path"`
|
||||
ExtraPlugins []string `toml:"extra_plugins"`
|
||||
|
||||
sig chan os.Signal
|
||||
}
|
||||
|
||||
func NewManager(rootDir string, extraPlugins ...string) (*Manager, error) {
|
||||
m := plugin.NewManager()
|
||||
// initialize only the config plugin so that it can be configured before
|
||||
// other plugins are initialized
|
||||
m.RegisterFunc(config.Initialize)
|
||||
if err := configure(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// configure the config plugin!
|
||||
cf := filepath.Join(rootDir, "config.toml")
|
||||
err := config.ConfigurePlugin(m,
|
||||
config.WithValuesFromTOMLFile(cf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Manager{
|
||||
plugins: m,
|
||||
rawConf: make(map[string]interface{}),
|
||||
sig: make(chan os.Signal),
|
||||
RootDir: rootDir,
|
||||
ExtraPlugins: extraPlugins,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) Loop() error {
|
||||
m := manager.plugins
|
||||
|
||||
// init the remaining built-in plugins
|
||||
m.RegisterFunc(event.Initialize)
|
||||
m.RegisterFunc(vm.Initialize)
|
||||
m.RegisterFunc(irc.Initialize)
|
||||
m.RegisterFunc(babel.Initialize)
|
||||
m.RegisterFunc(script.Initialize)
|
||||
m.Register(plugin.InitializeFromFile(filepath.Join(manager.RootDir, "plugins/squircy2_compat.so")))
|
||||
if err := configure(m); err != nil {
|
||||
return errors.Wrap(err, "unable to init built-in plugins")
|
||||
}
|
||||
|
||||
// start the event dispatcher
|
||||
d, err := event.FromPlugins(m)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "expected event plugin to exist")
|
||||
}
|
||||
go d.Loop()
|
||||
|
||||
// start the js runtime
|
||||
myVM, err := vm.FromPlugins(m)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "expected vm plugin to exist")
|
||||
}
|
||||
err = myVM.Start()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to start vm")
|
||||
}
|
||||
|
||||
// load remaining extra plugins
|
||||
for _, pl := range manager.ExtraPlugins {
|
||||
m.Register(plugin.InitializeFromFile(pl))
|
||||
}
|
||||
if err := configure(m); err != nil {
|
||||
return errors.Wrap(err, "unable to init extra plugins")
|
||||
}
|
||||
|
||||
signal.Notify(manager.sig, os.Interrupt, syscall.SIGTERM)
|
||||
<-manager.sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func configure(m *plugin.Manager) error {
|
||||
errs := m.Configure()
|
||||
if errs != nil && len(errs) > 0 {
|
||||
if len(errs) > 1 {
|
||||
return errors.WithMessage(errs[0], fmt.Sprintf("(and %d more...)", len(errs)-1))
|
||||
}
|
||||
return errs[0]
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// Package config is a flexible configuration framework.
|
||||
//
|
||||
// This package defines a common interface for generic interaction with any
|
||||
// structured configuration data. Configuration is organized by named sections
|
||||
// that themselves contain values or other sections.
|
||||
//
|
||||
// In addition, this package defines a mutable configuration definition API
|
||||
// for defining options, sections, and validation for options and sections.
|
||||
//
|
||||
// Underlying data for each section is stored in a map[string]interface{} or a
|
||||
// struct with its exported fields being used as options. The underlying data
|
||||
// is kept in sync when mutating with Config.Set() using reflection.
|
||||
//
|
||||
// Use config.New or config.Wrap to create a root section and specify whatever
|
||||
// options desired.
|
||||
// type Config struct {
|
||||
// Name string
|
||||
// Bio *struct {
|
||||
// Age int
|
||||
// }
|
||||
// }
|
||||
// co := &Config{"veonik", &struct{Age int}{30}}
|
||||
// c, err := config.Wrap(co,
|
||||
// config.WithRequiredOption("Name"),
|
||||
// config.WithGenericSection("Bio", config.WithInitValue(co.Bio), config.WithRequiredOption("Age")))
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// fmt.Printf("Hi, %s!\n", co.Name)
|
||||
// n, _ := c.String("Name")
|
||||
// b, _ := c.Section("Bio")
|
||||
// a, _ := b.Int("Age")
|
||||
// fmt.Printf("%s is %d.\n", n, a)
|
||||
// // Outputs:
|
||||
// // Hi, veonik!
|
||||
// // veonik is 30.
|
||||
//
|
||||
package config // import "code.dopame.me/veonik/squircy3/config"
|
||||
|
||||
type Value interface{}
|
||||
|
||||
type Config interface {
|
||||
Get(key string) (Value, bool)
|
||||
String(key string) (string, bool)
|
||||
Bool(key string) (bool, bool)
|
||||
Int(key string) (int, bool)
|
||||
Set(key string, val Value)
|
||||
|
||||
Section(key string) (Config, error)
|
||||
}
|
||||
|
||||
type Section interface {
|
||||
Prefix() string
|
||||
Prototype() Value
|
||||
Singleton() bool
|
||||
}
|
||||
|
||||
func New(options ...SetupOption) (Config, error) {
|
||||
s := newSetup("root")
|
||||
if err := s.apply(options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := walkAndWrap(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.config, s.validate()
|
||||
}
|
||||
|
||||
func Wrap(wrapped Value, options ...SetupOption) (Config, error) {
|
||||
return New(append([]SetupOption{WithInitValue(wrapped)}, options...)...)
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/config"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
Bio *struct {
|
||||
Age int
|
||||
}
|
||||
}
|
||||
co := &Config{"veonik", &struct{ Age int }{30}}
|
||||
c, err := config.Wrap(co,
|
||||
config.WithRequiredOption("Name"),
|
||||
config.WithGenericSection("Bio", config.WithInitValue(co.Bio), config.WithRequiredOption("Age")))
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Hi, %s!\n", co.Name)
|
||||
n, ok := c.String("Name")
|
||||
if !ok {
|
||||
t.Errorf("expected name to be a string")
|
||||
return
|
||||
}
|
||||
b, err := c.Section("Bio")
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
a, ok := b.Int("Age")
|
||||
if !ok {
|
||||
t.Errorf("expected age to be an int")
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s is %d.\n", n, a)
|
||||
// Outputs:
|
||||
// Hi, veonik!
|
||||
// veonik is 30.
|
||||
}
|
||||
|
||||
func TestWrap2(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
Bio struct {
|
||||
Age int
|
||||
}
|
||||
}
|
||||
co := &Config{"veonik", struct{ Age int }{30}}
|
||||
c, err := config.Wrap(co,
|
||||
config.WithRequiredOption("Name"),
|
||||
config.WithGenericSection("Bio", config.WithInitValue(&co.Bio), config.WithRequiredOption("Age")))
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Hi, %s!\n", co.Name)
|
||||
n, ok := c.String("Name")
|
||||
if !ok {
|
||||
t.Errorf("expected name to be a string")
|
||||
return
|
||||
}
|
||||
b, err := c.Section("Bio")
|
||||
if err != nil {
|
||||
t.Errorf("expected config to be valid, but got error: %s", err)
|
||||
return
|
||||
}
|
||||
a, ok := b.Int("Age")
|
||||
if !ok {
|
||||
t.Errorf("expected age to be an int")
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s is %d.\n", n, a)
|
||||
// Outputs:
|
||||
// Hi, veonik!
|
||||
// veonik is 30.
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type section struct {
|
||||
name string
|
||||
prototype protoFunc
|
||||
singleton bool
|
||||
}
|
||||
|
||||
func (sec section) Prefix() string {
|
||||
return sec.name
|
||||
}
|
||||
|
||||
func (sec section) Prototype() Value {
|
||||
if sec.prototype != nil {
|
||||
return sec.prototype()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sec section) Singleton() bool {
|
||||
return sec.singleton
|
||||
}
|
||||
|
||||
func WithGenericSection(name string, options ...SetupOption) SetupOption {
|
||||
return WithSection(§ion{name: name}, options...)
|
||||
}
|
||||
|
||||
func WithSection(sec Section, options ...SetupOption) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
n := sec.Prefix()
|
||||
if _, ok := s.sections[n]; ok {
|
||||
return errors.Errorf(`section "%s" already exists`, n)
|
||||
}
|
||||
opts := make([]SetupOption, len(options))
|
||||
copy(opts, options)
|
||||
if sec.Singleton() {
|
||||
opts = append(opts, WithSingleton(true))
|
||||
}
|
||||
// Don't bother settings prototypes that return nil.
|
||||
if pr := sec.Prototype(); !isNil(pr) {
|
||||
opts = append(opts, WithInitPrototype(sec.Prototype))
|
||||
}
|
||||
ns := newSetup(n)
|
||||
if err := ns.apply(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
s.sections[ns.name] = ns
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSingleton(singleton bool) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
s.singleton = singleton
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithInitValue(value Value) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
s.prototype = nil
|
||||
s.initial = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithInitPrototype(proto func() Value) SetupOption {
|
||||
return func(c *Setup) error {
|
||||
c.initial = nil
|
||||
c.prototype = proto
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithOption(name string) SetupOption {
|
||||
return WithOptions(name)
|
||||
}
|
||||
|
||||
func WithOptions(names ...string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
for _, n := range names {
|
||||
s.options[n] = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRequiredOption(name string) SetupOption {
|
||||
return WithRequiredOptions(name)
|
||||
}
|
||||
|
||||
func WithRequiredOptions(names ...string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
for _, n := range names {
|
||||
s.options[n] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithValuesFromTOMLFile(filename string) SetupOption {
|
||||
return func(s *Setup) error {
|
||||
s.raw = make(map[string]interface{})
|
||||
if _, err := toml.DecodeFile(filename, &s.raw); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func pluginFromPlugins(m *plugin.Manager) (*configPlugin, error) {
|
||||
p, err := m.Lookup("config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mp, ok := p.(*configPlugin)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid config: unexpected value type")
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func ConfigurePlugin(m *plugin.Manager, opts ...SetupOption) error {
|
||||
mp, err := pluginFromPlugins(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mp.Configure(opts...)
|
||||
}
|
||||
|
||||
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
|
||||
p := &configPlugin{}
|
||||
m.OnPluginInit(p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type configPlugin struct {
|
||||
baseOptions []SetupOption
|
||||
current Config
|
||||
}
|
||||
|
||||
type configurablePlugin interface {
|
||||
plugin.Plugin
|
||||
|
||||
Options() []SetupOption
|
||||
Configure(config Config) error
|
||||
}
|
||||
|
||||
func (p *configPlugin) HandlePluginInit(op plugin.Plugin) {
|
||||
cp, ok := op.(configurablePlugin)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := p.Configure(WithGenericSection(cp.Name(), cp.Options()...))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v, err := p.current.Section(cp.Name())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = cp.Configure(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *configPlugin) Name() string {
|
||||
return "config"
|
||||
}
|
||||
|
||||
func (p *configPlugin) Configure(opts ...SetupOption) error {
|
||||
p.baseOptions = append(p.baseOptions, opts...)
|
||||
nc, err := Wrap(p.current, p.baseOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.current = nc
|
||||
return nil
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SetupOption func(c *Setup) error
|
||||
|
||||
type protoFunc func() Value
|
||||
|
||||
// Setup is a container struct with information on how to setup a given Config.
|
||||
type Setup struct {
|
||||
name string
|
||||
prototype protoFunc
|
||||
singleton bool
|
||||
initial Value
|
||||
config Config
|
||||
|
||||
raw map[string]interface{}
|
||||
|
||||
sections map[string]*Setup
|
||||
options map[string]bool
|
||||
}
|
||||
|
||||
func newSetup(name string) *Setup {
|
||||
return &Setup{
|
||||
name: name,
|
||||
prototype: nil,
|
||||
singleton: false,
|
||||
initial: nil,
|
||||
config: nil,
|
||||
raw: make(map[string]interface{}),
|
||||
sections: make(map[string]*Setup),
|
||||
options: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Setup) apply(options ...SetupOption) error {
|
||||
for _, o := range options {
|
||||
if err := o(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Setup) validate() error {
|
||||
if s.config == nil {
|
||||
return errors.New(`expected config to be populated, found nil`)
|
||||
}
|
||||
for o, reqd := range s.options {
|
||||
if reqd {
|
||||
var nilValue Value
|
||||
v, ok := s.config.Get(o)
|
||||
if !ok || v == nil || v == nilValue {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
}
|
||||
if vs, ok := v.(string); ok && len(vs) == 0 {
|
||||
return errors.Errorf(`required option "%s" is empty`, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
for sn, ss := range s.sections {
|
||||
if err := ss.validate(); err != nil {
|
||||
return errors.Wrapf(err, `config "%s" contains an invalid section "%s"`, s.name, sn)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func walkAndWrap(s *Setup) error {
|
||||
if isNil(s.initial) && s.prototype != nil {
|
||||
s.initial = s.prototype()
|
||||
}
|
||||
if isNil(s.initial) {
|
||||
s.initial = make(map[string]interface{})
|
||||
}
|
||||
if rc, ok := s.initial.(Config); ok {
|
||||
s.config = rc
|
||||
} else {
|
||||
co, err := newConfigurable(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.config = co
|
||||
}
|
||||
for _, ns := range s.sections {
|
||||
if v, ok := s.raw[ns.name].(map[string]interface{}); ok {
|
||||
ns.raw = v
|
||||
}
|
||||
if err := walkAndWrap(ns); err != nil {
|
||||
return err
|
||||
}
|
||||
s.config.Set(ns.name, ns.config)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/structtag"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type configurableOpts map[string]Value
|
||||
|
||||
type Configurable struct {
|
||||
Value
|
||||
options configurableOpts
|
||||
inspector *valueInspector
|
||||
}
|
||||
|
||||
func newConfigurable(s *Setup) (*Configurable, error) {
|
||||
if isNil(s.initial) {
|
||||
return nil, errors.New("unable to wrap <nil>")
|
||||
}
|
||||
is, err := inspect(s.initial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Configurable{Value: s.initial, options: make(configurableOpts), inspector: is}
|
||||
for k, v := range s.raw {
|
||||
c.Set(k, v)
|
||||
}
|
||||
for k := range s.options {
|
||||
v, err := c.inspector.Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Set(k, v)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Configurable) Get(key string) (Value, bool) {
|
||||
if v, ok := c.options[key]; ok {
|
||||
return v, true
|
||||
}
|
||||
if v, err := c.inspector.Get(key); err == nil {
|
||||
return v, true
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Configurable) String(key string) (string, bool) {
|
||||
v, ok := c.Get(key)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if vs, ok := v.(string); ok {
|
||||
return vs, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (c *Configurable) Bool(key string) (bool, bool) {
|
||||
v, ok := c.Get(key)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if vs, ok := v.(bool); ok {
|
||||
return vs, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (c *Configurable) Int(key string) (int, bool) {
|
||||
v, ok := c.Get(key)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if vs, ok := v.(int); ok {
|
||||
return vs, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *Configurable) Set(key string, val Value) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
c.options[key] = val
|
||||
c.inspector.Set(key, val)
|
||||
}
|
||||
|
||||
func (c *Configurable) Section(key string) (Config, error) {
|
||||
v := c.options[key]
|
||||
if vv, ok := v.(*Configurable); ok {
|
||||
return vv, nil
|
||||
}
|
||||
if s, ok := v.(Config); ok {
|
||||
return s, nil
|
||||
}
|
||||
return nil, errors.Errorf(`section "%s" contains unexpected type %T: %v`, key, v, v)
|
||||
}
|
||||
|
||||
type valueInspector struct {
|
||||
value reflect.Value
|
||||
typ reflect.Type
|
||||
tags map[string]structField
|
||||
}
|
||||
|
||||
func inspect(v Value) (*valueInspector, error) {
|
||||
vo := reflect.ValueOf(v)
|
||||
if vo.Kind() == reflect.Ptr {
|
||||
vo = reflect.Indirect(vo)
|
||||
}
|
||||
tags := map[string]structField{}
|
||||
if vo.Kind() == reflect.Struct {
|
||||
t, f, err := structTags(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, fd := range f {
|
||||
tags[fd.Name] = fd
|
||||
}
|
||||
for _, rt := range t {
|
||||
tags[rt.Name] = rt.Field
|
||||
}
|
||||
}
|
||||
t := reflect.TypeOf(vo)
|
||||
return &valueInspector{
|
||||
value: vo,
|
||||
typ: t,
|
||||
tags: tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *valueInspector) Get(name string) (Value, error) {
|
||||
return i.valueNamed(name)
|
||||
}
|
||||
|
||||
func (i *valueInspector) Set(name string, val Value) {
|
||||
if vc, ok := val.(*Configurable); ok {
|
||||
val = vc.Value
|
||||
}
|
||||
rv := reflect.ValueOf(val)
|
||||
if i.value.Kind() == reflect.Map {
|
||||
i.value.SetMapIndex(reflect.ValueOf(name), rv)
|
||||
return
|
||||
}
|
||||
var m reflect.Value
|
||||
if t, ok := i.tags[name]; ok {
|
||||
m = i.value.FieldByIndex(t.Index)
|
||||
}
|
||||
if !m.CanSet() {
|
||||
return
|
||||
}
|
||||
trySet(m, rv)
|
||||
}
|
||||
|
||||
func trySet(m reflect.Value, rv reflect.Value) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
logrus.Warnln("config: recovered panic:", v)
|
||||
}
|
||||
}()
|
||||
want := m.Kind()
|
||||
have := rv.Kind()
|
||||
if want != have {
|
||||
if want == reflect.Ptr {
|
||||
rv = reflect.Indirect(rv)
|
||||
} else if have == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
}
|
||||
m.Set(rv)
|
||||
}
|
||||
|
||||
func (i *valueInspector) valueNamed(name string) (Value, error) {
|
||||
var m reflect.Value
|
||||
if i.value.Kind() == reflect.Map {
|
||||
m = i.value.MapIndex(reflect.ValueOf(name))
|
||||
if !m.IsValid() {
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
if t, ok := i.tags[name]; ok {
|
||||
m = i.value.FieldByIndex(t.Index)
|
||||
}
|
||||
if !m.IsValid() {
|
||||
return nil, errors.New("no field with name " + name)
|
||||
}
|
||||
}
|
||||
if m.CanInterface() {
|
||||
return m.Interface(), nil
|
||||
}
|
||||
return m.Elem(), nil
|
||||
}
|
||||
|
||||
type structTag struct {
|
||||
Key string
|
||||
Name string
|
||||
Field structField
|
||||
}
|
||||
|
||||
type structField struct {
|
||||
Name string
|
||||
Index []int
|
||||
}
|
||||
|
||||
func structTags(v Value) ([]structTag, []structField, error) {
|
||||
var tags []structTag
|
||||
var fields []structField
|
||||
t := reflect.ValueOf(v)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = reflect.Indirect(t)
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, nil, errors.New("value is not a struct or ptr to struct")
|
||||
}
|
||||
tt := t.Type()
|
||||
for i := 0; i < tt.NumField(); i++ {
|
||||
f := tt.Field(i)
|
||||
ff := structField{f.Name, f.Index}
|
||||
fields = append(fields, ff)
|
||||
tgs, err := structtag.Parse(string(f.Tag))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, tg := range tgs.Tags() {
|
||||
tags = append(tags, structTag{Key: tg.Key, Name: tg.Name, Field: ff})
|
||||
}
|
||||
}
|
||||
return tags, fields, nil
|
||||
}
|
||||
|
||||
func isNil(v Value) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
vo := reflect.ValueOf(v)
|
||||
if vo.Kind() == reflect.Ptr && !vo.IsNil() {
|
||||
vo = reflect.Indirect(vo)
|
||||
}
|
||||
if !vo.IsValid() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigurable_withMap(t *testing.T) {
|
||||
co := map[string]interface{}{}
|
||||
s := newSetup("root")
|
||||
err := s.apply(WithInitValue(co))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
c, err := newConfigurable(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
v, ok := c.Value.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected value to be map[string]interface{}, got %T", c.Value)
|
||||
}
|
||||
if len(v) > 0 {
|
||||
t.Fatalf("expected value to be empty: %s", v)
|
||||
}
|
||||
c.Set("Test", "value")
|
||||
if co["Test"] != "value" {
|
||||
t.Fatalf("expected Test field on struct to contain 'value', but got '%s'", co["Test"])
|
||||
}
|
||||
vs, ok := c.String("Test")
|
||||
if !ok {
|
||||
t.Fatalf("expected Get call to return a value")
|
||||
}
|
||||
if vs != "value" {
|
||||
t.Fatalf("expected value to contain 'value', got '%s'", vs)
|
||||
}
|
||||
}
|
||||
|
||||
type TestConfig struct {
|
||||
Test string
|
||||
}
|
||||
|
||||
func TestConfigurable_withStruct(t *testing.T) {
|
||||
co := &TestConfig{}
|
||||
s := newSetup("root")
|
||||
err := s.apply(WithInitValue(co))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
c, err := newConfigurable(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
v, ok := c.Value.(*TestConfig)
|
||||
if !ok {
|
||||
t.Fatalf("expected value to be *Config, got %T", c.Value)
|
||||
}
|
||||
if len(v.Test) > 0 {
|
||||
t.Fatalf("expected value to be empty: %s", v)
|
||||
}
|
||||
c.Set("Test", "value")
|
||||
if co.Test != "value" {
|
||||
t.Fatalf("expected Test field on struct to contain 'value', but got '%s'", co.Test)
|
||||
}
|
||||
vs, ok := c.String("Test")
|
||||
if !ok {
|
||||
t.Fatalf("expected Get call to return a value")
|
||||
}
|
||||
if vs != "value" {
|
||||
t.Fatalf("expected value to contain 'value', got '%s'", vs)
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package event // import "code.dopame.me/veonik/squircy3/event"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Name string
|
||||
Data map[string]interface{}
|
||||
|
||||
handled bool
|
||||
}
|
||||
|
||||
func (e *Event) StopPropagation() {
|
||||
e.handled = true
|
||||
}
|
||||
|
||||
type Handler func(ev *Event)
|
||||
|
||||
type Dispatcher struct {
|
||||
handlers map[string][]Handler
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
emitting chan *Event
|
||||
}
|
||||
|
||||
func NewDispatcher() *Dispatcher {
|
||||
return &Dispatcher{handlers: make(map[string][]Handler), emitting: make(chan *Event, 8)}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Loop() {
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-d.emitting:
|
||||
if !ok {
|
||||
// closed channel, return
|
||||
return
|
||||
}
|
||||
evn := ev.Name
|
||||
d.mu.RLock()
|
||||
handlers, ok := d.handlers[evn]
|
||||
handlers = append([]Handler{}, handlers...)
|
||||
d.mu.RUnlock()
|
||||
if !ok || len(handlers) == 0 {
|
||||
// nothing to do
|
||||
continue
|
||||
}
|
||||
for _, h := range handlers {
|
||||
h(ev)
|
||||
if ev.handled {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Bind(name string, handler Handler) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.handlers[name] = append(d.handlers[name], handler)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Unbind(name string, handler Handler) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
hi := fmt.Sprintf("%v", handler)
|
||||
hs, ok := d.handlers[name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
i := -1
|
||||
for j, h := range hs {
|
||||
ohi := fmt.Sprintf("%v", h)
|
||||
if hi == ohi {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < 0 {
|
||||
return
|
||||
}
|
||||
d.handlers[name] = append(hs[:i], hs[i+1:]...)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) UnbindAll(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dispatcher) UnbindAllHandlers() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Emit(name string, data map[string]interface{}) {
|
||||
d.emitting <- &Event{Name: name, Data: data}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func FromPlugins(m *plugin.Manager) (*Dispatcher, error) {
|
||||
plg, err := m.Lookup("event")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mplg, ok := plg.(*eventPlugin)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("event: received unexpected plugin type")
|
||||
}
|
||||
return mplg.dispatcher, nil
|
||||
}
|
||||
|
||||
func Initialize(m *plugin.Manager) (plugin.Plugin, error) {
|
||||
p := &eventPlugin{NewDispatcher()}
|
||||
m.OnPluginInit(p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type eventPlugin struct {
|
||||
dispatcher *Dispatcher
|
||||
}
|
||||
|
||||
func (p *eventPlugin) Name() string {
|
||||
return "event"
|
||||
}
|
||||
|
||||
func (p *eventPlugin) HandlePluginInit(o plugin.Plugin) {
|
||||
p.dispatcher.Emit("plugin.INIT", map[string]interface{}{"name": o.Name(), "plugin": o})
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
module code.dopame.me/veonik/squircy3
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dlclark/regexp2 v1.1.6 // indirect
|
||||
github.com/dop251/goja v0.0.0-20190623141854-52cab25ecbdd
|
||||
github.com/dop251/goja_nodejs v0.0.0-20171011081505-adff31b136e6
|
||||
github.com/fatih/structtag v1.0.0
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/thoj/go-ircevent v0.0.0-20190609082534-949efec00844
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
@ -0,0 +1,35 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20190623141854-52cab25ecbdd h1:ePVZ9P4UZGmvnYUZvb6me3srRLN0TRm5T7tAXv3Phh0=
|
||||
github.com/dop251/goja v0.0.0-20190623141854-52cab25ecbdd/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20171011081505-adff31b136e6 h1:RrkoB0pT3gnjXhL/t10BSP1mcr/0Ldea2uMyuBr2SWk=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20171011081505-adff31b136e6/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/fatih/structtag v1.0.0 h1:pTHj65+u3RKWYPSGaU290FpI/dXxTaHdVwVwbcPKmEc=
|
||||
github.com/fatih/structtag v1.0.0/go.mod h1:IKitwq45uXL/yqi5mYghiD3w9H6eTOvI9vnk8tXMphA=
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
|
||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/thoj/go-ircevent v0.0.0-20190609082534-949efec00844 h1:v3FnDf1+Vba1yLiLUxAXXNl59l8N1XDepneKzv0ZZ7M=
|
||||
github.com/thoj/go-ircevent v0.0.0-20190609082534-949efec00844/go.mod h1:muMD1uYqFQjanGrR/FH9Pvd7YpKzzYm/G+wvvzTlfR8=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c h1:/Onz8dZtKBCmB8P0JU7+WSCfMekXry7BflVO0SQQrCU=
|
||||
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c/go.mod h1:j6QavCO5cYWud1+2/PFTXL1y6tjjkhSs+qcWgibOIc0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
@ -0,0 +1,187 @@
|
||||
package irc // import "code.dopame.me/veonik/squircy3/irc"
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.dopame.me/veonik/squircy3/event"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/thoj/go-ircevent"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Nick string `toml:"nick"`
|
||||
Username string `toml:"user"`
|
||||
|
||||
Network string `toml:"network"`
|
||||
TLS bool `toml:"tls"`
|
||||
AutoConnect bool `toml:"auto"`
|
||||
|
||||
SASL bool `toml:"sasl"`
|
||||
SASLUsername string `toml:"sasl_username"`
|
||||
SASLPassword string `toml:"sasl_password"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
config *Config
|
||||
events *event.Dispatcher
|
||||
conn *Connection
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
*irc.Connection
|
||||
|
||||
current Config
|
||||
quitting chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (conn *Connection) Connect() error {
|
||||
conn.Connection.Lock()
|
||||
defer conn.Connection.Unlock()
|
||||
return conn.Connection.Connect(conn.current.Network)
|
||||
}
|
||||
|
||||
func (conn *Connection) Quit() error {
|
||||
select {
|
||||
case <-conn.done:
|
||||
// already done, nothing to do
|
||||
|
||||
case <-conn.quitting:
|
||||
// already quitting, nothing to do
|
||||
|
||||
default:
|
||||
fmt.Println("quitting")
|
||||
conn.Connection.Quit()
|
||||
close(conn.quitting)
|
||||
}
|
||||
// block until done
|
||||
select {
|
||||
case <-conn.done:
|
||||
break
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
conn.Connection.Disconnect()
|
||||
return errors.Errorf("timed out waiting for quit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Connection) controlLoop() {
|
||||
errC := conn.ErrorChan()
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-errC:
|
||||
fmt.Println("read from errC in controlLoop")
|
||||
if !ok {
|
||||
// channel was closed
|
||||
fmt.Println("conn errs already closed")
|
||||
continue
|
||||
}
|
||||
fmt.Println("got err from conn:", err)
|
||||
conn.Lock()
|
||||
co := conn.Connected()
|
||||
conn.Unlock()
|
||||
if !co {
|
||||
// all done!
|
||||
close(conn.done)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewManager(c *Config, ev *event.Dispatcher) *Manager {
|
||||
return &Manager{config: c, events: ev}
|
||||
}
|
||||
|
||||
func (m *Manager) Do(fn func(*Connection) error) error {
|
||||
m.mu.RLock()
|
||||
conn := m.conn
|
||||
m.mu.RUnlock()
|
||||
if conn == nil {
|
||||
return errors.New("not connected")
|
||||
}
|
||||
conn.Lock()
|
||||
defer conn.Unlock()
|
||||
return fn(conn)
|
||||
}
|
||||
|
||||
func (m *Manager) Connection() (*Connection, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.conn == nil {
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
return m.conn, nil
|
||||
}
|
||||
|
||||
func newConnection(c Config) *Connection {
|
||||
conn := &Connection{
|
||||
current: c,
|
||||
quitting: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
conn.Connection = irc.IRC(c.Nick, c.Username)
|
||||
if c.TLS {
|
||||
conn.UseTLS = true
|
||||
conn.TLSConfig = &tls.Config{}
|
||||
}
|
||||
if c.SASL {
|
||||
conn.UseSASL = true
|
||||
conn.SASLLogin = c.SASLUsername
|
||||
conn.SASLPassword = c.SASLPassword
|
||||
}
|
||||
conn.QuitMessage = "farewell"
|
||||
return conn
|
||||
}
|
||||
|
||||
func (m *Manager) Connect() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.conn != nil {
|
||||
return errors.New("already connected")
|
||||
}
|
||||
m.conn = newConnection(*m.config)
|
||||
m.conn.AddCallback("*", func(ev *irc.Event) {
|
||||
m.events.Emit("irc."+ev.Code, map[string]interface{}{
|
||||
"User": ev.User,
|
||||
"Host": ev.Host,
|
||||
"Source": ev.Source,
|
||||
"Code": ev.Code,
|
||||
"Message": ev.Message(),
|
||||
"Nick": ev.Nick,
|
||||
"Target": ev.Arguments[0],
|
||||
"Raw": ev.Raw,
|
||||
"Args": append([]string{}, ev.Arguments...),
|
||||
})
|
||||
})
|
||||
err := m.conn.Connect()
|
||||
if err == nil {
|
||||
go m.conn.controlLoop()
|
||||
go func() {
|
||||
m.events.Emit("irc.CONNECT", nil)
|
||||
<-m.conn.done
|
||||
m.events.Emit("irc.DISCONNECT", nil)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.conn = nil
|
||||
}()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) Disconnect() error {
|
||||
m.mu.RLock()
|
||||
conn := m.conn
|
||||
m.mu.RUnlock()
|
||||
if conn == nil {
|
||||
return errors.New("not connected")
|
||||
}
|
||||
return conn.Quit()
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"code.dopame.me/veonik/squircy3/config"
|
||||
"code.dopame.me/veonik/squircy3/event"
|
||||
"code.dopame.me/veonik/squircy3/plugin"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const pluginName = "irc"
|
||||
|
||||
func pluginFromPlugins(m *plugin.Manager) (*ircPlugin, error) {
|
||||
p, err := m.Lookup(pluginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mp, ok := p.(*ircPlugin)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("%s: received unexpected plugin type", pluginName)
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func FromPlugins(m *plugin.Manager) (*Manager, error) {
|
||||
mp, err := pluginFromPlugins(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||