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
48
docs/API.md
48
docs/API.md
|
@ -230,3 +230,51 @@ 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/BurntSushi/toml v1.3.2
|
||||||
github.com/aichaos/rivescript-go v0.4.0
|
github.com/aichaos/rivescript-go v0.4.0
|
||||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
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/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/mattn/go-shellwords v1.0.12
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25
|
github.com/microcosm-cc/bluemonday v1.0.25
|
||||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
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/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20230919151941-fc55792775de // 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/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.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 v1.6.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // 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
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/crypto v0.13.0 // indirect
|
golang.org/x/crypto v0.13.0 // indirect
|
||||||
golang.org/x/net v0.15.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/term v0.12.0 // indirect
|
||||||
golang.org/x/text v0.13.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 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-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
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 h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
||||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
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=
|
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-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 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
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/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 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
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-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 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
|
||||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
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=
|
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/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 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
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.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 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
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-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-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-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-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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const Branding = {{.Config.Branding}};
|
const Branding = {{.Config.Branding}};
|
||||||
const PublicChannels = {{.Config.GetChannels}};
|
const PublicChannels = {{.Config.GetChannels}};
|
||||||
|
const DMDisclaimer = {{.Config.DirectMessageHistory.DisclaimerMessage}};
|
||||||
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||||
const TURN = {{.Config.TURN}};
|
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/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
"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,
|
// 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.
|
// Blocklist cache sent over from your website.
|
||||||
var (
|
var (
|
||||||
// Map of username to the list of usernames they block.
|
// 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
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
// to write the settings.toml to disk so new defaults populate.
|
// to write the settings.toml to disk so new defaults populate.
|
||||||
var currentVersion = 10
|
var currentVersion = 11
|
||||||
|
|
||||||
// Config for your BareRTC app.
|
// Config for your BareRTC app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -51,6 +51,8 @@ type Config struct {
|
||||||
|
|
||||||
MessageFilters []*MessageFilter
|
MessageFilters []*MessageFilter
|
||||||
|
|
||||||
|
DirectMessageHistory DirectMessageHistory
|
||||||
|
|
||||||
Logging Logging
|
Logging Logging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +69,13 @@ type VIP struct {
|
||||||
MutuallySecret bool
|
MutuallySecret bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DirectMessageHistory struct {
|
||||||
|
Enabled bool
|
||||||
|
SQLiteDatabase string
|
||||||
|
RetentionDays int
|
||||||
|
DisclaimerMessage string
|
||||||
|
}
|
||||||
|
|
||||||
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
// GetChannels returns a JavaScript safe array of the default PublicChannels.
|
||||||
func (c Config) GetChannels() template.JS {
|
func (c Config) GetChannels() template.JS {
|
||||||
data, _ := json.Marshal(c.PublicChannels)
|
data, _ := json.Marshal(c.PublicChannels)
|
||||||
|
@ -185,6 +194,12 @@ func DefaultConfig() Config {
|
||||||
ChatServerResponse: "Watch your language.",
|
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{
|
Logging: Logging{
|
||||||
Directory: "./logs",
|
Directory: "./logs",
|
||||||
Channels: []string{"lobby", "offtopic"},
|
Channels: []string{"lobby", "offtopic"},
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/models"
|
||||||
"git.kirsle.net/apps/barertc/pkg/util"
|
"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)
|
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 {
|
if err := s.SendTo(msg.Channel, message); err != nil {
|
||||||
sub.ChatServer("Your message could not be delivered: %s", err)
|
sub.ChatServer("Your message could not be delivered: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -253,16 +259,28 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
|
||||||
|
|
||||||
// OnTakeback handles takebacks (delete your message for everybody)
|
// OnTakeback handles takebacks (delete your message for everybody)
|
||||||
func (s *Server) OnTakeback(sub *Subscriber, msg messages.Message) {
|
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.
|
// Permission check.
|
||||||
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
||||||
sub.midMu.Lock()
|
sub.midMu.Lock()
|
||||||
_, ok := sub.messageIDs[msg.MessageID]
|
_, ok := sub.messageIDs[msg.MessageID]
|
||||||
sub.midMu.Unlock()
|
sub.midMu.Unlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// 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.")
|
sub.ChatServer("That is not your message to take back.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast to everybody to remove this message.
|
// Broadcast to everybody to remove this message.
|
||||||
s.Broadcast(messages.Message{
|
s.Broadcast(messages.Message{
|
||||||
|
|
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"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"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
|
// 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.
|
// Setup the server: configure HTTP routes, etc.
|
||||||
func (s *Server) Setup() error {
|
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()
|
var mux = http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/", IndexPage())
|
mux.Handle("/", IndexPage())
|
||||||
|
@ -46,6 +57,7 @@ func (s *Server) Setup() error {
|
||||||
mux.Handle("/api/authenticate", s.Authenticate())
|
mux.Handle("/api/authenticate", s.Authenticate())
|
||||||
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
||||||
mux.Handle("/api/profile", s.UserProfile())
|
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("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))
|
||||||
|
|
||||||
|
|
200
src/App.vue
200
src/App.vue
|
@ -74,6 +74,7 @@ export default {
|
||||||
config: {
|
config: {
|
||||||
branding: Branding,
|
branding: Branding,
|
||||||
channels: PublicChannels,
|
channels: PublicChannels,
|
||||||
|
dmDisclaimer: DMDisclaimer,
|
||||||
website: WebsiteURL,
|
website: WebsiteURL,
|
||||||
permitNSFW: PermitNSFW,
|
permitNSFW: PermitNSFW,
|
||||||
webhookURLs: WebhookURLs,
|
webhookURLs: WebhookURLs,
|
||||||
|
@ -241,7 +242,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Chat history.
|
// Chat history.
|
||||||
history: [],
|
|
||||||
channels: {
|
channels: {
|
||||||
// There will be values here like:
|
// There will be values here like:
|
||||||
// "lobby": {
|
// "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 CSS controls for mobile.
|
||||||
responsive: {
|
responsive: {
|
||||||
leftDrawerOpen: false,
|
leftDrawerOpen: false,
|
||||||
|
@ -1324,6 +1335,8 @@ export default {
|
||||||
onMessage(msg) {
|
onMessage(msg) {
|
||||||
// Play sound effects if this is not the active channel or the window is not focused.
|
// Play sound effects if this is not the active channel or the window is not focused.
|
||||||
if (msg.channel.indexOf("@") === 0) {
|
if (msg.channel.indexOf("@") === 0) {
|
||||||
|
this.initDirectMessageHistory(msg.channel, msg.msgID);
|
||||||
|
|
||||||
if (msg.channel !== this.channel || !this.windowFocused) {
|
if (msg.channel !== this.channel || !this.windowFocused) {
|
||||||
// If we are ignoring unsolicited DMs, don't play the sound effect here.
|
// If we are ignoring unsolicited DMs, don't play the sound effect here.
|
||||||
if (this.prefs.closeDMs && this.channels[msg.channel] == undefined) {
|
if (this.prefs.closeDMs && this.channels[msg.channel] == undefined) {
|
||||||
|
@ -1786,6 +1799,7 @@ export default {
|
||||||
openDMs(user) {
|
openDMs(user) {
|
||||||
let channel = "@" + user.username;
|
let channel = "@" + user.username;
|
||||||
this.initHistory(channel);
|
this.initHistory(channel);
|
||||||
|
this.initDirectMessageHistory(channel);
|
||||||
this.setChannel(channel);
|
this.setChannel(channel);
|
||||||
|
|
||||||
// Responsive CSS: switch back to chat panel upon opening a DM.
|
// Responsive CSS: switch back to chat panel upon opening a DM.
|
||||||
|
@ -1881,6 +1895,7 @@ export default {
|
||||||
let channel = this.channel;
|
let channel = this.channel;
|
||||||
this.setChannel(this.config.channels[0].ID);
|
this.setChannel(this.config.channels[0].ID);
|
||||||
delete (this.channels[channel]);
|
delete (this.channels[channel]);
|
||||||
|
delete (this.directMessageHistory[channel]);
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Take back messages (for everyone) or remove locally */
|
/* 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.
|
// Default channel = your current channel.
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
channel = this.channel;
|
channel = this.channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign a timestamp locally?
|
||||||
|
if (timestamp === null) {
|
||||||
|
timestamp = new Date();
|
||||||
|
} else {
|
||||||
|
timestamp = new Date(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
// Are we ignoring DMs?
|
// Are we ignoring DMs?
|
||||||
if (this.prefs.closeDMs && channel.indexOf('@') === 0) {
|
if (this.prefs.closeDMs && channel.indexOf('@') === 0) {
|
||||||
// Don't allow an (incoming) DM to initialize a new chat room for us.
|
// 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>`);
|
message = message.replace(/@(here|all)\b/ig, `<strong class="has-background-at-mention">@$1</strong>`);
|
||||||
|
|
||||||
// Append the message.
|
// Append the message.
|
||||||
this.channels[channel].updated = new Date().getTime();
|
let toAppend = {
|
||||||
this.channels[channel].history.push({
|
|
||||||
action: action,
|
action: action,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
username: username,
|
username: username,
|
||||||
message: message,
|
message: message,
|
||||||
msgID: messageID,
|
msgID: messageID,
|
||||||
at: new Date(),
|
at: timestamp,
|
||||||
isChatServer,
|
isChatServer,
|
||||||
isChatClient,
|
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.
|
// Trim the history per the scrollback buffer.
|
||||||
if (this.scrollback > 0 && this.channels[channel].history.length > this.scrollback) {
|
if (this.scrollback > 0 && this.channels[channel].history.length > this.scrollback) {
|
||||||
|
@ -2781,12 +2808,15 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll the history down.
|
||||||
|
if (!unshift) {
|
||||||
this.scrollHistory(channel);
|
this.scrollHistory(channel);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark unread notifiers if this is not our channel.
|
// Mark unread notifiers if this is not our channel.
|
||||||
if (this.channel !== channel || !this.windowFocused) {
|
if (this.channel !== channel || !this.windowFocused) {
|
||||||
// Don't notify about presence broadcasts.
|
// Don't notify about presence broadcasts or history-backfilled messages.
|
||||||
if (action !== "presence" && !isChatServer) {
|
if (action !== "presence" && action !== "notification" && !isChatServer && !unshift) {
|
||||||
this.channels[channel].unread++;
|
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
|
* Webhook methods
|
||||||
*/
|
*/
|
||||||
|
@ -4089,38 +4223,8 @@ export default {
|
||||||
|
|
||||||
<div :class="fontSizeClass">
|
<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? -->
|
<!-- 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">
|
<em v-if="isDM">
|
||||||
Starting a direct message chat with {{ channel }}. Type a message and say hello!
|
Starting a direct message chat with {{ channel }}. Type a message and say hello!
|
||||||
</em>
|
</em>
|
||||||
|
@ -4129,9 +4233,25 @@ export default {
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</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">
|
<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)"
|
:position="i" :user="getUser(msg.username)" :is-offline="isUserOffline(msg.username)"
|
||||||
:username="username" :website-url="config.website" :is-dnd="isUsernameDND(msg.username)"
|
:username="username" :website-url="config.website" :is-dnd="isUsernameDND(msg.username)"
|
||||||
:is-muted="isMutedUser(msg.username)" :reactions="getReactions(msg)"
|
:is-muted="isMutedUser(msg.username)" :reactions="getReactions(msg)"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'vue3-emoji-picker/css';
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
message: Object, // chat Message object
|
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.)
|
appearance: String, // message style appearance (cards, compact, etc.)
|
||||||
user: Object, // User object of the Message author
|
user: Object, // User object of the Message author
|
||||||
isOffline: Boolean, // user is not currently online
|
isOffline: Boolean, // user is not currently online
|
||||||
|
@ -174,7 +174,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Presence message banners -->
|
<!-- 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 -->
|
<!-- Tiny avatar next to name and action buttons -->
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
|
@ -203,6 +203,11 @@ export default {
|
||||||
|
|
||||||
</div>
|
</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) -->
|
<!-- Card Style (default) -->
|
||||||
<div v-else-if="appearance === 'cards' || !appearance" class="box mb-2 px-4 pt-3 pb-1 position-relative">
|
<div v-else-if="appearance === 'cards' || !appearance" class="box mb-2 px-4 pt-3 pb-1 position-relative">
|
||||||
<div class="media mb-0">
|
<div class="media mb-0">
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
||||||
<label class="label" for="classification">Report classification:</label>
|
<label class="label" for="classification">Report classification:</label>
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="classification" v-model="classification" :disabled="busy">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user