@ -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 | |||
} | |||
if mp.manager == nil { | |||
return nil, errors.Errorf("%s: plugin is not configured", pluginName) | |||
} | |||
return mp.manager, nil | |||
} | |||
func Initialize(m *plugin.Manager) (plugin.Plugin, error) { | |||
ev, err := event.FromPlugins(m) | |||
if err != nil { | |||
return nil, errors.Wrapf(err, "%s: missing required dependency (event)", pluginName) | |||
} | |||
p := &ircPlugin{events: ev} | |||
return p, nil | |||
} | |||
type ircPlugin struct { | |||
events *event.Dispatcher | |||
manager *Manager | |||
} | |||
func (p *ircPlugin) Configure(c config.Config) error { | |||
co, err := configFromGeneric(c) | |||
if err != nil { | |||
return err | |||
} | |||
p.manager = NewManager(co, p.events) | |||
return nil | |||
} | |||
func configFromGeneric(g config.Config) (c *Config, err error) { | |||
gc, ok := g.(*config.Configurable) | |||
if !ok { | |||
return c, errors.Errorf("%s: value is not a *config.Configurable", pluginName) | |||
} | |||
if gcv, ok := gc.Value.(*Config); ok { | |||
return gcv, nil | |||
} | |||
return c, errors.Errorf("%s: value is not a *irc.Config", pluginName) | |||
} | |||
func (p *ircPlugin) Options() []config.SetupOption { | |||
return []config.SetupOption{ | |||
config.WithInitValue(&Config{}), | |||
config.WithRequiredOptions("nick", "user", "network")} | |||
} | |||
func (p *ircPlugin) Name() string { | |||
return pluginName | |||
} |
@ -0,0 +1,16 @@ | |||
{ | |||
"name": "squircy-core", | |||
"version": "0.0.1", | |||
"license": "MIT", | |||
"private": true, | |||
"dependencies": { | |||
"@babel/standalone": "^7.5.5", | |||
"assert": "^2.0.0", | |||
"assert-polyfill": "^0.0.0", | |||
"buffer": "^5.2.1", | |||
"core-js-bundle": "^3.1.4", | |||
"process": "^0.11.10", | |||
"regenerator-runtime": "^0.13.3", | |||
"regenerator-transform": "^0.14.1" | |||
} | |||
} |
@ -0,0 +1,142 @@ | |||
package plugin // import "code.dopame.me/veonik/squircy3/plugin" | |||
import ( | |||
"plugin" | |||
"sync" | |||
"github.com/pkg/errors" | |||
) | |||
type Plugin interface { | |||
Name() string | |||
} | |||
type InitHandler interface { | |||
HandlePluginInit(Plugin) | |||
} | |||
type Initializer interface { | |||
Initialize(*Manager) (Plugin, error) | |||
} | |||
type InitializerFunc func(*Manager) (Plugin, error) | |||
func (f InitializerFunc) Initialize(m *Manager) (Plugin, error) { | |||
return f(m) | |||
} | |||
func InitializeFromFile(p string) Initializer { | |||
return InitializerFunc(func(m *Manager) (Plugin, error) { | |||
pl, err := plugin.Open(p) | |||
if err != nil { | |||
return nil, errors.Wrapf(err, "unable to open plugin (%s)", p) | |||
} | |||
in, err := pl.Lookup("Initialize") | |||
if err != nil { | |||
return nil, errors.Wrapf(err, "plugin does not export Initialize (%s)", p) | |||
} | |||
fn, ok := in.(func(*Manager) (Plugin, error)) | |||
if !ok { | |||
err := errors.Errorf("plugin has invalid type for Initialize (%s): expected func(*plugin.Manager) (plugin.Plugin, error)", p) | |||
return nil, err | |||
} | |||
plg, err := fn(m) | |||
if err != nil { | |||
return nil, errors.Wrapf(err, "plugin init failed (%s)", p) | |||
} | |||
return plg, nil | |||
}) | |||
} | |||
type Manager struct { | |||
plugins []Initializer | |||
loaded map[string]Plugin | |||
onInit []InitHandler | |||
mu sync.RWMutex | |||
} | |||
func NewManager(plugins ...string) *Manager { | |||
plgs := make([]Initializer, len(plugins)) | |||
for i, n := range plugins { | |||
plgs[i] = InitializeFromFile(n) | |||
} | |||
return &Manager{ | |||
plugins: plgs, | |||
loaded: make(map[string]Plugin), | |||
} | |||
} | |||
func (m *Manager) OnPluginInit(h InitHandler) { | |||
m.mu.Lock() | |||
defer m.mu.Unlock() | |||
m.onInit = append(m.onInit, h) | |||
} | |||
func (m *Manager) Lookup(name string) (Plugin, error) { | |||
m.mu.RLock() | |||
defer m.mu.RUnlock() | |||
if plg, ok := m.loaded[name]; ok { | |||
return plg, nil | |||
} | |||
return nil, errors.Errorf("no plugin named %s", name) | |||
} | |||
func (m *Manager) Register(initfn Initializer) { | |||