mirror of
https://github.com/prosolis/gogobee.git
synced 2026-09-14 19:01:09 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec63f660d1 | ||
|
|
b1e6937c0c | ||
|
|
ae5e10435a |
@@ -14,7 +14,7 @@ require (
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/image v0.40.0
|
||||
maunium.net/go/mautrix v0.28.1
|
||||
maunium.net/go/mautrix v0.30.0
|
||||
modernc.org/sqlite v1.50.1
|
||||
)
|
||||
|
||||
@@ -27,21 +27,21 @@ require (
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.45 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.49 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tidwall/gjson v1.19.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
go.mau.fi/util v0.9.10 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
go.mau.fi/util v0.10.0 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
@@ -38,16 +38,16 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
|
||||
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/notnil/joker v0.0.0-20180219043703-3f2f69a75914 h1:xXPuFr3PVM4p6Vw3j0CP29oWYRVKO3cPZjR6D7BxggQ=
|
||||
github.com/notnil/joker v0.0.0-20180219043703-3f2f69a75914/go.mod h1:L0Sdr2nYdktjerdXpIn9wOCn+GebPs/nCL2qH6RTGa0=
|
||||
github.com/olebedev/when v1.1.0 h1:dlpoRa7huImhNtEx4yl0WYfTHVEWmJmIWd7fEkTHayc=
|
||||
github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -75,18 +75,18 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
|
||||
go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
|
||||
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
|
||||
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
|
||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -94,8 +94,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -105,8 +105,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -114,8 +114,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -128,8 +128,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -148,24 +148,24 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0=
|
||||
maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
|
||||
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
|
||||
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
|
||||
@@ -350,6 +350,12 @@ func scanExpeditionRows(rows *sql.Rows) ([]*Expedition, error) {
|
||||
// A double-fire on the same expedition is a no-op.
|
||||
func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
priorBriefing := e.LastBriefingAt
|
||||
// Everything logged since the previous briefing is what the overnight
|
||||
// digest reports. Captured before the CAS below clobbers the column.
|
||||
digestSince := e.StartDate
|
||||
if priorBriefing != nil {
|
||||
digestSince = *priorBriefing
|
||||
}
|
||||
threshold := time.Date(now.Year(), now.Month(), now.Day(),
|
||||
expeditionBriefingHour, 0, 0, 0, time.UTC)
|
||||
res, err := db.Get().Exec(`
|
||||
@@ -373,7 +379,7 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
// DM (rollover happened recently) or force-fires processNightCamp
|
||||
// itself (safety net for stalled autopilots).
|
||||
if isEventAnchored(e) {
|
||||
return p.deliverBriefingEventAnchored(e, priorBriefing)
|
||||
return p.deliverBriefingEventAnchored(e, priorBriefing, digestSince)
|
||||
}
|
||||
|
||||
burn, err := p.nightRolloverBurn(e)
|
||||
@@ -400,6 +406,9 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
|
||||
line := pickMorningBriefing(e.CurrentDay)
|
||||
body := renderMorningBriefing(e, line, burn)
|
||||
// The single daily message: fold in what the now-silent recap, night
|
||||
// check and ambient events recorded since the last briefing.
|
||||
body = appendOvernightDigest(body, e.ID, digestSince)
|
||||
if sl := p.shadowBriefingLine(e); sl != "" {
|
||||
body += "\n" + sl + "\n"
|
||||
}
|
||||
@@ -413,7 +422,13 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
body += "\n" + ml
|
||||
}
|
||||
|
||||
p.fanOutExpeditionDM(e, body, p.briefingPetPrefix)
|
||||
p.fanOutExpeditionDM(e, body, p.briefingPerReader)
|
||||
// N1/A6 anchor, relocated here from the retired night-camp digest DM.
|
||||
// Only on a run still under way: an expedition that just ended does not
|
||||
// want a mid-day event landing on top of the emergence.
|
||||
if e.Status == ExpeditionStatusActive {
|
||||
p.fireDigestEventAnchor(e)
|
||||
}
|
||||
// Emergence seam: a briefing-time forced extraction (starvation / abyss
|
||||
// collapse) surfaces the players alive — roll pet arrival. Combat/patrol
|
||||
// deaths never reach deliverBriefing (the row is already abandoned), so an
|
||||
@@ -489,7 +504,9 @@ func (p *AdventurePlugin) maybeDeliverDeferredBriefing(uid id.UserID, now time.T
|
||||
//
|
||||
// priorBriefing is the last_briefing_at value as of entry into deliverBriefing
|
||||
// (before the CAS clobbered it). nil means day-1 or genuinely never rolled.
|
||||
func (p *AdventurePlugin) deliverBriefingEventAnchored(e *Expedition, priorBriefing *time.Time) error {
|
||||
// digestSince is the same instant collapsed to a non-nil cutoff (start date on
|
||||
// day 1) — the window the overnight digest reports.
|
||||
func (p *AdventurePlugin) deliverBriefingEventAnchored(e *Expedition, priorBriefing *time.Time, digestSince time.Time) error {
|
||||
now := time.Now().UTC()
|
||||
var since time.Duration
|
||||
if priorBriefing != nil {
|
||||
@@ -516,6 +533,7 @@ func (p *AdventurePlugin) deliverBriefingEventAnchored(e *Expedition, priorBrief
|
||||
|
||||
line := pickMorningBriefing(e.CurrentDay)
|
||||
body := renderMorningBriefing(e, line, burn)
|
||||
body = appendOvernightDigest(body, e.ID, digestSince)
|
||||
if sl := p.shadowBriefingLine(e); sl != "" {
|
||||
body += "\n" + sl + "\n"
|
||||
}
|
||||
@@ -529,7 +547,13 @@ func (p *AdventurePlugin) deliverBriefingEventAnchored(e *Expedition, priorBrief
|
||||
body += "\n" + ml
|
||||
}
|
||||
|
||||
p.fanOutExpeditionDM(e, body, p.briefingPetPrefix)
|
||||
p.fanOutExpeditionDM(e, body, p.briefingPerReader)
|
||||
// N1/A6 anchor, relocated here from the retired night-camp digest DM.
|
||||
// Only on a run still under way: an expedition that just ended does not
|
||||
// want a mid-day event landing on top of the emergence.
|
||||
if e.Status == ExpeditionStatusActive {
|
||||
p.fireDigestEventAnchor(e)
|
||||
}
|
||||
if forced && e.Status == ExpeditionStatusAbandoned {
|
||||
for _, uid := range expeditionAudience(e) {
|
||||
p.maybeRollPetArrivalOnEmerge(uid)
|
||||
@@ -566,9 +590,10 @@ func (p *AdventurePlugin) deliverRecap(e *Expedition, now time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// E2b: night phase wandering check fires before the recap so its
|
||||
// outcome is part of today's log when the recap renders.
|
||||
var night *NightCheck
|
||||
// E2b: night phase wandering check. Still fires on the 21:00 clock and
|
||||
// still writes its own "night" log entry — processNightCheck owns that —
|
||||
// which is how the outcome reaches the next morning's digest now that
|
||||
// the recap itself no longer sends anything.
|
||||
if e.Camp != nil && e.Camp.Active {
|
||||
c, _ := LoadDnDCharacter(id.UserID(e.UserID))
|
||||
var charClass DnDClass
|
||||
@@ -579,7 +604,6 @@ func (p *AdventurePlugin) deliverRecap(e *Expedition, now time.Time) error {
|
||||
if err := processNightCheck(e, nc); err != nil {
|
||||
slog.Warn("expedition: night check", "expedition", e.ID, "err", err)
|
||||
}
|
||||
night = &nc
|
||||
// §7.4: Feywild double-day fires an extra wandering check.
|
||||
if e.ZoneID == ZoneFeywildCrossing {
|
||||
if today, _ := e.RegionState["feywild_today"].(string); today == string(FeywildDistortionDouble) {
|
||||
@@ -601,12 +625,11 @@ func (p *AdventurePlugin) deliverRecap(e *Expedition, now time.Time) error {
|
||||
return err
|
||||
}
|
||||
line := pickEveningRecap(e, dayEntries)
|
||||
body := renderEveningRecap(e, line, dayEntries)
|
||||
if night != nil {
|
||||
body += "\n" + renderNightCheck(*night)
|
||||
}
|
||||
|
||||
p.fanOutExpeditionDM(e, body, nil)
|
||||
// Once-a-day cadence: the night check above has already run and already
|
||||
// written its own "night" log entry, so the morning digest reports the
|
||||
// outcome. Nothing is sent here. The recap entry below still lands so
|
||||
// the site keeps a day boundary to render against.
|
||||
if err := appendExpeditionLog(e.ID, e.CurrentDay, "recap",
|
||||
fmt.Sprintf("evening recap — %d log entries today", len(dayEntries)), line); err != nil {
|
||||
return err
|
||||
|
||||
@@ -186,13 +186,10 @@ func (p *AdventurePlugin) deliverAmbient(e *Expedition, now time.Time) error {
|
||||
}
|
||||
|
||||
footer := p.applyAmbientEffect(e, ev)
|
||||
body := renderAmbientDM(e, ev, line, footer)
|
||||
|
||||
if uid := id.UserID(e.UserID); uid != "" {
|
||||
if err := p.SendDM(uid, body); err != nil {
|
||||
slog.Warn("expedition: ambient DM", "user", uid, "err", err)
|
||||
}
|
||||
}
|
||||
// Once-a-day cadence: the effect above still lands on schedule, but the
|
||||
// DM does not. The log entry below is what the next morning's briefing
|
||||
// digest reads back, and the site renders it as it happens.
|
||||
summary := fmt.Sprintf("ambient: %s", ev.Kind)
|
||||
if footer != "" {
|
||||
summary += " — " + footer
|
||||
|
||||
@@ -304,15 +304,11 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error {
|
||||
// every other quiet path stays silent until something interactive fires.
|
||||
if body, ok := buildAutoRunDM(e.ID, r, campBlock, campDecision); ok {
|
||||
p.fanOutExpeditionDM(e, body, nil)
|
||||
// N1/A6 — the end-of-day digest is the primary mid-day event anchor.
|
||||
// The anchor is a per-player roll against a per-player daily slot, so
|
||||
// each member rolls their own; a party does not share one event.
|
||||
if campDecision.Night {
|
||||
for _, member := range expeditionAudience(e) {
|
||||
p.maybeFireAnchoredEvent(member, advEventChanceDigest)
|
||||
}
|
||||
}
|
||||
}
|
||||
// N1/A6's digest event anchor has moved to the 06:00 briefing along with
|
||||
// the digest itself — see deliverBriefing. The anchor's whole premise is
|
||||
// firing at a moment the player is demonstrably reading a DM, and the
|
||||
// night camp no longer sends one.
|
||||
|
||||
// Emergence seam: a run-complete reached by the background ticker is
|
||||
// still a live emergence — roll pet arrival. See maybeRollPetArrivalOnEmerge.
|
||||
@@ -336,8 +332,10 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error {
|
||||
// Surface rules:
|
||||
// - stopFork / stopEnded / stopComplete → render the walk DM. These
|
||||
// are the interactive / climax beats and stay their own messages.
|
||||
// - Night camp pitched → render the EoD digest +
|
||||
// camp block. Walk stream is dropped (the digest summarizes the day).
|
||||
// - Night camp pitched → silent. The once-a-day cadence
|
||||
// (2026-07-26) retired the EoD digest DM: the camp writes its own
|
||||
// `rest` log entry and the day it summarised is already in the log,
|
||||
// so the 06:00 briefing reads the whole thing back the next morning.
|
||||
// - Boss-safety camp pitched → short hold notice + camp
|
||||
// block; walk stream dropped (compact bail was deliberate).
|
||||
// - Anything else → silent.
|
||||
@@ -354,24 +352,9 @@ func buildAutoRunDM(expID string, r autopilotWalkResult, camp string, dec autoCa
|
||||
return "", false
|
||||
}
|
||||
if dec.Night {
|
||||
// EoD digest. The camp pitch already bumped current_day in
|
||||
// nightRolloverBurn, so the day-that-just-ended is CurrentDay-1.
|
||||
// digest is the day rollup, then the camp block lays out the rest.
|
||||
fresh, ferr := getExpedition(expID)
|
||||
prevDay := 0
|
||||
if ferr == nil && fresh != nil {
|
||||
prevDay = fresh.CurrentDay - 1
|
||||
}
|
||||
digest := ""
|
||||
if prevDay > 0 {
|
||||
digest = renderEndOfDayDigest(expID, prevDay)
|
||||
}
|
||||
if digest == "" {
|
||||
// No structured day yet — fall back to a thin header so the
|
||||
// camp block isn't dropped on the player without context.
|
||||
digest = "🌙 *The day winds down.*\n\n"
|
||||
}
|
||||
return digest + camp, true
|
||||
// Silent: the morning briefing is the one daily message now, and it
|
||||
// renders this same day out of the expedition log.
|
||||
return "", false
|
||||
}
|
||||
if dec.Reason == "boss-safety hold — resting before re-engaging" {
|
||||
return "⏸ *Holding before the boss — pitching a rest camp.*\n" + camp, true
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package plugin
|
||||
|
||||
// Once-a-day cadence (2026-07-26).
|
||||
//
|
||||
// Adventure's web feed is now the place to watch a run move minute to minute,
|
||||
// so the bot no longer narrates every beat into Matrix. The three per-player
|
||||
// DM sources that fired on a clock — the 06:00 briefing, the 21:00 recap, and
|
||||
// the 6-hourly ambient event — collapse into a single morning message.
|
||||
//
|
||||
// The rule that keeps this honest: only the *messaging* goes quiet. Every
|
||||
// mechanical effect still fires on exactly the schedule it always did. The
|
||||
// ambient ticker still applies its ±SU nudges, the recap still runs the night
|
||||
// wandering check and its threat bump, the briefing still burns supply and
|
||||
// rolls the day. What changes is that ambient and recap now write their
|
||||
// outcome to the expedition log and stop there; the next morning's briefing
|
||||
// reads that log back and reports it.
|
||||
//
|
||||
// Interrupt-driven DMs are deliberately untouched. A fork needs a human, a
|
||||
// death and a run-completion are terminal, and a mischief hit or a rival
|
||||
// challenge is somebody else acting on you. Those still arrive when they
|
||||
// happen; batching them to the next morning would either strand a decision
|
||||
// behind an 8h auto-pick or report a finished story.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gogobee/internal/peteclient"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultPeteSiteURL — Pete's public site. Distinct from PETE_INGEST_URL,
|
||||
// which is the Headscale-only ingest endpoint and is not reachable by a
|
||||
// player clicking a link in a DM.
|
||||
defaultPeteSiteURL = "https://news.parodia.dev"
|
||||
|
||||
// digestMaxLines — how many prior-day log lines the morning digest
|
||||
// carries before it defers to the site. The cap is the whole point of
|
||||
// the change: the digest is a teaser for the feed, not a transcript.
|
||||
digestMaxLines = 8
|
||||
|
||||
// digestScanLimit — how far back the digest reads before it gives up on
|
||||
// finding the window's oldest entry. A day of autopilot ticks, ambient
|
||||
// beats and room events runs to a few dozen rows; this is slack, not a
|
||||
// budget.
|
||||
digestScanLimit = 500
|
||||
)
|
||||
|
||||
// peteSiteURL returns the public base URL for Pete's site, without a
|
||||
// trailing slash. Overridable so a dev instance can point its links at
|
||||
// a local Pete instead of prod.
|
||||
func peteSiteURL() string {
|
||||
if v := strings.TrimRight(os.Getenv("PETE_PUBLIC_URL"), "/"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultPeteSiteURL
|
||||
}
|
||||
|
||||
// adventureFeedURL is the general Adventure feed: everyone's activity.
|
||||
func adventureFeedURL() string {
|
||||
return peteSiteURL() + "/adventure"
|
||||
}
|
||||
|
||||
// adventureWhoURL is the reader's own adventurer page. Keyed by the same
|
||||
// salted, one-way roster token the board already publishes (see
|
||||
// pete_roster.go), so a DM link and a board link resolve to one page and
|
||||
// neither one leaks a Matrix handle.
|
||||
func adventureWhoURL(uid id.UserID) string {
|
||||
if uid == "" {
|
||||
return adventureFeedURL()
|
||||
}
|
||||
return peteSiteURL() + "/adventure/who/" + eventToken(uid, "roster")
|
||||
}
|
||||
|
||||
// digestSiteFooter appends the reader's own site link. Per-reader rather
|
||||
// than per-expedition: a party shares a briefing body but each member's
|
||||
// link goes to their own sheet.
|
||||
//
|
||||
// Gated on the Pete seam: peteclient.Enabled() is what starts the roster
|
||||
// ticker, and the roster push is what creates the page this link points at.
|
||||
// With the seam off (a dev instance, or a deploy without an ingest token)
|
||||
// every daily DM would otherwise carry a guaranteed 404.
|
||||
func digestSiteFooter(uid id.UserID, body string) string {
|
||||
if !peteclient.Enabled() {
|
||||
return body
|
||||
}
|
||||
return body + "\n\n🔗 _Watch it live: " + adventureWhoURL(uid) + "_"
|
||||
}
|
||||
|
||||
// briefingPerReader is the per-reader decorator for the one daily message:
|
||||
// the reader's own pet event on the front, the reader's own site link on the
|
||||
// back. Both are per-member, so a party's shared briefing body still reaches
|
||||
// each player personalised at both ends.
|
||||
func (p *AdventurePlugin) briefingPerReader(uid id.UserID, body string) string {
|
||||
return digestSiteFooter(uid, p.briefingPetPrefix(uid, body))
|
||||
}
|
||||
|
||||
// fireDigestEventAnchor rolls N1/A6's digest-anchored mid-day event for each
|
||||
// member. It used to hang off the autopilot's night-camp digest DM; that DM is
|
||||
// gone, so it moved here — the briefing is now the message the player is
|
||||
// demonstrably reading, which is the whole premise of an anchored roll.
|
||||
//
|
||||
// Still a per-player roll against a per-player daily slot: a party does not
|
||||
// share one event.
|
||||
func (p *AdventurePlugin) fireDigestEventAnchor(e *Expedition) {
|
||||
for _, member := range expeditionAudience(e) {
|
||||
p.maybeFireAnchoredEvent(member, advEventChanceDigest)
|
||||
}
|
||||
}
|
||||
|
||||
// appendOvernightDigest folds everything that happened since the previous
|
||||
// briefing into a briefing body. A log read failure is non-fatal: the briefing
|
||||
// is the player's only daily message now, so a missing digest block must never
|
||||
// cost them the whole DM.
|
||||
//
|
||||
// The window is a timestamp, not a day number, because the two disagree on
|
||||
// every event-anchored expedition: the autopilot's night camp rolls
|
||||
// current_day at camp time, so by 06:00 the day that just ended is already
|
||||
// current_day-1 — and on a night the autopilot never camped, it isn't. A
|
||||
// since-last-briefing window reports each entry exactly once either way.
|
||||
func appendOvernightDigest(body, expID string, since time.Time) string {
|
||||
entries, err := logEntriesSince(expID, since)
|
||||
if err != nil {
|
||||
slog.Warn("expedition: digest entries", "expedition", expID, "err", err)
|
||||
return body
|
||||
}
|
||||
digest := renderOvernightDigest(entries)
|
||||
if digest == "" {
|
||||
return body
|
||||
}
|
||||
return body + "\n" + digest
|
||||
}
|
||||
|
||||
// logEntriesSince returns an expedition's log entries stamped at or after
|
||||
// `since`, oldest first.
|
||||
//
|
||||
// The cutoff is applied in Go rather than in the WHERE clause on purpose:
|
||||
// dnd_expedition_log.timestamp is a DATETIME column filled by SQLite's own
|
||||
// CURRENT_TIMESTAMP, and comparing it against a bound parameter goes through
|
||||
// numeric affinity and does not reliably answer the question. Scanning the
|
||||
// column into a time.Time does.
|
||||
func logEntriesSince(expID string, since time.Time) ([]ExpeditionEntry, error) {
|
||||
recent, err := recentExpeditionLog(expID, digestScanLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// CURRENT_TIMESTAMP has one-second resolution while the cutoff we are
|
||||
// handed (a briefing stamp, or the start date on day 1) carries
|
||||
// sub-second precision. Floor it, or an entry written in the same second
|
||||
// as the previous briefing falls out of both windows and is never
|
||||
// reported. Re-reporting inside that one second is the safe direction.
|
||||
since = since.Truncate(time.Second)
|
||||
out := make([]ExpeditionEntry, 0, len(recent))
|
||||
for i := len(recent) - 1; i >= 0; i-- { // recent is newest-first
|
||||
if recent[i].Timestamp.Before(since) {
|
||||
continue
|
||||
}
|
||||
out = append(out, recent[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// digestSkipTypes — log entry types the morning digest never echoes.
|
||||
// `briefing` and `recap` are the frame itself, and the free-narration
|
||||
// types are the per-room prose the site renders in full.
|
||||
var digestSkipTypes = map[string]bool{
|
||||
"briefing": true,
|
||||
"recap": true,
|
||||
"narrative": true,
|
||||
"transit": true,
|
||||
"action": true,
|
||||
"journal": true,
|
||||
}
|
||||
|
||||
// renderOvernightDigest condenses one expedition-day of log entries into the
|
||||
// "here is what you missed" block that opens the morning briefing. Walks
|
||||
// collapse to a count; everything notable keeps its own summary line, capped
|
||||
// at digestMaxLines with an explicit overflow note so a truncated digest
|
||||
// never reads as a complete one.
|
||||
//
|
||||
// Returns "" when there is nothing worth reporting — a day with only walks
|
||||
// and narration gets no block at all rather than an empty header.
|
||||
func renderOvernightDigest(entries []ExpeditionEntry) string {
|
||||
var rooms int
|
||||
var lines []string
|
||||
for _, en := range entries {
|
||||
if en.Type == "walk" {
|
||||
rooms += walkEntryRooms(en.Summary)
|
||||
continue
|
||||
}
|
||||
if digestSkipTypes[en.Type] {
|
||||
continue
|
||||
}
|
||||
s := strings.TrimSpace(en.Summary)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, s)
|
||||
}
|
||||
if rooms == 0 && len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("📜 **Since yesterday**\n")
|
||||
if rooms > 0 {
|
||||
b.WriteString(fmt.Sprintf("• walked %s\n", pluralRooms(rooms)))
|
||||
}
|
||||
shown := lines
|
||||
overflow := 0
|
||||
if len(shown) > digestMaxLines {
|
||||
overflow = len(shown) - digestMaxLines
|
||||
shown = shown[:digestMaxLines]
|
||||
}
|
||||
for _, l := range shown {
|
||||
b.WriteString("• " + l + "\n")
|
||||
}
|
||||
if overflow > 0 {
|
||||
b.WriteString(fmt.Sprintf("• _...and %d more, on the site._\n", overflow))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// walkEntryRooms reads the room count back out of an auto-walk log summary
|
||||
// ("auto-walk: 3 room(s)"). One `walk` entry is one background tick, and a
|
||||
// tick covers as many rooms as the autopilot got through — counting entries
|
||||
// would report a 12-room day as a 3-room one. Unparseable summaries count as
|
||||
// a single room rather than vanishing.
|
||||
func walkEntryRooms(summary string) int {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(strings.TrimSpace(summary), "auto-walk: %d room", &n); err == nil && n > 0 {
|
||||
return n
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// pluralRooms renders a room count with the right noun.
|
||||
func pluralRooms(n int) string {
|
||||
if n == 1 {
|
||||
return "1 room"
|
||||
}
|
||||
return fmt.Sprintf("%d rooms", n)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Coverage for the once-a-day cadence (2026-07-26). The behavioural tests
|
||||
// below are the point of the file: before this change nothing asserted that
|
||||
// the recap and ambient paths sent a DM, so nothing would have caught them
|
||||
// silently continuing to send — or, worse, silently dropping the mechanical
|
||||
// effect along with the message.
|
||||
|
||||
func TestRenderOvernightDigest_CollapsesWalksAndSkipsFrameTypes(t *testing.T) {
|
||||
entries := []ExpeditionEntry{
|
||||
// One `walk` entry is one autopilot tick, and a tick can cover
|
||||
// several rooms — the digest reports rooms, not ticks.
|
||||
{Type: "walk", Summary: "auto-walk: 2 room(s)"},
|
||||
{Type: "walk", Summary: "auto-walk: 3 room(s)"},
|
||||
{Type: "briefing", Summary: "morning briefing — 1.0 SU consumed overnight"},
|
||||
{Type: "narrative", Summary: "the corridor bends left"},
|
||||
{Type: "ambient", Summary: "ambient: pack_rat — Supplies -0.5"},
|
||||
{Type: "night", Summary: "Signs of passage near camp; no encounter."},
|
||||
{Type: "recap", Summary: "evening recap — 6 log entries today"},
|
||||
}
|
||||
got := renderOvernightDigest(entries)
|
||||
|
||||
if !strings.Contains(got, "walked 5 rooms") {
|
||||
t.Errorf("walks not collapsed to a count:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "ambient: pack_rat") {
|
||||
t.Errorf("ambient entry missing from digest:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "Signs of passage") {
|
||||
t.Errorf("night check missing from digest:\n%s", got)
|
||||
}
|
||||
for _, unwanted := range []string{"morning briefing", "evening recap", "corridor bends"} {
|
||||
if strings.Contains(got, unwanted) {
|
||||
t.Errorf("digest echoed frame/narration entry %q:\n%s", unwanted, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderOvernightDigest_CapsAndReportsOverflow(t *testing.T) {
|
||||
var entries []ExpeditionEntry
|
||||
for i := 0; i < digestMaxLines+3; i++ {
|
||||
entries = append(entries, ExpeditionEntry{
|
||||
Type: "ambient",
|
||||
Summary: fmt.Sprintf("ambient event %d", i),
|
||||
})
|
||||
}
|
||||
got := renderOvernightDigest(entries)
|
||||
|
||||
if n := strings.Count(got, "ambient event"); n != digestMaxLines {
|
||||
t.Errorf("digest carried %d lines, want the cap of %d:\n%s", n, digestMaxLines, got)
|
||||
}
|
||||
// A truncated digest must say so — otherwise it reads as the whole day.
|
||||
if !strings.Contains(got, "and 3 more") {
|
||||
t.Errorf("overflow not reported:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderOvernightDigest_EmptyWhenNothingNotable(t *testing.T) {
|
||||
entries := []ExpeditionEntry{
|
||||
{Type: "briefing", Summary: "morning briefing"},
|
||||
{Type: "narrative", Summary: "dust everywhere"},
|
||||
}
|
||||
if got := renderOvernightDigest(entries); got != "" {
|
||||
t.Errorf("want no digest block for an unremarkable day, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdventureWhoURL_UsesRosterTokenNotHandle(t *testing.T) {
|
||||
// The roster token is salted from a DB-persisted secret.
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@digest-url:example")
|
||||
got := adventureWhoURL(uid)
|
||||
|
||||
want := "/adventure/who/" + eventToken(uid, "roster")
|
||||
if !strings.HasSuffix(got, want) {
|
||||
t.Errorf("who URL = %q, want suffix %q", got, want)
|
||||
}
|
||||
if strings.Contains(got, "digest-url") {
|
||||
t.Errorf("who URL leaked the Matrix handle: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdventureWhoURL_FallsBackToFeedWithoutUser(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
if got := adventureWhoURL(""); got != adventureFeedURL() {
|
||||
t.Errorf("empty user should fall back to the feed, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildAutoRunDM_NightCampIsSilent — the autopilot's end-of-day digest was
|
||||
// the last recurring second DM of the day. It goes quiet; the camp writes its
|
||||
// own `rest` log entry, so the morning briefing still reports it.
|
||||
func TestBuildAutoRunDM_NightCampIsSilent(t *testing.T) {
|
||||
r := autopilotWalkResult{rooms: 4, reason: stopOK, stream: []string{"…walked…"}}
|
||||
camp := "\n\n⛺ **Autopilot camp** — night"
|
||||
body, ok := buildAutoRunDM("expid", r, camp, autoCampDecision{
|
||||
Kind: CampTypeStandard, Night: true,
|
||||
})
|
||||
if ok || body != "" {
|
||||
t.Errorf("night camp should be silent, got ok=%v body=%q", ok, body)
|
||||
}
|
||||
}
|
||||
|
||||
// The two interactive surfaces the night-camp cut must not touch.
|
||||
func TestBuildAutoRunDM_KeepSetStillSurfaces(t *testing.T) {
|
||||
fork := autopilotWalkResult{rooms: 1, reason: stopFork, finalMsg: "pick a path"}
|
||||
if body, ok := buildAutoRunDM("expid", fork, "", autoCampDecision{}); !ok ||
|
||||
!strings.Contains(body, "pick a path") {
|
||||
t.Errorf("fork must still surface, got ok=%v body=%q", ok, body)
|
||||
}
|
||||
|
||||
hold := autopilotWalkResult{rooms: 2, reason: stopBossSafety}
|
||||
holdCamp := "\n\n⛺ **Rest camp**"
|
||||
if body, ok := buildAutoRunDM("expid", hold, holdCamp, autoCampDecision{
|
||||
Reason: "boss-safety hold — resting before re-engaging",
|
||||
}); !ok || !strings.Contains(body, "Holding before the boss") {
|
||||
t.Errorf("boss-safety hold must still surface, got ok=%v body=%q", ok, body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeliverAmbient_SilentButStillLogs — the ambient event still fires and
|
||||
// still records itself; it just stops DMing.
|
||||
func TestDeliverAmbient_SilentButStillLogs(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@digest-ambient:example")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
sink := installSink(p)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
||||
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.deliverAmbient(exp, exp.StartDate.Add(time.Hour)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if dms := sink.dmsTo(uid); len(dms) != 0 {
|
||||
t.Errorf("ambient sent %d DM(s), want 0:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
||||
}
|
||||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if e.Type == "ambient" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("ambient event fired without writing a log entry — the digest would lose it")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeliverRecap_SilentButStillRunsNightCheck — the recap's mechanical half
|
||||
// (wandering check, threat bump) must survive the message going away.
|
||||
func TestDeliverRecap_SilentButStillRunsNightCheck(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@digest-recap:example")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
sink := installSink(p)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
||||
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exp.Camp = &CampState{Active: true, Type: CampTypeStandard, EstablishedAt: exp.StartDate}
|
||||
if err := updateCamp(exp.ID, exp.Camp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
recapAt := exp.StartDate.Add(12 * time.Hour)
|
||||
if err := p.deliverRecap(exp, recapAt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if dms := sink.dmsTo(uid); len(dms) != 0 {
|
||||
t.Errorf("recap sent %d DM(s), want 0:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
||||
}
|
||||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||||
sawNight, sawRecap := false, false
|
||||
for _, e := range entries {
|
||||
switch e.Type {
|
||||
case "night":
|
||||
sawNight = true
|
||||
case "recap":
|
||||
sawRecap = true
|
||||
}
|
||||
}
|
||||
if !sawNight {
|
||||
t.Error("night wandering check did not run — the recap dropped its mechanics, not just its DM")
|
||||
}
|
||||
if !sawRecap {
|
||||
t.Error("recap log entry missing — the site loses its day boundary")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeliverBriefing_IsTheOneDailyMessage — the payoff: one DM, carrying the
|
||||
// prior day's silent activity and the reader's own site link.
|
||||
func TestDeliverBriefing_CarriesDigestAndSiteLink(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@digest-briefing:example")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
enablePeteSeam(t)
|
||||
p := &AdventurePlugin{}
|
||||
sink := installSink(p)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
||||
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Stand in for the day that just went by silently. The day number is
|
||||
// deliberately not CurrentDay: on an event-anchored run the night camp
|
||||
// rolls current_day when it pitches, so entries either side of the
|
||||
// rollover carry different day numbers. The digest windows on time.
|
||||
if err := appendExpeditionLog(exp.ID, exp.CurrentDay+1, "ambient",
|
||||
"ambient: pack_rat — Supplies -0.5", "Something nibbled the stores."); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := p.deliverBriefing(exp, exp.StartDate.Add(20*time.Hour)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dms := sink.dmsTo(uid)
|
||||
if len(dms) != 1 {
|
||||
t.Fatalf("briefing sent %d DM(s), want exactly 1:\n%s", len(dms), strings.Join(dms, "\n---\n"))
|
||||
}
|
||||
body := dms[0]
|
||||
if !strings.Contains(body, "Since yesterday") {
|
||||
t.Errorf("briefing missing the overnight digest block:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, "ambient: pack_rat") {
|
||||
t.Errorf("digest did not carry the silent ambient event:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, adventureWhoURL(uid)) {
|
||||
t.Errorf("briefing missing the reader's own site link:\n%s", body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user