diff --git a/go.mod b/go.mod index ce1ce1e..23f700f 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ module github.com/mattermost/mattermost-plugin-demo -go 1.26.2 +go 1.26.4 require ( github.com/gorilla/mux v1.8.1 github.com/mattermost/mattermost-plugin-agents v1.14.1-0.20260519141957-379b06b188d9 - github.com/mattermost/mattermost/server/public v0.3.1-0.20260402155910-d9d71af83e3f + github.com/mattermost/mattermost/server/public v0.4.4-0.20260710034453-0d7ae8e58d6a github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/beevik/etree v1.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect @@ -27,10 +27,10 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect - github.com/lib/pq v1.12.0 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/gosaml2 v0.10.0 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect @@ -48,19 +48,19 @@ require ( github.com/segmentio/encoding v0.5.4 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/stretchr/objx v0.5.3 // indirect - github.com/tinylib/msgp v1.6.3 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.54.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/grpc v1.81.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 4c14b86..11cfdbc 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1 dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= @@ -89,8 +89,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= -github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= @@ -112,8 +112,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= -github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= @@ -126,8 +126,8 @@ github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s= github.com/mattermost/mattermost-plugin-agents v1.14.1-0.20260519141957-379b06b188d9 h1:ovbzO370Fw8rzDRaQhZdY2HMan8QG4l0Gd1HaNEBKkY= github.com/mattermost/mattermost-plugin-agents v1.14.1-0.20260519141957-379b06b188d9/go.mod h1:Ca1M+q6C0EwPEbDBZyPyqKlRdwv3NXbo+vd19B6MOgU= -github.com/mattermost/mattermost/server/public v0.3.1-0.20260402155910-d9d71af83e3f h1:FXDfzbDTk86bKEgBATCTAb3AWsQVzJMn9ruLY72nmQk= -github.com/mattermost/mattermost/server/public v0.3.1-0.20260402155910-d9d71af83e3f/go.mod h1:QnF/1Evlh7e3G8ifwut7Q5Joy/t4oHYNcDoyBTYuXho= +github.com/mattermost/mattermost/server/public v0.4.4-0.20260710034453-0d7ae8e58d6a h1:l7IyGE+1tMjj/ymV1JH0Gq0WG7Ln/kCLya3FA1U+tag= +github.com/mattermost/mattermost/server/public v0.4.4-0.20260710034453-0d7ae8e58d6a/go.mod h1:rHFKFSnyNmyk1qieL00Fv+YuLb093Q7y8VTwBu/43ic= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -215,8 +215,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= -github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -247,14 +247,14 @@ golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -264,8 +264,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -289,14 +289,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -304,8 +304,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -320,8 +320,8 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= diff --git a/server/command_hooks.go b/server/command_hooks.go index f6daf9e..0a55731 100644 --- a/server/command_hooks.go +++ b/server/command_hooks.go @@ -51,6 +51,7 @@ const ( "- `/dialog error-no-elements` - Open an Interactive Dialog with no elements which always returns an general error.\n" + "- `/dialog field-refresh` - Open an Interactive Dialog with field refresh functionality.\n" + "- `/dialog multistep` - Open a multi-step Interactive Dialog demonstrating form refresh on submit.\n" + + "- `/dialog action-buttons` - Open an Interactive Dialog demonstrating an incident response board with triage and timeline notes.\n" + "- `/dialog help` - Show this help text" ) @@ -219,6 +220,9 @@ func getCommandDialogAutocompleteData() *model.AutocompleteData { multiSelect := model.NewAutocompleteData("multi-select", "", "Open an Interactive Dialog with multi-select fields.") command.AddCommand(multiSelect) + actionButtons := model.NewAutocompleteData("action-buttons", "", "Open an Interactive Dialog demonstrating an incident response board with triage and timeline notes.") + command.AddCommand(actionButtons) + help := model.NewAutocompleteData("help", "", "") command.AddCommand(help) @@ -537,6 +541,12 @@ func (p *Plugin) executeCommandDialog(args *model.CommandArgs) *model.CommandRes URL: fmt.Sprintf("%s/plugins/%s/dialog/multistep", *serverConfig.ServiceSettings.SiteURL, manifest.Id), Dialog: getDialogStep1(), } + case "action-buttons": + dialogRequest = model.OpenDialogRequest{ + TriggerId: args.TriggerId, + URL: fmt.Sprintf("%s/plugins/%s/dialog/3", *serverConfig.ServiceSettings.SiteURL, manifest.Id), + Dialog: getDialogIncidentBoard(), + } default: return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, diff --git a/server/dialog_action_buttons_test.go b/server/dialog_action_buttons_test.go new file mode 100644 index 0000000..42291fa --- /dev/null +++ b/server/dialog_action_buttons_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// The incident dialogs must pass the same server-side validation the real +// /dialogs/open endpoint applies. Both Dialog.Title and DialogElement.DisplayName +// are capped at 24 *bytes*, and the emoji-prefixed labels here sit close to that +// cap — exceeding it only surfaces as an opaque "Failed to open Interactive +// Dialog" at demo/e2e time, with no hint as to which field is at fault. +func TestIncidentDialogsAreValid(t *testing.T) { + t.Run("board", func(t *testing.T) { + d := getDialogIncidentBoard() + require.NoError(t, d.IsValid()) + }) + + for _, inc := range incidents { + t.Run("triage/"+inc.ID, func(t *testing.T) { + d := getDialogIncidentTriage(inc, "") + require.NoError(t, d.IsValid()) + }) + + t.Run("note/"+inc.ID, func(t *testing.T) { + d := getDialogTimelineNote(inc.ID) + require.NoError(t, d.IsValid()) + }) + } +} diff --git a/server/dialog_samples.go b/server/dialog_samples.go index 88b398c..62154f7 100644 --- a/server/dialog_samples.go +++ b/server/dialog_samples.go @@ -3,6 +3,7 @@ package main import ( "fmt" "strconv" + "strings" "github.com/mattermost/mattermost/server/public/model" ) @@ -1000,6 +1001,274 @@ func getDialogWithMultiSelectElements() model.Dialog { } } +type incident struct { + ID string + Title string + Severity string // "critical", "high", "medium", "low" + Status string // "investigating", "identified", "monitoring", "resolved" + Owner string +} + +type timelineEntry struct { + Time string + Type string // "update", "root_cause", "mitigation", "escalation" + Message string +} + +var incidents = []incident{ + {ID: "INC-301", Title: "Database replication lag", Severity: "critical", Status: "investigating", Owner: "alice"}, + {ID: "INC-302", Title: "CDN cache miss rate spike", Severity: "high", Status: "monitoring", Owner: "bob"}, + {ID: "INC-303", Title: "Login 2FA timeout errors", Severity: "medium", Status: "identified", Owner: ""}, +} + +var incidentTimelines = map[string][]timelineEntry{ + "INC-301": { + {Time: "14:32 UTC", Type: "update", Message: "Replica lag detected on db-replica-03, exceeding 30s threshold."}, + {Time: "14:45 UTC", Type: "escalation", Message: "Paged on-call DBA team."}, + }, + "INC-302": { + {Time: "09:15 UTC", Type: "update", Message: "Cache miss rate jumped from 2% to 38% after CDN config deploy."}, + }, + "INC-303": { + {Time: "11:00 UTC", Type: "root_cause", Message: "2FA provider timeout increased from 2s to 15s after their maintenance window."}, + {Time: "11:20 UTC", Type: "mitigation", Message: "Increased client-side timeout to 30s as temporary workaround."}, + }, +} + +func severityIcon(s string) string { + switch s { + case "critical": + return "\U0001f534" // red circle + case "high": + return "\U0001f7e0" // orange circle + case "medium": + return "\U0001f7e1" // yellow circle + default: + return "\U0001f7e2" // green circle + } +} + +func statusIcon(s string) string { + switch s { + case "investigating": + return "\U0001f50d" // magnifying glass + case "identified": + return "\U0001f3af" // target + case "monitoring": + return "\U0001f4c8" // chart increasing + case "resolved": + return "\u2705" // green check + default: + return "\u2b1c" // white square + } +} + +func findIncident(id string) (incident, bool) { + for _, inc := range incidents { + if inc.ID == id { + return inc, true + } + } + return incident{}, false +} + +// Dialog A: Incident Board — action buttons serve as both the listing and navigation. +func getDialogIncidentBoard() model.Dialog { + counts := map[string]int{} + for _, inc := range incidents { + counts[inc.Severity]++ + } + intro := fmt.Sprintf("**%d Active Incidents** — ", len(incidents)) + parts := []string{} + for _, sev := range []string{"critical", "high", "medium", "low"} { + if c, ok := counts[sev]; ok && c > 0 { + parts = append(parts, fmt.Sprintf("%d %s", c, sev)) + } + } + intro += strings.Join(parts, ", ") + + // Incident detail lives here rather than in the button labels: DisplayName is + // capped at DialogElementDisplayNameMaxLength (24 bytes), which a + // " (<status>)" label blows past. + intro += "\n\n| Incident | Title | Status |\n|---|---|---|\n" + for _, inc := range incidents { + intro += fmt.Sprintf("| %s %s | %s | %s %s |\n", + severityIcon(inc.Severity), inc.ID, + inc.Title, + statusIcon(inc.Status), inc.Status, + ) + } + intro += "\nSelect an incident to triage." + + elements := make([]model.DialogElement, 0, len(incidents)) + for _, inc := range incidents { + elements = append(elements, model.DialogElement{ + DisplayName: fmt.Sprintf("%s %s", severityIcon(inc.Severity), inc.ID), + Name: "btn_" + inc.ID, + Type: "action_button", + ActionButton: &model.DialogActionButton{ + URL: fmt.Sprintf("/plugins/%s/dialog/incident/triage", manifest.Id), + Context: map[string]string{ + "incident_id": inc.ID, + }, + }, + }) + } + + // Always-failing action button so the error state can be exercised without + // opening a child dialog. + elements = append(elements, model.DialogElement{ + DisplayName: "⚠️ Escalate (fails)", + Name: "btn_escalate_fail", + Type: "action_button", + ActionButton: &model.DialogActionButton{ + URL: fmt.Sprintf("/plugins/%s/dialog/incident/fail", manifest.Id), + }, + }) + + return model.Dialog{ + CallbackId: "incident_board", + Title: "Incident Response Board", + IntroductionText: intro, + SubmitLabel: "Dismiss", + NotifyOnCancel: true, + State: "incident_board", + Elements: elements, + } +} + +// Dialog B: Incident Triage — shows incident details and allows updating status/severity/owner. +func getDialogIncidentTriage(inc incident, ownerUserID string) model.Dialog { + owner := inc.Owner + if owner == "" { + owner = "_unassigned_" + } else { + owner = "@" + owner + } + intro := fmt.Sprintf("**%s**\n\n- Severity: %s %s\n- Status: %s %s\n- Owner: %s", + inc.Title, + severityIcon(inc.Severity), inc.Severity, + statusIcon(inc.Status), inc.Status, + owner, + ) + + return model.Dialog{ + CallbackId: "incident_triage_" + inc.ID, + Title: inc.ID + " — Triage", + IntroductionText: intro, + SubmitLabel: "Update Incident", + NotifyOnCancel: true, + State: "incident_triage", + Elements: []model.DialogElement{ + { + DisplayName: "Status", + Name: "status", + Type: "select", + Default: inc.Status, + Options: []*model.PostActionOptions{ + {Text: "\U0001f50d investigating", Value: "investigating"}, + {Text: "\U0001f3af identified", Value: "identified"}, + {Text: "\U0001f4c8 monitoring", Value: "monitoring"}, + {Text: "\u2705 resolved", Value: "resolved"}, + }, + }, + { + DisplayName: "Severity", + Name: "severity", + Type: "select", + Default: inc.Severity, + Options: []*model.PostActionOptions{ + {Text: "\U0001f534 critical", Value: "critical"}, + {Text: "\U0001f7e0 high", Value: "high"}, + {Text: "\U0001f7e1 medium", Value: "medium"}, + {Text: "\U0001f7e2 low", Value: "low"}, + }, + }, + { + DisplayName: "Assignee", + Name: "owner", + Type: "select", + Default: ownerUserID, + Placeholder: "Select a user...", + DataSource: "users", + Optional: true, + }, + { + DisplayName: "Add Timeline Note", + Name: "btn_add_note", + Type: "action_button", + ActionButton: &model.DialogActionButton{ + URL: fmt.Sprintf("/plugins/%s/dialog/incident/note", manifest.Id), + Context: map[string]string{ + "incident_id": inc.ID, + }, + }, + }, + }, + } +} + +// Dialog C: Timeline Note — shows existing timeline and lets the user add a new entry. +func getDialogTimelineNote(incID string) model.Dialog { + entries := incidentTimelines[incID] + + var intro string + if len(entries) == 0 { + intro = "No timeline entries yet." + } else { + intro = "| Time | Type | Message |\n|---|---|---|\n" + for _, e := range entries { + intro += fmt.Sprintf("| %s | %s | %s |\n", e.Time, e.Type, e.Message) + } + } + + return model.Dialog{ + CallbackId: "timeline_note_" + incID, + Title: incID + " — Add Note", + IntroductionText: intro, + SubmitLabel: "Add Note", + NotifyOnCancel: true, + State: "timeline_note", + Elements: []model.DialogElement{ + { + DisplayName: "Note Type", + Name: "note_type", + Type: "select", + Options: []*model.PostActionOptions{ + {Text: "Update", Value: "update"}, + {Text: "Root Cause", Value: "root_cause"}, + {Text: "Mitigation", Value: "mitigation"}, + {Text: "Escalation", Value: "escalation"}, + }, + }, + { + DisplayName: "Message", + Name: "message", + Type: "textarea", + Placeholder: "Describe what happened, what was found, or what action was taken...", + MaxLength: 1000, + }, + }, + } +} + +var mdEscaper = strings.NewReplacer( + `\`, `\\`, + "`", "\\`", + "*", `\*`, + "_", `\_`, + "~", `\~`, + "[", `\[`, + "|", `\|`, + "@", `\@`, +) + +// escapeMD escapes Markdown special characters and neutralizes @mention markers +// in user-supplied text before embedding it in a bot-authored post. +func escapeMD(s string) string { + return mdEscaper.Replace(s) +} + // Helper function to convert interface{} to string safely func interfaceToString(value interface{}) string { switch v := value.(type) { diff --git a/server/http_hooks.go b/server/http_hooks.go index b54cfd3..dc8a43b 100644 --- a/server/http_hooks.go +++ b/server/http_hooks.go @@ -57,6 +57,9 @@ func (p *Plugin) initializeAPI() { dialogRouter.HandleFunc("/error", p.handleDialogWithError) dialogRouter.HandleFunc("/field-refresh", p.handleDialogFieldRefresh) dialogRouter.HandleFunc("/multistep", p.handleDialogMultistep) + dialogRouter.HandleFunc("/incident/triage", p.handleIncidentTriage) + dialogRouter.HandleFunc("/incident/note", p.handleIncidentNote) + dialogRouter.HandleFunc("/incident/fail", p.handleIncidentFail) dialogRouter.HandleFunc("/products", p.handleDynamicProducts).Methods(http.MethodPost) dialogRouter.HandleFunc("/companies", p.handleDynamicCompanies).Methods(http.MethodPost) @@ -259,10 +262,54 @@ func (p *Plugin) handleDialog3(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() + user, appErr := p.API.GetUser(request.UserId) + if appErr != nil { + p.API.LogError("Failed to get user for dialog3", "err", appErr.Error()) + w.WriteHeader(http.StatusOK) + return + } + var message string - if request.Cancelled { + switch { + // Must precede the CallbackId cases: every incident dialog sets + // NotifyOnCancel, and a cancelled request carries an empty Submission, so + // falling into those cases would report a cancel as a successful update. + case request.Cancelled: message = "Dialog cancelled" - } else { + + case request.CallbackId == "incident_board": + message = fmt.Sprintf("Incident board dismissed by @%s.", user.Username) + + case strings.HasPrefix(request.CallbackId, "incident_triage_"): + incID := strings.TrimPrefix(request.CallbackId, "incident_triage_") + status := interfaceToString(request.Submission["status"]) + severity := interfaceToString(request.Submission["severity"]) + owner := interfaceToString(request.Submission["owner"]) + if owner == "" { + owner = "unassigned" + } else { + owner = "@" + owner + } + message = fmt.Sprintf("**%s — Triage Updated**\n- Status: %s %s\n- Severity: %s %s\n- Assignee: %s\n\nUpdated by @%s", + incID, + statusIcon(status), status, + severityIcon(severity), severity, + owner, + user.Username, + ) + + case strings.HasPrefix(request.CallbackId, "timeline_note_"): + incID := strings.TrimPrefix(request.CallbackId, "timeline_note_") + noteType := interfaceToString(request.Submission["note_type"]) + noteMessage := escapeMD(interfaceToString(request.Submission["message"])) + message = fmt.Sprintf("**%s — Timeline Note Added**\n- Type: %s\n- Note: %s\n\nAdded by @%s", + incID, + noteType, + noteMessage, + user.Username, + ) + + default: submission := request.Submission // Generic approach - format submission data as structured lines @@ -405,7 +452,7 @@ func (p *Plugin) handleEphemeralUpdate(w http.ResponseWriter, r *http.Request) { ChannelId: request.ChannelId, Message: "updated ephemeral action", Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{{ + "attachments": []*model.MessageAttachment{{ Actions: []*model.PostAction{{ Integration: &model.PostActionIntegration{ Context: model.StringInterface{ @@ -576,6 +623,91 @@ func (p *Plugin) handleInlineActionTriage(w http.ResponseWriter, r *http.Request p.writeJSON(w, resp) } +// handleIncidentTriage handles action button clicks from Dialog A (Incident Board). +// Opens Dialog B showing the triage form for the selected incident. +func (p *Plugin) handleIncidentTriage(w http.ResponseWriter, r *http.Request) { + var request model.PostActionIntegrationRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + p.API.LogError("Failed to decode incident triage request", "err", err) + w.WriteHeader(http.StatusBadRequest) + return + } + defer r.Body.Close() + + incidentID, _ := request.Context["incident_id"].(string) + inc, ok := findIncident(incidentID) + if !ok { + p.API.LogError("Incident not found", "incident_id", incidentID) + w.WriteHeader(http.StatusNotFound) + return + } + + var ownerUserID string + if inc.Owner != "" { + if u, appErr := p.API.GetUserByUsername(inc.Owner); appErr == nil { + ownerUserID = u.Id + } + } + + serverConfig := p.API.GetConfig() + dialogRequest := model.OpenDialogRequest{ + TriggerId: request.TriggerId, + URL: fmt.Sprintf("%s/plugins/%s/dialog/3", *serverConfig.ServiceSettings.SiteURL, manifest.Id), + Dialog: getDialogIncidentTriage(inc, ownerUserID), + } + + if appErr := p.API.OpenInteractiveDialog(dialogRequest); appErr != nil { + p.API.LogError("Failed to open incident triage dialog", "err", appErr.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + p.writeJSON(w, &model.PostActionIntegrationResponse{}) +} + +// handleIncidentNote handles action button clicks from Dialog B (Incident Triage). +// Opens Dialog C showing the timeline note form for the selected incident. +func (p *Plugin) handleIncidentNote(w http.ResponseWriter, r *http.Request) { + var request model.PostActionIntegrationRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + p.API.LogError("Failed to decode incident note request", "err", err) + w.WriteHeader(http.StatusBadRequest) + return + } + defer r.Body.Close() + + incidentID, _ := request.Context["incident_id"].(string) + inc, ok := findIncident(incidentID) + if !ok { + p.API.LogError("Incident not found", "incident_id", incidentID) + w.WriteHeader(http.StatusNotFound) + return + } + + serverConfig := p.API.GetConfig() + dialogRequest := model.OpenDialogRequest{ + TriggerId: request.TriggerId, + URL: fmt.Sprintf("%s/plugins/%s/dialog/3", *serverConfig.ServiceSettings.SiteURL, manifest.Id), + Dialog: getDialogTimelineNote(inc.ID), + } + + if appErr := p.API.OpenInteractiveDialog(dialogRequest); appErr != nil { + p.API.LogError("Failed to open timeline note dialog", "err", appErr.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + p.writeJSON(w, &model.PostActionIntegrationResponse{}) +} + +// handleIncidentFail always fails, exercising the action button's error state +// without opening a child dialog. +func (p *Plugin) handleIncidentFail(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + p.API.LogDebug("Incident escalation deliberately failed for action button error testing") + w.WriteHeader(http.StatusInternalServerError) +} + func formatJSON(v any) string { b, err := json.MarshalIndent(v, "", " ") if err != nil {