Direct Message History
* Add support for storing DM history between users in a SQLite3 database. * Opt-in by editing your settings.toml to set DirectMessageHistory/Enabled=true * Retention days (default 90) will flush old DMs on app startup. * On the front-end, DM history is checked when a DM thread is opened.
This commit is contained in:
parent
96d61614f4
commit
f0dd1d952c
50
docs/API.md
50
docs/API.md
|
@ -229,4 +229,52 @@ The response JSON given to the chat page from /api/profile looks like:
|
|||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## POST /api/message/history
|
||||
|
||||
Load prior history in a Direct Message conversation with another party.
|
||||
|
||||
Note: this API request is done by the BareRTC chat front-end page, as an
|
||||
ajax request for a current logged-in user.
|
||||
|
||||
The request body payload looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"JWTToken": "the caller's chat jwt token",
|
||||
"Username": "soandso",
|
||||
"BeforeID": 1234
|
||||
}
|
||||
```
|
||||
|
||||
The JWT token is the current chat user's token. This API only works when
|
||||
your BareRTC config requires the use of JWT tokens for authorization.
|
||||
|
||||
The "BeforeID" parameter is for pagination, and is optional. By default,
|
||||
the first page of recent messages are returned. To get the next page, provide
|
||||
the "BeforeID" which matches the MessageID of the oldest message from that
|
||||
page. The endpoint will return messages having an ID before this ID.
|
||||
|
||||
The response JSON given to the chat page from /api/profile looks like:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"OK": true,
|
||||
"Error": "only on error messages",
|
||||
"Messages": [
|
||||
{
|
||||
// Standard BareRTC Messages.
|
||||
"username": "soandso",
|
||||
"message": "hello!",
|
||||
"msgID": 1234,
|
||||
"timestamp": "2024-01-01 11:22:33"
|
||||
}
|
||||
],
|
||||
"Remaining": 12
|
||||
}
|
||||
```
|
||||
|
||||
The "Remaining" integer in the result shows how many older messages still
|
||||
remain to be retrieved, and tells the front-end page that it can request
|
||||
another page.
|
12
go.mod
12
go.mod
|
@ -7,8 +7,9 @@ require (
|
|||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/aichaos/rivescript-go v0.4.0
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
||||
|
@ -23,10 +24,13 @@ require (
|
|||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20230919151941-fc55792775de // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
|
@ -43,7 +47,11 @@ require (
|
|||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
modernc.org/libc v1.37.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
)
|
||||
|
|
28
go.sum
28
go.sum
|
@ -29,6 +29,8 @@ github.com/dop251/goja v0.0.0-20230919151941-fc55792775de h1:lA38Xtzr1Wo+iQdkN2E
|
|||
github.com/dop251/goja v0.0.0-20230919151941-fc55792775de/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -37,6 +39,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
|||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
|
@ -76,8 +80,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
|||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
|
@ -99,8 +103,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
|
@ -119,6 +124,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
|||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
|
@ -216,8 +223,9 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
@ -243,8 +251,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -273,5 +281,13 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
||||
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
<script type="text/javascript">
|
||||
const Branding = {{.Config.Branding}};
|
||||
const PublicChannels = {{.Config.GetChannels}};
|
||||
const DMDisclaimer = {{.Config.DirectMessageHistory.DisclaimerMessage}};
|
||||
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||
const TURN = {{.Config.TURN}};
|
||||
|
|
133
pkg/api.go
133
pkg/api.go
|
@ -13,6 +13,7 @@ import (
|
|||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||
"git.kirsle.net/apps/barertc/pkg/models"
|
||||
)
|
||||
|
||||
// Statistics (/api/statistics) returns info about the users currently logged onto the chat,
|
||||
|
@ -748,6 +749,138 @@ func (s *Server) UserProfile() http.HandlerFunc {
|
|||
})
|
||||
}
|
||||
|
||||
// MessageHistory (/api/message/history) fetches past direct messages for a user.
|
||||
//
|
||||
// This endpoint looks up earlier chat messages between the current user and a target.
|
||||
// It will only run with a valid JWT auth token, to protect users' privacy.
|
||||
//
|
||||
// It is a POST request with a json body containing the following schema:
|
||||
//
|
||||
// {
|
||||
// "JWTToken": "the caller's jwt token",
|
||||
// "Username": "other party",
|
||||
// "BeforeID": 1234,
|
||||
// }
|
||||
//
|
||||
// The "BeforeID" parameter is for pagination and is optional: by default the most
|
||||
// recent page of messages are returned. To retrieve an older page, the BeforeID will
|
||||
// contain the MessageID of the oldest message you received so far, so that the message
|
||||
// before that will be the first returned on the next page.
|
||||
//
|
||||
// The response JSON will look like the following:
|
||||
//
|
||||
// {
|
||||
// "OK": true,
|
||||
// "Error": "only on error responses",
|
||||
// "Messages": [
|
||||
// {
|
||||
// // Standard BareRTC Message objects...
|
||||
// "MessageID": 1234,
|
||||
// "Username": "other party",
|
||||
// "Message": "hello!",
|
||||
// }
|
||||
// ],
|
||||
// "Remaining": 42,
|
||||
// }
|
||||
//
|
||||
// The Remaining value is how many older messages still exist to be loaded.
|
||||
func (s *Server) MessageHistory() http.HandlerFunc {
|
||||
type request struct {
|
||||
JWTToken string
|
||||
Username string
|
||||
BeforeID int64
|
||||
}
|
||||
|
||||
type result struct {
|
||||
OK bool
|
||||
Error string `json:",omitempty"`
|
||||
Messages []messages.Message
|
||||
Remaining int
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// JSON writer for the response.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
// Parse the request.
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "Only POST methods allowed",
|
||||
})
|
||||
return
|
||||
} else if r.Header.Get("Content-Type") != "application/json" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "Only application/json content-types allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse the request payload.
|
||||
var (
|
||||
params request
|
||||
dec = json.NewDecoder(r.Body)
|
||||
)
|
||||
if err := dec.Decode(¶ms); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Are JWT tokens enabled on the server?
|
||||
if !config.Current.JWT.Enabled || params.JWTToken == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "JWT authentication is not available.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the user's JWT token.
|
||||
claims, _, err := jwt.ParseAndValidate(params.JWTToken)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the user from the chat roster.
|
||||
sub, err := s.GetSubscriber(claims.Subject)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "You are not logged into the chat room.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch a page of message history.
|
||||
messages, remaining, err := models.PaginateDirectMessages(sub.Username, params.Username, params.BeforeID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
enc.Encode(result{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
enc.Encode(result{
|
||||
OK: true,
|
||||
Messages: messages,
|
||||
Remaining: remaining,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Blocklist cache sent over from your website.
|
||||
var (
|
||||
// Map of username to the list of usernames they block.
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
// Version of the config format - when new fields are added, it will attempt
|
||||
// to write the settings.toml to disk so new defaults populate.
|
||||
var currentVersion = 10
|
||||
var currentVersion = 11
|
||||
|
||||
// Config for your BareRTC app.
|
||||
type Config struct {
|
||||
|
@ -51,6 +51,8 @@ type Config struct {
|
|||
|
||||
MessageFilters []*MessageFilter
|
||||
|
||||
DirectMessageHistory DirectMessageHistory
|
||||
|
||||
Logging Logging
|
||||
}
|
||||
|
||||
|
@ -67,6 +69,13 @@ type VIP struct {
|
|||
MutuallySecret bool
|
||||
}
|
||||
|
||||
type DirectMessageHistory struct {
|
||||
Enabled bool
|
||||
SQLiteDatabase string
|
||||
RetentionDays int
|
||||
DisclaimerMessage string
|
||||
}
|
||||
|
||||
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
||||
func (c Config) GetChannels() template.JS {
|
||||
data, _ := json.Marshal(c.PublicChannels)
|
||||
|
@ -185,6 +194,12 @@ func DefaultConfig() Config {
|
|||
ChatServerResponse: "Watch your language.",
|
||||
},
|
||||
},
|
||||
DirectMessageHistory: DirectMessageHistory{
|
||||
Enabled: false,
|
||||
SQLiteDatabase: "database.sqlite",
|
||||
RetentionDays: 90,
|
||||
DisclaimerMessage: `<i class="fa fa-info-circle mr-1"></i> <strong>Reminder:</strong> please conduct yourself honorably in Direct Messages.`,
|
||||
},
|
||||
Logging: Logging{
|
||||
Directory: "./logs",
|
||||
Channels: []string{"lobby", "offtopic"},
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||
"git.kirsle.net/apps/barertc/pkg/models"
|
||||
"git.kirsle.net/apps/barertc/pkg/util"
|
||||
)
|
||||
|
||||
|
@ -236,6 +237,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
|
|||
LogMessage(rcpt, sub.Username, sub.Username, msg)
|
||||
}
|
||||
|
||||
// Add it to the DM history SQLite database.
|
||||
if err := (models.DirectMessage{}).LogMessage(sub.Username, rcpt.Username, message); err != nil && err != models.ErrNotInitialized {
|
||||
log.Error("Logging DM history to SQLite: %s", err)
|
||||
}
|
||||
|
||||
if err := s.SendTo(msg.Channel, message); err != nil {
|
||||
sub.ChatServer("Your message could not be delivered: %s", err)
|
||||
}
|
||||
|
@ -253,14 +259,26 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
|
|||
|
||||
// OnTakeback handles takebacks (delete your message for everybody)
|
||||
func (s *Server) OnTakeback(sub *Subscriber, msg messages.Message) {
|
||||
// In case we're in a DM thread, remove this message ID from the history table
|
||||
// if the username matches.
|
||||
wasRemovedFromHistory, err := (models.DirectMessage{}).TakebackMessage(sub.Username, msg.MessageID, sub.IsAdmin())
|
||||
if err != nil && err != models.ErrNotInitialized {
|
||||
log.Error("Error taking back DM history message (%s, %d): %s", sub.Username, msg.MessageID, err)
|
||||
}
|
||||
|
||||
// Permission check.
|
||||
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
||||
sub.midMu.Lock()
|
||||
_, ok := sub.messageIDs[msg.MessageID]
|
||||
sub.midMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
sub.ChatServer("That is not your message to take back.")
|
||||
return
|
||||
// The messageID is not found in the current chat session, but did we remove
|
||||
// it from past DM history for the correct current user?
|
||||
if !wasRemovedFromHistory {
|
||||
sub.ChatServer("That is not your message to take back.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
29
pkg/models/database.go
Normal file
29
pkg/models/database.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
)
|
||||
|
||||
var (
|
||||
DB *sql.DB
|
||||
ErrNotInitialized = errors.New("database is not initialized")
|
||||
)
|
||||
|
||||
func Initialize(connString string) error {
|
||||
db, err := sql.Open("sqlite", connString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
DB = db
|
||||
|
||||
// Run table migrations
|
||||
if err := (DirectMessage{}).CreateTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
199
pkg/models/direct_messages.go
Normal file
199
pkg/models/direct_messages.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||
)
|
||||
|
||||
type DirectMessage struct {
|
||||
MessageID int64
|
||||
ChannelID string
|
||||
Username string
|
||||
Message string
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
const DirectMessagePerPage = 20
|
||||
|
||||
func (dm DirectMessage) CreateTable() error {
|
||||
if DB == nil {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
_, err := DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS direct_messages (
|
||||
message_id INTEGER PRIMARY KEY,
|
||||
channel_id TEXT,
|
||||
username TEXT,
|
||||
message TEXT,
|
||||
timestamp INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_channel_id ON direct_messages(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_timestamp ON direct_messages(timestamp);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete old messages past the retention period.
|
||||
if days := config.Current.DirectMessageHistory.RetentionDays; days > 0 {
|
||||
cutoff := time.Now().Add(time.Duration(-days) * 24 * time.Hour)
|
||||
log.Info("Deleting old DM history past %d days (cutoff: %s)", days, cutoff.Format(time.RFC3339))
|
||||
_, err := DB.Exec(
|
||||
"DELETE FROM direct_messages WHERE timestamp < ?",
|
||||
cutoff.Unix(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("Error removing old DMs: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogMessage adds a message to the DM history between two users.
|
||||
func (dm DirectMessage) LogMessage(fromUsername, toUsername string, msg messages.Message) error {
|
||||
if DB == nil {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
if msg.MessageID == 0 {
|
||||
return errors.New("message did not have a MessageID")
|
||||
}
|
||||
|
||||
var (
|
||||
channelID = CreateChannelID(fromUsername, toUsername)
|
||||
timestamp = time.Now().Unix()
|
||||
)
|
||||
|
||||
_, err := DB.Exec(`
|
||||
INSERT INTO direct_messages (message_id, channel_id, username, message, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, msg.MessageID, channelID, fromUsername, msg.Message, timestamp)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TakebackMessage removes a message by its MID from the DM history.
|
||||
//
|
||||
// Because the MessageID may have been from a previous chat session, the server can't immediately
|
||||
// verify the current user had permission to take it back. This function instead will check whether
|
||||
// a DM history exists sent by this username for that messageID, and if so, returns a
|
||||
// boolean true that the username/messageID matched which will satisfy the permission check
|
||||
// in the OnTakeback handler.
|
||||
func (dm DirectMessage) TakebackMessage(username string, messageID int64, isAdmin bool) (bool, error) {
|
||||
if DB == nil {
|
||||
return false, ErrNotInitialized
|
||||
}
|
||||
|
||||
// Does this messageID exist as sent by the user?
|
||||
if !isAdmin {
|
||||
var (
|
||||
row = DB.QueryRow(
|
||||
"SELECT message_id FROM direct_messages WHERE username = ? AND message_id = ?",
|
||||
username, messageID,
|
||||
)
|
||||
foundMsgID int64
|
||||
err = row.Scan(&foundMsgID)
|
||||
)
|
||||
if err != nil {
|
||||
return false, errors.New("no such message ID found as owned by that user")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete it.
|
||||
_, err := DB.Exec(
|
||||
"DELETE FROM direct_messages WHERE message_id = ?",
|
||||
messageID,
|
||||
)
|
||||
|
||||
// Return that it was successfully validated and deleted.
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// PaginateDirectMessages returns a page of messages, the count of remaining, and an error.
|
||||
func PaginateDirectMessages(fromUsername, toUsername string, beforeID int64) ([]messages.Message, int, error) {
|
||||
if DB == nil {
|
||||
return nil, 0, ErrNotInitialized
|
||||
}
|
||||
|
||||
var (
|
||||
result = []messages.Message{}
|
||||
channelID = CreateChannelID(fromUsername, toUsername)
|
||||
|
||||
// Compute the remaining messages after finding the final messageID this page.
|
||||
lastMessageID int64
|
||||
remaining int
|
||||
)
|
||||
|
||||
if beforeID == 0 {
|
||||
beforeID = math.MaxInt64
|
||||
}
|
||||
|
||||
rows, err := DB.Query(`
|
||||
SELECT message_id, username, message, timestamp
|
||||
FROM direct_messages
|
||||
WHERE channel_id = ?
|
||||
AND message_id < ?
|
||||
ORDER BY message_id DESC
|
||||
LIMIT ?
|
||||
`, channelID, beforeID, DirectMessagePerPage)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var row DirectMessage
|
||||
if err := rows.Scan(
|
||||
&row.MessageID,
|
||||
&row.Username,
|
||||
&row.Message,
|
||||
&row.Timestamp,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
msg := messages.Message{
|
||||
MessageID: row.MessageID,
|
||||
Username: row.Username,
|
||||
Message: row.Message,
|
||||
Timestamp: time.Unix(row.Timestamp, 0).Format(time.RFC3339),
|
||||
}
|
||||
result = append(result, msg)
|
||||
lastMessageID = msg.MessageID
|
||||
}
|
||||
|
||||
// Get a count of the remaining messages.
|
||||
row := DB.QueryRow(`
|
||||
SELECT COUNT(message_id)
|
||||
FROM direct_messages
|
||||
WHERE channel_id = ?
|
||||
AND message_id < ?
|
||||
`, channelID, lastMessageID)
|
||||
if err := row.Scan(&remaining); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return result, remaining, nil
|
||||
}
|
||||
|
||||
// CreateChannelID returns a deterministic channel ID for a direct message conversation.
|
||||
//
|
||||
// The usernames (passed in any order) are sorted alphabetically and composed into the channel ID.
|
||||
func CreateChannelID(fromUsername, toUsername string) string {
|
||||
var parts = []string{fromUsername, toUsername}
|
||||
sort.Strings(parts)
|
||||
return fmt.Sprintf(
|
||||
"@%s:@%s",
|
||||
parts[0],
|
||||
parts[1],
|
||||
)
|
||||
}
|
|
@ -4,6 +4,10 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/models"
|
||||
)
|
||||
|
||||
// Server is the primary back-end server struct for BareRTC, see main.go
|
||||
|
@ -32,6 +36,13 @@ func NewServer() *Server {
|
|||
|
||||
// Setup the server: configure HTTP routes, etc.
|
||||
func (s *Server) Setup() error {
|
||||
// Enable the SQLite database for DM history?
|
||||
if config.Current.DirectMessageHistory.Enabled {
|
||||
if err := models.Initialize(config.Current.DirectMessageHistory.SQLiteDatabase); err != nil {
|
||||
log.Error("Error initializing SQLite database: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var mux = http.NewServeMux()
|
||||
|
||||
mux.Handle("/", IndexPage())
|
||||
|
@ -46,6 +57,7 @@ func (s *Server) Setup() error {
|
|||
mux.Handle("/api/authenticate", s.Authenticate())
|
||||
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
||||
mux.Handle("/api/profile", s.UserProfile())
|
||||
mux.Handle("/api/message/history", s.MessageHistory())
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))
|
||||
|
||||
|
|
202
src/App.vue
202
src/App.vue
|
@ -74,6 +74,7 @@ export default {
|
|||
config: {
|
||||
branding: Branding,
|
||||
channels: PublicChannels,
|
||||
dmDisclaimer: DMDisclaimer,
|
||||
website: WebsiteURL,
|
||||
permitNSFW: PermitNSFW,
|
||||
webhookURLs: WebhookURLs,
|
||||
|
@ -241,7 +242,6 @@ export default {
|
|||
},
|
||||
|
||||
// Chat history.
|
||||
history: [],
|
||||
channels: {
|
||||
// There will be values here like:
|
||||
// "lobby": {
|
||||
|
@ -270,6 +270,17 @@ export default {
|
|||
// }
|
||||
},
|
||||
|
||||
// Loading older Direct Message chat history.
|
||||
directMessageHistory: {
|
||||
/* Will look like:
|
||||
"username": {
|
||||
"busy": false, // ajax request in flight
|
||||
"beforeID": 1234, // next page cursor
|
||||
"remaining": 50, // number of older messages remaining
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
// Responsive CSS controls for mobile.
|
||||
responsive: {
|
||||
leftDrawerOpen: false,
|
||||
|
@ -1324,6 +1335,8 @@ export default {
|
|||
onMessage(msg) {
|
||||
// Play sound effects if this is not the active channel or the window is not focused.
|
||||
if (msg.channel.indexOf("@") === 0) {
|
||||
this.initDirectMessageHistory(msg.channel, msg.msgID);
|
||||
|
||||
if (msg.channel !== this.channel || !this.windowFocused) {
|
||||
// If we are ignoring unsolicited DMs, don't play the sound effect here.
|
||||
if (this.prefs.closeDMs && this.channels[msg.channel] == undefined) {
|
||||
|
@ -1786,6 +1799,7 @@ export default {
|
|||
openDMs(user) {
|
||||
let channel = "@" + user.username;
|
||||
this.initHistory(channel);
|
||||
this.initDirectMessageHistory(channel);
|
||||
this.setChannel(channel);
|
||||
|
||||
// Responsive CSS: switch back to chat panel upon opening a DM.
|
||||
|
@ -1881,6 +1895,7 @@ export default {
|
|||
let channel = this.channel;
|
||||
this.setChannel(this.config.channels[0].ID);
|
||||
delete (this.channels[channel]);
|
||||
delete (this.directMessageHistory[channel]);
|
||||
},
|
||||
|
||||
/* Take back messages (for everyone) or remove locally */
|
||||
|
@ -2711,12 +2726,19 @@ export default {
|
|||
};
|
||||
}
|
||||
},
|
||||
pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient, messageID }) {
|
||||
pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient, messageID, timestamp = null, unshift = false }) {
|
||||
// Default channel = your current channel.
|
||||
if (!channel) {
|
||||
channel = this.channel;
|
||||
}
|
||||
|
||||
// Assign a timestamp locally?
|
||||
if (timestamp === null) {
|
||||
timestamp = new Date();
|
||||
} else {
|
||||
timestamp = new Date(timestamp);
|
||||
}
|
||||
|
||||
// Are we ignoring DMs?
|
||||
if (this.prefs.closeDMs && channel.indexOf('@') === 0) {
|
||||
// Don't allow an (incoming) DM to initialize a new chat room for us.
|
||||
|
@ -2761,17 +2783,22 @@ export default {
|
|||
message = message.replace(/@(here|all)\b/ig, `<strong class="has-background-at-mention">@$1</strong>`);
|
||||
|
||||
// Append the message.
|
||||
this.channels[channel].updated = new Date().getTime();
|
||||
this.channels[channel].history.push({
|
||||
let toAppend = {
|
||||
action: action,
|
||||
channel: channel,
|
||||
username: username,
|
||||
message: message,
|
||||
msgID: messageID,
|
||||
at: new Date(),
|
||||
at: timestamp,
|
||||
isChatServer,
|
||||
isChatClient,
|
||||
});
|
||||
};
|
||||
this.channels[channel].updated = new Date().getTime();
|
||||
if (unshift) {
|
||||
this.channels[channel].history.unshift(toAppend);
|
||||
} else {
|
||||
this.channels[channel].history.push(toAppend);
|
||||
}
|
||||
|
||||
// Trim the history per the scrollback buffer.
|
||||
if (this.scrollback > 0 && this.channels[channel].history.length > this.scrollback) {
|
||||
|
@ -2781,12 +2808,15 @@ export default {
|
|||
);
|
||||
}
|
||||
|
||||
this.scrollHistory(channel);
|
||||
// Scroll the history down.
|
||||
if (!unshift) {
|
||||
this.scrollHistory(channel);
|
||||
}
|
||||
|
||||
// Mark unread notifiers if this is not our channel.
|
||||
if (this.channel !== channel || !this.windowFocused) {
|
||||
// Don't notify about presence broadcasts.
|
||||
if (action !== "presence" && !isChatServer) {
|
||||
// Don't notify about presence broadcasts or history-backfilled messages.
|
||||
if (action !== "presence" && action !== "notification" && !isChatServer && !unshift) {
|
||||
this.channels[channel].unread++;
|
||||
}
|
||||
}
|
||||
|
@ -3136,6 +3166,110 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Direct Message History Loading
|
||||
*/
|
||||
initDirectMessageHistory(channel, ignoreID) {
|
||||
if (this.directMessageHistory[channel] == undefined) {
|
||||
this.directMessageHistory[channel] = {
|
||||
busy: false,
|
||||
beforeID: 0,
|
||||
ignoreID: ignoreID,
|
||||
remaining: -1,
|
||||
};
|
||||
|
||||
// Push the disclaimer message to the bottom of the chat history.
|
||||
let disclaimer = this.config.dmDisclaimer;
|
||||
this.pushHistory({
|
||||
channel: channel,
|
||||
username: "ChatServer",
|
||||
message: disclaimer,
|
||||
action: "notification",
|
||||
});
|
||||
|
||||
// Immediately request the first page.
|
||||
window.requestAnimationFrame(() => {
|
||||
this.loadDirectMessageHistory(channel).then(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollHistory(channel);
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
async loadDirectMessageHistory(channel) {
|
||||
if (!this.jwt.valid) return;
|
||||
this.directMessageHistory[channel].busy = true;
|
||||
return fetch("/api/message/history", {
|
||||
method: "POST",
|
||||
mode: "same-origin",
|
||||
cache: "no-cache",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"JWTToken": this.jwt.token,
|
||||
"Username": this.normalizeUsername(channel),
|
||||
"BeforeID": this.directMessageHistory[channel].beforeID,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.Error) {
|
||||
console.error("DirectMessageHistory: ", data.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// No more messages?
|
||||
if (data.Messages.length === 0) {
|
||||
this.directMessageHistory[channel].remaining = data.Remaining;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is the first of historic messages, insert a dividing line
|
||||
// (ChatServer presence message) to separate them.
|
||||
if (this.directMessageHistory[channel].remaining === -1) {
|
||||
this.pushHistory({
|
||||
channel: channel,
|
||||
username: "ChatServer",
|
||||
action: "presence",
|
||||
message: "Messages from your past chat session are available above this line.",
|
||||
unshift: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Prepend these messages to the chat log.
|
||||
let beforeID = 0;
|
||||
for (let msg of data.Messages) {
|
||||
beforeID = msg.msgID;
|
||||
|
||||
// Deduplicate: if this DM thread was opened because somebody sent us a message, their
|
||||
// message will appear on the history side of the banner as well as the current side.
|
||||
if (msg.msgID === this.directMessageHistory[channel].ignoreID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.pushHistory({
|
||||
channel: channel,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
messageID: msg.msgID,
|
||||
timestamp: msg.timestamp,
|
||||
unshift: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Update pagination state information.
|
||||
this.directMessageHistory[channel].remaining = data.Remaining;
|
||||
this.directMessageHistory[channel].beforeID = beforeID;
|
||||
}).catch(resp => {
|
||||
console.error("DirectMessageHistory: ", resp);
|
||||
}).finally(() => {
|
||||
this.directMessageHistory[channel].busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Webhook methods
|
||||
*/
|
||||
|
@ -4089,38 +4223,8 @@ export default {
|
|||
|
||||
<div :class="fontSizeClass">
|
||||
|
||||
<!-- Disclaimer at the top of DMs -->
|
||||
<!-- TODO: make this disclaimer configurable for other sites to modify -->
|
||||
<div class="notification is-warning is-light" v-if="isDM">
|
||||
<!-- If reporting is enabled -->
|
||||
<div v-if="isWebhookEnabled('report')">
|
||||
<div class="block">
|
||||
<i class="fa fa-info-circle mr-1"></i>
|
||||
<strong>PSA:</strong> If you see something, say something!
|
||||
</div>
|
||||
<div class="block">
|
||||
If you encounter an inappropriate, abusive or criminal message in chat, please flag it
|
||||
by clicking the red
|
||||
<i class="fa fa-flag has-text-danger mx-1"></i> icon to report the message and
|
||||
let your website administrator know. Server-side chat filters are engaged
|
||||
to help flag messages automatically, but we appreciate our members helping to flag
|
||||
messages in case the automated filters missed any.
|
||||
</div>
|
||||
<div class="block">
|
||||
Do your part to help keep this website great! <i class="fa-regular fa-star"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="fa fa-info-circle mr-1"></i>
|
||||
<strong>Reminder:</strong> please conduct yourself honorably in Direct Messages.
|
||||
Please refer to <span v-html="config.branding"></span>'s Privacy Policy or Terms of Service
|
||||
with
|
||||
regard to DMs.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No history? -->
|
||||
<div v-if="chatHistory.length === 0">
|
||||
<div v-if="chatHistory.length === 0 || (chatHistory.length === 1 && chatHistory[0].action === 'notification')">
|
||||
<em v-if="isDM">
|
||||
Starting a direct message chat with {{ channel }}. Type a message and say hello!
|
||||
</em>
|
||||
|
@ -4129,9 +4233,25 @@ export default {
|
|||
</em>
|
||||
</div>
|
||||
|
||||
<!-- Load more history link in DMs -->
|
||||
<div v-if="isDM && directMessageHistory[channel] != undefined && jwt.valid" class="mb-2">
|
||||
<div v-if="directMessageHistory[channel].busy" class="notification is-info is-light">
|
||||
<i class="fa fa-spinner fa-spin mr-1"></i>
|
||||
Loading...
|
||||
</div>
|
||||
<div v-else-if="directMessageHistory[channel].remaining !== 0">
|
||||
<a href="#" @click.prevent="loadDirectMessageHistory(channel)">
|
||||
Load more messages
|
||||
<span v-if="directMessageHistory[channel].remaining > 0">
|
||||
({{directMessageHistory[channel].remaining}} remaining)
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(msg, i) in chatHistory" v-bind:key="i">
|
||||
|
||||
<MessageBox :message="msg" :is-presence="msg.action === 'presence'" :appearance="messageStyle"
|
||||
<MessageBox :message="msg" :action="msg.action" :appearance="messageStyle"
|
||||
:position="i" :user="getUser(msg.username)" :is-offline="isUserOffline(msg.username)"
|
||||
:username="username" :website-url="config.website" :is-dnd="isUsernameDND(msg.username)"
|
||||
:is-muted="isMutedUser(msg.username)" :reactions="getReactions(msg)"
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'vue3-emoji-picker/css';
|
|||
export default {
|
||||
props: {
|
||||
message: Object, // chat Message object
|
||||
isPresence: Boolean, // presence message (joined/left room, kicked, etc.)
|
||||
action: String, // presence, notification, or (default) normal chat message
|
||||
appearance: String, // message style appearance (cards, compact, etc.)
|
||||
user: Object, // User object of the Message author
|
||||
isOffline: Boolean, // user is not currently online
|
||||
|
@ -174,7 +174,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<!-- Presence message banners -->
|
||||
<div v-if="isPresence" class="notification is-success is-light py-1 px-3 mb-2">
|
||||
<div v-if="action === 'presence'" class="notification is-success is-light py-1 px-3 mb-2">
|
||||
|
||||
<!-- Tiny avatar next to name and action buttons -->
|
||||
<div class="columns is-mobile">
|
||||
|
@ -203,6 +203,11 @@ export default {
|
|||
|
||||
</div>
|
||||
|
||||
<!-- Notification message banners (e.g. DM disclaimer) -->
|
||||
<div v-else-if="action === 'notification'" class="notification is-warning is-light mb-2">
|
||||
<span v-html="message.message"></span>
|
||||
</div>
|
||||
|
||||
<!-- Card Style (default) -->
|
||||
<div v-else-if="appearance === 'cards' || !appearance" class="box mb-2 px-4 pt-3 pb-1 position-relative">
|
||||
<div class="media mb-0">
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
<label class="label" for="classification">Report classification:</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select id="classification" v-model="classification" :disabled="busy">
|
||||
<option v-for="i in reportClassifications" :value="i">{{ i }}</option>
|
||||
<option v-for="i in reportClassifications" v-bind:key="i" :value="i">{{ i }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user