Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0586bea4 | ||
|
|
22b8e0fb70 | ||
|
|
aef22d0a8b | ||
|
|
550ce91a7f | ||
|
|
9d8f561b54 | ||
|
|
e12299ac20 | ||
|
|
29ae26153f | ||
|
|
8109bb3fa0 | ||
|
|
27605997c1 | ||
|
|
6c455764e1 | ||
|
|
31a3d4948b |
@@ -2,6 +2,7 @@ package broker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"local/truckstop/config"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,7 +38,8 @@ func (j JobLocation) String() string {
|
|||||||
|
|
||||||
func (j Job) FormatMultilineText() string {
|
func (j Job) FormatMultilineText() string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"--- %s => %s ---\nPickup: %s\nDropoff: %s\nNotes: %d lbs, %d miles, %s",
|
"--- %s: %s => %s ---\nPickup: %s\nDropoff: %s\nNotes: %d lbs, %d miles, %s",
|
||||||
|
config.Get().Name,
|
||||||
j.Pickup.State,
|
j.Pickup.State,
|
||||||
j.Dropoff.State,
|
j.Dropoff.State,
|
||||||
j.Pickup.String(),
|
j.Pickup.String(),
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func (ntg NTGVision) search(states []config.State) (io.ReadCloser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ntg NTGVision) refreshAuth() error {
|
func (ntg NTGVision) refreshAuth() error {
|
||||||
|
time.Sleep(time.Minute * 2) // TODO
|
||||||
b, _ := json.Marshal(map[string]string{
|
b, _ := json.Marshal(map[string]string{
|
||||||
"username": config.Get().Brokers.NTG.Username,
|
"username": config.Get().Brokers.NTG.Username,
|
||||||
"password": config.Get().Brokers.NTG.Password,
|
"password": config.Get().Brokers.NTG.Password,
|
||||||
@@ -146,7 +147,7 @@ func (ntg NTGVision) _search(states []config.State) (io.ReadCloser, error) {
|
|||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := ioutil.ReadAll(resp.Body)
|
b, _ := ioutil.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if resp.StatusCode > 400 && resp.StatusCode < 404 {
|
if resp.StatusCode > 400 && resp.StatusCode < 500 && resp.StatusCode != 404 && resp.StatusCode != 410 {
|
||||||
return nil, ErrNoAuth
|
return nil, ErrNoAuth
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("bad status searching ntg: %d: %s", resp.StatusCode, b)
|
return nil, fmt.Errorf("bad status searching ntg: %d: %s", resp.StatusCode, b)
|
||||||
|
|||||||
15
config.json
15
config.json
@@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Interval": "6h0m0s",
|
"Name": "pa",
|
||||||
|
"Interval": {
|
||||||
|
"Email": "15m0s..15m0s",
|
||||||
|
"OK": "6h0m0s..6h0m0s",
|
||||||
|
"Error": "6h0m0s..6h0m0s"
|
||||||
|
},
|
||||||
"States": [
|
"States": [
|
||||||
"GA"
|
"FL",
|
||||||
|
"GA",
|
||||||
|
"NC"
|
||||||
],
|
],
|
||||||
"Storage": [
|
"Storage": [
|
||||||
"map"
|
"map"
|
||||||
@@ -9,12 +16,14 @@
|
|||||||
"Client": "breellocaldev@gmail.com",
|
"Client": "breellocaldev@gmail.com",
|
||||||
"Message": {
|
"Message": {
|
||||||
"Matrix": {
|
"Matrix": {
|
||||||
|
"ReceiveEnabled": true,
|
||||||
|
"Client": "@belandbroc:matrix.org",
|
||||||
"Mock": true,
|
"Mock": true,
|
||||||
"Homeserver": "https://matrix-client.matrix.org",
|
"Homeserver": "https://matrix-client.matrix.org",
|
||||||
"Username": "@breellocaldev:matrix.org",
|
"Username": "@breellocaldev:matrix.org",
|
||||||
"Token": "syt_YnJlZWxsb2NhbGRldg_HTewKMMePdEcLvceAKEz_2fHsHa",
|
"Token": "syt_YnJlZWxsb2NhbGRldg_HTewKMMePdEcLvceAKEz_2fHsHa",
|
||||||
"Device": "TGNIOGKATZ",
|
"Device": "TGNIOGKATZ",
|
||||||
"Room": "!vVwjXhWXMxZtOwexKa:matrix.org"
|
"Room": "!rMvyKroCAJMRqFwTwC:matrix.org"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Once": true,
|
"Once": true,
|
||||||
|
|||||||
@@ -10,18 +10,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Interval Duration
|
Name string
|
||||||
States []State
|
Interval struct {
|
||||||
Storage []string
|
Email Duration
|
||||||
Client string
|
OK Duration
|
||||||
Message struct {
|
Error Duration
|
||||||
|
}
|
||||||
|
States []State
|
||||||
|
Storage []string
|
||||||
|
Client string
|
||||||
|
Message struct {
|
||||||
Matrix struct {
|
Matrix struct {
|
||||||
Mock bool
|
ReceiveEnabled bool
|
||||||
Homeserver string
|
Client string
|
||||||
Username string
|
Mock bool
|
||||||
Token string
|
Homeserver string
|
||||||
Device string
|
Username string
|
||||||
Room string
|
Token string
|
||||||
|
Device string
|
||||||
|
Room string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Once bool
|
Once bool
|
||||||
|
|||||||
@@ -2,17 +2,29 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Duration time.Duration
|
type Duration struct {
|
||||||
|
least time.Duration
|
||||||
|
most time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
func (d Duration) Get() time.Duration {
|
func (d Duration) Get() time.Duration {
|
||||||
return time.Duration(d)
|
jitter := d.most - d.least
|
||||||
|
if jitter >= time.Second {
|
||||||
|
jitter = time.Second * time.Duration(rand.Int()%int(jitter.Seconds()))
|
||||||
|
} else {
|
||||||
|
jitter = 0
|
||||||
|
}
|
||||||
|
return time.Duration(d.least + jitter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(d.Get().String())
|
return json.Marshal(d.least.String() + ".." + d.most.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||||
@@ -20,10 +32,25 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
|
|||||||
if err := json.Unmarshal(b, &s); err != nil {
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d2, err := time.ParseDuration(s)
|
if !strings.Contains(s, "..") {
|
||||||
|
s = s + ".." + s
|
||||||
|
}
|
||||||
|
splits := strings.Split(s, "..")
|
||||||
|
if len(splits) != 2 {
|
||||||
|
return errors.New("unexpected ..")
|
||||||
|
}
|
||||||
|
least, err := time.ParseDuration(splits[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*d = Duration(d2)
|
most, err := time.ParseDuration(splits[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if least > most {
|
||||||
|
most, least = least, most
|
||||||
|
}
|
||||||
|
d.most = most
|
||||||
|
d.least = least
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
105
config/duration_test.go
Normal file
105
config/duration_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDurationMarshal(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
in string
|
||||||
|
want Duration
|
||||||
|
wantJS string
|
||||||
|
}{
|
||||||
|
"empty string": {
|
||||||
|
wantJS: `"0s..0s"`,
|
||||||
|
},
|
||||||
|
"random in": {
|
||||||
|
in: "abc",
|
||||||
|
want: Duration{},
|
||||||
|
wantJS: `"0s..0s"`,
|
||||||
|
},
|
||||||
|
"1 dur": {
|
||||||
|
in: "1m",
|
||||||
|
want: Duration{
|
||||||
|
least: time.Minute,
|
||||||
|
most: time.Minute,
|
||||||
|
},
|
||||||
|
wantJS: `"1m0s..1m0s"`,
|
||||||
|
},
|
||||||
|
"descending": {
|
||||||
|
in: "2m..1m",
|
||||||
|
want: Duration{
|
||||||
|
least: time.Minute,
|
||||||
|
most: 2 * time.Minute,
|
||||||
|
},
|
||||||
|
wantJS: `"1m0s..2m0s"`,
|
||||||
|
},
|
||||||
|
"happy span": {
|
||||||
|
in: "1m..2m",
|
||||||
|
want: Duration{
|
||||||
|
least: time.Minute,
|
||||||
|
most: 2 * time.Minute,
|
||||||
|
},
|
||||||
|
wantJS: `"1m0s..2m0s"`,
|
||||||
|
},
|
||||||
|
"multi unit descending": {
|
||||||
|
in: "1h1m..2m3ms",
|
||||||
|
want: Duration{
|
||||||
|
least: 2*time.Minute + 3*time.Millisecond,
|
||||||
|
most: time.Hour + time.Minute,
|
||||||
|
},
|
||||||
|
wantJS: `"2m0.003s..1h1m0s"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
var got Duration
|
||||||
|
err := json.Unmarshal([]byte(`"`+c.in+`"`), &got)
|
||||||
|
if got != c.want {
|
||||||
|
t.Fatalf("err %v: want %+v, got %+v", err, c.want, got)
|
||||||
|
}
|
||||||
|
js, _ := json.Marshal(got)
|
||||||
|
if string(js) != c.wantJS {
|
||||||
|
t.Fatalf("want marshalled %s, got %s", c.wantJS, js)
|
||||||
|
}
|
||||||
|
var got2 Duration
|
||||||
|
json.Unmarshal(js, &got2)
|
||||||
|
if got != got2 {
|
||||||
|
t.Fatalf("want unmarshal-marshal-unmarshal %+v, got %+v", got, got2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationGet(t *testing.T) {
|
||||||
|
t.Run("same", func(t *testing.T) {
|
||||||
|
d := Duration{least: time.Second, most: time.Second}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := d.Get(); got != time.Second {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("1ms span", func(t *testing.T) {
|
||||||
|
d := Duration{least: time.Second, most: time.Second + time.Millisecond}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := d.Get(); got != time.Second {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("5s span", func(t *testing.T) {
|
||||||
|
d := Duration{least: time.Second, most: 6 * time.Second}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := d.Get(); got > time.Second*6 || got < time.Second {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
go.mod
13
go.mod
@@ -9,6 +9,7 @@ replace local/logb => ../logb
|
|||||||
replace local/sandbox/contact/contact => ../sandbox/contact/contact
|
replace local/sandbox/contact/contact => ../sandbox/contact/contact
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
||||||
local/sandbox/contact/contact v0.0.0-00010101000000-000000000000
|
local/sandbox/contact/contact v0.0.0-00010101000000-000000000000
|
||||||
local/storage v0.0.0-00010101000000-000000000000
|
local/storage v0.0.0-00010101000000-000000000000
|
||||||
)
|
)
|
||||||
@@ -33,7 +34,6 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.9 // indirect
|
github.com/json-iterator/go v1.1.9 // indirect
|
||||||
github.com/klauspost/compress v1.9.5 // indirect
|
github.com/klauspost/compress v1.9.5 // indirect
|
||||||
github.com/klauspost/cpuid v1.2.3 // indirect
|
github.com/klauspost/cpuid v1.2.3 // indirect
|
||||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 // indirect
|
|
||||||
github.com/minio/md5-simd v1.1.0 // indirect
|
github.com/minio/md5-simd v1.1.0 // indirect
|
||||||
github.com/minio/minio-go/v6 v6.0.57 // indirect
|
github.com/minio/minio-go/v6 v6.0.57 // indirect
|
||||||
github.com/minio/sha256-simd v0.1.1 // indirect
|
github.com/minio/sha256-simd v0.1.1 // indirect
|
||||||
@@ -46,22 +46,25 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rfjakob/eme v0.0.0-20171028163933-2222dbd4ba46 // indirect
|
github.com/rfjakob/eme v0.0.0-20171028163933-2222dbd4ba46 // indirect
|
||||||
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c // indirect
|
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c // indirect
|
||||||
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.0.2 // indirect
|
github.com/xdg-go/scram v1.0.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.2 // indirect
|
github.com/xdg-go/stringprep v1.0.2 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||||
go.mongodb.org/mongo-driver v1.7.2 // indirect
|
go.mongodb.org/mongo-driver v1.7.2 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // indirect
|
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // indirect
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
||||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
||||||
google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect
|
google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect
|
||||||
google.golang.org/appengine v1.3.0 // indirect
|
google.golang.org/appengine v1.3.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
local/logb v0.0.0-00010101000000-000000000000 // indirect
|
local/logb v0.0.0-00010101000000-000000000000 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
23
go.sum
23
go.sum
@@ -184,13 +184,15 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
github.com/t3rm1n4l/go-mega v0.0.0-20190205172012-55a226cf41da/go.mod h1:XWL4vDyd3JKmJx+hZWUVgCNmmhZ2dTBcaNDcxH465s0=
|
github.com/t3rm1n4l/go-mega v0.0.0-20190205172012-55a226cf41da/go.mod h1:XWL4vDyd3JKmJx+hZWUVgCNmmhZ2dTBcaNDcxH465s0=
|
||||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
@@ -208,15 +210,18 @@ golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
|
||||||
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba h1:YDkOrzGLLYybtuP6ZgebnO4OWYEYVMFSniazXsxrFN8=
|
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba h1:YDkOrzGLLYybtuP6ZgebnO4OWYEYVMFSniazXsxrFN8=
|
||||||
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -234,8 +239,13 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
|
|
||||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -268,7 +278,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
|||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
88
main.go
88
main.go
@@ -21,16 +21,26 @@ var stateFinder = regexp.MustCompile(`[A-Za-z]+`)
|
|||||||
func main() {
|
func main() {
|
||||||
lock := &sync.Mutex{}
|
lock := &sync.Mutex{}
|
||||||
go func() {
|
go func() {
|
||||||
c := time.NewTicker(time.Minute)
|
for {
|
||||||
for range c.C {
|
time.Sleep(config.Get().Interval.Email.Get())
|
||||||
if !config.Get().EmailerEnabled {
|
if err := config.Refresh(); err != nil {
|
||||||
continue
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
if config.Get().EmailerEnabled {
|
||||||
|
lock.Lock()
|
||||||
|
if err := email(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
lock.Unlock()
|
||||||
|
}
|
||||||
|
if config.Get().Message.Matrix.ReceiveEnabled {
|
||||||
|
lock.Lock()
|
||||||
|
if err := matrixrecv(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
lock.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lock.Lock()
|
|
||||||
if err := email(); err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := _main(); err != nil {
|
if err := _main(); err != nil {
|
||||||
@@ -39,6 +49,36 @@ func main() {
|
|||||||
lock.Lock()
|
lock.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matrixrecv() error {
|
||||||
|
log.Printf("checking matrix...")
|
||||||
|
defer log.Printf("/checking matrix...")
|
||||||
|
sender := message.NewMatrix()
|
||||||
|
messages, err := sender.Receive()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
states := map[config.State]struct{}{}
|
||||||
|
for _, msg := range messages {
|
||||||
|
if len(states) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, state := range parseOutStates([]byte(msg)) {
|
||||||
|
states[state] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(states) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conf := *config.Get()
|
||||||
|
conf.States = []config.State{}
|
||||||
|
for k := range states {
|
||||||
|
conf.States = append(conf.States, k)
|
||||||
|
}
|
||||||
|
log.Printf("%+v, %+v", states, conf)
|
||||||
|
config.Set(conf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func email() error {
|
func email() error {
|
||||||
log.Printf("checking email...")
|
log.Printf("checking email...")
|
||||||
ch, err := config.Get().Emailer.ReadIMAP()
|
ch, err := config.Get().Emailer.ReadIMAP()
|
||||||
@@ -94,20 +134,35 @@ func parseOutStates(b []byte) []config.State {
|
|||||||
|
|
||||||
func _main() error {
|
func _main() error {
|
||||||
for {
|
for {
|
||||||
if err := config.Refresh(); err != nil {
|
err := _mainOne()
|
||||||
return err
|
if err != nil {
|
||||||
}
|
log.Println(err)
|
||||||
if err := once(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if config.Get().Once {
|
if config.Get().Once {
|
||||||
break
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(config.Get().Interval.Error.Get())
|
||||||
|
} else {
|
||||||
|
time.Sleep(config.Get().Interval.OK.Get())
|
||||||
}
|
}
|
||||||
time.Sleep(config.Get().Interval.Get())
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _mainOne() error {
|
||||||
|
log.Println("config.refreshing...")
|
||||||
|
if err := config.Refresh(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("once...")
|
||||||
|
if err := once(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("/_mainOne")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func once() error {
|
func once() error {
|
||||||
alljobs, err := getJobs()
|
alljobs, err := getJobs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,6 +180,7 @@ func once() error {
|
|||||||
if err := sendJob(jobs[i]); err != nil {
|
if err := sendJob(jobs[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Println("sent job", jobs[i])
|
||||||
if err := config.Get().DB().Set(jobs[i].ID, []byte(`sent`)); err != nil {
|
if err := config.Get().DB().Set(jobs[i].ID, []byte(`sent`)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Matrix struct {
|
|||||||
username string
|
username string
|
||||||
token string
|
token string
|
||||||
room string
|
room string
|
||||||
|
client string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMatrix() Matrix {
|
func NewMatrix() Matrix {
|
||||||
@@ -23,19 +24,49 @@ func NewMatrix() Matrix {
|
|||||||
token: conf.Token,
|
token: conf.Token,
|
||||||
room: conf.Room,
|
room: conf.Room,
|
||||||
mock: conf.Mock,
|
mock: conf.Mock,
|
||||||
|
client: conf.Client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Matrix) client() (*gomatrix.Client, error) {
|
func (m Matrix) getclient() (*gomatrix.Client, error) {
|
||||||
return gomatrix.NewClient(m.homeserver, m.username, m.token)
|
return gomatrix.NewClient(m.homeserver, m.username, m.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Matrix) Receive() ([]string, error) {
|
||||||
|
if m.mock {
|
||||||
|
log.Printf("matrix.Receive()")
|
||||||
|
return []string{"FL, GA, NC"}, nil
|
||||||
|
}
|
||||||
|
c, err := m.getclient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
messages := make([]string, 0)
|
||||||
|
result, err := c.Messages(m.room, "", "", 'b', 50)
|
||||||
|
for _, event := range result.Chunk {
|
||||||
|
if event.Sender != m.client {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch event.Type {
|
||||||
|
case "m.room.message":
|
||||||
|
b, ok := event.Body()
|
||||||
|
if ok {
|
||||||
|
messages = append(messages, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return messages, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Matrix) Send(text string) error {
|
func (m Matrix) Send(text string) error {
|
||||||
if m.mock {
|
if m.mock {
|
||||||
log.Printf("matrix.Send(%s)", text)
|
log.Printf("matrix.Send(%s)", text)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c, err := m.client()
|
c, err := m.getclient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,28 @@ func TestMatrixSend(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMatrixReceive(t *testing.T) {
|
||||||
|
if len(os.Getenv("INTEGRATION")) == 0 {
|
||||||
|
t.Skip("$INTEGRATION not set")
|
||||||
|
}
|
||||||
|
var c config.Config
|
||||||
|
b, err := ioutil.ReadFile("../config.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &c); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var sender Sender = Matrix{
|
||||||
|
homeserver: c.Message.Matrix.Homeserver,
|
||||||
|
username: c.Message.Matrix.Username,
|
||||||
|
token: c.Message.Matrix.Token,
|
||||||
|
room: c.Message.Matrix.Room,
|
||||||
|
}
|
||||||
|
if msgs, err := sender.Receive(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
t.Logf("%+v", msgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ package message
|
|||||||
|
|
||||||
type Sender interface {
|
type Sender interface {
|
||||||
Send(string) error
|
Send(string) error
|
||||||
|
Receive() ([]string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ todo:
|
|||||||
subtasks:
|
subtasks:
|
||||||
- banlist criteria like vendors, brokers, metadata
|
- banlist criteria like vendors, brokers, metadata
|
||||||
- quiet hours
|
- quiet hours
|
||||||
- setup pa on element
|
- setup ma on element
|
||||||
|
- accept states via element for one system
|
||||||
|
- set up copy for caleb, broc
|
||||||
done:
|
done:
|
||||||
|
- setup pa on element
|
||||||
|
- configurable email interval
|
||||||
|
- jitter on intervals, including dedicated err span
|
||||||
- email doesnt get all matches
|
- email doesnt get all matches
|
||||||
- send jobs
|
- send jobs
|
||||||
- read jobs
|
- read jobs
|
||||||
|
|||||||
Reference in New Issue
Block a user