Browse Source

Life, the universe, and everything

pull/1/head
Tyler Sommer 2 years ago
commit
1201a8dd50
Signed by: tyler-sommer GPG Key ID: C09C010500DBD008
40 changed files with 3572 additions and 0 deletions
  1. +3
    -0
      .dockerignore
  2. +7
    -0
      .gitignore
  3. +37
    -0
      Dockerfile
  4. +21
    -0
      LICENSE
  5. +50
    -0
      Makefile
  6. +34
    -0
      README.md
  7. +75
    -0
      cmd/squircy/main.go
  8. +108
    -0
      cmd/squircy/manager.go
  9. +71
    -0
      config/config.go
  10. +81
    -0
      config/config_test.go
  11. +114
    -0
      config/option.go
  12. +78
    -0
      config/plugin.go
  13. +97
    -0
      config/setup.go
  14. +250
    -0
      config/value.go
  15. +71
    -0
      config/value_test.go
  16. +98
    -0
      event/event.go
  17. +37
    -0
      event/plugin.go
  18. +19
    -0
      go.mod
  19. +35
    -0
      go.sum
  20. +187
    -0
      irc/irc.go
  21. +79
    -0
      irc/plugin.go
  22. +16
    -0
      package.json
  23. +142
    -0
      plugin/plugin.go
  24. +59
    -0
      plugins/babel/babel.go
  25. +56
    -0
      plugins/babel/plugin.go
  26. +43
    -0
      plugins/squircy2_compat/data/data.go
  27. +115
    -0
      plugins/squircy2_compat/data/model.go
  28. +160
    -0
      plugins/squircy2_compat/data/tiedot_shim.go
  29. +173
    -0
      plugins/squircy2_compat/helper.go
  30. +264
    -0
      plugins/squircy2_compat/init_runtime.go
  31. +79
    -0
      plugins/squircy2_compat/plugin.go
  32. +15
    -0
      plugins_shared/nlp/nlp.go
  33. +14
    -0
      plugins_shared/squircy2_compat/main.go
  34. +89
    -0
      script/plugin.go
  35. +55
    -0
      script/script.go
  36. +88
    -0
      vm/plugin.go
  37. +181
    -0
      vm/require.go
  38. +218
    -0
      vm/scheduler.go
  39. +200
    -0
      vm/vm.go
  40. +53
    -0
      vm/vm_test.go

+ 3
- 0
.dockerignore View File

@ -0,0 +1,3 @@
out/*
node_modules/*
Dockerfile

+ 7
- 0
.gitignore View File

@ -0,0 +1,7 @@
.*
!.gitignore
!.dockerignore
out/*
node_modules/*
yarn.lock
package-lock.json

+ 37
- 0
Dockerfile View File

@ -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

+ 21
- 0
LICENSE View File

@ -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.

+ 50
- 0
Makefile View File

@ -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)

+ 34
- 0
README.md View File

@ -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
```

+ 75
- 0
cmd/squircy/main.go View File

@ -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)
}
}

+ 108
- 0
cmd/squircy/manager.go View File

@ -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
}

+ 71
- 0
config/config.go View File

@ -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...)...)
}

+ 81
- 0
config/config_test.go View File

@ -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.
}

+ 114
- 0
config/option.go View File

@ -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(&section{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
}
}

+ 78
- 0
config/plugin.go View File

@ -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
}

+ 97
- 0
config/setup.go View File

@ -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
}

+ 250
- 0
config/value.go View File

@ -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
}

+ 71
- 0
config/value_test.go View File

@ -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)
}
}

+ 98
- 0
event/event.go View File

@ -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}
}

+ 37
- 0
event/plugin.go View File

@ -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})
}

+ 19
- 0
go.mod View File

@ -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
)

+ 35
- 0
go.sum View File

@ -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=

+ 187
- 0
irc/irc.go View File

@ -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()
}

+ 79
- 0
irc/plugin.go View File

@ -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
}

+ 16
- 0
package.json View File

@ -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"
}
}

+ 142
- 0
plugin/plugin.go View File

@ -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) {