Compare commits

...

13 Commits

Author SHA1 Message Date
Bel LaPointe
6569631928 on set new states, send confirmation message 2022-01-11 22:21:45 -05:00
Bel LaPointe
e963162303 todo 2022-01-11 17:06:56 -05:00
Bel LaPointe
ac9ccf633a Setnewstates as method, sort, check and set to skip noop 2022-01-11 17:04:16 -05:00
Bel LaPointe
b4007a8eb5 todo 2022-01-11 16:59:41 -05:00
Bel LaPointe
358eb0a724 change interval email 2022-01-11 09:47:21 -05:00
Bel LaPointe
2d0586bea4 wait 2m before ntg auth refresh 2022-01-11 09:43:14 -05:00
Bel LaPointe
22b8e0fb70 receive from enabled 2022-01-11 09:42:42 -05:00
Bel LaPointe
aef22d0a8b new room unencrypted in matrix, impl matrix recv 2022-01-11 09:23:15 -05:00
Bel LaPointe
550ce91a7f accept more noauth codes from ntg 2022-01-11 08:35:07 -05:00
Bel LaPointe
9d8f561b54 logs 2022-01-11 08:24:46 -05:00
Bel LaPointe
e12299ac20 add config name for identifying who wants what 2022-01-11 08:16:02 -05:00
Bel LaPointe
29ae26153f add logs 2022-01-11 07:58:08 -05:00
Bel LaPointe
8109bb3fa0 sleep on start before getting email 2022-01-11 07:57:11 -05:00
11 changed files with 195 additions and 44 deletions

View File

