Compare commits
523
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8344da524
|
||
|
|
a9d1217db7
|
||
|
|
6558c0eab6
|
||
|
|
e1d09b7572
|
||
|
|
d206429bab
|
||
|
|
b4239af403 | ||
|
|
a6bf93f525 | ||
|
|
6636b686cd | ||
|
|
8b4922abec | ||
|
|
2173aa219d | ||
|
|
9636fd4619 | ||
|
|
2c88db62a5 | ||
|
|
df2f548de8 | ||
|
|
264876cbd3 | ||
|
|
a382a895d0 | ||
|
|
3fa7984d3f | ||
|
|
08abb4f635 | ||
|
|
bce170f47c | ||
|
|
c963512934 | ||
|
|
228e821a5f | ||
|
|
15d0e3dd1f | ||
|
|
8f9229d47d | ||
|
|
29c4e4a350 | ||
|
|
914fd924d1 | ||
|
|
dd5fa8dd57 | ||
|
|
dddd6b8f8d | ||
|
|
e26525549c | ||
|
|
ca825e8819 | ||
|
|
995235eba7 | ||
|
|
7b40006841 | ||
|
|
dea1af6990 | ||
|
|
8c31cca4d1 | ||
|
|
4c64fa26ac | ||
|
|
96119efa41 | ||
|
|
93c0a20935 | ||
|
|
e200692502 | ||
|
|
5cb4850fce | ||
|
|
2ccd3aad87 | ||
|
|
76be78d1b8 | ||
|
|
41ec8294b2 | ||
|
|
8bb9a026b2 | ||
|
|
1ad8dc15d6 | ||
|
|
43fdadd361 | ||
|
|
94f42910da | ||
|
|
a80bdf8ea1 | ||
|
|
d9af841b9d | ||
|
|
dfdc6cc353 | ||
|
|
b9f400c809 | ||
|
|
7e1d9616b6 | ||
|
|
8d5c61b72e | ||
|
|
1c803f636b | ||
|
|
9aff5089b9 | ||
|
|
9ad0b5c364 | ||
|
|
75ec8e4fce | ||
|
|
e8a8311a33 | ||
|
|
eb11659940 | ||
|
|
bafff45e72 | ||
|
|
94bb3f3567 | ||
|
|
0dce87ec5a | ||
|
|
846ff17221 | ||
|
|
a7e5f20337 | ||
|
|
51cd16dcc1 | ||
|
|
07fc051086 | ||
|
|
d10aa36317 | ||
|
|
6b7e686707 | ||
|
|
67ad3172d3 | ||
|
|
28a9daf8b3 | ||
|
|
4258353d1b | ||
|
|
d1a89a8b22 | ||
|
|
b367ffcff7 | ||
|
|
e2b4929dd1 | ||
|
|
775b4ff6dc | ||
|
|
102a8ff168 | ||
|
|
2526b4fd2e | ||
|
|
2ef5d15dfe | ||
|
|
c4d49280bb | ||
|
|
28b1614769 | ||
|
|
d663b9ff82 | ||
|
|
355bfbf338 | ||
|
|
e1431a0afd | ||
|
|
7337382394 | ||
|
|
6b8e50b5bd | ||
|
|
e0cf689396 | ||
|
|
cac299302f | ||
|
|
cba7dcf237 | ||
|
|
2d3d7f284c | ||
|
|
f914186a85 | ||
|
|
4d540628c3 | ||
|
|
35eb4a42de | ||
|
|
e2aab760fb | ||
|
|
fe9aacc1fd | ||
|
|
1281eceac3 | ||
|
|
aeb674e0c3 | ||
|
|
eaa8434902 | ||
|
|
623463f117 | ||
|
|
ac909963c1 | ||
|
|
63e0f6da3d | ||
|
|
4c0233a601 | ||
|
|
e0d484f2e2 | ||
|
|
97e31e63c2 | ||
|
|
e7a6c36055 | ||
|
|
e3c23af080 | ||
|
|
6ebc28c524 | ||
|
|
4fcce62342 | ||
|
|
4a5f771220 | ||
|
|
933d349095 | ||
|
|
28604ab644 | ||
|
|
5f943b4f46 | ||
|
|
9ce1b0eaaf | ||
|
|
ef9d36bd03 | ||
|
|
c23726786a | ||
|
|
0ee21156d8 | ||
|
|
a42425e195 | ||
|
|
6113ab8573 | ||
|
|
b54c372489 | ||
|
|
246c207ca3 | ||
|
|
d7f3628800 | ||
|
|
1cacdcd8a7 | ||
|
|
8e19719f59 | ||
|
|
db681b5fa8 | ||
|
|
217d54815b | ||
|
|
4f75a3e258 | ||
|
|
efce6c1f54 | ||
|
|
f3e1e14e21 | ||
|
|
7b867af8c8 | ||
|
|
b8f4ba211b | ||
|
|
6387896c30 | ||
|
|
7171b621c1 | ||
|
|
be5bd6f6c2 | ||
|
|
26b12faabf | ||
|
|
f11cc9eb16 | ||
|
|
16a1de12db | ||
|
|
f6045fbd63 | ||
|
|
23b9c3226e | ||
|
|
a5f14217f2 | ||
|
|
dbff9dd50d | ||
|
|
f274a21271 | ||
|
|
3832ea3852 | ||
|
|
1b3accf9a6 | ||
|
|
f1290f70a2 | ||
|
|
7377ebc798 | ||
|
|
6312e1de98 | ||
|
|
23ecb58471 | ||
|
|
fb5464a8c5 | ||
|
|
a65dd748ea | ||
|
|
c77759a4e2 | ||
|
|
b22033049a | ||
|
|
9d539b6db8 | ||
|
|
3f4af08ac3 | ||
|
|
78e1e69161 | ||
|
|
93b8bafd8a | ||
|
|
aed8e2056c | ||
|
|
399c7d38c2 | ||
|
|
619489ef19 | ||
|
|
23e2604c22 | ||
|
|
78757a406b | ||
|
|
b41fdf848a | ||
|
|
cc458f4987 | ||
|
|
721a7826cc | ||
|
|
fc7ff3b59f | ||
|
|
e688ac964c | ||
|
|
115b22df7c | ||
|
|
bd720e32e2 | ||
|
|
d266f562c9 | ||
|
|
71ba8b13f2 | ||
|
|
513bac52cf | ||
|
|
e2b3141058 | ||
|
|
ac9dfa2160 | ||
|
|
92da934ddf | ||
|
|
465518282f | ||
|
|
4d9893d40d | ||
|
|
d60c680b7d | ||
|
|
b8f4c6d4a6 | ||
|
|
3d2304dac8 | ||
|
|
a78eab0c2d | ||
|
|
3e719f2726 | ||
|
|
f25ec59220 | ||
|
|
1dc84d00d6 | ||
|
|
97e318e1c5 | ||
|
|
43ee6ef4dd | ||
|
|
a0d3409422 | ||
|
|
732863ec35 | ||
|
|
e2e84fb516 | ||
|
|
95c3b205a3 | ||
|
|
bb70a5c62d | ||
|
|
a2c987ee9b | ||
|
|
882f292aeb | ||
|
|
5a19cd4897 | ||
|
|
c41f83ff94 | ||
|
|
63c6484c55 | ||
|
|
79fb0d4e17 | ||
|
|
072fe57daf | ||
|
|
96a40fbc51 | ||
|
|
c9d3cd662d | ||
|
|
92bc671282 | ||
|
|
f971b1851b | ||
|
|
afdbf3a831 | ||
|
|
499dcaa734 | ||
|
|
3f6d7affe8 | ||
|
|
fa7e0c6ee1 | ||
|
|
77fac8bd27 | ||
|
|
d129c33143 | ||
|
|
6a343272cf | ||
|
|
41b39a7863 | ||
|
|
c6cc8b7931 | ||
|
|
390c20e812 | ||
|
|
5ea6ca5a91 | ||
|
|
e5e526cbe0 | ||
|
|
e500bd7305 | ||
|
|
ef72021f87 | ||
|
|
81773e568c | ||
|
|
2a0296116d | ||
|
|
6ac641168d | ||
|
|
74d51ebaa7 | ||
|
|
7339d23c0c | ||
|
|
27708665b6 | ||
|
|
2bbe497faf | ||
|
|
46143e4722 | ||
|
|
874dc4445a | ||
|
|
414a8aeed8 | ||
|
|
a529502809 | ||
|
|
d245cc1ebd | ||
|
|
eec8743c18 | ||
|
|
eb223599f0 | ||
|
|
5a773df0d5 | ||
|
|
03a7eb9359 | ||
|
|
21fe8b8831 | ||
|
|
57d569f7d9 | ||
|
|
5af8338a92 | ||
|
|
7922610442 | ||
|
|
cd76c3a22f | ||
|
|
002107f1c8 | ||
|
|
d1035a2967 | ||
|
|
0022399cd6 | ||
|
|
a796c2e8fe | ||
|
|
597a88ec09 | ||
|
|
8dccb1f641 | ||
|
|
52bbbd85e4 | ||
|
|
e2b92d88b2 | ||
|
|
065ba49821 | ||
|
|
ac4019ec0f | ||
|
|
40a05c2dd5 | ||
|
|
2caf1de850 | ||
|
|
4612540b10 | ||
|
|
6bf3e12b0a | ||
|
|
c4157bcdfd | ||
|
|
8f618148c0 | ||
|
|
6fd81b536a | ||
|
|
61a6f59838 | ||
|
|
ebb7f539b9 | ||
|
|
486e89cd87 | ||
|
|
a0446a51e6 | ||
|
|
77b98bc6bc | ||
|
|
1f4506d43f | ||
|
|
46369e104b | ||
|
|
c9de3d3cc1 | ||
|
|
5b1069cedb | ||
|
|
531c5ca1c7 | ||
|
|
bcb4d53195 | ||
|
|
065efc50db | ||
|
|
5dc52158d8 | ||
|
|
095e83eecd | ||
|
|
13c3481970 | ||
|
|
b26ad4e48c | ||
|
|
d62a198ab5 | ||
|
|
193dfdffb5 | ||
|
|
5b88238147 | ||
|
|
ae977bc748 | ||
|
|
a8c44facfc | ||
|
|
bbccbd102e | ||
|
|
34c59d3c34 | ||
|
|
4e13fdb296 | ||
|
|
65a1c6835b | ||
|
|
f4c6b2d4a0 | ||
|
|
e48732acbe | ||
|
|
2fb6ff1637 | ||
|
|
c11fcb550d | ||
|
|
d6cd1b0f12 | ||
|
|
4bca9dd958 | ||
|
|
7b9f516305 | ||
|
|
874bb9511e | ||
|
|
070e06718c | ||
|
|
a740961cd6 | ||
|
|
0846e457a1 | ||
|
|
d8fe5dc70e | ||
|
|
929d27a3b7 | ||
|
|
dd89543aec | ||
|
|
7d5551c702 | ||
|
|
1bcdad7502 | ||
|
|
f018adb842 | ||
|
|
95760cf9b7 | ||
|
|
3e44d29b7a | ||
|
|
3864e9870b | ||
|
|
e24831f4cf | ||
|
|
9e0bb3ff27 | ||
|
|
4ceb43b05b | ||
|
|
dad0690a1f | ||
|
|
72ac80fe34 | ||
|
|
d9ac224ac2 | ||
|
|
ee391277b2 | ||
|
|
a0a8a470ae | ||
|
|
cacc76603d | ||
|
|
f576230ee6 | ||
|
|
7792102fba | ||
|
|
967b8c5776 | ||
|
|
dac9a320a5 | ||
|
|
04f0a6b153 | ||
|
|
766a0fb368 | ||
|
|
a3bf2b1466 | ||
|
|
da340df397 | ||
|
|
f142185191 | ||
|
|
613f00bd48 | ||
|
|
78e4fc90e4 | ||
|
|
000c4401c1 | ||
|
|
36537a6ea6 | ||
|
|
61cf6b1ca0 | ||
|
|
4ce68d553a | ||
|
|
43c64c7874 | ||
|
|
a5955b8be6 | ||
|
|
8d4656588d | ||
|
|
c29b4d5e34 | ||
|
|
a44f9c1b6c | ||
|
|
1d509d91e2 | ||
|
|
9f8108f6ce | ||
|
|
8748611d21 | ||
|
|
eca4e7b8db | ||
|
|
cf5b607200 | ||
|
|
fe96c22be9 | ||
|
|
e232358180 | ||
|
|
5eaac5db31 | ||
|
|
0525e9aed6 | ||
|
|
ebc0104de1 | ||
|
|
8ef2edeb05 | ||
|
|
aee4699c8b | ||
|
|
9007ea2d9a | ||
|
|
19f7fff024 | ||
|
|
0e6ff12ffe | ||
|
|
aba49b532c | ||
|
|
971c77f5c0 | ||
|
|
df19bce74d | ||
|
|
1ca7a877e4 | ||
|
|
ae76abbb34 | ||
|
|
d9ee7578d1 | ||
|
|
da46a2ff7f | ||
|
|
48bb70c1ce | ||
|
|
422a5112ec | ||
|
|
48f76fe284 | ||
|
|
8eb7aedba1 | ||
|
|
1f22094d82 | ||
|
|
4d8e569b0b | ||
|
|
14e02df3d6 | ||
|
|
96cb802b88 | ||
|
|
56c1a78889 | ||
|
|
2ac8e3a475 | ||
|
+2 |
2513b15b40 | ||
|
|
1d49801a27 | ||
|
|
aa0b74e236 | ||
|
|
14286f1470 | ||
|
|
a0a40864cc | ||
|
|
b583fa67e1 | ||
|
|
4f43833756 | ||
|
|
c5b1a315ae | ||
|
|
99dc8fc1c0 | ||
|
|
fc741ba4b6 | ||
|
|
2249739f4e | ||
|
|
35f8e989a6 | ||
|
|
8bf98289f4 | ||
|
|
8ef5c8b940 | ||
|
|
f4b6ab1a5b | ||
|
|
a04e953a51 | ||
|
|
2600e62626 | ||
|
|
759c4460da | ||
|
|
5b60588cd5 | ||
|
|
fe25271c53 | ||
|
|
94ebf11779 | ||
|
|
94fb37c445 | ||
|
|
f642ba4f15 | ||
|
|
7b108a5489 | ||
|
|
adb39f382c | ||
|
|
e140f82f00 | ||
|
|
d39e02811a | ||
|
|
e6c974601e | ||
|
|
d390b0722d | ||
|
|
14a4808f9a | ||
|
|
c9f50b51a9 | ||
|
|
95bb90b6ef | ||
|
|
23435c554b | ||
|
|
a6ce15727e | ||
|
|
5dbf55bfc7 | ||
|
|
708b5b2afc | ||
|
|
a33b2d13b6 | ||
|
|
983848d706 | ||
|
|
1d25bd703d | ||
|
|
b2a7d81d12 | ||
|
|
2acbda625b | ||
|
|
439da013d8 | ||
|
|
ac0204104d | ||
|
|
4767f7f615 | ||
|
|
7e30ed3a1f | ||
|
|
1349e5cc44 | ||
|
|
a5442f8723 | ||
|
|
09029e1ccd | ||
|
|
9616d69158 | ||
|
|
6eff5f2ceb | ||
|
|
72b2598e89 | ||
|
|
a3c457363e | ||
|
|
e40ba1d725 | ||
|
|
7d42f01ecb | ||
|
|
984f5bd562 | ||
|
|
65e1a4fc3f | ||
|
|
56eef2e601 | ||
|
|
8dd43ea6cc | ||
|
|
82271a9b08 | ||
|
|
288219d40c | ||
|
|
9b569f3293 | ||
|
|
fec0e9d11f | ||
|
|
cd8cf2fc7a | ||
|
|
304652047d | ||
|
|
52243fda36 | ||
|
|
47ea42d7f2 | ||
|
|
30a008d85b | ||
|
|
c6570edfa4 | ||
|
|
9fa761b130 | ||
|
|
4ac08252c1 | ||
|
|
799e880718 | ||
|
|
201689cb66 | ||
|
|
f81e8dcda7 | ||
|
|
d3b0cb261a | ||
|
|
3fd18a73ae | ||
|
|
6c02683122 | ||
|
|
26f602a6a5 | ||
|
|
4380b6c8d9 | ||
|
|
5a9075d017 | ||
|
|
e059c6bcdf | ||
|
|
0b93575209 | ||
|
|
07b5f3b96f | ||
|
|
75bab43954 | ||
|
|
717dd67ffc | ||
|
|
a7922c1721 | ||
|
|
dc742e3d10 | ||
|
|
83ed34d422 | ||
|
|
1967da067e | ||
|
|
b7cd4b6323 | ||
|
|
3a11e5a9ab | ||
|
|
fc997fd49b | ||
|
|
3535823e4d | ||
|
|
f626dbafe0 | ||
|
|
cb3d7ea46e | ||
|
|
0262b449c2 | ||
|
|
ecd68e4d4b | ||
|
|
6afa3ab832 | ||
|
|
5762e91a7c | ||
|
|
5a145eb407 | ||
|
|
1d3e52d2bb | ||
|
|
1a5e7cb113 | ||
|
|
aa4a3024d9 | ||
|
|
06a40a346e | ||
|
|
59dc83a02a | ||
|
|
57135960cb | ||
|
|
12a6ebed54 | ||
|
|
16eb1754c7 | ||
|
|
cd06207866 | ||
|
|
f38d7bb6c8 | ||
|
|
ec85a0f264 | ||
|
|
dc98ac0fe4 | ||
|
|
371a42c49d | ||
|
|
d9f22f99b1 | ||
|
|
10147cdf9f | ||
|
|
95819ca1a5 | ||
|
|
f7906cd7ce | ||
|
|
859ff3f082 | ||
|
|
9c3a4acb16 | ||
|
|
921f4f9e4c | ||
|
|
30d51d81ac | ||
|
|
3e1cd98e5f | ||
|
|
39078babbe | ||
|
|
3cafe0533c | ||
|
|
2731b2a26e | ||
|
|
ac69fc38b0 | ||
|
|
e604d52f64 | ||
|
|
7c11c91324 | ||
|
|
2c6bba025c | ||
|
|
87dbb58711 | ||
|
|
aa9d799128 | ||
|
|
f8a3d0559b | ||
|
|
de8bc8d41b | ||
|
|
9bbb09c911 | ||
|
|
e50e4473dd | ||
|
|
4b52800061 | ||
|
|
c618fb0a4e | ||
|
|
ecdfd977dd | ||
|
|
92aa873365 | ||
|
|
e6e73d1750 | ||
|
|
a450e62397 | ||
|
|
04eaf8c20e | ||
|
|
a7134edafb | ||
|
|
7837c7656c | ||
|
|
5792343eef | ||
|
|
c066cd9035 | ||
|
|
9e533498a3 | ||
|
|
68645ab8e7 | ||
|
|
cd3015466e | ||
|
|
91788c9087 | ||
|
|
a02e9a5601 | ||
|
|
a1a879a768 | ||
|
|
e3c93d9c59 | ||
|
|
58e11d4880 | ||
|
|
568b9cace4 | ||
|
|
dcba3818c6 | ||
|
|
50ee6426b6 | ||
|
|
a06db37cd9 | ||
|
|
2d03ea5691 | ||
|
|
9c973f3d5b | ||
|
|
f4cae0f1dd | ||
|
|
db0ef04127 | ||
|
|
43e18267b6 | ||
|
|
50d6443a13 | ||
|
|
d7bee391d6 | ||
|
|
75b4feddeb | ||
|
|
fd271c23f0 | ||
|
|
e0fcce3efb | ||
|
|
10cb2e4b5b | ||
|
|
ec8bfb3e0f |
+3
-3
@@ -7,7 +7,7 @@ A collection of design contribution guidelines and resources for the Owncast int
|
|||||||
|
|
||||||
## 👋 Welcome
|
## 👋 Welcome
|
||||||
|
|
||||||
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anybody from corporate events, government meetings, game streamers, musicians, churches, TV stations, and more.
|
Owncast is a live streaming and chat server targeted to anybody who has live streaming needs. This means anybody from corporate events, government meetings, game streamers, musicians, churches, TV stations, and more.
|
||||||
|
|
||||||
Read the detailed [product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to learn more.
|
Read the detailed [product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to learn more.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Read the detailed [product definition](https://github.com/owncast/owncast/blob/d
|
|||||||
|
|
||||||
## 🎭 Target audience
|
## 🎭 Target audience
|
||||||
|
|
||||||
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anything from corporate events, government meetings, game streams, concerts, TV stations, and more.
|
Owncast is a live streaming and chat server targeted to anybody who has live streaming needs. This means anything from corporate events, government meetings, game streams, concerts, TV stations, and more.
|
||||||
|
|
||||||
## 🧑🎨 Product design opportunities
|
## 🧑🎨 Product design opportunities
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ https://owncast.online/components/?path=%2Fdocs%2Fowncast-styles-colors-componen
|
|||||||
### Design Files, Screenshots, etc
|
### Design Files, Screenshots, etc
|
||||||
|
|
||||||
We do not currently have any design files that fully represent the state of
|
We do not currently have any design files that fully represent the state of
|
||||||
the Owncast interface. However going forward it would be nice to resolve this
|
the Owncast interface. However, going forward it would be nice to resolve this
|
||||||
and collaborate on designs.
|
and collaborate on designs.
|
||||||
|
|
||||||
We do have a [PenPot organization](https://design.penpot.app/#/dashboard/team/8373f780-f255-11ec-b774-f940e3befd53/projects). Please ask for access.
|
We do have a [PenPot organization](https://design.penpot.app/#/dashboard/team/8373f780-f255-11ec-b774-f940e3befd53/projects). Please ask for access.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Add comment
|
- name: Add comment
|
||||||
uses: peter-evans/create-or-update-comment@8d281ecfdf52405614030db6e4265486aa119f52
|
uses: peter-evans/create-or-update-comment@71ac479718f8aed504782bc920d802da994c05fa
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
body: |
|
body: |
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run API tests
|
- name: Run API tests
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: sudo apt-get update && sudo apt-get install google-chrome-stable
|
run: sudo apt-get update && sudo apt-get install google-chrome-stable
|
||||||
|
|
||||||
- name: Run Browser tests
|
- name: Run Browser tests
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 20
|
timeout_minutes: 20
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
||||||
|
|
||||||
uses: chromaui/action@v1
|
uses: chromaui/action@v11
|
||||||
# Chromatic GitHub Action options
|
# Chromatic GitHub Action options
|
||||||
with:
|
with:
|
||||||
workingDir: web
|
workingDir: web
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
# the `language` matrix defined below to confirm you have the correct set of
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
# supported CodeQL languages.
|
# supported CodeQL languages.
|
||||||
#
|
#
|
||||||
name: "CodeQL"
|
name: 'CodeQL'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ develop ]
|
branches: [develop]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'static/**'
|
- 'static/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ develop ]
|
branches: [develop]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'static/**'
|
- 'static/**'
|
||||||
|
|
||||||
@@ -30,41 +30,46 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'go', 'javascript' ]
|
language: ['go', 'javascript']
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
- uses: actions/setup-go@v5
|
||||||
- name: Initialize CodeQL
|
with:
|
||||||
uses: github/codeql-action/init@v3
|
go-version: '1.22'
|
||||||
with:
|
cache: true
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
config-file: ./.github/codeql/${{ matrix.language }}.yml
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Initializes the CodeQL tools for scanning.
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
- name: Initialize CodeQL
|
||||||
- name: Autobuild
|
uses: github/codeql-action/init@v3
|
||||||
uses: github/codeql-action/autobuild@v3
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
config-file: ./.github/codeql/${{ matrix.language }}.yml
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# 📚 https://git.io/JvXDl
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# and modify them (or add more) to build your code if your project
|
# 📚 https://git.io/JvXDl
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# make bootstrap
|
# and modify them (or add more) to build your code if your project
|
||||||
# make release
|
# uses a compiled language
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
#- run: |
|
||||||
uses: github/codeql-action/analyze@v3
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
EARTHLY_BUILD_TAG: 'nightly'
|
EARTHLY_BUILD_TAG: 'nightly'
|
||||||
EARTHLY_BUILD_BRANCH: 'develop'
|
EARTHLY_BUILD_BRANCH: 'develop'
|
||||||
EARTHLY_PUSH: true
|
EARTHLY_PUSH: true
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 20
|
timeout_minutes: 20
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ jobs:
|
|||||||
name: Generate API Documentation
|
name: Generate API Documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run redoc on openapi.yaml
|
- name: Run redoc on openapi.yaml
|
||||||
run: |
|
run: |
|
||||||
npx redoc-cli bundle openapi.yaml -o docs/api/index.html --options '{"hideHostname": true, "pathInMiddlePanel": true}'
|
npx @redocly/cli --config docs/api/redocly.yaml build-docs openapi.yaml -o docs/api/index.html
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: EndBug/add-and-commit@v9
|
uses: EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
author_name: Owncast
|
author_name: Owncast
|
||||||
author_email: owncast@owncast.online
|
author_email: owncast@owncast.online
|
||||||
message: "Commit updated API documentation"
|
message: 'Commit updated API documentation'
|
||||||
add: "docs/api/index.html"
|
add: 'docs/api/index.html'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
cache: true
|
cache: true
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v4
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
only-new-issues: true
|
only-new-issues: true
|
||||||
args: --timeout=3m
|
args: --timeout=3m
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.20.x, 1.21.x]
|
go-version: [1.21.x, 1.22.x]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
@@ -43,14 +43,14 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Local stroage
|
- name: Local stroage
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
command: cd test/automated/hls && ./run.sh
|
command: cd test/automated/hls && ./run.sh
|
||||||
|
|
||||||
- name: S3 storage
|
- name: S3 storage
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files-yaml
|
id: changed-files-yaml
|
||||||
uses: tj-actions/changed-files@v41
|
uses: tj-actions/changed-files@v44
|
||||||
with:
|
with:
|
||||||
path: 'web'
|
path: 'web'
|
||||||
files_ignore: |
|
files_ignore: |
|
||||||
@@ -72,6 +72,9 @@ jobs:
|
|||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
|
|
||||||
|
- name: Debug changed files output
|
||||||
|
run: 'pwd && echo "Changed files: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}"'
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
uses: EndBug/add-and-commit@v9
|
uses: EndBug/add-and-commit@v9
|
||||||
@@ -80,6 +83,7 @@ jobs:
|
|||||||
author_email: owncast@owncast.online
|
author_email: owncast@owncast.online
|
||||||
message: 'Javascript formatting autofixes'
|
message: 'Javascript formatting autofixes'
|
||||||
add: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
add: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
|
cwd: './web' # Ensure this is the correct relative directory
|
||||||
pull: '--rebase --autostash'
|
pull: '--rebase --autostash'
|
||||||
|
|
||||||
unused-code:
|
unused-code:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Automate screenshots
|
- name: Automate screenshots
|
||||||
uses: nick-fields/retry@v2
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 4
|
max_attempts: 4
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ run:
|
|||||||
# Define the Go version limit.
|
# Define the Go version limit.
|
||||||
# Mainly related to generics support in go1.18.
|
# Mainly related to generics support in go1.18.
|
||||||
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
|
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
|
||||||
go: '1.21'
|
go: '1.22'
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
# The linter has a default list of ignorable errors. Turning this on will enable that list.
|
# The linter has a default list of ignorable errors. Turning this on will enable that list.
|
||||||
@@ -69,7 +69,7 @@ linters-settings:
|
|||||||
|
|
||||||
gosimple:
|
gosimple:
|
||||||
# Select the Go version to target. The default is '1.13'.
|
# Select the Go version to target. The default is '1.13'.
|
||||||
go: '1.21'
|
go: '1.22'
|
||||||
# https://staticcheck.io/docs/options#checks
|
# https://staticcheck.io/docs/options#checks
|
||||||
checks: ['all']
|
checks: ['all']
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ ENV NAME=${NAME}
|
|||||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags "-extldflags \"-static\" -s -w -X github.com/owncast/owncast/config.GitCommit=$GIT_COMMIT -X github.com/owncast/owncast/config.VersionNumber=$VERSION -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -o owncast .
|
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags "-extldflags \"-static\" -s -w -X github.com/owncast/owncast/config.GitCommit=$GIT_COMMIT -X github.com/owncast/owncast/config.VersionNumber=$VERSION -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -o owncast .
|
||||||
|
|
||||||
# Create the image by copying the result of the build into a new alpine image
|
# Create the image by copying the result of the build into a new alpine image
|
||||||
FROM alpine:3.19.1
|
FROM alpine:3.20.1
|
||||||
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates && update-ca-certificates
|
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast
|
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast
|
||||||
|
|||||||
@@ -1,37 +1,48 @@
|
|||||||
<br />
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/owncast/owncast" alt="Owncast">
|
<a href="https://github.com/owncast/owncast" alt="Owncast">
|
||||||
<img src="https://owncast.online/images/logo.png" alt="Logo" width="200">
|
<img src="https://owncast.online/images/logo.png" alt="Owncast Logo" width="200">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>Take control over your content and stream it yourself.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/owncast/owncast/blob/develop/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Take control over your content and stream it yourself.</strong>
|
<a href="https://owncast.online"><strong>Explore the docs »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
<a href="https://owncast.online"><strong>Explore the docs »</strong></a>
|
<a href="https://watch.owncast.online/">View Demo</a>
|
||||||
<br />
|
·
|
||||||
<a href="https://watch.owncast.online/">View Demo</a>
|
<a href="https://owncast.online/faq/">FAQ</a>
|
||||||
·
|
·
|
||||||
<a href="https://broadcast.owncast.online/">Use Our Server for Testing</a>
|
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
|
||||||
·
|
|
||||||
<a href="https://owncast.online/faq/">FAQ</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
|
|
||||||
</p>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<!-- TABLE OF CONTENTS -->
|
<!-- TABLE OF CONTENTS -->
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [About the Project](#about-the-project)
|
- 📒 [About the Project](#about-the-project)
|
||||||
- [Getting Started](#getting-started)
|
- 🚀 [Getting Started](#getting-started)
|
||||||
- [Use with your broadcasting software](#use-with-your-existing-broadcasting-software)
|
- 👨💻 [Use with your broadcasting software](#use-with-your-existing-broadcasting-software)
|
||||||
- [Building from source](#building-from-source)
|
- 🛠 [Building from source](#building-from-source)
|
||||||
- [Contributing](#contributing)
|
- 🚨 [Important note about source code and the develop branch](#important-note-about-source-code-and-the-develop-branch)
|
||||||
- [License](#license)
|
- 🗄️ [Backend](#backend)
|
||||||
|
- ⚛️ [Frontend](#frontend)
|
||||||
|
- 👏 [Contributing](#contributing)
|
||||||
|
- 💵 [Donors](#donors)
|
||||||
|
- 📝 [License](#license)
|
||||||
- [Contact](#contact)
|
- [Contact](#contact)
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
@@ -95,7 +106,7 @@ The Owncast backend is a service written in Go.
|
|||||||
1. Ensure you have prerequisites installed.
|
1. Ensure you have prerequisites installed.
|
||||||
- C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/)
|
- C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/)
|
||||||
- [ffmpeg](https://ffmpeg.org/download.html)
|
- [ffmpeg](https://ffmpeg.org/download.html)
|
||||||
1. Install the [Go toolchain](https://golang.org/dl/) (1.21 or above).
|
1. Install the [Go toolchain](https://golang.org/dl/) (1.22 or above).
|
||||||
1. Clone the repo. `git clone https://github.com/owncast/owncast`
|
1. Clone the repo. `git clone https://github.com/owncast/owncast`
|
||||||
1. `go run main.go` will run from the source.
|
1. `go run main.go` will run from the source.
|
||||||
1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin.
|
1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin.
|
||||||
@@ -117,18 +128,50 @@ And while we have a small team of kind, talented and thoughtful volunteers, we h
|
|||||||
We abide by our [Code of Conduct](https://owncast.online/contribute/) and feel strongly about open, appreciative, and empathetic people joining us.
|
We abide by our [Code of Conduct](https://owncast.online/contribute/) and feel strongly about open, appreciative, and empathetic people joining us.
|
||||||
We’ve been very lucky to have this so far, so maybe you can help us with your skills and passion, too!
|
We’ve been very lucky to have this so far, so maybe you can help us with your skills and passion, too!
|
||||||
|
|
||||||
|
If you're new to the project, maybe you'd be interested in looking at [](https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||||
|
|
||||||
There is a larger, more detailed, and more up-to-date [guide for helping contribute to Owncast on our website](https://owncast.online/help/).
|
There is a larger, more detailed, and more up-to-date [guide for helping contribute to Owncast on our website](https://owncast.online/help/).
|
||||||
|
|
||||||
|
### Donors
|
||||||
|
The Owncast project is possible thanks to the people who make a donation to support us and our work.
|
||||||
|
Thank you to all our donors who help keep Owncast running by donating on OpenCollective. You can support this project by [becoming a backer/sponsor](https://opencollective.com/owncast#suppor).
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="https://opencollective.com/owncast#support">
|
||||||
|
<img alt="GitHub issues by-label" src="https://opencollective.com/owncast/tiers/backers.svg?avatarHeight=36&width=600" alt="Backer button">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the MIT License. See `LICENSE` for more information.
|
Distributed under the MIT License. See `LICENSE` for more information.
|
||||||
|
|
||||||
## Supported by
|
## Support
|
||||||
|
|
||||||
- This project is tested with [BrowserStack](https://browserstack.com).
|
|
||||||
|
|
||||||
|
<ul style="font-size:21px; color:black; ">
|
||||||
|
<li>Browser testing via <a
|
||||||
|
href="https://www.lambdatest.com/" target="_blank"><img
|
||||||
|
src="https://www.lambdatest.com/support/img/logo.svg"
|
||||||
|
style="vertical-align: middle;margin-left:5px" width="147" height="26"
|
||||||
|
/></a></li>
|
||||||
|
<li>Project chat provided by
|
||||||
|
<a href="https://rocket.chat" target="_blank">
|
||||||
|
<img src="https://owncast.online/images/sponsors/rocketchat.png" width="147" height="26" style="vertical-align: middle;margin-left:5px">
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>CDN services by
|
||||||
|
<a href="https://fastly.com" target="_blank">
|
||||||
|
<img src="https://owncast.online/images/sponsors/fastly.png" height="26" style="vertical-align: middle;margin-left:5px">
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>UI testing with Chromatic
|
||||||
|
<a href="https://chromatic.com" target="_blank">
|
||||||
|
<img src="https://owncast.online/images/sponsors/chromatic.png" height="26" style="vertical-align: middle;margin-left:5px">
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<!-- CONTACT -->
|
<!-- CONTACT -->
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ func Start(datastore *data.Datastore) {
|
|||||||
persistence.Setup(datastore)
|
persistence.Setup(datastore)
|
||||||
workerpool.InitOutboundWorkerPool()
|
workerpool.InitOutboundWorkerPool()
|
||||||
inbox.InitInboxWorkerPool()
|
inbox.InitInboxWorkerPool()
|
||||||
StartRouter()
|
|
||||||
|
|
||||||
// Generate the keys for signing federated activity if needed.
|
// Generate the keys for signing federated activity if needed.
|
||||||
if data.GetPrivateKey() == "" {
|
if data.GetPrivateKey() == "" {
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package activitypub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/controllers"
|
|
||||||
"github.com/owncast/owncast/router/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartRouter will start the federation specific http router.
|
|
||||||
func StartRouter() {
|
|
||||||
// WebFinger
|
|
||||||
http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler)
|
|
||||||
|
|
||||||
// Host Metadata
|
|
||||||
http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController)
|
|
||||||
|
|
||||||
// Nodeinfo v1
|
|
||||||
http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController)
|
|
||||||
|
|
||||||
// x-nodeinfo v2
|
|
||||||
http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller)
|
|
||||||
|
|
||||||
// Nodeinfo v2
|
|
||||||
http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller)
|
|
||||||
|
|
||||||
// Instance details
|
|
||||||
http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller)
|
|
||||||
|
|
||||||
// Single ActivityPub Actor
|
|
||||||
http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler))
|
|
||||||
|
|
||||||
// Single AP object
|
|
||||||
http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler))
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
var _datastore *data.Datastore
|
||||||
@@ -27,41 +21,3 @@ func Setup(db *data.Datastore) {
|
|||||||
_datastore.MustExec(createTableSQL)
|
_datastore.MustExec(createTableSQL)
|
||||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
|
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAuth will add an external authentication token and type for a user.
|
|
||||||
func AddAuth(userID, authToken string, authType Type) error {
|
|
||||||
return _datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
|
|
||||||
UserID: userID,
|
|
||||||
Token: authToken,
|
|
||||||
Type: string(authType),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByAuth will return an existing user given auth details if a user
|
|
||||||
// has previously authenticated with that method.
|
|
||||||
func GetUserByAuth(authToken string, authType Type) *user.User {
|
|
||||||
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
|
|
||||||
Token: authToken,
|
|
||||||
Type: string(authType),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes []string
|
|
||||||
if u.Scopes.Valid {
|
|
||||||
scopes = strings.Split(u.Scopes.String, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user.User{
|
|
||||||
ID: u.ID,
|
|
||||||
DisplayName: u.DisplayName,
|
|
||||||
DisplayColor: int(u.DisplayColor),
|
|
||||||
CreatedAt: u.CreatedAt.Time,
|
|
||||||
DisabledAt: &u.DisabledAt.Time,
|
|
||||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
|
||||||
NameChangedAt: &u.NamechangedAt.Time,
|
|
||||||
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
|
||||||
|
|
||||||
|
# setup
|
||||||
|
package="generated"
|
||||||
|
folderPath="webserver/handlers/generated"
|
||||||
|
specPath="openapi.yaml"
|
||||||
|
|
||||||
|
# validate scripts are installed
|
||||||
|
if ! command -v swagger-cli &>/dev/null; then
|
||||||
|
echo "Please install \`swagger-cli\` before running this script"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v oapi-codegen &>/dev/null; then
|
||||||
|
echo "Please install \`oapi-codegen\` before running this script"
|
||||||
|
echo "Hint: run \`go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest\` to install"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# validate schema
|
||||||
|
swagger-cli validate $specPath
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Open API specification is not valid"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
rm -r $folderPath
|
||||||
|
mkdir -p $folderPath
|
||||||
|
|
||||||
|
# codegen
|
||||||
|
oapi-codegen -generate types -o $folderPath/$package-types.gen.go -package $package $specPath
|
||||||
|
oapi-codegen -generate "chi-server" -o $folderPath/$package.gen.go -package $package $specPath
|
||||||
|
|
||||||
|
# go
|
||||||
|
go mod tidy
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import "path/filepath"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
|
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
|
||||||
StaticVersionNumber = "0.1.3" // Shown when you build from develop
|
StaticVersionNumber = "0.2.0" // Shown when you build from develop
|
||||||
// FfmpegSuggestedVersion is the version of ffmpeg we suggest.
|
// FfmpegSuggestedVersion is the version of ffmpeg we suggest.
|
||||||
FfmpegSuggestedVersion = "v4.1.5" // Requires the v
|
FfmpegSuggestedVersion = "v4.1.5" // Requires the v
|
||||||
// DataDirectory is the directory we save data to.
|
// DataDirectory is the directory we save data to.
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/rtmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DisconnectInboundConnection will force-disconnect an inbound stream.
|
|
||||||
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rtmp.Disconnect()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/controllers"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetCustomColorVariableValues sets the custom color variables.
|
|
||||||
func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requirePOST(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type request struct {
|
|
||||||
Value map[string]string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var values request
|
|
||||||
|
|
||||||
if err := decoder.Decode(&values); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := data.SetCustomColorVariableValues(values.Value); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers.WriteSimpleResponse(w, true, "custom appearance variables updated")
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/controllers"
|
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type deleteExternalAPIUserRequest struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type createExternalAPIUserRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateExternalAPIUser will generate a 3rd party access token.
|
|
||||||
func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var request createExternalAPIUserRequest
|
|
||||||
if err := decoder.Decode(&request); err != nil {
|
|
||||||
controllers.BadRequestHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all the scopes provided are valid
|
|
||||||
if !user.HasValidScopes(request.Scopes) {
|
|
||||||
controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := utils.GenerateAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
controllers.InternalErrorHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
|
||||||
|
|
||||||
if err := user.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil {
|
|
||||||
controllers.InternalErrorHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
controllers.WriteResponse(w, user.ExternalAPIUser{
|
|
||||||
AccessToken: token,
|
|
||||||
DisplayName: request.Name,
|
|
||||||
DisplayColor: color,
|
|
||||||
Scopes: request.Scopes,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
LastUsedAt: nil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalAPIUsers will return all 3rd party access tokens.
|
|
||||||
func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
tokens, err := user.GetExternalAPIUser()
|
|
||||||
if err != nil {
|
|
||||||
controllers.InternalErrorHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
controllers.WriteResponse(w, tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExternalAPIUser will return a single 3rd party access token.
|
|
||||||
func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method != controllers.POST {
|
|
||||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var request deleteExternalAPIUserRequest
|
|
||||||
if err := decoder.Decode(&request); err != nil {
|
|
||||||
controllers.BadRequestHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Token == "" {
|
|
||||||
controllers.BadRequestHandler(w, errors.New("must provide a token"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.DeleteExternalAPIUser(request.Token); err != nil {
|
|
||||||
controllers.InternalErrorHandler(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers.WriteSimpleResponse(w, true, "deleted token")
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/core/chat"
|
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/router/middleware"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExternalGetChatMessages gets all of the chat messages.
|
|
||||||
func ExternalGetChatMessages(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.EnableCors(w)
|
|
||||||
getChatMessages(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChatMessages gets all of the chat messages.
|
|
||||||
func GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.EnableCors(w)
|
|
||||||
getChatMessages(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getChatMessages(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
messages := chat.GetChatHistory()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
if err := json.NewEncoder(w).Encode(j{"error": "method not implemented (PRs are accepted)"}); err != nil {
|
|
||||||
InternalErrorHandler(w, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterAnonymousChatUser will register a new user.
|
|
||||||
func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.EnableCors(w)
|
|
||||||
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
// All OPTIONS requests should have a wildcard CORS header.
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
// nolint:goconst
|
|
||||||
WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerAnonymousUserRequest struct {
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerAnonymousUserResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
AccessToken string `json:"accessToken"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var request registerAnonymousUserRequest
|
|
||||||
if err := decoder.Decode(&request); err != nil { //nolint
|
|
||||||
// this is fine. register a new user anyway.
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.DisplayName == "" {
|
|
||||||
request.DisplayName = r.Header.Get("X-Forwarded-User")
|
|
||||||
}
|
|
||||||
|
|
||||||
proposedNewDisplayName := utils.MakeSafeStringOfLength(request.DisplayName, config.MaxChatDisplayNameLength)
|
|
||||||
newUser, accessToken, err := user.CreateAnonymousUser(proposedNewDisplayName)
|
|
||||||
if err != nil {
|
|
||||||
WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := registerAnonymousUserResponse{
|
|
||||||
ID: newUser.ID,
|
|
||||||
AccessToken: accessToken,
|
|
||||||
DisplayName: newUser.DisplayName,
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
middleware.DisableCache(w)
|
|
||||||
|
|
||||||
WriteResponse(w, response)
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub"
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/router/middleware"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type webConfigResponse struct {
|
|
||||||
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
CustomStyles string `json:"customStyles"`
|
|
||||||
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
|
||||||
OfflineMessage string `json:"offlineMessage"`
|
|
||||||
Logo string `json:"logo"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
|
||||||
ExtraPageContent string `json:"extraPageContent"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
|
||||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
|
||||||
Notifications notificationsConfigResponse `json:"notifications"`
|
|
||||||
Federation federationConfigResponse `json:"federation"`
|
|
||||||
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
|
||||||
HideViewerCount bool `json:"hideViewerCount"`
|
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
|
||||||
NSFW bool `json:"nsfw"`
|
|
||||||
Authentication authenticationConfigResponse `json:"authentication"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type federationConfigResponse struct {
|
|
||||||
Account string `json:"account,omitempty"`
|
|
||||||
FollowerCount int `json:"followerCount,omitempty"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type browserNotificationsConfigResponse struct {
|
|
||||||
PublicKey string `json:"publicKey,omitempty"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type notificationsConfigResponse struct {
|
|
||||||
Browser browserNotificationsConfigResponse `json:"browser"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authenticationConfigResponse struct {
|
|
||||||
IndieAuthEnabled bool `json:"indieAuthEnabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWebConfig gets the status of the server.
|
|
||||||
func GetWebConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.EnableCors(w)
|
|
||||||
middleware.DisableCache(w)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
configuration := getConfigResponse()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(configuration); err != nil {
|
|
||||||
BadRequestHandler(w, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigResponse() webConfigResponse {
|
|
||||||
pageContent := utils.RenderPageContentMarkdown(data.GetExtraPageBodyContent())
|
|
||||||
offlineMessage := utils.RenderSimpleMarkdown(data.GetCustomOfflineMessage())
|
|
||||||
socialHandles := data.GetSocialHandles()
|
|
||||||
for i, handle := range socialHandles {
|
|
||||||
platform := models.GetSocialHandle(handle.Platform)
|
|
||||||
if platform != nil {
|
|
||||||
handle.Icon = platform.Icon
|
|
||||||
socialHandles[i] = handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverSummary := data.GetServerSummary()
|
|
||||||
|
|
||||||
var federationResponse federationConfigResponse
|
|
||||||
federationEnabled := data.GetFederationEnabled()
|
|
||||||
|
|
||||||
followerCount, _ := activitypub.GetFollowerCount()
|
|
||||||
if federationEnabled {
|
|
||||||
serverURLString := data.GetServerURL()
|
|
||||||
serverURL, _ := url.Parse(serverURLString)
|
|
||||||
account := fmt.Sprintf("%s@%s", data.GetDefaultFederationUsername(), serverURL.Host)
|
|
||||||
federationResponse = federationConfigResponse{
|
|
||||||
Enabled: federationEnabled,
|
|
||||||
FollowerCount: int(followerCount),
|
|
||||||
Account: account,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
browserPushEnabled := data.GetBrowserPushConfig().Enabled
|
|
||||||
browserPushPublicKey, err := data.GetBrowserPushPublicKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("unable to fetch browser push notifications public key", err)
|
|
||||||
browserPushEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationsResponse := notificationsConfigResponse{
|
|
||||||
Browser: browserNotificationsConfigResponse{
|
|
||||||
Enabled: browserPushEnabled,
|
|
||||||
PublicKey: browserPushPublicKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationResponse := authenticationConfigResponse{
|
|
||||||
IndieAuthEnabled: data.GetServerURL() != "",
|
|
||||||
}
|
|
||||||
|
|
||||||
return webConfigResponse{
|
|
||||||
Name: data.GetServerName(),
|
|
||||||
Summary: serverSummary,
|
|
||||||
OfflineMessage: offlineMessage,
|
|
||||||
Logo: "/logo",
|
|
||||||
Tags: data.GetServerMetadataTags(),
|
|
||||||
Version: config.GetReleaseString(),
|
|
||||||
NSFW: data.GetNSFW(),
|
|
||||||
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
|
||||||
ExtraPageContent: pageContent,
|
|
||||||
StreamTitle: data.GetStreamTitle(),
|
|
||||||
SocialHandles: socialHandles,
|
|
||||||
ChatDisabled: data.GetChatDisabled(),
|
|
||||||
ExternalActions: data.GetExternalActions(),
|
|
||||||
CustomStyles: data.GetCustomStyles(),
|
|
||||||
MaxSocketPayloadSize: config.MaxSocketPayloadSize,
|
|
||||||
Federation: federationResponse,
|
|
||||||
Notifications: notificationsResponse,
|
|
||||||
Authentication: authenticationResponse,
|
|
||||||
AppearanceVariables: data.GetCustomColorVariableValues(),
|
|
||||||
HideViewerCount: data.GetHideViewerCount(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllSocialPlatforms will return a list of all social platform types.
|
|
||||||
func GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.EnableCors(w)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
platforms := models.GetAllSocialHandles()
|
|
||||||
if err := json.NewEncoder(w).Encode(platforms); err != nil {
|
|
||||||
InternalErrorHandler(w, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
// POST is the HTTP POST method.
|
|
||||||
const POST = "POST"
|
|
||||||
|
|
||||||
// GET is the HTTP GET method.
|
|
||||||
const GET = "GET"
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contentTypeJPEG = "image/jpeg"
|
|
||||||
contentTypeGIF = "image/gif"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetThumbnail will return the thumbnail image as a response.
|
|
||||||
func GetThumbnail(w http.ResponseWriter, r *http.Request) {
|
|
||||||
imageFilename := "thumbnail.jpg"
|
|
||||||
imagePath := filepath.Join(config.TempDir, imageFilename)
|
|
||||||
|
|
||||||
var imageBytes []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if utils.DoesFileExists(imagePath) {
|
|
||||||
imageBytes, err = getImage(imagePath)
|
|
||||||
} else {
|
|
||||||
GetLogo(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
GetLogo(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
|
||||||
writeBytesAsImage(imageBytes, contentTypeJPEG, w, cacheTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPreview will return the preview gif as a response.
|
|
||||||
func GetPreview(w http.ResponseWriter, r *http.Request) {
|
|
||||||
imageFilename := "preview.gif"
|
|
||||||
imagePath := filepath.Join(config.TempDir, imageFilename)
|
|
||||||
|
|
||||||
var imageBytes []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if utils.DoesFileExists(imagePath) {
|
|
||||||
imageBytes, err = getImage(imagePath)
|
|
||||||
} else {
|
|
||||||
GetLogo(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
GetLogo(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
|
||||||
writeBytesAsImage(imageBytes, contentTypeGIF, w, cacheTime)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/metrics"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReportPlaybackMetrics will accept playback metrics from a client and save
|
|
||||||
// them for future video health reporting.
|
|
||||||
func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != POST {
|
|
||||||
WriteSimpleResponse(w, false, r.Method+" not supported")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type reportPlaybackMetricsRequest struct {
|
|
||||||
Bandwidth float64 `json:"bandwidth"`
|
|
||||||
Latency float64 `json:"latency"`
|
|
||||||
Errors float64 `json:"errors"`
|
|
||||||
DownloadDuration float64 `json:"downloadDuration"`
|
|
||||||
QualityVariantChanges float64 `json:"qualityVariantChanges"`
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var request reportPlaybackMetricsRequest
|
|
||||||
if err := decoder.Decode(&request); err != nil {
|
|
||||||
log.Errorln("error decoding playback metrics payload:", err)
|
|
||||||
WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clientID := utils.GenerateClientIDFromRequest(r)
|
|
||||||
|
|
||||||
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
|
|
||||||
if request.Bandwidth != 0.0 {
|
|
||||||
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Latency != 0.0 {
|
|
||||||
metrics.RegisterPlayerLatency(clientID, request.Latency)
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.DownloadDuration != 0.0 {
|
|
||||||
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
|
|
||||||
}
|
|
||||||
+25
-10
@@ -13,19 +13,21 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/user"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/services/geoip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents a single chat client.
|
// Client represents a single chat client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ConnectedAt time.Time `json:"connectedAt"`
|
ConnectedAt time.Time `json:"connectedAt"`
|
||||||
timeoutTimer *time.Timer
|
timeoutTimer *time.Timer
|
||||||
rateLimiter *rate.Limiter
|
rateLimiter *rate.Limiter
|
||||||
conn *websocket.Conn
|
messageFilter *ChatMessageFilter
|
||||||
User *user.User `json:"user"`
|
conn *websocket.Conn
|
||||||
server *Server
|
User *models.User `json:"user"`
|
||||||
Geo *geoip.GeoDetails `json:"geo"`
|
server *Server
|
||||||
|
Geo *geoip.GeoDetails `json:"geo"`
|
||||||
// Buffered channel of outbound messages.
|
// Buffered channel of outbound messages.
|
||||||
send chan []byte
|
send chan []byte
|
||||||
accessToken string
|
accessToken string
|
||||||
@@ -90,6 +92,7 @@ func (c *Client) readPump() {
|
|||||||
// Allow 3 messages every two seconds.
|
// Allow 3 messages every two seconds.
|
||||||
limit := rate.Every(2 * time.Second / 3)
|
limit := rate.Every(2 * time.Second / 3)
|
||||||
c.rateLimiter = rate.NewLimiter(limit, 1)
|
c.rateLimiter = rate.NewLimiter(limit, 1)
|
||||||
|
c.messageFilter = NewMessageFilter()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
c.close()
|
c.close()
|
||||||
@@ -129,6 +132,12 @@ func (c *Client) readPump() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this message passes the optional language filter
|
||||||
|
if data.GetChatSlurFilterEnabled() && !c.messageFilter.Allow(string(message)) {
|
||||||
|
c.sendAction("Sorry, that message contained language that is not allowed in this chat.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space))
|
message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space))
|
||||||
c.handleEvent(message)
|
c.handleEvent(message)
|
||||||
}
|
}
|
||||||
@@ -200,7 +209,13 @@ func (c *Client) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) passesRateLimit() bool {
|
func (c *Client) passesRateLimit() bool {
|
||||||
return c.User.IsModerator() || (c.rateLimiter.Allow() && !c.inTimeout)
|
// If spam rate limiting is disabled, or the user is a moderator, always
|
||||||
|
// allow the message.
|
||||||
|
if !data.GetChatSpamProtectionEnabled() || c.User.IsModerator() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (c.rateLimiter.Allow() && !c.inTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startChatRejectionTimeout() {
|
func (c *Client) startChatRejectionTimeout() {
|
||||||
|
|||||||
+13
-7
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -46,12 +46,14 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userRepository := userrepository.Get()
|
||||||
|
|
||||||
// Check if the name is not already assigned to a registered user.
|
// Check if the name is not already assigned to a registered user.
|
||||||
if available, err := user.IsDisplayNameAvailable(proposedUsername); err != nil {
|
if available, err := userRepository.IsDisplayNameAvailable(proposedUsername); err != nil {
|
||||||
log.Errorln("error checking if name is available", err)
|
log.Errorln("error checking if name is available", err)
|
||||||
return
|
return
|
||||||
} else if !available {
|
} else if !available {
|
||||||
message := fmt.Sprintf("The name **%s** has been already registered. If this is your name, please authenticate.", proposedUsername)
|
message := fmt.Sprintf("The name **%s** has already been registered. If this is your name, please authenticate.", proposedUsername)
|
||||||
s.sendActionToClient(eventData.client, message)
|
s.sendActionToClient(eventData.client, message)
|
||||||
|
|
||||||
// Resend the client's user so their username is in sync.
|
// Resend the client's user so their username is in sync.
|
||||||
@@ -60,7 +62,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
savedUser := user.GetUserByToken(eventData.client.accessToken)
|
savedUser := userRepository.GetUserByToken(eventData.client.accessToken)
|
||||||
oldName := savedUser.DisplayName
|
oldName := savedUser.DisplayName
|
||||||
|
|
||||||
// Check that the new name is different from old.
|
// Check that the new name is different from old.
|
||||||
@@ -70,7 +72,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the new name
|
// Save the new name
|
||||||
if err := user.ChangeUsername(eventData.client.User.ID, proposedUsername); err != nil {
|
if err := userRepository.ChangeUsername(eventData.client.User.ID, proposedUsername); err != nil {
|
||||||
log.Errorln("error changing username", err)
|
log.Errorln("error changing username", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +105,8 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) userColorChanged(eventData chatClientEvent) {
|
func (s *Server) userColorChanged(eventData chatClientEvent) {
|
||||||
|
userRepository := userrepository.Get()
|
||||||
|
|
||||||
var receivedEvent events.ColorChangeEvent
|
var receivedEvent events.ColorChangeEvent
|
||||||
if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil {
|
if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil {
|
||||||
log.Errorln("error unmarshalling to ColorChangeEvent", err)
|
log.Errorln("error unmarshalling to ColorChangeEvent", err)
|
||||||
@@ -116,7 +120,7 @@ func (s *Server) userColorChanged(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the new color
|
// Save the new color
|
||||||
if err := user.ChangeUserColor(eventData.client.User.ID, receivedEvent.NewColor); err != nil {
|
if err := userRepository.ChangeUserColor(eventData.client.User.ID, receivedEvent.NewColor); err != nil {
|
||||||
log.Errorln("error changing user display color", err)
|
log.Errorln("error changing user display color", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +130,8 @@ func (s *Server) userColorChanged(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) userMessageSent(eventData chatClientEvent) {
|
func (s *Server) userMessageSent(eventData chatClientEvent) {
|
||||||
|
userRepository := userrepository.Get()
|
||||||
|
|
||||||
var event events.UserMessageEvent
|
var event events.UserMessageEvent
|
||||||
if err := json.Unmarshal(eventData.data, &event); err != nil {
|
if err := json.Unmarshal(eventData.data, &event); err != nil {
|
||||||
log.Errorln("error unmarshalling to UserMessageEvent", err)
|
log.Errorln("error unmarshalling to UserMessageEvent", err)
|
||||||
@@ -148,7 +154,7 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.User = user.GetUserByToken(eventData.client.accessToken)
|
event.User = userRepository.GetUserByToken(eventData.client.accessToken)
|
||||||
|
|
||||||
// Guard against nil users
|
// Guard against nil users
|
||||||
if event.User == nil {
|
if event.User == nil {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/owncast/owncast/core/user"
|
import "github.com/owncast/owncast/models"
|
||||||
|
|
||||||
// ConnectedClientInfo represents the information about a connected client.
|
// ConnectedClientInfo represents the information about a connected client.
|
||||||
type ConnectedClientInfo struct {
|
type ConnectedClientInfo struct {
|
||||||
User *user.User `json:"user"`
|
User *models.User `json:"user"`
|
||||||
Event
|
Event
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"mvdan.cc/xurls"
|
"mvdan.cc/xurls"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
"github.com/owncast/owncast/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,21 +30,21 @@ type EventPayload map[string]interface{}
|
|||||||
// OutboundEvent represents an event that is sent out to all listeners of the chat server.
|
// OutboundEvent represents an event that is sent out to all listeners of the chat server.
|
||||||
type OutboundEvent interface {
|
type OutboundEvent interface {
|
||||||
GetBroadcastPayload() EventPayload
|
GetBroadcastPayload() EventPayload
|
||||||
GetMessageType() EventType
|
GetMessageType() models.EventType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event is any kind of event. A type is required to be specified.
|
// Event is any kind of event. A type is required to be specified.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Type EventType `json:"type,omitempty"`
|
Type models.EventType `json:"type,omitempty"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserEvent is an event with an associated user.
|
// UserEvent is an event with an associated user.
|
||||||
type UserEvent struct {
|
type UserEvent struct {
|
||||||
User *user.User `json:"user"`
|
User *models.User `json:"user"`
|
||||||
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
|
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
|
||||||
ClientID uint `json:"clientId,omitempty"`
|
ClientID uint `json:"clientId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageEvent is an event that has a message body.
|
// MessageEvent is an event that has a message body.
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
goaway "github.com/TwiN/go-away"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatMessageFilter is a allow/deny chat message filter.
|
||||||
|
type ChatMessageFilter struct{}
|
||||||
|
|
||||||
|
// NewMessageFilter will return an instance of the chat message filter.
|
||||||
|
func NewMessageFilter() *ChatMessageFilter {
|
||||||
|
return &ChatMessageFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow will test if this message should be allowed to be sent.
|
||||||
|
func (*ChatMessageFilter) Allow(message string) bool {
|
||||||
|
return !goaway.IsProfane(message)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFiltering(t *testing.T) {
|
||||||
|
filter := NewMessageFilter()
|
||||||
|
|
||||||
|
filteredTestMessages := []string{
|
||||||
|
"Hello, fucking world!",
|
||||||
|
"Suck my dick",
|
||||||
|
"Eat my ass",
|
||||||
|
"fuck this shit",
|
||||||
|
"@$$h073",
|
||||||
|
"F u C k th1$ $h!t",
|
||||||
|
"u r fag",
|
||||||
|
"fucking sucks",
|
||||||
|
}
|
||||||
|
|
||||||
|
unfilteredTestMessages := []string{
|
||||||
|
"bass fish",
|
||||||
|
"assumptions",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range filteredTestMessages {
|
||||||
|
result := filter.Allow(m)
|
||||||
|
if result {
|
||||||
|
t.Errorf("%s should be seen as a filtered profane message", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range unfilteredTestMessages {
|
||||||
|
result := filter.Allow(m)
|
||||||
|
if !result {
|
||||||
|
t.Errorf("%s should not be filtered", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
|
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ const (
|
|||||||
|
|
||||||
func setupPersistence() {
|
func setupPersistence() {
|
||||||
_datastore = data.GetDatastore()
|
_datastore = data.GetDatastore()
|
||||||
data.CreateMessagesTable(_datastore.DB)
|
tables.CreateMessagesTable(_datastore.DB)
|
||||||
data.CreateBanIPTable(_datastore.DB)
|
data.CreateBanIPTable(_datastore.DB)
|
||||||
|
|
||||||
chatDataPruner := time.NewTicker(5 * time.Minute)
|
chatDataPruner := time.NewTicker(5 * time.Minute)
|
||||||
@@ -104,7 +105,7 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
|||||||
isBot := (row.userType != nil && *row.userType == "API")
|
isBot := (row.userType != nil && *row.userType == "API")
|
||||||
scopeSlice := strings.Split(scopes, ",")
|
scopeSlice := strings.Split(scopes, ",")
|
||||||
|
|
||||||
u := user.User{
|
u := models.User{
|
||||||
ID: *row.userID,
|
ID: *row.userID,
|
||||||
DisplayName: displayName,
|
DisplayName: displayName,
|
||||||
DisplayColor: displayColor,
|
DisplayColor: displayColor,
|
||||||
|
|||||||
+11
-5
@@ -14,9 +14,10 @@ import (
|
|||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
|
"github.com/owncast/owncast/services/geoip"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ func (s *Server) Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Addclient registers new connection as a User.
|
// Addclient registers new connection as a User.
|
||||||
func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken string, userAgent string, ipAddress string) *Client {
|
func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken string, userAgent string, ipAddress string) *Client {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
server: s,
|
server: s,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
@@ -239,8 +240,11 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userRepository := userrepository.Get()
|
||||||
|
|
||||||
// A user is required to use the websocket
|
// A user is required to use the websocket
|
||||||
user := user.GetUserByToken(accessToken)
|
user := userRepository.GetUserByToken(accessToken)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
// Send error that registration is required
|
// Send error that registration is required
|
||||||
_ = conn.WriteJSON(events.EventPayload{
|
_ = conn.WriteJSON(events.EventPayload{
|
||||||
@@ -335,8 +339,10 @@ func SendConnectedClientInfoToUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userRepository := userrepository.Get()
|
||||||
|
|
||||||
// Get an updated reference to the user.
|
// Get an updated reference to the user.
|
||||||
user := user.GetUserByID(userID)
|
user := userRepository.GetUserByID(userID)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return fmt.Errorf("user not found")
|
return fmt.Errorf("user not found")
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -13,10 +13,10 @@ import (
|
|||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/rtmp"
|
"github.com/owncast/owncast/core/rtmp"
|
||||||
"github.com/owncast/owncast/core/transcoder"
|
"github.com/owncast/owncast/core/transcoder"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/notifications"
|
"github.com/owncast/owncast/notifications"
|
||||||
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/owncast/owncast/yp"
|
"github.com/owncast/owncast/yp"
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,7 @@ func Start() error {
|
|||||||
log.Errorln("storage error", err)
|
log.Errorln("storage error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.SetupUsers()
|
tables.SetupUsers(data.GetDatastore().DB)
|
||||||
auth.Setup(data.GetDatastore())
|
auth.Setup(data.GetDatastore())
|
||||||
|
|
||||||
fileWriter.SetupFileWriterReceiverService(&handler)
|
fileWriter.SetupFileWriterReceiverService(&handler)
|
||||||
@@ -104,7 +104,7 @@ func transitionToOfflineVideoStreamContent() {
|
|||||||
_transcoder.SetLatencyLevel(models.GetLatencyLevel(4))
|
_transcoder.SetLatencyLevel(models.GetLatencyLevel(4))
|
||||||
_transcoder.SetIsEvent(true)
|
_transcoder.SetIsEvent(true)
|
||||||
|
|
||||||
offlineFilePath, err := saveOfflineClipToDisk("offline.ts")
|
offlineFilePath, err := saveOfflineClipToDisk("offline-v2.ts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("unable to save offline clip:", err)
|
log.Fatalln("unable to save offline clip:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -59,6 +59,8 @@ const (
|
|||||||
suggestedUsernamesKey = "suggested_usernames"
|
suggestedUsernamesKey = "suggested_usernames"
|
||||||
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
|
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
|
||||||
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
|
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
|
||||||
|
chatSpamProtectionEnabledKey = "chat_spam_protection_enabled"
|
||||||
|
chatSlurFilterEnabledKey = "chat_slur_filter_enabled"
|
||||||
notificationsEnabledKey = "notifications_enabled"
|
notificationsEnabledKey = "notifications_enabled"
|
||||||
discordConfigurationKey = "discord_configuration"
|
discordConfigurationKey = "discord_configuration"
|
||||||
browserPushConfigurationKey = "browser_push_configuration"
|
browserPushConfigurationKey = "browser_push_configuration"
|
||||||
@@ -113,7 +115,11 @@ func GetAdminPassword() string {
|
|||||||
|
|
||||||
// SetAdminPassword will set the admin password.
|
// SetAdminPassword will set the admin password.
|
||||||
func SetAdminPassword(key string) error {
|
func SetAdminPassword(key string) error {
|
||||||
return _datastore.SetString(adminPasswordKey, key)
|
hashed_pass, err := utils.HashPassword(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return _datastore.SetString(adminPasswordKey, hashed_pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogoPath will return the path for the logo, relative to webroot.
|
// GetLogoPath will return the path for the logo, relative to webroot.
|
||||||
@@ -528,6 +534,36 @@ func GetChatEstbalishedUsersOnlyMode() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetChatSpamProtectionEnabled will enable chat spam protection if set to true.
|
||||||
|
func SetChatSpamProtectionEnabled(enabled bool) error {
|
||||||
|
return _datastore.SetBool(chatSpamProtectionEnabledKey, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatSpamProtectionEnabled will return if chat spam protection is enabled.
|
||||||
|
func GetChatSpamProtectionEnabled() bool {
|
||||||
|
enabled, err := _datastore.GetBool(chatSpamProtectionEnabledKey)
|
||||||
|
if err == nil {
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChatSlurFilterEnabled will enable the chat slur filter.
|
||||||
|
func SetChatSlurFilterEnabled(enabled bool) error {
|
||||||
|
return _datastore.SetBool(chatSlurFilterEnabledKey, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatSlurFilterEnabled will return if the chat slur filter is enabled.
|
||||||
|
func GetChatSlurFilterEnabled() bool {
|
||||||
|
enabled, err := _datastore.GetBool(chatSlurFilterEnabledKey)
|
||||||
|
if err == nil {
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GetExternalActions will return the registered external actions.
|
// GetExternalActions will return the registered external actions.
|
||||||
func GetExternalActions() []models.ExternalAction {
|
func GetExternalActions() []models.ExternalAction {
|
||||||
configEntry, err := _datastore.Get(externalActionsKey)
|
configEntry, err := _datastore.Get(externalActionsKey)
|
||||||
|
|||||||
+5
-3
@@ -11,6 +11,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -74,8 +76,8 @@ func SetupPersistence(file string) error {
|
|||||||
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
||||||
|
|
||||||
createWebhooksTable()
|
createWebhooksTable()
|
||||||
createUsersTable(db)
|
tables.CreateUsersTable(db)
|
||||||
createAccessTokenTable(db)
|
tables.CreateAccessTokenTable(db)
|
||||||
|
|
||||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (
|
||||||
"key" string NOT NULL PRIMARY KEY,
|
"key" string NOT NULL PRIMARY KEY,
|
||||||
@@ -108,7 +110,7 @@ func SetupPersistence(file string) error {
|
|||||||
|
|
||||||
// is database schema outdated?
|
// is database schema outdated?
|
||||||
if version < schemaVersion {
|
if version < schemaVersion {
|
||||||
if err := migrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
if err := tables.MigrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
datastoreValuesVersion = 3
|
datastoreValuesVersion = 4
|
||||||
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ func migrateDatastoreValues(datastore *Datastore) {
|
|||||||
migrateToDatastoreValues2(datastore)
|
migrateToDatastoreValues2(datastore)
|
||||||
case 2:
|
case 2:
|
||||||
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
||||||
|
case 3:
|
||||||
|
migrateToDatastoreValues4(datastore)
|
||||||
default:
|
default:
|
||||||
log.Fatalln("missing datastore values migration step")
|
log.Fatalln("missing datastore values migration step")
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,8 @@ func migrateToDatastoreValues1(datastore *Datastore) {
|
|||||||
|
|
||||||
func migrateToDatastoreValues2(datastore *Datastore) {
|
func migrateToDatastoreValues2(datastore *Datastore) {
|
||||||
oldAdminPassword, _ := datastore.GetString("stream_key")
|
oldAdminPassword, _ := datastore.GetString("stream_key")
|
||||||
_ = SetAdminPassword(oldAdminPassword)
|
// Avoids double hashing the password
|
||||||
|
_ = datastore.SetString("admin_password_key", oldAdminPassword)
|
||||||
_ = SetStreamKeys([]models.StreamKey{
|
_ = SetStreamKeys([]models.StreamKey{
|
||||||
{Key: oldAdminPassword, Comment: "Default stream key"},
|
{Key: oldAdminPassword, Comment: "Default stream key"},
|
||||||
})
|
})
|
||||||
@@ -73,3 +76,11 @@ func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
|
|||||||
|
|
||||||
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateToDatastoreValues4(datastore *Datastore) {
|
||||||
|
unhashed_pass, _ := datastore.GetString("admin_password_key")
|
||||||
|
err := SetAdminPassword(unhashed_pass)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("error migrating admin password:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,32 +9,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateMessagesTable will create the chat messages table if needed.
|
|
||||||
func CreateMessagesTable(db *sql.DB) {
|
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
|
|
||||||
"id" string NOT NULL,
|
|
||||||
"user_id" TEXT,
|
|
||||||
"body" TEXT,
|
|
||||||
"eventType" TEXT,
|
|
||||||
"hidden_at" DATETIME,
|
|
||||||
"timestamp" DATETIME,
|
|
||||||
"title" TEXT,
|
|
||||||
"subtitle" TEXT,
|
|
||||||
"image" TEXT,
|
|
||||||
"link" TEXT,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
);`
|
|
||||||
MustExec(createTableSQL, db)
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
|
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessagesCount will return the number of messages in the database.
|
// GetMessagesCount will return the number of messages in the database.
|
||||||
func GetMessagesCount() int64 {
|
func GetMessagesCount() int64 {
|
||||||
query := `SELECT COUNT(*) FROM messages`
|
query := `SELECT COUNT(*) FROM messages`
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
|
|||||||
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-DISCONTINUITY\n")
|
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-DISCONTINUITY\n")
|
||||||
// If "offline" content gets changed then change the duration below
|
// If "offline" content gets changed then change the duration below
|
||||||
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXTINF:8.000000,\n")
|
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXTINF:8.000000,\n")
|
||||||
_, _ = atomicWriteTmpPlaylistFile.WriteString("offline.ts\n")
|
_, _ = atomicWriteTmpPlaylistFile.WriteString("offline-v2.ts\n")
|
||||||
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-ENDLIST\n")
|
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-ENDLIST\n")
|
||||||
|
|
||||||
if err := atomicWriteTmpPlaylistFile.Close(); err != nil {
|
if err := atomicWriteTmpPlaylistFile.Close(); err != nil {
|
||||||
|
|||||||
+1
-1
@@ -8,8 +8,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/geoip"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/services/geoip"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ func SetStreamAsDisconnected() {
|
|||||||
_stats.LastConnectTime = nil
|
_stats.LastConnectTime = nil
|
||||||
_broadcaster = nil
|
_broadcaster = nil
|
||||||
|
|
||||||
offlineFilename := "offline.ts"
|
offlineFilename := "offline-v2.ts"
|
||||||
|
|
||||||
offlineFilePath, err := saveOfflineClipToDisk(offlineFilename)
|
offlineFilePath, err := saveOfflineClipToDisk(offlineFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/teris-io/shortid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
|
||||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
|
||||||
type ExternalAPIUser struct {
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
AccessToken string `json:"accessToken"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
Type string `json:"type,omitempty"` // Should be API
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
DisplayColor int `json:"displayColor"`
|
|
||||||
IsBot bool `json:"isBot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
|
||||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
|
|
||||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
|
||||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
|
|
||||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
|
||||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
|
|
||||||
)
|
|
||||||
|
|
||||||
// For a scope to be seen as "valid" it must live in this slice.
|
|
||||||
var validAccessTokenScopes = []string{
|
|
||||||
ScopeCanSendChatMessages,
|
|
||||||
ScopeCanSendSystemMessages,
|
|
||||||
ScopeHasAdminAccess,
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertExternalAPIUser will add a new API user to the database.
|
|
||||||
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error {
|
|
||||||
log.Traceln("Adding new API user")
|
|
||||||
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
scopesString := strings.Join(scopes, ",")
|
|
||||||
id := shortid.MustGenerate()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := addAccessTokenForUser(token, id); err != nil {
|
|
||||||
return errors.Wrap(err, "unable to save access token for new external api user")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExternalAPIUser will delete a token from the database.
|
|
||||||
func DeleteExternalAPIUser(token string) error {
|
|
||||||
log.Traceln("Deleting access token")
|
|
||||||
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
result, err := stmt.Exec(token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
|
||||||
tx.Rollback() //nolint
|
|
||||||
return errors.New(token + " not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
|
||||||
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) {
|
|
||||||
// This will split the scopes from comma separated to individual rows
|
|
||||||
// so we can efficiently find if a token supports a single scope.
|
|
||||||
// This is SQLite specific, so if we ever support other database
|
|
||||||
// backends we need to support other methods.
|
|
||||||
query := `SELECT
|
|
||||||
id,
|
|
||||||
scopes,
|
|
||||||
display_name,
|
|
||||||
display_color,
|
|
||||||
created_at,
|
|
||||||
last_used
|
|
||||||
FROM
|
|
||||||
user_access_tokens
|
|
||||||
INNER JOIN (
|
|
||||||
WITH RECURSIVE split(
|
|
||||||
id,
|
|
||||||
scopes,
|
|
||||||
display_name,
|
|
||||||
display_color,
|
|
||||||
created_at,
|
|
||||||
last_used,
|
|
||||||
disabled_at,
|
|
||||||
scope,
|
|
||||||
rest
|
|
||||||
) AS (
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
scopes,
|
|
||||||
display_name,
|
|
||||||
display_color,
|
|
||||||
created_at,
|
|
||||||
last_used,
|
|
||||||
disabled_at,
|
|
||||||
'',
|
|
||||||
scopes || ','
|
|
||||||
FROM
|
|
||||||
users AS u
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
scopes,
|
|
||||||
display_name,
|
|
||||||
display_color,
|
|
||||||
created_at,
|
|
||||||
last_used,
|
|
||||||
disabled_at,
|
|
||||||
substr(rest, 0, instr(rest, ',')),
|
|
||||||
substr(rest, instr(rest, ',') + 1)
|
|
||||||
FROM
|
|
||||||
split
|
|
||||||
WHERE
|
|
||||||
rest <> ''
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
display_name,
|
|
||||||
display_color,
|
|
||||||
created_at,
|
|
||||||
last_used,
|
|
||||||
disabled_at,
|
|
||||||
scopes,
|
|
||||||
scope
|
|
||||||
FROM
|
|
||||||
split
|
|
||||||
WHERE
|
|
||||||
scope <> ''
|
|
||||||
) ON user_access_tokens.user_id = id
|
|
||||||
WHERE
|
|
||||||
disabled_at IS NULL
|
|
||||||
AND token = ?
|
|
||||||
AND scope = ?;`
|
|
||||||
|
|
||||||
row := _datastore.DB.QueryRow(query, token, scope)
|
|
||||||
integration, err := makeExternalAPIUserFromRow(row)
|
|
||||||
|
|
||||||
return integration, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
|
||||||
func GetIntegrationNameForAccessToken(token string) *string {
|
|
||||||
name, err := _datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &name
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalAPIUser will return all API users with access tokens.
|
|
||||||
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
|
|
||||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL"
|
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
return []ExternalAPIUser{}, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
integrations, err := makeExternalAPIUsersFromRows(rows)
|
|
||||||
|
|
||||||
return integrations, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
|
||||||
func SetExternalAPIUserAccessTokenAsUsed(token string) error {
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
if _, err := stmt.Exec(token); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) {
|
|
||||||
var id string
|
|
||||||
var displayName string
|
|
||||||
var displayColor int
|
|
||||||
var scopes string
|
|
||||||
var createdAt time.Time
|
|
||||||
var lastUsedAt *time.Time
|
|
||||||
|
|
||||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln("unable to convert row to api user", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
integration := ExternalAPIUser{
|
|
||||||
ID: id,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
Scopes: strings.Split(scopes, ","),
|
|
||||||
LastUsedAt: lastUsedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &integration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) {
|
|
||||||
integrations := make([]ExternalAPIUser, 0)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var id string
|
|
||||||
var accessToken string
|
|
||||||
var displayName string
|
|
||||||
var displayColor int
|
|
||||||
var scopes string
|
|
||||||
var createdAt time.Time
|
|
||||||
var lastUsedAt *time.Time
|
|
||||||
|
|
||||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
integration := ExternalAPIUser{
|
|
||||||
ID: id,
|
|
||||||
AccessToken: accessToken,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
Scopes: strings.Split(scopes, ","),
|
|
||||||
LastUsedAt: lastUsedAt,
|
|
||||||
IsBot: true,
|
|
||||||
}
|
|
||||||
integrations = append(integrations, integration)
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasValidScopes will verify that all the scopes provided are valid.
|
|
||||||
func HasValidScopes(scopes []string) bool {
|
|
||||||
for _, scope := range scopes {
|
|
||||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope)
|
|
||||||
if !foundInSlice {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tokenName = "test token name"
|
|
||||||
token = "test-token-123"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testScopes = []string{"test-scope"}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
if err := data.SetupPersistence(":memory:"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
SetupUsers()
|
|
||||||
|
|
||||||
m.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateExternalAPIUser(t *testing.T) {
|
|
||||||
if err := InsertExternalAPIUser(token, tokenName, 0, testScopes); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user := GetUserByToken(token)
|
|
||||||
if user == nil {
|
|
||||||
t.Fatal("api user not found after creating")
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.DisplayName != tokenName {
|
|
||||||
t.Errorf("expected display name %q, got %q", tokenName, user.DisplayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Scopes[0] != testScopes[0] {
|
|
||||||
t.Errorf("expected scopes %q, got %q", testScopes, user.Scopes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteExternalAPIUser(t *testing.T) {
|
|
||||||
if err := DeleteExternalAPIUser(token); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyTokenDisabled(t *testing.T) {
|
|
||||||
users, err := GetExternalAPIUser()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users) > 0 {
|
|
||||||
t.Fatal("disabled user returned in list of all API users")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyGetUserTokenDisabled(t *testing.T) {
|
|
||||||
user := GetUserByToken(token)
|
|
||||||
if user == nil {
|
|
||||||
t.Fatal("user not returned in GetUserByToken after disabling")
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.DisabledAt == nil {
|
|
||||||
t.Fatal("user returned in GetUserByToken after disabling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled(t *testing.T) {
|
|
||||||
user, _ := GetExternalAPIUserForAccessTokenAndScope(token, testScopes[0])
|
|
||||||
|
|
||||||
if user != nil {
|
|
||||||
t.Fatal("user returned in GetExternalAPIUserForAccessTokenAndScope after disabling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateAdditionalAPIUser(t *testing.T) {
|
|
||||||
if err := InsertExternalAPIUser("ignore-me", "token-to-be-ignored", 0, testScopes); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAgainVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled(t *testing.T) {
|
|
||||||
user, _ := GetExternalAPIUserForAccessTokenAndScope(token, testScopes[0])
|
|
||||||
|
|
||||||
if user != nil {
|
|
||||||
t.Fatal("user returned in TestAgainVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled after disabling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/teris-io/shortid"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
|
||||||
|
|
||||||
const (
|
|
||||||
moderatorScopeKey = "MODERATOR"
|
|
||||||
minSuggestedUsernamePoolLength = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// User represents a single chat user.
|
|
||||||
type User struct {
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
|
||||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
|
||||||
AuthenticatedAt *time.Time `json:"-"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
PreviousNames []string `json:"previousNames"`
|
|
||||||
Scopes []string `json:"scopes,omitempty"`
|
|
||||||
DisplayColor int `json:"displayColor"`
|
|
||||||
IsBot bool `json:"isBot"`
|
|
||||||
Authenticated bool `json:"authenticated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEnabled will return if this single user is enabled.
|
|
||||||
func (u *User) IsEnabled() bool {
|
|
||||||
return u.DisabledAt == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsModerator will return if the user has moderation privileges.
|
|
||||||
func (u *User) IsModerator() bool {
|
|
||||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
|
|
||||||
return hasModerationScope
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupUsers will perform the initial initialization of the user package.
|
|
||||||
func SetupUsers() {
|
|
||||||
_datastore = data.GetDatastore()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateDisplayName() string {
|
|
||||||
suggestedUsernamesList := data.GetSuggestedUsernamesList()
|
|
||||||
|
|
||||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength {
|
|
||||||
index := utils.RandomIndex(len(suggestedUsernamesList))
|
|
||||||
return suggestedUsernamesList[index]
|
|
||||||
} else {
|
|
||||||
return utils.GeneratePhrase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
|
||||||
func CreateAnonymousUser(displayName string) (*User, string, error) {
|
|
||||||
// Try to assign a name that was requested.
|
|
||||||
if displayName != "" {
|
|
||||||
// If name isn't available then generate a random one.
|
|
||||||
if available, _ := IsDisplayNameAvailable(displayName); !available {
|
|
||||||
displayName = generateDisplayName()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
displayName = generateDisplayName()
|
|
||||||
}
|
|
||||||
|
|
||||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
|
||||||
|
|
||||||
id := shortid.MustGenerate()
|
|
||||||
user := &User{
|
|
||||||
ID: id,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user.
|
|
||||||
if err := create(user); err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign it an access token.
|
|
||||||
accessToken, err := utils.GenerateAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("Unable to create access token for new user")
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
if err := addAccessTokenForUser(accessToken, id); err != nil {
|
|
||||||
return nil, "", errors.Wrap(err, "unable to save access token for new user")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, accessToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
|
||||||
func IsDisplayNameAvailable(displayName string) (bool, error) {
|
|
||||||
if available, err := _datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil {
|
|
||||||
return false, errors.Wrap(err, "unable to check if display name is available")
|
|
||||||
} else if available != 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
|
||||||
func ChangeUsername(userID string, username string) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
if err := _datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
|
|
||||||
DisplayName: username,
|
|
||||||
ID: userID,
|
|
||||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
|
||||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "unable to change display name")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
|
||||||
func ChangeUserColor(userID string, color int) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
if err := _datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
|
||||||
DisplayColor: int32(color),
|
|
||||||
ID: userID,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "unable to change display color")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addAccessTokenForUser(accessToken, userID string) error {
|
|
||||||
return _datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
|
|
||||||
Token: accessToken,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func create(user *User) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)")
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("error creating new user", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnabled will set the enabled status of a single user by ID.
|
|
||||||
func SetEnabled(userID string, enabled bool) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() //nolint
|
|
||||||
|
|
||||||
var stmt *sql.Stmt
|
|
||||||
if !enabled {
|
|
||||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?")
|
|
||||||
} else {
|
|
||||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
if _, err := stmt.Exec(userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByToken will return a user by an access token.
|
|
||||||
func GetUserByToken(token string) *User {
|
|
||||||
u, err := _datastore.GetQueries().GetUserByAccessToken(context.Background(), token)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes []string
|
|
||||||
if u.Scopes.Valid {
|
|
||||||
scopes = strings.Split(u.Scopes.String, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
var disabledAt *time.Time
|
|
||||||
if u.DisabledAt.Valid {
|
|
||||||
disabledAt = &u.DisabledAt.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
var authenticatedAt *time.Time
|
|
||||||
if u.AuthenticatedAt.Valid {
|
|
||||||
authenticatedAt = &u.AuthenticatedAt.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
return &User{
|
|
||||||
ID: u.ID,
|
|
||||||
DisplayName: u.DisplayName,
|
|
||||||
DisplayColor: int(u.DisplayColor),
|
|
||||||
CreatedAt: u.CreatedAt.Time,
|
|
||||||
DisabledAt: disabledAt,
|
|
||||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
|
||||||
NameChangedAt: &u.NamechangedAt.Time,
|
|
||||||
AuthenticatedAt: authenticatedAt,
|
|
||||||
Authenticated: authenticatedAt != nil,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
|
||||||
// different user. Used for logging in with external auth.
|
|
||||||
func SetAccessTokenToOwner(token, userID string) error {
|
|
||||||
return _datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
|
|
||||||
UserID: userID,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
|
||||||
// in some way.
|
|
||||||
func SetUserAsAuthenticated(userID string) error {
|
|
||||||
return errors.Wrap(_datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetModerator will add or remove moderator status for a single user by ID.
|
|
||||||
func SetModerator(userID string, isModerator bool) error {
|
|
||||||
if isModerator {
|
|
||||||
return addScopeToUser(userID, moderatorScopeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeScopeFromUser(userID, moderatorScopeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addScopeToUser(userID string, scope string) error {
|
|
||||||
u := GetUserByID(userID)
|
|
||||||
if u == nil {
|
|
||||||
return errors.New("user not found when modifying scope")
|
|
||||||
}
|
|
||||||
|
|
||||||
scopesString := u.Scopes
|
|
||||||
scopes := utils.StringSliceToMap(scopesString)
|
|
||||||
scopes[scope] = true
|
|
||||||
|
|
||||||
scopesSlice := utils.StringMapKeys(scopes)
|
|
||||||
|
|
||||||
return setScopesOnUser(userID, scopesSlice)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeScopeFromUser(userID string, scope string) error {
|
|
||||||
u := GetUserByID(userID)
|
|
||||||
scopesString := u.Scopes
|
|
||||||
scopes := utils.StringSliceToMap(scopesString)
|
|
||||||
delete(scopes, scope)
|
|
||||||
|
|
||||||
scopesSlice := utils.StringMapKeys(scopes)
|
|
||||||
|
|
||||||
return setScopesOnUser(userID, scopesSlice)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setScopesOnUser(userID string, scopes []string) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() //nolint
|
|
||||||
|
|
||||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ","))
|
|
||||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
var val *string
|
|
||||||
if scopesSliceString == "" {
|
|
||||||
val = nil
|
|
||||||
} else {
|
|
||||||
val = &scopesSliceString
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := stmt.Exec(val, userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID will return a user by a user ID.
|
|
||||||
func GetUserByID(id string) *User {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
|
|
||||||
row := _datastore.DB.QueryRow(query, id)
|
|
||||||
if row == nil {
|
|
||||||
log.Errorln(row)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return getUserFromRow(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
|
||||||
func GetDisabledUsers() []*User {
|
|
||||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
|
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
users := getUsersFromRows(rows)
|
|
||||||
|
|
||||||
sort.Slice(users, func(i, j int) bool {
|
|
||||||
return users[i].DisabledAt.Before(*users[j].DisabledAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetModeratorUsers will return a list of users with moderator access.
|
|
||||||
func GetModeratorUsers() []*User {
|
|
||||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (
|
|
||||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS (
|
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users
|
|
||||||
UNION ALL
|
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at,
|
|
||||||
substr(rest, 0, instr(rest, ',')),
|
|
||||||
substr(rest, instr(rest, ',')+1)
|
|
||||||
FROM split
|
|
||||||
WHERE rest <> '')
|
|
||||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope
|
|
||||||
FROM split
|
|
||||||
WHERE scope <> ''
|
|
||||||
ORDER BY created_at
|
|
||||||
) AS token WHERE token.scope = ?`
|
|
||||||
|
|
||||||
rows, err := _datastore.DB.Query(query, moderatorScopeKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
users := getUsersFromRows(rows)
|
|
||||||
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUsersFromRows(rows *sql.Rows) []*User {
|
|
||||||
users := make([]*User, 0)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var id string
|
|
||||||
var displayName string
|
|
||||||
var displayColor int
|
|
||||||
var createdAt time.Time
|
|
||||||
var disabledAt *time.Time
|
|
||||||
var previousUsernames string
|
|
||||||
var userNameChangedAt *time.Time
|
|
||||||
var scopesString *string
|
|
||||||
|
|
||||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil {
|
|
||||||
log.Errorln("error creating collection of users from results", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes []string
|
|
||||||
if scopesString != nil {
|
|
||||||
scopes = strings.Split(*scopesString, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &User{
|
|
||||||
ID: id,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
DisabledAt: disabledAt,
|
|
||||||
PreviousNames: strings.Split(previousUsernames, ","),
|
|
||||||
NameChangedAt: userNameChangedAt,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
users = append(users, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(users, func(i, j int) bool {
|
|
||||||
return users[i].CreatedAt.Before(users[j].CreatedAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUserFromRow(row *sql.Row) *User {
|
|
||||||
var id string
|
|
||||||
var displayName string
|
|
||||||
var displayColor int
|
|
||||||
var createdAt time.Time
|
|
||||||
var disabledAt *time.Time
|
|
||||||
var previousUsernames string
|
|
||||||
var userNameChangedAt *time.Time
|
|
||||||
var scopesString *string
|
|
||||||
|
|
||||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes []string
|
|
||||||
if scopesString != nil {
|
|
||||||
scopes = strings.Split(*scopesString, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &User{
|
|
||||||
ID: id,
|
|
||||||
DisplayName: displayName,
|
|
||||||
DisplayColor: displayColor,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
DisabledAt: disabledAt,
|
|
||||||
PreviousNames: strings.Split(previousUsernames, ","),
|
|
||||||
NameChangedAt: userNameChangedAt,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSendChatEvent(t *testing.T) {
|
func TestSendChatEvent(t *testing.T) {
|
||||||
timestamp := time.Unix(72, 6).UTC()
|
timestamp := time.Unix(72, 6).UTC()
|
||||||
user := user.User{
|
user := models.User{
|
||||||
ID: "user id",
|
ID: "user id",
|
||||||
DisplayName: "display name",
|
DisplayName: "display name",
|
||||||
DisplayColor: 4,
|
DisplayColor: 4,
|
||||||
@@ -64,7 +63,7 @@ func TestSendChatEvent(t *testing.T) {
|
|||||||
|
|
||||||
func TestSendChatEventUsernameChanged(t *testing.T) {
|
func TestSendChatEventUsernameChanged(t *testing.T) {
|
||||||
timestamp := time.Unix(72, 6).UTC()
|
timestamp := time.Unix(72, 6).UTC()
|
||||||
user := user.User{
|
user := models.User{
|
||||||
ID: "user id",
|
ID: "user id",
|
||||||
DisplayName: "display name",
|
DisplayName: "display name",
|
||||||
DisplayColor: 4,
|
DisplayColor: 4,
|
||||||
@@ -112,7 +111,7 @@ func TestSendChatEventUsernameChanged(t *testing.T) {
|
|||||||
|
|
||||||
func TestSendChatEventUserJoined(t *testing.T) {
|
func TestSendChatEventUserJoined(t *testing.T) {
|
||||||
timestamp := time.Unix(72, 6).UTC()
|
timestamp := time.Unix(72, 6).UTC()
|
||||||
user := user.User{
|
user := models.User{
|
||||||
ID: "user id",
|
ID: "user id",
|
||||||
DisplayName: "display name",
|
DisplayName: "display name",
|
||||||
DisplayColor: 4,
|
DisplayColor: 4,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,13 +16,13 @@ type WebhookEvent struct {
|
|||||||
|
|
||||||
// WebhookChatMessage represents a single chat message sent as a webhook payload.
|
// WebhookChatMessage represents a single chat message sent as a webhook payload.
|
||||||
type WebhookChatMessage struct {
|
type WebhookChatMessage struct {
|
||||||
User *user.User `json:"user,omitempty"`
|
User *models.User `json:"user,omitempty"`
|
||||||
Timestamp *time.Time `json:"timestamp,omitempty"`
|
Timestamp *time.Time `json:"timestamp,omitempty"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty"`
|
||||||
RawBody string `json:"rawBody,omitempty"`
|
RawBody string `json:"rawBody,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
ClientID uint `json:"clientId,omitempty"`
|
ClientID uint `json:"clientId,omitempty"`
|
||||||
Visible bool `json:"visible"`
|
Visible bool `json:"visible"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
||||||
|
|||||||
+4567
-2410
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
decorators:
|
||||||
|
remove-x-internal: on
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
module github.com/owncast/owncast
|
module github.com/owncast/owncast
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.50.33
|
github.com/aws/aws-sdk-go v1.54.13
|
||||||
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
|
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
|
||||||
github.com/go-fed/httpsig v1.1.0
|
github.com/go-fed/httpsig v1.1.0
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/grafov/m3u8 v0.12.0
|
github.com/grafov/m3u8 v0.12.0
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
||||||
github.com/oschwald/geoip2-golang v1.9.0
|
github.com/oschwald/geoip2-golang v1.11.0
|
||||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
||||||
github.com/schollz/sqlite3dump v1.3.1
|
github.com/schollz/sqlite3dump v1.3.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
|
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
|
||||||
github.com/yuin/goldmark v1.7.0
|
github.com/yuin/goldmark v1.7.4
|
||||||
golang.org/x/mod v0.16.0
|
golang.org/x/mod v0.18.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ require (
|
|||||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
golang.org/x/crypto v0.24.0
|
||||||
golang.org/x/net v0.22.0
|
golang.org/x/net v0.26.0
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/prometheus/client_golang v1.19.0
|
require github.com/prometheus/client_golang v1.19.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
@@ -47,41 +47,48 @@ require (
|
|||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/nakabonne/tstorage v0.3.6
|
github.com/nakabonne/tstorage v0.3.6
|
||||||
github.com/shirou/gopsutil/v3 v3.24.2
|
github.com/shirou/gopsutil/v4 v4.24.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/SherClockHolmes/webpush-go v1.3.0
|
require github.com/SherClockHolmes/webpush-go v1.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-test/deep v1.0.4 // indirect
|
github.com/go-test/deep v1.0.4 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
|
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CAFxX/httpcompression v0.0.9
|
github.com/CAFxX/httpcompression v0.0.9
|
||||||
|
github.com/TwiN/go-away v1.6.13
|
||||||
github.com/andybalholm/cascadia v1.3.2
|
github.com/andybalholm/cascadia v1.3.2
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/jellydator/ttlcache/v3 v3.2.0
|
github.com/jellydator/ttlcache/v3 v3.2.0
|
||||||
github.com/mssola/user_agent v0.6.0
|
github.com/mssola/user_agent v0.6.0
|
||||||
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/yuin/goldmark-emoji v1.0.2
|
github.com/yuin/goldmark-emoji v1.0.3
|
||||||
gopkg.in/evanphx/json-patch.v5 v5.9.0
|
gopkg.in/evanphx/json-patch.v5 v5.9.0
|
||||||
mvdan.cc/xurls v1.1.0
|
mvdan.cc/xurls v1.1.0
|
||||||
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/go-fed/activity => github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026
|
replace github.com/go-fed/activity => github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
|
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
|
||||||
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
||||||
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
|
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
|
||||||
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
|
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
|
||||||
|
github.com/TwiN/go-away v1.6.13 h1:aB6l/FPXmA5ds+V7I9zdhxzpsLLUvVtEuS++iU/ZmgE=
|
||||||
|
github.com/TwiN/go-away v1.6.13/go.mod h1:MpvIC9Li3minq+CGgbgUDvQ9tDaeW35k5IXZrF9MVas=
|
||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/aws/aws-sdk-go v1.50.33 h1:/SKPJ7ZVPCFOYZyTKo5YdjeUEeOn2J2M0qfDTXWAoEU=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
github.com/aws/aws-sdk-go v1.50.33/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
|
github.com/aws/aws-sdk-go v1.53.5 h1:1OcVWMjGlwt7EU5OWmmEEXqaYfmX581EK317QJZXItM=
|
||||||
|
github.com/aws/aws-sdk-go v1.53.5/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||||
|
github.com/aws/aws-sdk-go v1.54.13 h1:zpCuiG+/mFdDY/klKJvmSioAZWk45F4rLGq0JWVAAzk=
|
||||||
|
github.com/aws/aws-sdk-go v1.54.13/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
@@ -35,10 +47,16 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
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 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/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
|
github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
|
||||||
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||||
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
|
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
|
||||||
@@ -49,6 +67,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
|||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||||
|
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
@@ -69,6 +88,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
||||||
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||||
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
|
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
|
||||||
@@ -77,10 +98,16 @@ github.com/nakabonne/tstorage v0.3.6 h1:usp7pTohax8mynnFiUSUQ2QVBCKLCkYx3gmb3+rJ
|
|||||||
github.com/nakabonne/tstorage v0.3.6/go.mod h1:1xUrK3s1MXSlU6dn96xHerHx/MdO4BGmsAHEUbsaOxU=
|
github.com/nakabonne/tstorage v0.3.6/go.mod h1:1xUrK3s1MXSlU6dn96xHerHx/MdO4BGmsAHEUbsaOxU=
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||||
|
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||||
|
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||||
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
|
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
|
||||||
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
|
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
|
||||||
|
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||||
|
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||||
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
|
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
|
||||||
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
|
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||||
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026 h1:E1nxiX44BcMQTSSs8MHLm2rXnqXNedYZkFI31gXMsJc=
|
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026 h1:E1nxiX44BcMQTSSs8MHLm2rXnqXNedYZkFI31gXMsJc=
|
||||||
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
@@ -92,8 +119,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||||
@@ -106,8 +133,11 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
|||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
|
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
|
||||||
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
|
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
|
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
|
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
@@ -116,9 +146,11 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
|||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -138,10 +170,14 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g
|
|||||||
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
||||||
|
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
@@ -151,12 +187,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -165,13 +205,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -186,23 +230,27 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
|
golang.org/x/sys v0.21.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=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -226,3 +274,4 @@ 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=
|
||||||
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
|
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
|
||||||
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
|
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
|
||||||
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/metrics"
|
"github.com/owncast/owncast/metrics"
|
||||||
"github.com/owncast/owncast/router"
|
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/owncast/owncast/webserver/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -105,7 +105,7 @@ func main() {
|
|||||||
|
|
||||||
go metrics.Start(core.GetStatus)
|
go metrics.Start(core.GetStatus)
|
||||||
|
|
||||||
if err := router.Start(); err != nil {
|
if err := router.Start(*enableVerboseLogging); err != nil {
|
||||||
log.Fatalln("failed to start/run the router", err)
|
log.Fatalln("failed to start/run the router", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,7 +60,8 @@ func collectChatClientCount() {
|
|||||||
currentChatMessageCount.Set(float64(cmc))
|
currentChatMessageCount.Set(float64(cmc))
|
||||||
|
|
||||||
// Total user count
|
// Total user count
|
||||||
uc := data.GetUsersCount()
|
userRepository := userrepository.Get()
|
||||||
|
uc := userRepository.GetUsersCount()
|
||||||
// Insert user count into Prometheus collector.
|
// Insert user count into Prometheus collector.
|
||||||
chatUserCount.Set(float64(uc))
|
chatUserCount.Set(float64(uc))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Type represents a form of authentication.
|
||||||
|
type AuthType string
|
||||||
|
|
||||||
|
// The different auth types we support.
|
||||||
|
const (
|
||||||
|
// IndieAuth https://indieauth.spec.indieweb.org/.
|
||||||
|
IndieAuth AuthType = "indieauth"
|
||||||
|
Fediverse AuthType = "fediverse"
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||||
|
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
|
||||||
|
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||||
|
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
|
||||||
|
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||||
|
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
|
||||||
|
|
||||||
|
ModeratorScopeKey = "MODERATOR"
|
||||||
|
)
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/services/geoip"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||||
|
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||||
|
type ExternalAPIUser struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Type string `json:"type,omitempty"` // Should be API
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
DisplayColor int `json:"displayColor"`
|
||||||
|
IsBot bool `json:"isBot"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
moderatorScopeKey = "MODERATOR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
||||||
|
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
||||||
|
AuthenticatedAt *time.Time `json:"-"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
PreviousNames []string `json:"previousNames"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
DisplayColor int `json:"displayColor"`
|
||||||
|
IsBot bool `json:"isBot"`
|
||||||
|
Authenticated bool `json:"authenticated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled will return if this single user is enabled.
|
||||||
|
func (u *User) IsEnabled() bool {
|
||||||
|
return u.DisabledAt == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsModerator will return if the user has moderation privileges.
|
||||||
|
func (u *User) IsModerator() bool {
|
||||||
|
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
|
||||||
|
return hasModerationScope
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/services/geoip"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/db"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
|
|
||||||
"github.com/owncast/owncast/notifications/browser"
|
"github.com/owncast/owncast/notifications/browser"
|
||||||
"github.com/owncast/owncast/notifications/discord"
|
"github.com/owncast/owncast/notifications/discord"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -21,7 +25,7 @@ type Notifier struct {
|
|||||||
|
|
||||||
// Setup will perform any pre-use setup for the notifier.
|
// Setup will perform any pre-use setup for the notifier.
|
||||||
func Setup(datastore *data.Datastore) {
|
func Setup(datastore *data.Datastore) {
|
||||||
createNotificationsTable(datastore.DB)
|
tables.CreateNotificationsTable(datastore.DB)
|
||||||
initializeBrowserPushIfNeeded()
|
initializeBrowserPushIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,3 +154,31 @@ func (n *Notifier) Notify() {
|
|||||||
n.notifyDiscord()
|
n.notifyDiscord()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveNotificationForChannel removes a notification destination.
|
||||||
|
func RemoveNotificationForChannel(channel, destination string) error {
|
||||||
|
log.Debugln("Removing notification for channel", channel)
|
||||||
|
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), db.RemoveNotificationDestinationForChannelParams{
|
||||||
|
Channel: channel,
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationDestinationsForChannel will return a collection of
|
||||||
|
// destinations to notify for a given channel.
|
||||||
|
func GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
||||||
|
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNotification saves a new user notification destination.
|
||||||
|
func AddNotification(channel, destination string) error {
|
||||||
|
return data.GetDatastore().GetQueries().AddNotification(context.Background(), db.AddNotificationParams{
|
||||||
|
Channel: channel,
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createNotificationsTable(db *sql.DB) {
|
|
||||||
log.Traceln("Creating federation followers table...")
|
|
||||||
|
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS notifications (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"channel" TEXT NOT NULL,
|
|
||||||
"destination" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
|
|
||||||
|
|
||||||
data.MustExec(createTableSQL, db)
|
|
||||||
data.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddNotification saves a new user notification destination.
|
|
||||||
func AddNotification(channel, destination string) error {
|
|
||||||
return data.GetDatastore().GetQueries().AddNotification(context.Background(), db.AddNotificationParams{
|
|
||||||
Channel: channel,
|
|
||||||
Destination: destination,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveNotificationForChannel removes a notification destination.
|
|
||||||
func RemoveNotificationForChannel(channel, destination string) error {
|
|
||||||
log.Debugln("Removing notification for channel", channel)
|
|
||||||
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), db.RemoveNotificationDestinationForChannelParams{
|
|
||||||
Channel: channel,
|
|
||||||
Destination: destination,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNotificationDestinationsForChannel will return a collection of
|
|
||||||
// destinations to notify for a given channel.
|
|
||||||
func GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
|
||||||
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
+4403
-2239
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateMessagesTable will create the chat messages table if needed.
|
||||||
|
func CreateMessagesTable(db *sql.DB) {
|
||||||
|
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
"id" string NOT NULL,
|
||||||
|
"user_id" TEXT,
|
||||||
|
"body" TEXT,
|
||||||
|
"eventType" TEXT,
|
||||||
|
"hidden_at" DATETIME,
|
||||||
|
"timestamp" DATETIME,
|
||||||
|
"title" TEXT,
|
||||||
|
"subtitle" TEXT,
|
||||||
|
"image" TEXT,
|
||||||
|
"link" TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);`
|
||||||
|
utils.MustExec(createTableSQL, db)
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package data
|
package tables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
func MigrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||||
log.Printf("Migrating database from version %d to %d", from, to)
|
log.Printf("Migrating database from version %d to %d", from, to)
|
||||||
dbBackupFile := filepath.Join(config.BackupDirectory, fmt.Sprintf("owncast-v%d.bak", from))
|
dbBackupFile := filepath.Join(config.BackupDirectory, fmt.Sprintf("owncast-v%d.bak", from))
|
||||||
utils.Backup(db, dbBackupFile)
|
utils.Backup(db, dbBackupFile)
|
||||||
@@ -94,7 +94,7 @@ func migrateToSchema6(db *sql.DB) {
|
|||||||
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
||||||
// the table and recreate it.
|
// the table and recreate it.
|
||||||
// Drop the old messages table
|
// Drop the old messages table
|
||||||
MustExec(`DROP TABLE messages`, db)
|
utils.MustExec(`DROP TABLE messages`, db)
|
||||||
|
|
||||||
// Recreate it
|
// Recreate it
|
||||||
CreateMessagesTable(db)
|
CreateMessagesTable(db)
|
||||||
@@ -103,7 +103,7 @@ func migrateToSchema6(db *sql.DB) {
|
|||||||
// nolint:cyclop
|
// nolint:cyclop
|
||||||
func migrateToSchema5(db *sql.DB) {
|
func migrateToSchema5(db *sql.DB) {
|
||||||
// Create the access tokens table.
|
// Create the access tokens table.
|
||||||
createAccessTokenTable(db)
|
CreateAccessTokenTable(db)
|
||||||
|
|
||||||
// 1. Authenticated bool added to the users table.
|
// 1. Authenticated bool added to the users table.
|
||||||
// 2. Access tokens are now stored in their own table.
|
// 2. Access tokens are now stored in their own table.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateNotificationsTable(db *sql.DB) {
|
||||||
|
log.Traceln("Creating federation followers table...")
|
||||||
|
|
||||||
|
createTableSQL := `CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"channel" TEXT NOT NULL,
|
||||||
|
"destination" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
|
||||||
|
|
||||||
|
utils.MustExec(createTableSQL, db)
|
||||||
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
package data
|
package tables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createAccessTokenTable(db *sql.DB) {
|
func SetupUsers(db *sql.DB) {
|
||||||
|
CreateUsersTable(db)
|
||||||
|
CreateAccessTokenTable(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAccessTokenTable(db *sql.DB) {
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
|
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
|
||||||
"token" TEXT NOT NULL PRIMARY KEY,
|
"token" TEXT NOT NULL PRIMARY KEY,
|
||||||
"user_id" TEXT NOT NULL,
|
"user_id" TEXT NOT NULL,
|
||||||
@@ -25,7 +31,7 @@ func createAccessTokenTable(db *sql.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUsersTable(db *sql.DB) {
|
func CreateUsersTable(db *sql.DB) {
|
||||||
log.Traceln("Creating users table...")
|
log.Traceln("Creating users table...")
|
||||||
|
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
|
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -43,25 +49,8 @@ func createUsersTable(db *sql.DB) {
|
|||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);`
|
);`
|
||||||
|
|
||||||
MustExec(createTableSQL, db)
|
utils.MustExec(createTableSQL, db)
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsersCount will return the number of users in the database.
|
|
||||||
func GetUsersCount() int64 {
|
|
||||||
query := `SELECT COUNT(*) FROM users`
|
|
||||||
rows, err := _db.Query(query)
|
|
||||||
if err != nil || rows.Err() != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var count int64
|
|
||||||
for rows.Next() {
|
|
||||||
if err := rows.Scan(&count); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,806 @@
|
|||||||
|
package userrepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/db"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/teris-io/shortid"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
ChangeUserColor(userID string, color int) error
|
||||||
|
ChangeUsername(userID string, username string) error
|
||||||
|
CreateAnonymousUser(displayName string) (*models.User, string, error)
|
||||||
|
DeleteExternalAPIUser(token string) error
|
||||||
|
GetDisabledUsers() []*models.User
|
||||||
|
GetExternalAPIUser() ([]models.ExternalAPIUser, error)
|
||||||
|
GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error)
|
||||||
|
GetModeratorUsers() []*models.User
|
||||||
|
GetUserByID(id string) *models.User
|
||||||
|
GetUserByToken(token string) *models.User
|
||||||
|
InsertExternalAPIUser(token string, name string, color int, scopes []string) error
|
||||||
|
IsDisplayNameAvailable(displayName string) (bool, error)
|
||||||
|
SetAccessTokenToOwner(token, userID string) error
|
||||||
|
SetEnabled(userID string, enabled bool) error
|
||||||
|
SetModerator(userID string, isModerator bool) error
|
||||||
|
SetUserAsAuthenticated(userID string) error
|
||||||
|
HasValidScopes(scopes []string) bool
|
||||||
|
GetUserByAuth(authToken string, authType models.AuthType) *models.User
|
||||||
|
AddAuth(userID, authToken string, authType models.AuthType) error
|
||||||
|
SetExternalAPIUserAccessTokenAsUsed(token string) error
|
||||||
|
GetUsersCount() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SqlUserRepository struct {
|
||||||
|
datastore *data.Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is temporary during the transition period.
|
||||||
|
var temporaryGlobalInstance UserRepository
|
||||||
|
|
||||||
|
// Get will return the user repository.
|
||||||
|
func Get() UserRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
i := New(data.GetDatastore())
|
||||||
|
temporaryGlobalInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// New will create a new instance of the UserRepository.
|
||||||
|
func New(datastore *data.Datastore) UserRepository {
|
||||||
|
r := SqlUserRepository{
|
||||||
|
datastore: datastore,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
||||||
|
func (r *SqlUserRepository) CreateAnonymousUser(displayName string) (*models.User, string, error) {
|
||||||
|
if displayName == "" {
|
||||||
|
return nil, "", errors.New("display name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to assign a name that was requested.
|
||||||
|
// If name isn't available then generate a random one.
|
||||||
|
if available, _ := r.IsDisplayNameAvailable(displayName); !available {
|
||||||
|
rand, _ := utils.GenerateRandomString(3)
|
||||||
|
displayName += rand
|
||||||
|
}
|
||||||
|
|
||||||
|
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||||
|
|
||||||
|
id := shortid.MustGenerate()
|
||||||
|
user := &models.User{
|
||||||
|
ID: id,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user.
|
||||||
|
if err := r.create(user); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign it an access token.
|
||||||
|
accessToken, err := utils.GenerateAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("Unable to create access token for new user")
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := r.addAccessTokenForUser(accessToken, id); err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "unable to save access token for new user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
||||||
|
func (r *SqlUserRepository) IsDisplayNameAvailable(displayName string) (bool, error) {
|
||||||
|
if available, err := r.datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil {
|
||||||
|
return false, errors.Wrap(err, "unable to check if display name is available")
|
||||||
|
} else if available != 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUsername will change the user associated to userID from one display name to another.
|
||||||
|
func (r *SqlUserRepository) ChangeUsername(userID string, username string) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
if err := r.datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
|
||||||
|
DisplayName: username,
|
||||||
|
ID: userID,
|
||||||
|
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
||||||
|
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "unable to change display name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeUserColor will change the user associated to userID from one display name to another.
|
||||||
|
func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
||||||
|
DisplayColor: int32(color),
|
||||||
|
ID: userID,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "unable to change display color")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) addAccessTokenForUser(accessToken, userID string) error {
|
||||||
|
return r.datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
|
||||||
|
Token: accessToken,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) create(user *models.User) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("error creating new user", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled will set the enabled status of a single user by ID.
|
||||||
|
func (r *SqlUserRepository) SetEnabled(userID string, enabled bool) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() //nolint
|
||||||
|
|
||||||
|
var stmt *sql.Stmt
|
||||||
|
if !enabled {
|
||||||
|
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?")
|
||||||
|
} else {
|
||||||
|
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err := stmt.Exec(userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByToken will return a user by an access token.
|
||||||
|
func (r *SqlUserRepository) GetUserByToken(token string) *models.User {
|
||||||
|
u, err := r.datastore.GetQueries().GetUserByAccessToken(context.Background(), token)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if u.Scopes.Valid {
|
||||||
|
scopes = strings.Split(u.Scopes.String, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
var disabledAt *time.Time
|
||||||
|
if u.DisabledAt.Valid {
|
||||||
|
disabledAt = &u.DisabledAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticatedAt *time.Time
|
||||||
|
if u.AuthenticatedAt.Valid {
|
||||||
|
authenticatedAt = &u.AuthenticatedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.User{
|
||||||
|
ID: u.ID,
|
||||||
|
DisplayName: u.DisplayName,
|
||||||
|
DisplayColor: int(u.DisplayColor),
|
||||||
|
CreatedAt: u.CreatedAt.Time,
|
||||||
|
DisabledAt: disabledAt,
|
||||||
|
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||||
|
NameChangedAt: &u.NamechangedAt.Time,
|
||||||
|
AuthenticatedAt: authenticatedAt,
|
||||||
|
Authenticated: authenticatedAt != nil,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||||
|
// different user. Used for logging in with external auth.
|
||||||
|
func (r *SqlUserRepository) SetAccessTokenToOwner(token, userID string) error {
|
||||||
|
return r.datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
|
||||||
|
UserID: userID,
|
||||||
|
Token: token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserAsAuthenticated will mark that a user has been authenticated
|
||||||
|
// in some way.
|
||||||
|
func (r *SqlUserRepository) SetUserAsAuthenticated(userID string) error {
|
||||||
|
return errors.Wrap(r.datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuth will add an external authentication token and type for a user.
|
||||||
|
func (r *SqlUserRepository) AddAuth(userID, authToken string, authType models.AuthType) error {
|
||||||
|
return r.datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
|
||||||
|
UserID: userID,
|
||||||
|
Token: authToken,
|
||||||
|
Type: string(authType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByAuth will return an existing user given auth details if a user
|
||||||
|
// has previously authenticated with that method.
|
||||||
|
func (r *SqlUserRepository) GetUserByAuth(authToken string, authType models.AuthType) *models.User {
|
||||||
|
u, err := r.datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
|
||||||
|
Token: authToken,
|
||||||
|
Type: string(authType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if u.Scopes.Valid {
|
||||||
|
scopes = strings.Split(u.Scopes.String, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.User{
|
||||||
|
ID: u.ID,
|
||||||
|
DisplayName: u.DisplayName,
|
||||||
|
DisplayColor: int(u.DisplayColor),
|
||||||
|
CreatedAt: u.CreatedAt.Time,
|
||||||
|
DisabledAt: &u.DisabledAt.Time,
|
||||||
|
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||||
|
NameChangedAt: &u.NamechangedAt.Time,
|
||||||
|
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetModerator will add or remove moderator status for a single user by ID.
|
||||||
|
func (r *SqlUserRepository) SetModerator(userID string, isModerator bool) error {
|
||||||
|
if isModerator {
|
||||||
|
return r.addScopeToUser(userID, models.ModeratorScopeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.removeScopeFromUser(userID, models.ModeratorScopeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) addScopeToUser(userID string, scope string) error {
|
||||||
|
u := r.GetUserByID(userID)
|
||||||
|
if u == nil {
|
||||||
|
return errors.New("user not found when modifying scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
scopesString := u.Scopes
|
||||||
|
scopes := utils.StringSliceToMap(scopesString)
|
||||||
|
scopes[scope] = true
|
||||||
|
|
||||||
|
scopesSlice := utils.StringMapKeys(scopes)
|
||||||
|
|
||||||
|
return r.setScopesOnUser(userID, scopesSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) removeScopeFromUser(userID string, scope string) error {
|
||||||
|
u := r.GetUserByID(userID)
|
||||||
|
scopesString := u.Scopes
|
||||||
|
scopes := utils.StringSliceToMap(scopesString)
|
||||||
|
delete(scopes, scope)
|
||||||
|
|
||||||
|
scopesSlice := utils.StringMapKeys(scopes)
|
||||||
|
|
||||||
|
return r.setScopesOnUser(userID, scopesSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) setScopesOnUser(userID string, scopes []string) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() //nolint
|
||||||
|
|
||||||
|
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ","))
|
||||||
|
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
var val *string
|
||||||
|
if scopesSliceString == "" {
|
||||||
|
val = nil
|
||||||
|
} else {
|
||||||
|
val = &scopesSliceString
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := stmt.Exec(val, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID will return a user by a user ID.
|
||||||
|
func (r *SqlUserRepository) GetUserByID(id string) *models.User {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
|
||||||
|
row := r.datastore.DB.QueryRow(query, id)
|
||||||
|
if row == nil {
|
||||||
|
log.Errorln(row)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.getUserFromRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||||
|
func (r *SqlUserRepository) GetDisabledUsers() []*models.User {
|
||||||
|
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
|
||||||
|
|
||||||
|
rows, err := r.datastore.DB.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
users := r.getUsersFromRows(rows)
|
||||||
|
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
return users[i].DisabledAt.Before(*users[j].DisabledAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModeratorUsers will return a list of users with moderator access.
|
||||||
|
func (r *SqlUserRepository) GetModeratorUsers() []*models.User {
|
||||||
|
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (
|
||||||
|
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS (
|
||||||
|
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users
|
||||||
|
UNION ALL
|
||||||
|
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at,
|
||||||
|
substr(rest, 0, instr(rest, ',')),
|
||||||
|
substr(rest, instr(rest, ',')+1)
|
||||||
|
FROM split
|
||||||
|
WHERE rest <> '')
|
||||||
|
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope
|
||||||
|
FROM split
|
||||||
|
WHERE scope <> ''
|
||||||
|
ORDER BY created_at
|
||||||
|
) AS token WHERE token.scope = ?`
|
||||||
|
|
||||||
|
rows, err := r.datastore.DB.Query(query, models.ModeratorScopeKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
users := r.getUsersFromRows(rows)
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) getUsersFromRows(rows *sql.Rows) []*models.User {
|
||||||
|
users := make([]*models.User, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var displayName string
|
||||||
|
var displayColor int
|
||||||
|
var createdAt time.Time
|
||||||
|
var disabledAt *time.Time
|
||||||
|
var previousUsernames string
|
||||||
|
var userNameChangedAt *time.Time
|
||||||
|
var scopesString *string
|
||||||
|
|
||||||
|
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil {
|
||||||
|
log.Errorln("error creating collection of users from results", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if scopesString != nil {
|
||||||
|
scopes = strings.Split(*scopesString, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
ID: id,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
DisabledAt: disabledAt,
|
||||||
|
PreviousNames: strings.Split(previousUsernames, ","),
|
||||||
|
NameChangedAt: userNameChangedAt,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
return users[i].CreatedAt.Before(users[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) getUserFromRow(row *sql.Row) *models.User {
|
||||||
|
var id string
|
||||||
|
var displayName string
|
||||||
|
var displayColor int
|
||||||
|
var createdAt time.Time
|
||||||
|
var disabledAt *time.Time
|
||||||
|
var previousUsernames string
|
||||||
|
var userNameChangedAt *time.Time
|
||||||
|
var scopesString *string
|
||||||
|
|
||||||
|
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if scopesString != nil {
|
||||||
|
scopes = strings.Split(*scopesString, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.User{
|
||||||
|
ID: id,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
DisabledAt: disabledAt,
|
||||||
|
PreviousNames: strings.Split(previousUsernames, ","),
|
||||||
|
NameChangedAt: userNameChangedAt,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertExternalAPIUser will add a new API user to the database.
|
||||||
|
func (r *SqlUserRepository) InsertExternalAPIUser(token string, name string, color int, scopes []string) error {
|
||||||
|
log.Traceln("Adding new API user")
|
||||||
|
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
scopesString := strings.Join(scopes, ",")
|
||||||
|
id := shortid.MustGenerate()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.addAccessTokenForUser(token, id); err != nil {
|
||||||
|
return errors.Wrap(err, "unable to save access token for new external api user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExternalAPIUser will delete a token from the database.
|
||||||
|
func (r *SqlUserRepository) DeleteExternalAPIUser(token string) error {
|
||||||
|
log.Traceln("Deleting access token")
|
||||||
|
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
result, err := stmt.Exec(token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
||||||
|
tx.Rollback() //nolint
|
||||||
|
return errors.New(token + " not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||||
|
func (r *SqlUserRepository) GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error) {
|
||||||
|
// This will split the scopes from comma separated to individual rows
|
||||||
|
// so we can efficiently find if a token supports a single scope.
|
||||||
|
// This is SQLite specific, so if we ever support other database
|
||||||
|
// backends we need to support other methods.
|
||||||
|
query := `SELECT
|
||||||
|
id,
|
||||||
|
scopes,
|
||||||
|
display_name,
|
||||||
|
display_color,
|
||||||
|
created_at,
|
||||||
|
last_used
|
||||||
|
FROM
|
||||||
|
user_access_tokens
|
||||||
|
INNER JOIN (
|
||||||
|
WITH RECURSIVE split(
|
||||||
|
id,
|
||||||
|
scopes,
|
||||||
|
display_name,
|
||||||
|
display_color,
|
||||||
|
created_at,
|
||||||
|
last_used,
|
||||||
|
disabled_at,
|
||||||
|
scope,
|
||||||
|
rest
|
||||||
|
) AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
scopes,
|
||||||
|
display_name,
|
||||||
|
display_color,
|
||||||
|
created_at,
|
||||||
|
last_used,
|
||||||
|
disabled_at,
|
||||||
|
'',
|
||||||
|
scopes || ','
|
||||||
|
FROM
|
||||||
|
users AS u
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
scopes,
|
||||||
|
display_name,
|
||||||
|
display_color,
|
||||||
|
created_at,
|
||||||
|
last_used,
|
||||||
|
disabled_at,
|
||||||
|
substr(rest, 0, instr(rest, ',')),
|
||||||
|
substr(rest, instr(rest, ',') + 1)
|
||||||
|
FROM
|
||||||
|
split
|
||||||
|
WHERE
|
||||||
|
rest <> ''
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
display_name,
|
||||||
|
display_color,
|
||||||
|
created_at,
|
||||||
|
last_used,
|
||||||
|
disabled_at,
|
||||||
|
scopes,
|
||||||
|
scope
|
||||||
|
FROM
|
||||||
|
split
|
||||||
|
WHERE
|
||||||
|
scope <> ''
|
||||||
|
) ON user_access_tokens.user_id = id
|
||||||
|
WHERE
|
||||||
|
disabled_at IS NULL
|
||||||
|
AND token = ?
|
||||||
|
AND scope = ?;`
|
||||||
|
|
||||||
|
row := r.datastore.DB.QueryRow(query, token, scope)
|
||||||
|
integration, err := r.makeExternalAPIUserFromRow(row)
|
||||||
|
|
||||||
|
return integration, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||||
|
func (r *SqlUserRepository) GetIntegrationNameForAccessToken(token string) *string {
|
||||||
|
name, err := r.datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAPIUser will return all API users with access tokens.
|
||||||
|
func (r *SqlUserRepository) GetExternalAPIUser() ([]models.ExternalAPIUser, error) { //nolint
|
||||||
|
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL"
|
||||||
|
|
||||||
|
rows, err := r.datastore.DB.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return []models.ExternalAPIUser{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
integrations, err := r.makeExternalAPIUsersFromRows(rows)
|
||||||
|
|
||||||
|
return integrations, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||||
|
func (r *SqlUserRepository) SetExternalAPIUserAccessTokenAsUsed(token string) error {
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
if _, err := stmt.Exec(token); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) makeExternalAPIUserFromRow(row *sql.Row) (*models.ExternalAPIUser, error) {
|
||||||
|
var id string
|
||||||
|
var displayName string
|
||||||
|
var displayColor int
|
||||||
|
var scopes string
|
||||||
|
var createdAt time.Time
|
||||||
|
var lastUsedAt *time.Time
|
||||||
|
|
||||||
|
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln("unable to convert row to api user", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
integration := models.ExternalAPIUser{
|
||||||
|
ID: id,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Scopes: strings.Split(scopes, ","),
|
||||||
|
LastUsedAt: lastUsedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &integration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlUserRepository) makeExternalAPIUsersFromRows(rows *sql.Rows) ([]models.ExternalAPIUser, error) {
|
||||||
|
integrations := make([]models.ExternalAPIUser, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var accessToken string
|
||||||
|
var displayName string
|
||||||
|
var displayColor int
|
||||||
|
var scopes string
|
||||||
|
var createdAt time.Time
|
||||||
|
var lastUsedAt *time.Time
|
||||||
|
|
||||||
|
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
integration := models.ExternalAPIUser{
|
||||||
|
ID: id,
|
||||||
|
AccessToken: accessToken,
|
||||||
|
DisplayName: displayName,
|
||||||
|
DisplayColor: displayColor,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Scopes: strings.Split(scopes, ","),
|
||||||
|
LastUsedAt: lastUsedAt,
|
||||||
|
IsBot: true,
|
||||||
|
}
|
||||||
|
integrations = append(integrations, integration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasValidScopes will verify that all the scopes provided are valid.
|
||||||
|
func (r *SqlUserRepository) HasValidScopes(scopes []string) bool {
|
||||||
|
// For a scope to be seen as "valid" it must live in this slice.
|
||||||
|
validAccessTokenScopes := []string{
|
||||||
|
models.ScopeCanSendChatMessages,
|
||||||
|
models.ScopeCanSendSystemMessages,
|
||||||
|
models.ScopeHasAdminAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range scopes {
|
||||||
|
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope)
|
||||||
|
if !foundInSlice {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsersCount will return the number of users in the database.
|
||||||
|
func (r *SqlUserRepository) GetUsersCount() int {
|
||||||
|
query := `SELECT COUNT(*) FROM users`
|
||||||
|
rows, err := r.datastore.DB.Query(query)
|
||||||
|
if err != nil || rows.Err() != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var count int
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(&count); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/CAFxX/httpcompression"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub"
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/controllers"
|
|
||||||
"github.com/owncast/owncast/controllers/admin"
|
|
||||||
fediverseauth "github.com/owncast/owncast/controllers/auth/fediverse"
|
|
||||||
"github.com/owncast/owncast/controllers/auth/indieauth"
|
|
||||||
"github.com/owncast/owncast/controllers/moderation"
|
|
||||||
"github.com/owncast/owncast/core/chat"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/core/user"
|
|
||||||
"github.com/owncast/owncast/router/middleware"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/owncast/owncast/yp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start starts the router for the http, ws, and rtmp.
|
|
||||||
func Start() error {
|
|
||||||
// The primary web app.
|
|
||||||
http.HandleFunc("/", controllers.IndexHandler)
|
|
||||||
|
|
||||||
// The admin web app.
|
|
||||||
http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler))
|
|
||||||
|
|
||||||
// Images
|
|
||||||
http.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
|
|
||||||
http.HandleFunc("/preview.gif", controllers.GetPreview)
|
|
||||||
http.HandleFunc("/logo", controllers.GetLogo)
|
|
||||||
|
|
||||||
// Custom Javascript
|
|
||||||
http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript)
|
|
||||||
|
|
||||||
// Return a single emoji image.
|
|
||||||
http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage)
|
|
||||||
|
|
||||||
// return the logo
|
|
||||||
|
|
||||||
// return a logo that's compatible with external social networks
|
|
||||||
http.HandleFunc("/logo/external", controllers.GetCompatibleLogo)
|
|
||||||
|
|
||||||
// robots.txt
|
|
||||||
http.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt)
|
|
||||||
|
|
||||||
// status of the system
|
|
||||||
http.HandleFunc("/api/status", controllers.GetStatus)
|
|
||||||
|
|
||||||
// custom emoji supported in the chat
|
|
||||||
http.HandleFunc("/api/emoji", controllers.GetCustomEmojiList)
|
|
||||||
|
|
||||||
// chat rest api
|
|
||||||
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages))
|
|
||||||
|
|
||||||
// web config api
|
|
||||||
http.HandleFunc("/api/config", controllers.GetWebConfig)
|
|
||||||
|
|
||||||
// return the YP protocol data
|
|
||||||
http.HandleFunc("/api/yp", yp.GetYPResponse)
|
|
||||||
|
|
||||||
// list of all social platforms
|
|
||||||
http.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms)
|
|
||||||
|
|
||||||
// return the list of video variants available
|
|
||||||
http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants)
|
|
||||||
|
|
||||||
// tell the backend you're an active viewer
|
|
||||||
http.HandleFunc("/api/ping", controllers.Ping)
|
|
||||||
|
|
||||||
// register a new chat user
|
|
||||||
http.HandleFunc("/api/chat/register", controllers.RegisterAnonymousChatUser)
|
|
||||||
|
|
||||||
// return remote follow details
|
|
||||||
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)
|
|
||||||
|
|
||||||
// return followers
|
|
||||||
http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers))
|
|
||||||
|
|
||||||
// save client video playback metrics
|
|
||||||
http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics)
|
|
||||||
|
|
||||||
// Register for notifications
|
|
||||||
http.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications))
|
|
||||||
|
|
||||||
// Authenticated admin requests
|
|
||||||
|
|
||||||
// Current inbound broadcaster
|
|
||||||
http.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(admin.Status))
|
|
||||||
|
|
||||||
// Return HLS video
|
|
||||||
http.HandleFunc("/hls/", controllers.HandleHLSRequest)
|
|
||||||
|
|
||||||
// Disconnect inbound stream
|
|
||||||
http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection))
|
|
||||||
|
|
||||||
// Server config
|
|
||||||
http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig))
|
|
||||||
|
|
||||||
// Get viewer count over time
|
|
||||||
http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime))
|
|
||||||
|
|
||||||
// Get active viewers
|
|
||||||
http.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers))
|
|
||||||
|
|
||||||
// Get hardware stats
|
|
||||||
http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats))
|
|
||||||
|
|
||||||
// Get a a detailed list of currently connected chat clients
|
|
||||||
http.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(admin.GetConnectedChatClients))
|
|
||||||
|
|
||||||
// Get all logs
|
|
||||||
http.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(admin.GetLogs))
|
|
||||||
|
|
||||||
// Get warning/error logs
|
|
||||||
http.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(admin.GetWarnings))
|
|
||||||
|
|
||||||
// Get all chat messages for the admin, unfiltered.
|
|
||||||
http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages))
|
|
||||||
|
|
||||||
// Update chat message visibility
|
|
||||||
http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
|
|
||||||
|
|
||||||
// Enable/disable a user
|
|
||||||
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
|
|
||||||
|
|
||||||
// Ban/unban an IP address
|
|
||||||
http.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress))
|
|
||||||
|
|
||||||
// Remove an IP address ban
|
|
||||||
http.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress))
|
|
||||||
|
|
||||||
// Return all the banned IP addresses
|
|
||||||
http.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans))
|
|
||||||
|
|
||||||
// Get a list of disabled users
|
|
||||||
http.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers))
|
|
||||||
|
|
||||||
// Set moderator status for a user
|
|
||||||
http.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(admin.UpdateUserModerator))
|
|
||||||
|
|
||||||
// Get a list of moderator users
|
|
||||||
http.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators))
|
|
||||||
|
|
||||||
// return followers
|
|
||||||
http.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers)))
|
|
||||||
|
|
||||||
// Get a list of pending follow requests
|
|
||||||
http.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests))
|
|
||||||
|
|
||||||
// Get a list of rejected or blocked follows
|
|
||||||
http.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(admin.GetBlockedAndRejectedFollowers))
|
|
||||||
|
|
||||||
// Set the following state of a follower or follow request.
|
|
||||||
http.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(admin.ApproveFollower))
|
|
||||||
|
|
||||||
// Upload custom emoji
|
|
||||||
http.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(admin.UploadCustomEmoji))
|
|
||||||
|
|
||||||
// Delete custom emoji
|
|
||||||
http.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(admin.DeleteCustomEmoji))
|
|
||||||
|
|
||||||
// Update config values
|
|
||||||
|
|
||||||
// Change the current streaming key in memory
|
|
||||||
http.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(admin.SetAdminPassword))
|
|
||||||
|
|
||||||
// Set an array of valid stream keys
|
|
||||||
http.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(admin.SetStreamKeys))
|
|
||||||
|
|
||||||
// Change the extra page content in memory
|
|
||||||
http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent))
|
|
||||||
|
|
||||||
// Stream title
|
|
||||||
http.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(admin.SetStreamTitle))
|
|
||||||
|
|
||||||
// Server name
|
|
||||||
http.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(admin.SetServerName))
|
|
||||||
|
|
||||||
// Server summary
|
|
||||||
http.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary))
|
|
||||||
|
|
||||||
// Offline message
|
|
||||||
http.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(admin.SetCustomOfflineMessage))
|
|
||||||
|
|
||||||
// Server welcome message
|
|
||||||
http.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(admin.SetServerWelcomeMessage))
|
|
||||||
|
|
||||||
// Disable chat
|
|
||||||
http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled))
|
|
||||||
|
|
||||||
// Disable chat user join messages
|
|
||||||
http.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled))
|
|
||||||
|
|
||||||
// Enable/disable chat established user mode
|
|
||||||
http.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode))
|
|
||||||
|
|
||||||
// Set chat usernames that are not allowed
|
|
||||||
http.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList))
|
|
||||||
|
|
||||||
// Set the suggested chat usernames that will be assigned automatically
|
|
||||||
http.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList))
|
|
||||||
|
|
||||||
// Set video codec
|
|
||||||
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
|
|
||||||
|
|
||||||
// Set style/color/css values
|
|
||||||
http.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues))
|
|
||||||
|
|
||||||
// Return all webhooks
|
|
||||||
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
|
|
||||||
|
|
||||||
// Delete a single webhook
|
|
||||||
http.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(admin.DeleteWebhook))
|
|
||||||
|
|
||||||
// Create a single webhook
|
|
||||||
http.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(admin.CreateWebhook))
|
|
||||||
|
|
||||||
// Get all access tokens
|
|
||||||
http.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(admin.GetExternalAPIUsers))
|
|
||||||
|
|
||||||
// Delete a single access token
|
|
||||||
http.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(admin.DeleteExternalAPIUser))
|
|
||||||
|
|
||||||
// Create a single access token
|
|
||||||
http.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser))
|
|
||||||
|
|
||||||
// Return the auto-update features that are supported for this instance.
|
|
||||||
http.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions))
|
|
||||||
|
|
||||||
// Begin the auto update
|
|
||||||
http.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart))
|
|
||||||
|
|
||||||
// Force quit the service to restart it
|
|
||||||
http.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit))
|
|
||||||
|
|
||||||
// Send a system message to chat
|
|
||||||
http.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage))
|
|
||||||
|
|
||||||
// Send a system message to a single client
|
|
||||||
http.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessageToConnectedClient)))
|
|
||||||
|
|
||||||
// Send a user message to chat *NO LONGER SUPPORTED
|
|
||||||
http.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendUserMessage))
|
|
||||||
|
|
||||||
// Send a message to chat as a specific 3rd party bot/integration based on its access token
|
|
||||||
http.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendIntegrationChatMessage))
|
|
||||||
|
|
||||||
// Send a user action to chat
|
|
||||||
http.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendChatAction))
|
|
||||||
|
|
||||||
// Hide chat message
|
|
||||||
http.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalUpdateMessageVisibility))
|
|
||||||
|
|
||||||
// Stream title
|
|
||||||
http.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalSetStreamTitle))
|
|
||||||
|
|
||||||
// Get chat history
|
|
||||||
http.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, controllers.ExternalGetChatMessages))
|
|
||||||
|
|
||||||
// Connected clients
|
|
||||||
http.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalGetConnectedChatClients))
|
|
||||||
|
|
||||||
// Logo path
|
|
||||||
http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogo))
|
|
||||||
|
|
||||||
// Server tags
|
|
||||||
http.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(admin.SetTags))
|
|
||||||
|
|
||||||
// ffmpeg
|
|
||||||
http.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(admin.SetFfmpegPath))
|
|
||||||
|
|
||||||
// Server http port
|
|
||||||
http.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(admin.SetWebServerPort))
|
|
||||||
|
|
||||||
// Server http listen address
|
|
||||||
http.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(admin.SetWebServerIP))
|
|
||||||
|
|
||||||
// Server rtmp port
|
|
||||||
http.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(admin.SetRTMPServerPort))
|
|
||||||
|
|
||||||
// Websocket host override
|
|
||||||
http.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride))
|
|
||||||
|
|
||||||
// Custom video serving endpoint
|
|
||||||
http.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint))
|
|
||||||
|
|
||||||
// Is server marked as NSFW
|
|
||||||
http.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW))
|
|
||||||
|
|
||||||
// directory enabled
|
|
||||||
http.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(admin.SetDirectoryEnabled))
|
|
||||||
|
|
||||||
// social handles
|
|
||||||
http.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(admin.SetSocialHandles))
|
|
||||||
|
|
||||||
// set the number of video segments and duration per segment in a playlist
|
|
||||||
http.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(admin.SetStreamLatencyLevel))
|
|
||||||
|
|
||||||
// set an array of video output configurations
|
|
||||||
http.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(admin.SetStreamOutputVariants))
|
|
||||||
|
|
||||||
// set s3 configuration
|
|
||||||
http.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(admin.SetS3Configuration))
|
|
||||||
|
|
||||||
// set server url
|
|
||||||
http.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(admin.SetServerURL))
|
|
||||||
|
|
||||||
// reset the YP registration
|
|
||||||
http.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration))
|
|
||||||
|
|
||||||
// set external action links
|
|
||||||
http.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions))
|
|
||||||
|
|
||||||
// set custom style css
|
|
||||||
http.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles))
|
|
||||||
|
|
||||||
// set custom style javascript
|
|
||||||
http.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript))
|
|
||||||
|
|
||||||
// Video playback metrics
|
|
||||||
http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics))
|
|
||||||
|
|
||||||
// Is the viewer count hidden from viewers
|
|
||||||
http.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount))
|
|
||||||
|
|
||||||
// set disabling of search indexing
|
|
||||||
http.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(admin.SetDisableSearchIndexing))
|
|
||||||
|
|
||||||
// Inline chat moderation actions
|
|
||||||
|
|
||||||
// Update chat message visibility
|
|
||||||
http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
|
|
||||||
|
|
||||||
// Enable/disable a user
|
|
||||||
http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled))
|
|
||||||
|
|
||||||
// Get a user's details
|
|
||||||
http.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(moderation.GetUserDetails))
|
|
||||||
|
|
||||||
// Configure Federation features
|
|
||||||
|
|
||||||
// enable/disable federation features
|
|
||||||
http.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(admin.SetFederationEnabled))
|
|
||||||
|
|
||||||
// set if federation activities are private
|
|
||||||
http.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(admin.SetFederationActivityPrivate))
|
|
||||||
|
|
||||||
// set if fediverse engagement appears in chat
|
|
||||||
http.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(admin.SetFederationShowEngagement))
|
|
||||||
|
|
||||||
// set local federated username
|
|
||||||
http.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(admin.SetFederationUsername))
|
|
||||||
|
|
||||||
// set federated go live message
|
|
||||||
http.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage))
|
|
||||||
|
|
||||||
// Federation blocked domains
|
|
||||||
http.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(admin.SetFederationBlockDomains))
|
|
||||||
|
|
||||||
// send a public message to the Fediverse from the server's user
|
|
||||||
http.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage))
|
|
||||||
|
|
||||||
// Return federated activities
|
|
||||||
http.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions)))
|
|
||||||
|
|
||||||
// Prometheus metrics
|
|
||||||
http.Handle("/api/admin/prometheus", middleware.RequireAdminAuth(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
promhttp.Handler().ServeHTTP(rw, r)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Configure outbound notification channels.
|
|
||||||
http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration))
|
|
||||||
http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration))
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
|
|
||||||
// Start auth flow
|
|
||||||
http.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow))
|
|
||||||
http.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect)
|
|
||||||
http.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint)
|
|
||||||
|
|
||||||
http.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest))
|
|
||||||
http.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest)
|
|
||||||
|
|
||||||
// ActivityPub has its own router
|
|
||||||
activitypub.Start(data.GetDatastore())
|
|
||||||
|
|
||||||
// websocket
|
|
||||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
chat.HandleClientConnection(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Optional public static files
|
|
||||||
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.PublicFilesPath))))
|
|
||||||
|
|
||||||
port := config.WebServerPort
|
|
||||||
ip := config.WebServerIP
|
|
||||||
|
|
||||||
h2s := &http2.Server{}
|
|
||||||
|
|
||||||
// Create a custom mux handler to intercept the /debug/vars endpoint.
|
|
||||||
// This is a hack because Prometheus enables this endpoint by default
|
|
||||||
// due to its use of expvar and we do not want this exposed.
|
|
||||||
defaultMux := h2c.NewHandler(http.DefaultServeMux, h2s)
|
|
||||||
m := http.NewServeMux()
|
|
||||||
|
|
||||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/debug/vars" {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
} else if r.URL.Path == "/embed/chat/" || r.URL.Path == "/embed/chat" {
|
|
||||||
// Redirect /embed/chat
|
|
||||||
http.Redirect(w, r, "/embed/chat/readonly", http.StatusTemporaryRedirect)
|
|
||||||
} else {
|
|
||||||
defaultMux.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
compress, _ := httpcompression.DefaultAdapter() // Use the default configuration
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", ip, port),
|
|
||||||
ReadHeaderTimeout: 4 * time.Second,
|
|
||||||
Handler: compress(m),
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip != "0.0.0.0" {
|
|
||||||
log.Infof("Web server is listening at %s:%d.", ip, port)
|
|
||||||
} else {
|
|
||||||
log.Infof("Web server is listening on port %d.", port)
|
|
||||||
}
|
|
||||||
log.Infoln("Configure this server by visiting /admin.")
|
|
||||||
|
|
||||||
return server.ListenAndServe()
|
|
||||||
}
|
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+2
-2
@@ -51,12 +51,12 @@ func GetWebIndexTemplate() (*template.Template, error) {
|
|||||||
return tmpl, err
|
return tmpl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed offline.tsclip
|
//go:embed offline-v2.ts
|
||||||
var offlineVideoSegment []byte
|
var offlineVideoSegment []byte
|
||||||
|
|
||||||
// GetOfflineSegment will return the offline video segment data.
|
// GetOfflineSegment will return the offline video segment data.
|
||||||
func GetOfflineSegment() []byte {
|
func GetOfflineSegment() []byte {
|
||||||
return getFileSystemStaticFileOrDefault("offline.tsclip", offlineVideoSegment)
|
return getFileSystemStaticFileOrDefault("offline-v2.ts", offlineVideoSegment)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed img/logo.png
|
//go:embed img/logo.png
|
||||||
|
|||||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1008],{34214:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"}},{tag:"path",attrs:{d:"M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"}}]},name:"eye-invisible",theme:"outlined"}},71008:function(e,t,a){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,n=(r=a(87866))&&r.__esModule?r:{default:r};t.default=n,e.exports=n},87866:function(e,t,a){var r=a(64836),n=a(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=r(a(42122)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var a=i(t);if(a&&a.has(e))return a.get(e);var r={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(r,l,f):r[l]=e[l]}return r.default=e,a&&a.set(e,r),r}(a(67294)),f=r(a(34214)),o=r(a(17298));function i(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,a=new WeakMap;return(i=function(e){return e?a:t})(e)}var c=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=c}}]);
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[6121],{96121:function(e,r,o){function t(e){var r={as:"keyword",do:"keyword",else:"keyword",end:"keyword",exception:"keyword",fun:"keyword",functor:"keyword",if:"keyword",in:"keyword",include:"keyword",let:"keyword",of:"keyword",open:"keyword",rec:"keyword",struct:"keyword",then:"keyword",type:"keyword",val:"keyword",while:"keyword",with:"keyword"},o=e.extraWords||{};for(var t in o)o.hasOwnProperty(t)&&(r[t]=e.extraWords[t]);var n=[];for(var i in r)n.push(i);function d(o,t){var n=o.next();if('"'===n)return t.tokenize=k,t.tokenize(o,t);if("{"===n&&o.eat("|"))return t.longString=!0,t.tokenize=y,t.tokenize(o,t);if("("===n&&o.match(/^\*(?!\))/))return t.commentLevel++,t.tokenize=w,t.tokenize(o,t);if("~"===n||"?"===n)return o.eatWhile(/\w/),"variableName.special";if("`"===n)return o.eatWhile(/\w/),"quote";if("/"===n&&e.slashComments&&o.eat("/"))return o.skipToEnd(),"comment";if(/\d/.test(n))return"0"===n&&o.eat(/[bB]/)&&o.eatWhile(/[01]/),"0"===n&&o.eat(/[xX]/)&&o.eatWhile(/[0-9a-fA-F]/),"0"===n&&o.eat(/[oO]/)?o.eatWhile(/[0-7]/):(o.eatWhile(/[\d_]/),o.eat(".")&&o.eatWhile(/[\d]/),o.eat(/[eE]/)&&o.eatWhile(/[\d\-+]/)),"number";if(/[+\-*&%=<>!?|@\.~:]/.test(n))return"operator";if(/[\w\xa1-\uffff]/.test(n)){o.eatWhile(/[\w\xa1-\uffff]/);var i=o.current();return r.hasOwnProperty(i)?r[i]:"variable"}return null}function k(e,r){for(var o,t=!1,n=!1;null!=(o=e.next());){if('"'===o&&!n){t=!0;break}n=!n&&"\\"===o}return t&&!n&&(r.tokenize=d),"string"}function w(e,r){for(var o,t;r.commentLevel>0&&null!=(t=e.next());)"("===o&&"*"===t&&r.commentLevel++,"*"===o&&")"===t&&r.commentLevel--,o=t;return r.commentLevel<=0&&(r.tokenize=d),"comment"}function y(e,r){for(var o,t;r.longString&&null!=(t=e.next());)"|"===o&&"}"===t&&(r.longString=!1),o=t;return r.longString||(r.tokenize=d),"string"}return{startState:function(){return{tokenize:d,commentLevel:0,longString:!1}},token:function(e,r){return e.eatSpace()?null:r.tokenize(e,r)},languageData:{autocomplete:n,commentTokens:{line:e.slashComments?"//":void 0,block:{open:"(*",close:"*)"}}}}}o.r(r),o.d(r,{fSharp:function(){return i},oCaml:function(){return n},sml:function(){return d}});let n=t({name:"ocaml",extraWords:{and:"keyword",assert:"keyword",begin:"keyword",class:"keyword",constraint:"keyword",done:"keyword",downto:"keyword",external:"keyword",function:"keyword",initializer:"keyword",lazy:"keyword",match:"keyword",method:"keyword",module:"keyword",mutable:"keyword",new:"keyword",nonrec:"keyword",object:"keyword",private:"keyword",sig:"keyword",to:"keyword",try:"keyword",value:"keyword",virtual:"keyword",when:"keyword",raise:"builtin",failwith:"builtin",true:"builtin",false:"builtin",asr:"builtin",land:"builtin",lor:"builtin",lsl:"builtin",lsr:"builtin",lxor:"builtin",mod:"builtin",or:"builtin",raise_notrace:"builtin",trace:"builtin",exit:"builtin",print_string:"builtin",print_endline:"builtin",int:"type",float:"type",bool:"type",char:"type",string:"type",unit:"type",List:"builtin"}}),i=t({name:"fsharp",extraWords:{abstract:"keyword",assert:"keyword",base:"keyword",begin:"keyword",class:"keyword",default:"keyword",delegate:"keyword","do!":"keyword",done:"keyword",downcast:"keyword",downto:"keyword",elif:"keyword",extern:"keyword",finally:"keyword",for:"keyword",function:"keyword",global:"keyword",inherit:"keyword",inline:"keyword",interface:"keyword",internal:"keyword",lazy:"keyword","let!":"keyword",match:"keyword",member:"keyword",module:"keyword",mutable:"keyword",namespace:"keyword",new:"keyword",null:"keyword",override:"keyword",private:"keyword",public:"keyword","return!":"keyword",return:"keyword",select:"keyword",static:"keyword",to:"keyword",try:"keyword",upcast:"keyword","use!":"keyword",use:"keyword",void:"keyword",when:"keyword","yield!":"keyword",yield:"keyword",atomic:"keyword",break:"keyword",checked:"keyword",component:"keyword",const:"keyword",constraint:"keyword",constructor:"keyword",continue:"keyword",eager:"keyword",event:"keyword",external:"keyword",fixed:"keyword",method:"keyword",mixin:"keyword",object:"keyword",parallel:"keyword",process:"keyword",protected:"keyword",pure:"keyword",sealed:"keyword",tailcall:"keyword",trait:"keyword",virtual:"keyword",volatile:"keyword",List:"builtin",Seq:"builtin",Map:"builtin",Set:"builtin",Option:"builtin",int:"builtin",string:"builtin",not:"builtin",true:"builtin",false:"builtin",raise:"builtin",failwith:"builtin"},slashComments:!0}),d=t({name:"sml",extraWords:{abstype:"keyword",and:"keyword",andalso:"keyword",case:"keyword",datatype:"keyword",fn:"keyword",handle:"keyword",infix:"keyword",infixr:"keyword",local:"keyword",nonfix:"keyword",op:"keyword",orelse:"keyword",raise:"keyword",withtype:"keyword",eqtype:"keyword",sharing:"keyword",sig:"keyword",signature:"keyword",structure:"keyword",where:"keyword",true:"keyword",false:"keyword",int:"builtin",real:"builtin",string:"builtin",char:"builtin",bool:"builtin"},slashComments:!0})}}]);
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1027],{96121:function(e,r,o){function t(e){var r={as:"keyword",do:"keyword",else:"keyword",end:"keyword",exception:"keyword",fun:"keyword",functor:"keyword",if:"keyword",in:"keyword",include:"keyword",let:"keyword",of:"keyword",open:"keyword",rec:"keyword",struct:"keyword",then:"keyword",type:"keyword",val:"keyword",while:"keyword",with:"keyword"},o=e.extraWords||{};for(var t in o)o.hasOwnProperty(t)&&(r[t]=e.extraWords[t]);var n=[];for(var i in r)n.push(i);function d(o,t){var n=o.next();if('"'===n)return t.tokenize=k,t.tokenize(o,t);if("{"===n&&o.eat("|"))return t.longString=!0,t.tokenize=y,t.tokenize(o,t);if("("===n&&o.match(/^\*(?!\))/))return t.commentLevel++,t.tokenize=w,t.tokenize(o,t);if("~"===n||"?"===n)return o.eatWhile(/\w/),"variableName.special";if("`"===n)return o.eatWhile(/\w/),"quote";if("/"===n&&e.slashComments&&o.eat("/"))return o.skipToEnd(),"comment";if(/\d/.test(n))return"0"===n&&o.eat(/[bB]/)&&o.eatWhile(/[01]/),"0"===n&&o.eat(/[xX]/)&&o.eatWhile(/[0-9a-fA-F]/),"0"===n&&o.eat(/[oO]/)?o.eatWhile(/[0-7]/):(o.eatWhile(/[\d_]/),o.eat(".")&&o.eatWhile(/[\d]/),o.eat(/[eE]/)&&o.eatWhile(/[\d\-+]/)),"number";if(/[+\-*&%=<>!?|@\.~:]/.test(n))return"operator";if(/[\w\xa1-\uffff]/.test(n)){o.eatWhile(/[\w\xa1-\uffff]/);var i=o.current();return r.hasOwnProperty(i)?r[i]:"variable"}return null}function k(e,r){for(var o,t=!1,n=!1;null!=(o=e.next());){if('"'===o&&!n){t=!0;break}n=!n&&"\\"===o}return t&&!n&&(r.tokenize=d),"string"}function w(e,r){for(var o,t;r.commentLevel>0&&null!=(t=e.next());)"("===o&&"*"===t&&r.commentLevel++,"*"===o&&")"===t&&r.commentLevel--,o=t;return r.commentLevel<=0&&(r.tokenize=d),"comment"}function y(e,r){for(var o,t;r.longString&&null!=(t=e.next());)"|"===o&&"}"===t&&(r.longString=!1),o=t;return r.longString||(r.tokenize=d),"string"}return{startState:function(){return{tokenize:d,commentLevel:0,longString:!1}},token:function(e,r){return e.eatSpace()?null:r.tokenize(e,r)},languageData:{autocomplete:n,commentTokens:{line:e.slashComments?"//":void 0,block:{open:"(*",close:"*)"}}}}}o.r(r),o.d(r,{fSharp:function(){return i},oCaml:function(){return n},sml:function(){return d}});let n=t({name:"ocaml",extraWords:{and:"keyword",assert:"keyword",begin:"keyword",class:"keyword",constraint:"keyword",done:"keyword",downto:"keyword",external:"keyword",function:"keyword",initializer:"keyword",lazy:"keyword",match:"keyword",method:"keyword",module:"keyword",mutable:"keyword",new:"keyword",nonrec:"keyword",object:"keyword",private:"keyword",sig:"keyword",to:"keyword",try:"keyword",value:"keyword",virtual:"keyword",when:"keyword",raise:"builtin",failwith:"builtin",true:"builtin",false:"builtin",asr:"builtin",land:"builtin",lor:"builtin",lsl:"builtin",lsr:"builtin",lxor:"builtin",mod:"builtin",or:"builtin",raise_notrace:"builtin",trace:"builtin",exit:"builtin",print_string:"builtin",print_endline:"builtin",int:"type",float:"type",bool:"type",char:"type",string:"type",unit:"type",List:"builtin"}}),i=t({name:"fsharp",extraWords:{abstract:"keyword",assert:"keyword",base:"keyword",begin:"keyword",class:"keyword",default:"keyword",delegate:"keyword","do!":"keyword",done:"keyword",downcast:"keyword",downto:"keyword",elif:"keyword",extern:"keyword",finally:"keyword",for:"keyword",function:"keyword",global:"keyword",inherit:"keyword",inline:"keyword",interface:"keyword",internal:"keyword",lazy:"keyword","let!":"keyword",match:"keyword",member:"keyword",module:"keyword",mutable:"keyword",namespace:"keyword",new:"keyword",null:"keyword",override:"keyword",private:"keyword",public:"keyword","return!":"keyword",return:"keyword",select:"keyword",static:"keyword",to:"keyword",try:"keyword",upcast:"keyword","use!":"keyword",use:"keyword",void:"keyword",when:"keyword","yield!":"keyword",yield:"keyword",atomic:"keyword",break:"keyword",checked:"keyword",component:"keyword",const:"keyword",constraint:"keyword",constructor:"keyword",continue:"keyword",eager:"keyword",event:"keyword",external:"keyword",fixed:"keyword",method:"keyword",mixin:"keyword",object:"keyword",parallel:"keyword",process:"keyword",protected:"keyword",pure:"keyword",sealed:"keyword",tailcall:"keyword",trait:"keyword",virtual:"keyword",volatile:"keyword",List:"builtin",Seq:"builtin",Map:"builtin",Set:"builtin",Option:"builtin",int:"builtin",string:"builtin",not:"builtin",true:"builtin",false:"builtin",raise:"builtin",failwith:"builtin"},slashComments:!0}),d=t({name:"sml",extraWords:{abstype:"keyword",and:"keyword",andalso:"keyword",case:"keyword",datatype:"keyword",fn:"keyword",handle:"keyword",infix:"keyword",infixr:"keyword",local:"keyword",nonfix:"keyword",op:"keyword",orelse:"keyword",raise:"keyword",withtype:"keyword",eqtype:"keyword",sharing:"keyword",sig:"keyword",signature:"keyword",structure:"keyword",where:"keyword",true:"keyword",false:"keyword",int:"builtin",real:"builtin",string:"builtin",char:"builtin",bool:"builtin"},slashComments:!0})}}]);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[108],{65089:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240zm460 600H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 10-56 0z"}}]},name:"lock",theme:"outlined"}},30108:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,u=(n=r(34811))&&n.__esModule?n:{default:n};t.default=u,e.exports=u},34811:function(e,t,r){var n=r(64836),u=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=u(e)&&"function"!=typeof e)return{default:e};var r=l(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var o=a?Object.getOwnPropertyDescriptor(e,f):null;o&&(o.get||o.set)?Object.defineProperty(n,f,o):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),o=n(r(65089)),c=n(r(3908));function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(l=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(c.default,(0,a.default)((0,a.default)({},e),{},{ref:t,icon:o.default}))});t.default=i}}]);
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2807],{98494:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M881.7 187.4l-45.1-45.1a8.03 8.03 0 00-11.3 0L667.8 299.9l-54.7-54.7a7.94 7.94 0 00-13.5 4.7L576.1 439c-.6 5.2 3.7 9.5 8.9 8.9l189.2-23.5c6.6-.8 9.3-8.8 4.7-13.5l-54.7-54.7 157.6-157.6c3-3 3-8.1-.1-11.2zM439 576.1l-189.2 23.5c-6.6.8-9.3 8.9-4.7 13.5l54.7 54.7-157.5 157.5a8.03 8.03 0 000 11.3l45.1 45.1c3.1 3.1 8.2 3.1 11.3 0l157.6-157.6 54.7 54.7a7.94 7.94 0 0013.5-4.7L447.9 585a7.9 7.9 0 00-8.9-8.9z"}}]},name:"shrink",theme:"outlined"}},42807:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(6044))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},6044:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(a,l,f):a[l]=e[l]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(98494)),o=a(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=i}}]);
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1189],{92262:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M881.7 187.4l-45.1-45.1a8.03 8.03 0 00-11.3 0L667.8 299.9l-54.7-54.7a7.94 7.94 0 00-13.5 4.7L576.1 439c-.6 5.2 3.7 9.5 8.9 8.9l189.2-23.5c6.6-.8 9.3-8.8 4.7-13.5l-54.7-54.7 157.6-157.6c3-3 3-8.1-.1-11.2zM439 576.1l-189.2 23.5c-6.6.8-9.3 8.9-4.7 13.5l54.7 54.7-157.5 157.5a8.03 8.03 0 000 11.3l45.1 45.1c3.1 3.1 8.2 3.1 11.3 0l157.6-157.6 54.7 54.7a7.94 7.94 0 0013.5-4.7L447.9 585a7.9 7.9 0 00-8.9-8.9z"}}]},name:"shrink",theme:"outlined"}},91189:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(85e3))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},85e3:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),l=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(a,l,f):a[l]=e[l]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(92262)),o=a(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=i}}]);
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1377],{3797:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"0 0 1024 1024",focusable:"false"},children:[{tag:"path",attrs:{d:"M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"}}]},name:"loading",theme:"outlined"}},81377:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(66443))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},66443:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var o=u?Object.getOwnPropertyDescriptor(e,f):null;o&&(o.get||o.set)?Object.defineProperty(n,f,o):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),o=n(r(3797)),l=n(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(l.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:o.default}))});t.default=i}}]);
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1460],{76545:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"}}]},name:"edit",theme:"outlined"}},51460:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(65689))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},65689:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(76545)),o=n(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3801],{60762:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"}}]},name:"exclamation-circle",theme:"filled"}},83801:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(35398))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},35398:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(a,f,l):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),l=a(r(60762)),o=a(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1483],{86233:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"}}]},name:"exclamation-circle",theme:"filled"}},31483:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(75036))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},75036:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(a,f,l):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),l=a(r(86233)),o=a(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
||||||
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1557],{96994:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M876.6 239.5c-.5-.9-1.2-1.8-2-2.5-5-5-13.1-5-18.1 0L684.2 409.3l-67.9-67.9L788.7 169c.8-.8 1.4-1.6 2-2.5 3.6-6.1 1.6-13.9-4.5-17.5-98.2-58-226.8-44.7-311.3 39.7-67 67-89.2 162-66.5 247.4l-293 293c-3 3-2.8 7.9.3 11l169.7 169.7c3.1 3.1 8.1 3.3 11 .3l292.9-292.9c85.5 22.8 180.5.7 247.6-66.4 84.4-84.5 97.7-213.1 39.7-311.3zM786 499.8c-58.1 58.1-145.3 69.3-214.6 33.6l-8.8 8.8-.1-.1-274 274.1-79.2-79.2 230.1-230.1s0 .1.1.1l52.8-52.8c-35.7-69.3-24.5-156.5 33.6-214.6a184.2 184.2 0 01144-53.5L537 318.9a32.05 32.05 0 000 45.3l124.5 124.5a32.05 32.05 0 0045.3 0l132.8-132.8c3.7 51.8-14.4 104.8-53.6 143.9z"}}]},name:"tool",theme:"outlined"}},61557:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(4388))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},4388:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(a,l,f):a[l]=e[l]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(96994)),o=a(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=i}}]);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1576],{73551:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM305.8 637.7c3.1 3.1 8.1 3.1 11.3 0l138.3-137.6L583 628.5c3.1 3.1 8.2 3.1 11.3 0l275.4-275.3c3.1-3.1 3.1-8.2 0-11.3l-39.6-39.6a8.03 8.03 0 00-11.3 0l-230 229.9L461.4 404a8.03 8.03 0 00-11.3 0L266.3 586.7a8.03 8.03 0 000 11.3l39.5 39.7z"}}]},name:"line-chart",theme:"outlined"}},71576:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(38099))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},38099:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(a,l,f):a[l]=e[l]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(73551)),o=a(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=i}}]);
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1664],{49659:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M866.9 169.9L527.1 54.1C523 52.7 517.5 52 512 52s-11 .7-15.1 2.1L157.1 169.9c-8.3 2.8-15.1 12.4-15.1 21.2v482.4c0 8.8 5.7 20.4 12.6 25.9L499.3 968c3.5 2.7 8 4.1 12.6 4.1s9.2-1.4 12.6-4.1l344.7-268.6c6.9-5.4 12.6-17 12.6-25.9V191.1c.2-8.8-6.6-18.3-14.9-21.2zM694.5 340.7L481.9 633.4a16.1 16.1 0 01-26 0l-126.4-174c-3.8-5.3 0-12.7 6.5-12.7h55.2c5.1 0 10 2.5 13 6.6l64.7 89 150.9-207.8c3-4.1 7.8-6.6 13-6.6H688c6.5.1 10.3 7.5 6.5 12.8z"}}]},name:"safety-certificate",theme:"filled"}},1664:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(94992))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},94992:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=o(t);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(a,f,l):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),l=a(r(49659)),c=a(r(17298));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(o=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(c.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1680],{76456:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M946.5 505L560.1 118.8l-25.9-25.9a31.5 31.5 0 00-44.4 0L77.5 505a63.9 63.9 0 00-18.8 46c.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8a63.6 63.6 0 0018.7-45.3c0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204zm217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z"}}]},name:"home",theme:"outlined"}},81680:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(13127))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},13127:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var o=u?Object.getOwnPropertyDescriptor(e,f):null;o&&(o.get||o.set)?Object.defineProperty(a,f,o):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),o=a(r(76456)),l=a(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(l.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:o.default}))});t.default=i}}]);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[177],{98226:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"}}]},name:"smile",theme:"outlined"}},80177:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(70232))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},70232:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var o=u?Object.getOwnPropertyDescriptor(e,f):null;o&&(o.get||o.set)?Object.defineProperty(a,f,o):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),o=a(r(98226)),l=a(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(l.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:o.default}))});t.default=i}}]);
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[7418],{12578:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:function(e,t){return{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M761.1 288.3L687.8 215 325.1 577.6l-15.6 89 88.9-15.7z",fill:t}},{tag:"path",attrs:{d:"M880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32zm-622.3-84c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89z",fill:e}}]}},name:"edit",theme:"twotone"}},37418:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(89099))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},89099:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(t);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(12578)),o=n(r(17298));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1825],{17139:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:function(e,t){return{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M761.1 288.3L687.8 215 325.1 577.6l-15.6 89 88.9-15.7z",fill:t}},{tag:"path",attrs:{d:"M880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32zm-622.3-84c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89z",fill:e}}]}},name:"edit",theme:"twotone"}},61825:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(20524))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},20524:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(17139)),o=n(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user