@@ -2,6 +2,7 @@ package broker
import (
"fmt"
"local/truckstop/config"
"time"
)
@@ -37,7 +38,8 @@ func (j JobLocation) String() string {
func (j Job) FormatMultilineText() string {
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.Dropoff.State,
j.Pickup.String(),

View File

@@ -100,6 +100,7 @@ func (ntg NTGVision) search(states []config.State) (io.ReadCloser, error) {
}
func (ntg NTGVision) refreshAuth() error {
time.Sleep(time.Minute * 2) // TODO
b, _ := json.Marshal(map[string]string{
"username": config.Get().Brokers.NTG.Username,
"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 {
b, _ := ioutil.ReadAll(resp.Body)
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, fmt.Errorf("bad status searching ntg: %d: %s", resp.StatusCode, b)

View File

@@ -1,11 +1,14 @@
{
"Name": "pa",
"Interval": {
"OK": "6h0m0s",
"Error": "6h",
"Email": "15m"
"Email": "10s..30s",
"OK": "6h0m0s..6h0m0s",
"Error": "6h0m0s..6h0m0s"
},
"States": [
"GA"
"FL",
"GA",
"NC"
],
"Storage": [
"map"
@@ -13,12 +16,14 @@
"Client": "breellocaldev@gmail.com",
"Message": {
"Matrix": {
"ReceiveEnabled": true,
"Client": "@belandbroc:matrix.org",
"Mock": true,
"Homeserver": "https://matrix-client.matrix.org",
"Username": "@breellocaldev:matrix.org",
"Token": "syt_YnJlZWxsb2NhbGRldg_HTewKMMePdEcLvceAKEz_2fHsHa",
"Device": "TGNIOGKATZ",
"Room": "!vVwjXhWXMxZtOwexKa:matrix.org"
"Room": "!rMvyKroCAJMRqFwTwC:matrix.org"
}
},
"Once": true,

View File

@@ -10,6 +10,7 @@ import (
)
type Config struct {
Name string
Interval struct {
Email Duration
OK Duration
@@ -20,6 +21,8 @@ type Config struct {
Client string
Message struct {
Matrix struct {
ReceiveEnabled bool
Client string
Mock bool
Homeserver string
Username string

13
go.mod
View File

@@ -9,6 +9,7 @@ replace local/logb => ../logb
replace local/sandbox/contact/contact => ../sandbox/contact/contact
require (
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
local/sandbox/contact/contact 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/klauspost/compress v1.9.5 // 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/minio-go/v6 v6.0.57 // 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/rfjakob/eme v0.0.0-20171028163933-2222dbd4ba46 // 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/tidwall/pretty v1.2.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.7.2 // indirect
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // 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/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect
google.golang.org/appengine v1.3.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
)

23
go.sum
View File

@@ -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.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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
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/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
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.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/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
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-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-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-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-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-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-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-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/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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-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-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-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.5/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/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.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

78
main.go
View File

@@ -11,6 +11,7 @@ import (
"local/truckstop/message"
"log"
"regexp"
"sort"
"strings"
"sync"
"time"
@@ -22,6 +23,10 @@ func main() {
lock := &sync.Mutex{}
go func() {
for {
time.Sleep(config.Get().Interval.Email.Get())
if err := config.Refresh(); err != nil {
log.Println(err)
} else {
if config.Get().EmailerEnabled {
lock.Lock()
if err := email(); err != nil {
@@ -29,7 +34,14 @@ func main() {
}
lock.Unlock()
}
time.Sleep(config.Get().Interval.Email.Get())
if config.Get().Message.Matrix.ReceiveEnabled {
lock.Lock()
if err := matrixrecv(); err != nil {
log.Print(err)
}
lock.Unlock()
}
}
}
}()
if err := _main(); err != nil {
@@ -38,6 +50,50 @@ func main() {
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{}{}
}
}
setNewStates(states)
return nil
}
func setNewStates(states map[config.State]struct{}) {
if len(states) == 0 {
return
}
newstates := []config.State{}
for k := range states {
newstates = append(newstates, k)
}
sort.Slice(newstates, func(i, j int) bool {
return newstates[i] < newstates[j]
})
conf := *config.Get()
if fmt.Sprint(newstates) == fmt.Sprint(conf.States) {
return
}
conf.States = newstates
log.Printf("updating config new states: %+v", conf)
config.Set(conf)
if err := sendNewStates(conf.States); err != nil {
log.Printf("failed to send new states %+v: %v", conf.States, err)
}
}
func email() error {
log.Printf("checking email...")
ch, err := config.Get().Emailer.ReadIMAP()
@@ -60,16 +116,7 @@ func email() error {
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)
setNewStates(states)
return nil
}
@@ -110,12 +157,15 @@ func _main() error {
}
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
}
@@ -136,6 +186,7 @@ func once() error {
if err := sendJob(jobs[i]); err != nil {
return err
}
log.Println("sent job", jobs[i])
if err := config.Get().DB().Set(jobs[i].ID, []byte(`sent`)); err != nil {
return err
}
@@ -183,3 +234,8 @@ func sendJob(job broker.Job) error {
sender := message.NewMatrix()
return sender.Send(job.FormatMultilineText())
}
func sendNewStates(states []config.State) error {
sender := message.NewMatrix()
return sender.Send(fmt.Sprintf("now searching for loads from: %+v", states))
}

View File

@@ -13,6 +13,7 @@ type Matrix struct {
username string
token string
room string
client string
}
func NewMatrix() Matrix {
@@ -23,19 +24,49 @@ func NewMatrix() Matrix {
token: conf.Token,
room: conf.Room,
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)
}
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 {
if m.mock {
log.Printf("matrix.Send(%s)", text)
return nil
}
c, err := m.client()
c, err := m.getclient()
if err != nil {
return err
}

View File

@@ -30,3 +30,28 @@ func TestMatrixSend(t *testing.T) {
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)
}
}

View File

@@ -2,4 +2,5 @@ package message
type Sender interface {
Send(string) error
Receive() ([]string, error)
}

View File

@@ -1,11 +1,24 @@
todo:
- send matrix msg on config change
- rm email
- modify old items once no longer available
- many users -> 1 ntg query
- accept after date
- "caleb: my-usual-stuff" to alias
- rate LIMIT
- more than NTG
- accept pause commands
- rate limit brokers
- write to matrix on config change like states
- todo: filter out jobs like CA
subtasks:
- banlist criteria like vendors, brokers, metadata
- quiet hours
- setup pa on element
- setup ma on element
- accept states via element for one system
- set up copy for caleb, broc
done:
- setup pa on element
- configurable email interval
- jitter on intervals, including dedicated err span
- email doesnt get all matches