Compare commits
1259 Commits
v0.1.3
...
2c2bf2b5bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c2bf2b5bb | ||
|
|
32bf67b026 | ||
|
|
d578f01348 | ||
|
|
8985947d7f | ||
|
|
e1acfee49d | ||
|
|
49c07594fb | ||
|
|
dc48e0bca7 | ||
|
|
0b5d7c8a4d | ||
|
|
56d52c283c | ||
|
|
8fa2546bef | ||
|
|
70bbcb97b8 | ||
|
|
d8abe74e5a | ||
|
|
681111ceb2 | ||
|
|
284833d6a0 | ||
|
|
a566b9c9f1 | ||
|
|
621aebdd3c | ||
|
|
2be188a82f | ||
|
|
e17b443726 | ||
|
|
dfc934ce84 | ||
|
|
512e4dc575 | ||
|
|
421cdd0693 | ||
|
|
df06d9ad97 | ||
|
|
7f42981ba7 | ||
|
|
f3029ca782 | ||
|
|
58657804cb | ||
|
|
d21b10f118 | ||
|
|
ae48128441 | ||
|
|
b278027b88 | ||
|
|
f45a5b16fa | ||
|
|
f3d660853c | ||
|
|
aecf7aa9bb | ||
|
|
37c18ec8ab | ||
|
|
4c7ccdbbf9 | ||
|
|
a20a9931f7 | ||
|
|
b177333ec6 | ||
|
|
5042c7ced2 | ||
|
|
71c88c94a3 | ||
|
|
41a6449836 | ||
|
|
b656f88417 | ||
|
|
ee5dc3b7c8 | ||
|
|
1476405609 | ||
|
|
7830b3765a | ||
|
|
f9f61876df | ||
|
|
17d433749c | ||
|
|
9989653d00 | ||
|
|
6399df7f9e | ||
|
|
99acc19cee | ||
|
|
eca880ac1f | ||
|
|
f215809f1d | ||
|
|
77d2bacbad | ||
|
|
1b1144c6df | ||
|
|
971e3ea092 | ||
|
|
24e2552e4c | ||
|
|
56f6aa8add | ||
|
|
de8579bcd6 | ||
|
|
56ede52346 | ||
|
|
8269ae3209 | ||
|
|
674c9168ec | ||
|
|
801e91d2f1 | ||
|
|
1e1dc0ff49 | ||
|
|
fb75f1bd95 | ||
|
|
af56597341 | ||
|
|
214c202552 | ||
|
|
ad64956027 | ||
|
|
13d2ad9079 | ||
|
|
83ef466ee1 | ||
|
|
a8026c13bc | ||
|
|
d31d2948c3 | ||
|
|
4d68c7b561 | ||
|
|
25cd9b6d53 | ||
|
|
ecba3cc003 | ||
|
|
89dc1776d7 | ||
|
|
97fcdfd914 | ||
|
|
0af864ea8f | ||
|
|
121cd37c8c | ||
|
|
06c09f12de | ||
|
|
d1117494b0 | ||
|
|
68a903b83a | ||
|
|
c8c91506cd | ||
|
|
a1a497f5c1 | ||
|
|
219f76d891 | ||
|
|
ad9b01f442 | ||
|
|
962d03710d | ||
|
|
2ebc64b3b1 | ||
|
|
5ca7249388 | ||
|
|
50a4224224 | ||
|
|
ac009f574e | ||
|
|
b9d8fbffe2 | ||
|
|
40b97e6bc1 | ||
|
|
134093b9d2 | ||
|
|
d123a2c020 | ||
|
|
fbd501f57e | ||
|
|
ed7bd001ee | ||
|
|
83b3a0da5e | ||
|
|
e04e079243 | ||
|
|
d4fd75b9c7 | ||
|
|
6fbb3b9565 | ||
|
|
42005608fa | ||
|
|
538ebaf7bb | ||
|
|
2be0aebd6d | ||
|
|
fdaf25d92e | ||
|
|
5fc4e56bf9 | ||
|
|
392e437b9a | ||
|
|
3066d6b82e | ||
|
|
2f941be5c0 | ||
|
|
ff058ca777 | ||
|
|
3f321d3a1c | ||
|
|
490c38bd31 | ||
|
|
019cdb169c | ||
|
|
9d0c7c1edb | ||
|
|
88a96f0eae | ||
|
|
b7d8a5ea99 | ||
|
|
5d8c6348bc | ||
|
|
a934b89a53 | ||
|
|
9ccf19eb64 | ||
|
|
de3eac2b2f | ||
|
|
76abc0bbfb | ||
|
|
3bb2ba18e5 | ||
|
|
e71144a68d | ||
|
|
7a367a933b | ||
|
|
c1cb96fa57 | ||
|
|
28c6947a43 | ||
|
|
00b9dec218 | ||
|
|
8e89dfb345 | ||
|
|
ed21911288 | ||
|
|
d9aeb32852 | ||
|
|
3262d3b875 | ||
|
|
af88ee386b | ||
|
|
50f39284ce | ||
|
|
9c36e196b7 | ||
|
|
274c85ac2b | ||
|
|
5eaa06cf15 | ||
|
|
86c20e0a39 | ||
|
|
90a8ebbfa4 | ||
|
|
e995836ee4 | ||
|
|
fe040070de | ||
|
|
9d3290c2c1 | ||
|
|
97880ce93e | ||
|
|
c14ec935da | ||
|
|
8a3e91fdc8 | ||
|
|
bdcb481a51 | ||
|
|
eed34b528e | ||
|
|
5a3600e01f | ||
|
|
022e505ee0 | ||
|
|
b9733109fa | ||
|
|
3d146de750 | ||
|
|
b475fbaa5d | ||
|
|
fd14133e91 | ||
|
|
f0c07e6a7b | ||
|
|
ee568866ec | ||
|
|
943d8efe0d | ||
|
|
c9dcc88320 | ||
|
|
8819cd8980 | ||
|
|
016951a3be | ||
|
|
c44d56af63 | ||
|
|
5501e20e82 | ||
|
|
83c601b5c9 | ||
|
|
0fccaa61fe | ||
|
|
05545cb6da | ||
|
|
0e0f8d7284 | ||
|
|
734636039f | ||
|
|
14b73706ce | ||
|
|
f2600a02fb | ||
|
|
f7a7eed782 | ||
|
|
d2bfdab339 | ||
|
|
756d407ce2 | ||
|
|
ec3437f9e1 | ||
|
|
8362589364 | ||
|
|
bc2f8fed9c | ||
|
|
959ffb58ac | ||
|
|
4ddc19f91f | ||
|
|
6ea0cb10c5 | ||
|
|
da23ee8e3e | ||
|
|
85084e4b60 | ||
|
|
4315d19daa | ||
|
|
91efec9200 | ||
|
|
6baa2e28d7 | ||
|
|
476be88c86 | ||
|
|
abe0c49b38 | ||
|
|
8ca104634b | ||
|
|
06706702f8 | ||
|
|
b3c42769cb | ||
|
|
7178b3b557 | ||
|
|
01dc997fb4 | ||
|
|
a00e726c22 | ||
|
|
2015a566cb | ||
|
|
45392aa5ad | ||
|
|
b07413a314 | ||
|
|
90f6d4333c | ||
|
|
beeeaede5e | ||
|
|
1c09c7c544 | ||
|
|
352df6a604 | ||
|
|
db635a8119 | ||
|
|
1f06deb52f | ||
|
|
29099d5d07 | ||
|
|
488b5d7d87 | ||
|
|
1e9a34b0eb | ||
|
|
e9b778eba6 | ||
|
|
51536bae82 | ||
|
|
54d8ad5acb | ||
|
|
2e0e79162a | ||
|
|
0e0167d4bd | ||
|
|
d0a1e78b69 | ||
|
|
fa6da3b475 | ||
|
|
87c0b88fda | ||
|
|
23a91674bc | ||
|
|
9ddf99ad1a | ||
|
|
3dbc075f9b | ||
|
|
4ac3068fcb | ||
|
|
c23b04c3ab | ||
|
|
edc920f7db | ||
|
|
78a7a45ff0 | ||
|
|
2e01006126 | ||
|
|
4cada70b2c | ||
|
|
464c3154c2 | ||
|
|
dfb47b5e58 | ||
|
|
1454ba5f2d | ||
|
|
28b6b38613 | ||
|
|
8c917d29d4 | ||
|
|
940a0e2fde | ||
|
|
a4ba8827cc | ||
|
|
59fafc6fc6 | ||
|
|
c7b0f59e51 | ||
|
|
6c64e67af9 | ||
|
|
889c10945b | ||
|
|
78146f1ee3 | ||
|
|
a747aea71c | ||
|
|
1e362d39ac | ||
|
|
3d9bd9d353 | ||
|
|
f424fe6dae | ||
|
|
d939d9f43d | ||
|
|
6295f8d1a1 | ||
|
|
85ded31098 | ||
|
|
f8444459b8 | ||
|
|
7b4959cc6d | ||
|
|
df3a9424b1 | ||
|
|
742df5322f | ||
|
|
e5a4404770 | ||
|
|
39f486cd52 | ||
|
|
21a40540c2 | ||
|
|
bedbf544ff | ||
|
|
5ba606903c | ||
|
|
18ce952706 | ||
|
|
0ac5937eea | ||
|
|
390c51bac0 | ||
|
|
bbb436e971 | ||
|
|
c78622ada6 | ||
|
|
b95a9d7b7a | ||
|
|
6d37dc1e1e | ||
|
|
65c03ee57b | ||
|
|
ec1a0c5fb3 | ||
|
|
2ff71edb76 | ||
|
|
1e470e2ce1 | ||
|
|
b8f3c7ba54 | ||
|
|
04a079b623 | ||
|
|
d911b0a8ba | ||
|
|
cc48275237 | ||
|
|
6d50f7004d | ||
|
|
e276e42e1e | ||
|
|
0b22538af1 | ||
|
|
c712c5f793 | ||
|
|
5b04578765 | ||
|
|
50852921fc | ||
|
|
8a86914827 | ||
|
|
08bda166fe | ||
|
|
deb938d521 | ||
|
|
eec9317b93 | ||
|
|
bb25bc9a82 | ||
|
|
8f6508e7fd | ||
|
|
4ec00e9033 | ||
|
|
384cd4b730 | ||
|
|
193edb4871 | ||
|
|
369667e8f5 | ||
|
|
6c6f313de6 | ||
|
|
f0323731dd | ||
|
|
dd5a14cd1a | ||
|
|
e59285d998 | ||
|
|
c528d3921f | ||
|
|
c00440d918 | ||
|
|
b35b139fbc | ||
|
|
3afe880b45 | ||
|
|
0a878401a4 | ||
|
|
71c141980a | ||
|
|
d33954c68d | ||
|
|
e14713631f | ||
|
|
f45cbd421c | ||
|
|
06dcf82fe1 | ||
|
|
0812dbb1e9 | ||
|
|
e551096dd1 | ||
|
|
8d08ae5147 | ||
|
|
99652825b6 | ||
|
|
e074d3e83a | ||
|
|
e5aa3c3cfb | ||
|
|
b55e4045c1 | ||
|
|
ac3f7a2ac8 | ||
|
|
5f71210744 | ||
|
|
bc6f5c02cf | ||
|
|
b104d1222a | ||
|
|
8b1601b76a | ||
|
|
1e86f3cd8b | ||
|
|
a6ff8f14d4 | ||
|
|
ed04e2e0af | ||
|
|
cf237c2923 | ||
|
|
33807f0acc | ||
|
|
f439f400e2 | ||
|
|
9dc090e3dc | ||
|
|
6c8186b995 | ||
|
|
f705c887ff | ||
|
|
73e9e7eb7c | ||
|
|
b8ff62d2c5 | ||
|
|
b5dcbf5d72 | ||
|
|
f0402792d6 | ||
|
|
c5abe686eb | ||
|
|
61802b6008 | ||
|
|
e6ce12d625 | ||
|
|
6b84a5248c | ||
|
|
b73ea79431 | ||
|
|
edcc428b3c | ||
|
|
b00c48e369 | ||
|
|
ee1dc904e1 | ||
|
|
9ce46753fc | ||
|
|
2fffd175d7 | ||
|
|
0749708942 | ||
|
|
b7a1bc53c3 | ||
|
|
43f99ae3c8 | ||
|
|
5bd79b01b2 | ||
|
|
ad165bc76b | ||
|
|
36373a96e1 | ||
|
|
f5ebcc76eb | ||
|
|
3eb214d842 | ||
|
|
e1e6d18bf3 | ||
|
|
00ee31d224 | ||
|
|
63459b2354 | ||
|
|
656a3156f2 | ||
|
|
2447a57e00 | ||
|
|
066de108db | ||
|
|
31a1c8b960 | ||
|
|
21ae3069e2 | ||
|
|
32d796d56c | ||
|
|
ace38e6b1e | ||
|
|
1b0d17a602 | ||
|
|
27260122e2 | ||
|
|
150a788fc5 | ||
|
|
0597953ea1 | ||
|
|
a3bebd98c5 | ||
|
|
c5c1bc928b | ||
|
|
6e7e4c9fc1 | ||
|
|
99311911f1 | ||
|
|
593a94131d | ||
|
|
d076fefa99 | ||
|
|
0b9260161b | ||
|
|
dc3aafa6cc | ||
|
|
7c6dbc94bc | ||
|
|
e5239c169b | ||
|
|
5cd4c14943 | ||
|
|
ef66e2c3a0 | ||
|
|
dcb0ff5f51 | ||
|
|
07b69e1795 | ||
|
|
e4cf7742d1 | ||
|
|
7de49e8605 | ||
|
|
8a2a0a1249 | ||
|
|
2794a5d1d8 | ||
|
|
4e29efe24d | ||
|
|
c9924cdbab | ||
|
|
36e1b678ce | ||
|
|
6fad3b00fa | ||
|
|
e7760103ee | ||
|
|
63fb720955 | ||
|
|
43f4d680c7 | ||
|
|
2db008ad88 | ||
|
|
0f4113b673 | ||
|
|
a91a5ef9e4 | ||
|
|
cbd984282b | ||
|
|
cf61b51de7 | ||
|
|
7efea74285 | ||
|
|
45d1b187b1 | ||
|
|
dfac42b6ad | ||
|
|
0758786790 | ||
|
|
d72c7639f2 | ||
|
|
bd68667b00 | ||
|
|
983db3c675 | ||
|
|
84f0dd07ae | ||
|
|
ac3cc16250 | ||
|
|
e9cfa5e408 | ||
|
|
7217eaa289 | ||
|
|
b30d2de195 | ||
|
|
b2f341a3e5 | ||
|
|
30bb93ee77 | ||
|
|
4e53acddcf | ||
|
|
96753349bc | ||
|
|
387dcd464d | ||
|
|
3cbc9ca57c | ||
|
|
6b7901c7dc | ||
|
|
41075416f8 | ||
|
|
53a1e5d585 | ||
|
|
3050d64909 | ||
|
|
99fbefd558 | ||
|
|
0bca16deb6 | ||
|
|
5e5f023b8a | ||
|
|
02bfc6b3d9 | ||
|
|
ec975f1614 | ||
|
|
6d768f0b0a | ||
|
|
968af5f934 | ||
|
|
7a65e6d808 | ||
|
|
6cf93b98e1 | ||
|
|
bba2f3c694 | ||
|
|
aa288b910b | ||
|
|
63f52dc2eb | ||
|
|
a914603245 | ||
|
|
83acaf97ec | ||
|
|
487180785c | ||
|
|
eae90af0dd | ||
|
|
67ee31842b | ||
|
|
f66fae7724 | ||
|
|
e038c65f8b | ||
|
|
d4aa54874e | ||
|
|
54f4179282 | ||
|
|
9fb4b4a749 | ||
|
|
b59d66cdf2 | ||
|
|
1660147ef3 | ||
|
|
a7ca77960f | ||
|
|
ffb6b38561 | ||
|
|
18a73ed5a6 | ||
|
|
3b03b2467d | ||
|
|
6fa2e3aa7a | ||
|
|
837b30fbbb | ||
|
|
82241ffce8 | ||
|
|
3d848f505f | ||
|
|
e52247e5ef | ||
|
|
ed91588ba1 | ||
|
|
6572cbab18 | ||
|
|
56af5ee111 | ||
|
|
c61ede6bab | ||
|
|
f6a1b1b638 | ||
|
|
86ce875da8 | ||
|
|
3ccc92c317 | ||
|
|
b1dde41918 | ||
|
|
095b8c13fb | ||
|
|
cbc6ba47ee | ||
|
|
78c775747b | ||
|
|
065fbd03ad | ||
|
|
c01d738476 | ||
|
|
73fdf2f087 | ||
|
|
cda13d212f | ||
|
|
14461c10c6 | ||
|
|
0f814d2476 | ||
|
|
91f77afaf0 | ||
|
|
c4f4f00d95 | ||
|
|
0a5279c92a | ||
|
|
8b43f65904 | ||
|
|
a16bedab82 | ||
|
|
41bded3a6b | ||
|
|
150d847f7f | ||
|
|
6d5e1173a3 | ||
|
|
9b1c4128d8 | ||
|
|
4764e27b68 | ||
|
|
755308eca8 | ||
|
|
4a317b799c | ||
|
|
f75c4af2d8 | ||
|
|
5c252e05f9 | ||
|
|
59f200c960 | ||
|
|
6c9e4704cc | ||
|
|
f7b61d8d9a | ||
|
|
39e77dc2cc | ||
|
|
7cdf18de99 | ||
|
|
89a33ea4e3 | ||
|
|
90b70612c9 | ||
|
|
208fafaaab | ||
|
|
8d723340fe | ||
|
|
5e57bdb926 | ||
|
|
46d777a46a | ||
|
|
bff66a5921 | ||
|
|
65cd387677 | ||
|
|
416462bd53 | ||
|
|
29a79c6fc8 | ||
|
|
ebab382da3 | ||
|
|
58aa45e509 | ||
|
|
59f09d4260 | ||
|
|
d84e8db88c | ||
|
|
06a8266c7c | ||
|
|
af785cccb5 | ||
|
|
91d63702d3 | ||
|
|
78aec72c1f | ||
|
|
9dff14d5dd | ||
|
|
785064ae30 | ||
|
|
4475c0caba | ||
|
|
aa12e45588 | ||
|
|
af130a7c47 | ||
|
|
50464f35d0 | ||
|
|
5b551fc293 | ||
|
|
ff74d6ce95 | ||
|
|
7b98260176 | ||
|
|
dcf4b716c3 | ||
|
|
a28fc8411e | ||
|
|
b70323fa05 | ||
|
|
9c1267b39e | ||
|
|
f0f9567f41 | ||
|
|
00d2ac8cb2 | ||
|
|
deaa3754e0 | ||
|
|
8d9268753b | ||
|
|
fb9594e322 | ||
|
|
f9df95a9fe | ||
|
|
04b1b30b7d | ||
|
|
545b9983f7 | ||
|
|
7ca17eae84 | ||
|
|
b8d3da6b8a | ||
|
|
1fdedf8f5f | ||
|
|
c43bdda277 | ||
|
|
f2b78b9ca4 | ||
|
|
13b017e22e | ||
|
|
b4896f139f | ||
|
|
dbcd6827b7 | ||
|
|
7084ece117 | ||
|
|
a8f358b2a5 | ||
|
|
c73e106c17 | ||
|
|
cd23d7b573 | ||
|
|
2433d26445 | ||
|
|
1bc5be6064 | ||
|
|
224fb776a6 | ||
|
|
e1735e0175 | ||
|
|
4a7876c4f5 | ||
|
|
ded6d5cb6d | ||
|
|
5218e78537 | ||
|
|
7c3d4d26df | ||
|
|
b9e22e4d9c | ||
|
|
8c1c95e2ef | ||
|
|
4dd00fbbcc | ||
|
|
ae75d8e1b8 | ||
|
|
79cdf7299c | ||
|
|
10b5c6d252 | ||
|
|
1b17947835 | ||
|
|
8e26fec4ad | ||
|
|
2532f3c29b | ||
|
|
235e716d78 | ||
|
|
f66774e02b | ||
|
|
b2444a1d88 | ||
|
|
95842639b6 | ||
|
|
d1e3c3889f | ||
|
|
0dcf0f5967 | ||
|
|
856564e746 | ||
|
|
af0abab590 | ||
|
|
4c078d1342 | ||
|
|
7f7307a4e7 | ||
|
|
8cc0e5399c | ||
|
|
fb8627953a | ||
|
|
b8bbe89a94 | ||
|
|
628c7ee717 | ||
|
|
0f525e9df7 | ||
|
|
0328e0aa11 | ||
|
|
1251ab21a4 | ||
|
|
1862d9bdca | ||
|
|
ae5b59d221 | ||
|
|
10bace1e7f | ||
|
|
d90f5b92ec | ||
|
|
67d038ecfc | ||
|
|
6d0d3be857 | ||
|
|
1c5c5bba57 | ||
|
|
5867400af2 | ||
|
|
ad87eef636 | ||
|
|
8e25f49cd0 | ||
|
|
63d4ddf2d0 | ||
|
|
ebd74103cf | ||
|
|
8f71861db3 | ||
|
|
0e273de0fc | ||
|
|
867bfa3b8b | ||
|
|
359cf9c02a | ||
|
|
76e3f9e07a | ||
|
|
2d241508f5 | ||
|
|
5dd2d27fe7 | ||
|
|
f1a37c1db3 | ||
|
|
fb8b3d174f | ||
|
|
6d43f1d6cf | ||
|
|
d40a9ae6d7 | ||
|
|
2f04a417c8 | ||
|
|
dd3e364a6e | ||
|
|
6db7eec41c | ||
|
|
fc76c3be7c | ||
|
|
6edcd5df3d | ||
|
|
fb1455e468 | ||
|
|
b1899cc060 | ||
|
|
b11329564d | ||
|
|
72ee2e5cb3 | ||
|
|
ddd37ddfe2 | ||
|
|
964ed53375 | ||
|
|
411583e7af | ||
|
|
6323730a80 | ||
|
|
f02aca5f94 | ||
|
|
8370574b14 | ||
|
|
05f451723a | ||
|
|
ad9dd6140c | ||
|
|
6dc41de650 | ||
|
|
d5ab817379 | ||
|
|
d787408c5f | ||
|
|
34085cbd3f | ||
|
|
950f80d7f5 | ||
|
|
0cd9bc7053 | ||
|
|
898a6e8744 | ||
|
|
184c4cd382 | ||
|
|
ceebf4d4e1 | ||
|
|
b183e5a5ca | ||
|
|
bc05379321 | ||
|
|
147d217e1a | ||
|
|
ddd13b74f8 | ||
|
|
bcdf45b37f | ||
|
|
f14d36b5cc | ||
|
|
f81d4650fc | ||
|
|
312c787ea6 | ||
|
|
e0d04a75ca | ||
|
|
e93f5fb7e9 | ||
|
|
40c3eb1f75 | ||
|
|
aafca27c58 | ||
|
|
49c956498d | ||
|
|
9abbf0dfb7 | ||
|
|
d0961df566 | ||
|
|
acc20111c5 | ||
|
|
0f62d88793 | ||
|
|
22521d9b85 | ||
|
|
bd6c41aed2 | ||
|
|
f42f6d9890 | ||
|
|
41e36fbaea | ||
|
|
8f41dc6466 | ||
|
|
77a029f9d8 | ||
|
|
36f6360a0f | ||
|
|
e1895d0ad0 | ||
|
|
801baf69e4 | ||
|
|
30ec3e6813 | ||
|
|
825a2225eb | ||
|
|
b8fe546312 | ||
|
|
32e6b050ac | ||
|
|
7709d7b9a6 | ||
|
|
d4f52320ee | ||
|
|
8693c80095 | ||
|
|
59bdc92689 | ||
|
|
4b919a7f2f | ||
|
|
314d2c5df2 | ||
|
|
95d1912a92 | ||
|
|
4c551a0501 | ||
|
|
0350d11b11 | ||
|
|
c67e22c9f7 | ||
|
|
eb80b0cf0e | ||
|
|
fcd0004ffb | ||
|
|
ecc2742f70 | ||
|
|
49db2e7a0f | ||
|
|
fa30e08686 | ||
|
|
5f663d5f1b | ||
|
|
9898fb45b5 | ||
|
|
3fc127ba34 | ||
|
|
db1f64ee45 | ||
|
|
4579d9074e | ||
|
|
56d4ac0c62 | ||
|
|
24719039a3 | ||
|
|
a67a1be36c | ||
|
|
81fb47dca4 | ||
|
|
490d7f2e42 | ||
|
|
468abb272a | ||
|
|
37bcb67f6b | ||
|
|
496fced9b3 | ||
|
|
ca9428ba6e | ||
|
|
c12e2a5add | ||
|
|
b9503cc9f9 | ||
|
|
fb244578a3 | ||
|
|
c8b9a058c0 | ||
|
|
54dd1b7dc2 | ||
|
|
d6265b936f | ||
|
|
01d70828ca | ||
|
|
a0e53a7e51 | ||
|
|
16de438ced | ||
|
|
9207d0a36e | ||
|
|
b63c82955f | ||
|
|
dcea496ed5 | ||
|
|
49a1ef5748 | ||
|
|
c60936b124 | ||
|
|
28eb3e0332 | ||
|
|
eb08054596 | ||
|
|
585fb4d35b | ||
|
|
0db6869c6b | ||
|
|
f6d8fac3c4 | ||
|
|
1202487efc | ||
|
|
1c0363f9d6 | ||
|
|
f6ef566496 | ||
|
|
bb307c36ad | ||
|
|
0e724a49ee | ||
|
|
2263c79bfa | ||
|
|
2b60bc701e | ||
|
|
8304aba714 | ||
|
|
188bb7e0b6 | ||
|
|
fb760df090 | ||
|
|
fc1d281d71 | ||
|
|
d1f333e89c | ||
|
|
dcc838a7e2 | ||
|
|
cce6a2e523 | ||
|
|
fca4a701fe | ||
|
|
3acce5046b | ||
|
|
917299db4a | ||
|
|
fd6ab358e0 | ||
|
|
5e621baf23 | ||
|
|
57652900f3 | ||
|
|
1b746120c7 | ||
|
|
fdcfb34531 | ||
|
|
1cecdbbf54 | ||
|
|
bf07c977d7 | ||
|
|
05c54eaad8 | ||
|
|
91b176a8e4 | ||
|
|
c4756a9a45 | ||
|
|
e7d90d441e | ||
|
|
fe5fbea623 | ||
|
|
8e5454ee7e | ||
|
|
b04ca2ec1a | ||
|
|
430c4e20d7 | ||
|
|
9ff2677ced | ||
|
|
79fc96fde8 | ||
|
|
c7d2e4d6fe | ||
|
|
60a467071e | ||
|
|
6fb7d81126 | ||
|
|
161a580f69 | ||
|
|
d1b5923351 | ||
|
|
ddd828e00f | ||
|
|
4c4bc90460 | ||
|
|
aa8193bb9e | ||
|
|
d6d2c94ab9 | ||
|
|
6d076ef241 | ||
|
|
634cf552b9 | ||
|
|
2141a6ef04 | ||
|
|
b41a6f5873 | ||
|
|
4c548a1fb6 | ||
|
|
a14a0af75c | ||
|
|
09534aab99 | ||
|
|
3236cbbdd2 | ||
|
|
23982b6cdf | ||
|
|
315af47c86 | ||
|
|
a9a626b507 | ||
|
|
8a2374309a | ||
|
|
37a4c965f9 | ||
|
|
d8864a1dc0 | ||
|
|
1ba486c72c | ||
|
|
89f145ac52 | ||
|
|
8546dc309e | ||
|
|
6cdcbb3005 | ||
|
|
e864562995 | ||
|
|
be498069aa | ||
|
|
c791be2122 | ||
|
|
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 | ||
|
|
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 |
@@ -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.
|
||||||
|
|||||||
1
.earthlyignore
Normal file
1
.earthlyignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test/automated/api/node_modules
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
|
# Read first
|
||||||
|
|
||||||
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later.
|
If this is an unsolicited change, or there is no existing issue filed for it, please open a GitHub issue before creating a pull request. This will allow us to discuss the motivations and the big picture behind the change first. It's possible there may be other solutions that should be discussed for what you think should be built. It is possible your change will be rejected unless some discussion around your proposal happens first. While creating this PR means you probably already did the work, it still makes sense to file an issue now, and into the future when you have proposed changes.
|
||||||
|
|
||||||
If there is no issue filed for this particular change it's highly recommended you file one. While creating this PR means you probably already did the work, in the future make sure an issue is filed beforehand so changes, fixes and features can be discussed ahead of time.
|
## Description
|
||||||
|
|
||||||
# Description
|
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later and start a conversation, even if your PR is not ready for review.
|
||||||
|
|
||||||
Fixes # (issue)
|
Fixes # (issue)
|
||||||
|
|
||||||
|
## Screenshot Examples or Logs
|
||||||
|
|
||||||
|
If this is a frontend change, please include a screenshot of the change. If this is a backend change, please include relevant logs or examples of the change in action if applicable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Some things you might want to mention:
|
Some things you might want to mention:
|
||||||
@@ -16,4 +21,4 @@ Some things you might want to mention:
|
|||||||
3. If you're fixing something, what was wrong? How should we stop from having this issue happen again?
|
3. If you're fixing something, what was wrong? How should we stop from having this issue happen again?
|
||||||
4. If this is a new feature or addition to functionality, why should it be added? What are the use cases? Who was asking for this functionality?
|
4. If this is a new feature or addition to functionality, why should it be added? What are the use cases? Who was asking for this functionality?
|
||||||
|
|
||||||
If this is an unsolicited change or have no issue associated please do your best to detail the motivations behind this PR, and think about filing an issue to discuss changes ahead of time in the future.
|
Thank you so much for contributing to Owncast! 🎉
|
||||||
|
|||||||
31
.github/stale.yml
vendored
31
.github/stale.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
# Number of days of inactivity before an issue becomes stale
|
|
||||||
daysUntilStale: 60
|
|
||||||
# Number of days of inactivity before a stale issue is closed
|
|
||||||
daysUntilClose: 7
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- backlog
|
|
||||||
# Label to use when marking an issue as stale
|
|
||||||
staleLabel: stale
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs. If this
|
|
||||||
was a feature request that others have shown no interest in then it's
|
|
||||||
likely to not get implemented due to lack of interest. If others also
|
|
||||||
want to see this feature then now is the time to say something!
|
|
||||||
Thank you for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
||||||
closeComment: false
|
|
||||||
exemptMilestones: true
|
|
||||||
|
|
||||||
# Since old PRs are less useful than old issues ping them sooner.
|
|
||||||
pulls:
|
|
||||||
daysUntilStale: 30
|
|
||||||
markComment: >
|
|
||||||
This pull request has not had any activity in 30 days. Since things move fast it's best
|
|
||||||
to get PRs merged in. If this PR addresses a previously filed issue that needs to be
|
|
||||||
resolved please work to get it merged in, or allow somebody else to work on a fix.
|
|
||||||
This PR will be closed if no further activity occurs. Thank you for your contributions!
|
|
||||||
exemptLabels:
|
|
||||||
- bot
|
|
||||||
10
.github/workflows/actions-lint.yml
vendored
10
.github/workflows/actions-lint.yml
vendored
@@ -13,7 +13,15 @@ jobs:
|
|||||||
name: GitHub actions
|
name: GitHub actions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- uses: docker://rhysd/actionlint:latest
|
- uses: docker://rhysd/actionlint:latest
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/auto-comment-on-label.yaml
vendored
2
.github/workflows/auto-comment-on-label.yaml
vendored
@@ -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@7157823c0f1cb7170b464dc3ffb1555a01ce94c3
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
body: |
|
body: |
|
||||||
|
|||||||
26
.github/workflows/automated-end-to-end-api.yaml
vendored
26
.github/workflows/automated-end-to-end-api.yaml
vendored
@@ -18,24 +18,44 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
|
|
||||||
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{go,mod,sum}'
|
||||||
|
|
||||||
- uses: earthly/actions-setup@v1
|
- uses: earthly/actions-setup@v1
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
|
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
|
||||||
|
|
||||||
- name: Earthly version
|
- name: Earthly version
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
run: earthly --version
|
run: earthly --version
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
id: qemu
|
id: qemu
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt:latest
|
image: tonistiigi/binfmt:latest
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run API tests
|
- name: Run API tests
|
||||||
uses: nick-fields/retry@v2
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
15
.github/workflows/browser-testing.yml
vendored
15
.github/workflows/browser-testing.yml
vendored
@@ -19,12 +19,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: latest
|
node-version: '22.9.0'
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -40,14 +47,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
|
||||||
|
|||||||
9
.github/workflows/build-storybook.yml
vendored
9
.github/workflows/build-storybook.yml
vendored
@@ -11,8 +11,15 @@ jobs:
|
|||||||
if: github.repository == 'owncast/owncast'
|
if: github.repository == 'owncast/owncast'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
30
.github/workflows/chromatic.yml
vendored
30
.github/workflows/chromatic.yml
vendored
@@ -27,23 +27,37 @@ jobs:
|
|||||||
uses: fkirc/skip-duplicate-actions@v5
|
uses: fkirc/skip-duplicate-actions@v5
|
||||||
with:
|
with:
|
||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
- name: Check out code
|
|
||||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
|
||||||
with:
|
with:
|
||||||
# Make sure the actual branch is checked out when running on pull requests
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
path: 'web'
|
||||||
|
files_ignore: |
|
||||||
|
static/**
|
||||||
|
web/next.config.js
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{js,ts,tsx,jsx,md}'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true'}}
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
|
||||||
|
uses: chromaui/action@v11
|
||||||
|
|
||||||
uses: chromaui/action@v1
|
|
||||||
# Chromatic GitHub Action options
|
# Chromatic GitHub Action options
|
||||||
with:
|
with:
|
||||||
workingDir: web
|
workingDir: web
|
||||||
|
|||||||
72
.github/workflows/codeql-analysis.yml
vendored
72
.github/workflows/codeql-analysis.yml
vendored
@@ -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,53 @@ 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: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
- name: Check out repository code
|
||||||
- name: Initialize CodeQL
|
uses: actions/checkout@v4
|
||||||
uses: github/codeql-action/init@v3
|
if: github.event_name == 'push'
|
||||||
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
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
- uses: actions/setup-go@v5
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
with:
|
||||||
- name: Autobuild
|
go-version: '1.22'
|
||||||
uses: github/codeql-action/autobuild@v3
|
cache: true
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# Initializes the CodeQL tools for scanning.
|
||||||
# 📚 https://git.io/JvXDl
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@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
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# and modify them (or add more) to build your code if your project
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
# uses a compiled language
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
#- run: |
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# make bootstrap
|
# 📚 https://git.io/JvXDl
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
uses: github/codeql-action/analyze@v3
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
10
.github/workflows/container-lint.yml
vendored
10
.github/workflows/container-lint.yml
vendored
@@ -19,7 +19,15 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: aquasec/trivy
|
image: aquasec/trivy
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- name: Check critical issues
|
- name: Check critical issues
|
||||||
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile
|
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile
|
||||||
|
|||||||
11
.github/workflows/container.yaml
vendored
11
.github/workflows/container.yaml
vendored
@@ -37,10 +37,15 @@ jobs:
|
|||||||
image: tonistiigi/binfmt:latest
|
image: tonistiigi/binfmt:latest
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }}
|
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }}
|
||||||
@@ -49,7 +54,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
|
||||||
|
|||||||
53
.github/workflows/css-lint.yaml
vendored
Normal file
53
.github/workflows/css-lint.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: CSS Lint and Formatting
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'web/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'web/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
css-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
path: 'web'
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{css,scss}'
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22.9.0'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Run Prettier
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
run: npx prettier --check ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
|
|
||||||
|
- name: Run Stylelint
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
run: npx stylelint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
@@ -9,19 +9,26 @@ jobs:
|
|||||||
name: Generate API Documentation
|
name: Generate API Documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- name: Run redoc on openapi.yaml
|
- name: Check out repository code
|
||||||
run: |
|
uses: actions/checkout@v4
|
||||||
npx redoc-cli bundle openapi.yaml -o docs/api/index.html --options '{"hideHostname": true, "pathInMiddlePanel": true}'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Run redoc on openapi.yaml
|
||||||
uses: EndBug/add-and-commit@v9
|
run: |
|
||||||
with:
|
npx @redocly/cli --config docs/api/redocly.yaml build-docs openapi.yaml -o docs/api/index.html
|
||||||
author_name: Owncast
|
|
||||||
author_email: owncast@owncast.online
|
- name: Commit changes
|
||||||
message: "Commit updated API documentation"
|
uses: EndBug/add-and-commit@v9
|
||||||
add: "docs/api/index.html"
|
with:
|
||||||
env:
|
author_name: Owncast
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
author_email: owncast@owncast.online
|
||||||
|
message: 'Commit updated API documentation'
|
||||||
|
add: 'docs/api/index.html'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/go-lint.yml
vendored
4
.github/workflows/go-lint.yml
vendored
@@ -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
|
||||||
|
|||||||
23
.github/workflows/go-tests.yaml
vendored
23
.github/workflows/go-tests.yaml
vendored
@@ -12,13 +12,22 @@ 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:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{go,mod,sum}'
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -28,12 +37,14 @@ jobs:
|
|||||||
go-test-
|
go-test-
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '^1'
|
go-version: '^1'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|
||||||
test-bsds:
|
test-bsds:
|
||||||
@@ -49,6 +60,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{go,mod,sum}'
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -59,10 +78,12 @@ jobs:
|
|||||||
go-test-
|
go-test-
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '^1'
|
go-version: '^1'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
31
.github/workflows/hls-tests.yml
vendored
31
.github/workflows/hls-tests.yml
vendored
@@ -24,10 +24,29 @@ jobs:
|
|||||||
uses: fkirc/skip-duplicate-actions@v5
|
uses: fkirc/skip-duplicate-actions@v5
|
||||||
with:
|
with:
|
||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-go@v5
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files-yaml
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
src:
|
||||||
|
- '**/*.{go,mod,sum}'
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
@@ -43,14 +62,16 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Local stroage
|
- name: Local stroage
|
||||||
uses: nick-fields/retry@v2
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
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
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
|
|||||||
@@ -28,18 +28,25 @@ jobs:
|
|||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
# Make sure the actual branch is checked out when running on pull requests
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
persist-credentials: true
|
|
||||||
|
- name: Setup Nodejs
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22.9.0'
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- 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@v45
|
||||||
with:
|
with:
|
||||||
path: 'web'
|
path: 'web'
|
||||||
files_ignore: |
|
files_ignore: |
|
||||||
@@ -47,7 +54,7 @@ jobs:
|
|||||||
web/next.config.js
|
web/next.config.js
|
||||||
files_yaml: |
|
files_yaml: |
|
||||||
src:
|
src:
|
||||||
- '**/*.{js,ts,tsx,jsx,css,md}'
|
- '**/*.{js,ts,tsx,jsx,md}'
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -64,22 +71,34 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint and fix
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
|
||||||
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
|
|
||||||
- name: Prettier
|
- name: Lint
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
|
||||||
|
run: npx eslint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||||
|
|
||||||
|
- name: Prettier formatting
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
|
||||||
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: Prettier check
|
||||||
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
|
||||||
|
run: npx prettier ${{ 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' && github.event_name != 'pull_request'
|
||||||
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: '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:
|
||||||
@@ -97,13 +116,20 @@ jobs:
|
|||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
# Make sure the actual branch is checked out when running on pull requests
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
- name: Setup Nodejs
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22.9.0'
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -123,6 +149,10 @@ jobs:
|
|||||||
- name: Check for unused JS code and dependencies
|
- name: Check for unused JS code and dependencies
|
||||||
run: npx knip --include dependencies,files,exports
|
run: npx knip --include dependencies,files,exports
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: ./web
|
||||||
|
run: npm test
|
||||||
|
|
||||||
# After any formatting and linting is complete we can run the build
|
# After any formatting and linting is complete we can run the build
|
||||||
# and bundle step. This both will verify that the build is successful as
|
# and bundle step. This both will verify that the build is successful as
|
||||||
# well as commiting the updated static files into the repository for use.
|
# well as commiting the updated static files into the repository for use.
|
||||||
@@ -139,6 +169,11 @@ jobs:
|
|||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
|
|
||||||
|
- name: Setup Nodejs
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22.9.0'
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
@@ -151,13 +186,15 @@ jobs:
|
|||||||
${{ runner.os }}-build-
|
${{ runner.os }}-build-
|
||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Checkout
|
- name: Check out pull request code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
# Make sure the actual branch is checked out when running on pull requests
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- name: Bundle web app (next.js build)
|
- name: Bundle web app (next.js build)
|
||||||
run: build/web/bundleWeb.sh
|
run: build/web/bundleWeb.sh
|
||||||
45
.github/workflows/javascript-tests.yml
vendored
45
.github/workflows/javascript-tests.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
name: Javascript Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'web/**'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'web/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
jest-run:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- id: skip_check
|
|
||||||
uses: fkirc/skip-duplicate-actions@v5
|
|
||||||
with:
|
|
||||||
concurrent_skipping: 'same_content_newer'
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18.9.0
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules-javascript-tests
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
working-directory: ./web
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
working-directory: ./web
|
|
||||||
run: npm test
|
|
||||||
15
.github/workflows/screenshots.yml
vendored
15
.github/workflows/screenshots.yml
vendored
@@ -14,10 +14,19 @@ jobs:
|
|||||||
Screenshots:
|
Screenshots:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- 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 +42,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
|
||||||
|
|||||||
10
.github/workflows/shellcheck.yml
vendored
10
.github/workflows/shellcheck.yml
vendored
@@ -20,7 +20,15 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: docker.io/ubuntu:24.04
|
image: docker.io/ubuntu:24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Check out pull request code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
- name: Install shellcheck
|
- name: Install shellcheck
|
||||||
run: apt update && apt install -y shellcheck bash && shellcheck --version
|
run: apt update && apt install -y shellcheck bash && shellcheck --version
|
||||||
|
|||||||
46
.github/workflows/stale.yml
vendored
Normal file
46
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 */2 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
exempt-all-milestones: true
|
||||||
|
|
||||||
|
days-before-issue-stale: 60
|
||||||
|
days-before-issue-close: 67
|
||||||
|
exempt-issue-labels: backlog,long-lived,bot
|
||||||
|
stale-issue-message: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. If this
|
||||||
|
was a feature request that others have shown no interest in, then it's
|
||||||
|
unlikely to get implemented due to lack of interest. If others also
|
||||||
|
want to see this feature then now is the time to say something! If this
|
||||||
|
is a bug report or you have questions that still need answering, please say
|
||||||
|
something. Feel free to drop by [our chat](https://owncast.rocket.chat) if
|
||||||
|
you'd like to discuss in real-time with people.
|
||||||
|
close-issue-message: >
|
||||||
|
This issue has been automatically closed due to inactivity. This isn't done
|
||||||
|
to be a jerk, or because the project doesn't care. But simply to keep the focus
|
||||||
|
on things that are actively discussed, and has continued interest from the community and
|
||||||
|
Owncast developers. Feel free to to comment if there is still discussion to be
|
||||||
|
had, or if you plan to work on it. Feel free to drop by [our chat](https://owncast.rocket.chat)
|
||||||
|
if you'd like to discuss in real-time with people. Thank you for being involved!
|
||||||
|
|
||||||
|
days-before-pr-stale: 30
|
||||||
|
days-before-pr-close: 37
|
||||||
|
stale-pr-message: >
|
||||||
|
This pull request has not had any activity in 30 days. If it has been abandoned
|
||||||
|
no future actions are necessary, it will be automatically closed. If this is a PR
|
||||||
|
with no clear plan on how to move forward on it getting into the project, then
|
||||||
|
further discussion is needed. Now is a good time to discuss if this is still
|
||||||
|
something that should be worked on. If this PR is idle simply because nobody
|
||||||
|
has reviewed it, then feel free to ping somebody. However, if this PR is not linked to an
|
||||||
|
existing issue regarding something that was previously determined to be important, then even
|
||||||
|
more discussion needs to take place before it can get anywhere.
|
||||||
|
This PR will be closed if no further activity occurs. Thank you for your contributions!
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
|
||||||
@@ -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.
|
||||||
@@ -28,7 +28,6 @@ linters:
|
|||||||
- bodyclose
|
- bodyclose
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- exportloopref
|
|
||||||
- goconst
|
- goconst
|
||||||
- godot
|
- godot
|
||||||
- godox
|
- godox
|
||||||
@@ -49,7 +48,7 @@ linters:
|
|||||||
- cyclop
|
- cyclop
|
||||||
- gosimple
|
- gosimple
|
||||||
- unused
|
- unused
|
||||||
- exportloopref
|
- copyloopvar
|
||||||
- gocritic
|
- gocritic
|
||||||
- forbidigo
|
- forbidigo
|
||||||
- unparam
|
- unparam
|
||||||
@@ -67,12 +66,6 @@ linters-settings:
|
|||||||
# should ignore tests
|
# should ignore tests
|
||||||
skip-tests: true
|
skip-tests: true
|
||||||
|
|
||||||
gosimple:
|
|
||||||
# Select the Go version to target. The default is '1.13'.
|
|
||||||
go: '1.21'
|
|
||||||
# https://staticcheck.io/docs/options#checks
|
|
||||||
checks: ['all']
|
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
- ifElseChain
|
- ifElseChain
|
||||||
|
|||||||
@@ -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.3
|
||||||
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
|
||||||
|
|||||||
10
Earthfile
10
Earthfile
@@ -1,6 +1,6 @@
|
|||||||
VERSION --new-platform 0.6
|
VERSION --new-platform 0.6
|
||||||
|
|
||||||
FROM --platform=linux/amd64 alpine:3.15.5
|
FROM --platform=linux/amd64 alpine:3.20.3
|
||||||
ARG version=develop
|
ARG version=develop
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
@@ -87,7 +87,7 @@ build:
|
|||||||
RUN upx -t owncast
|
RUN upx -t owncast
|
||||||
END
|
END
|
||||||
|
|
||||||
SAVE ARTIFACT owncast owncast
|
SAVE ARTIFACT --keep-ts owncast owncast
|
||||||
|
|
||||||
package:
|
package:
|
||||||
RUN apk add --update --no-cache zip >> /dev/null
|
RUN apk add --update --no-cache zip >> /dev/null
|
||||||
@@ -109,7 +109,7 @@ package:
|
|||||||
ARG NAME=custom
|
ARG NAME=custom
|
||||||
END
|
END
|
||||||
|
|
||||||
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
|
COPY --keep-ts (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
|
||||||
ENV ZIPNAME owncast-$version-$NAME.zip
|
ENV ZIPNAME owncast-$version-$NAME.zip
|
||||||
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.zip .
|
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.zip .
|
||||||
SAVE ARTIFACT --keep-ts /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
|
SAVE ARTIFACT --keep-ts /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
|
||||||
@@ -119,11 +119,11 @@ docker:
|
|||||||
# in as space separated strings using the full account/repo:tag format.
|
# in as space separated strings using the full account/repo:tag format.
|
||||||
# https://github.com/earthly/earthly/blob/aea38448fa9c0064b1b70d61be717ae740689fb9/docs/earthfile/earthfile.md#assigning-multiple-image-names
|
# https://github.com/earthly/earthly/blob/aea38448fa9c0064b1b70d61be717ae740689fb9/docs/earthfile/earthfile.md#assigning-multiple-image-names
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
FROM --platform=$TARGETPLATFORM alpine:3.15.5
|
FROM --platform=$TARGETPLATFORM alpine:3.20.3
|
||||||
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates unzip && update-ca-certificates
|
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates unzip && 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
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --platform=$TARGETPLATFORM +package/owncast.zip /app
|
COPY --keep-ts --platform=$TARGETPLATFORM +package/owncast.zip /app
|
||||||
RUN unzip -x owncast.zip && mkdir data
|
RUN unzip -x owncast.zip && mkdir data
|
||||||
|
|
||||||
# temporarily disable until we figure out how to move forward
|
# temporarily disable until we figure out how to move forward
|
||||||
|
|||||||
93
README.md
93
README.md
@@ -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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/outbox"
|
"github.com/owncast/owncast/activitypub/outbox"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
@@ -14,16 +15,16 @@ import (
|
|||||||
|
|
||||||
// Start will initialize and start the federation support.
|
// Start will initialize and start the federation support.
|
||||||
func Start(datastore *data.Datastore) {
|
func Start(datastore *data.Datastore) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
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 configRepository.GetPrivateKey() == "" {
|
||||||
privateKey, publicKey, err := crypto.GenerateKeys()
|
privateKey, publicKey, err := crypto.GenerateKeys()
|
||||||
_ = data.SetPrivateKey(string(privateKey))
|
_ = configRepository.SetPrivateKey(string(privateKey))
|
||||||
_ = data.SetPublicKey(string(publicKey))
|
_ = configRepository.SetPublicKey(string(publicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Unable to get private key", err)
|
log.Errorln("Unable to get private key", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrivacyAudience represents the audience for an activity.
|
// PrivacyAudience represents the audience for an activity.
|
||||||
@@ -87,8 +87,10 @@ func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vo
|
|||||||
// MakeActivityPublic sets the required properties to make this activity
|
// MakeActivityPublic sets the required properties to make this activity
|
||||||
// seen as public.
|
// seen as public.
|
||||||
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
|
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// TO the public if we're not treating ActivityPub as "private".
|
// TO the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
public, _ := url.Parse(PUBLIC)
|
public, _ := url.Parse(PUBLIC)
|
||||||
|
|
||||||
to := streams.NewActivityStreamsToProperty()
|
to := streams.NewActivityStreamsToProperty()
|
||||||
@@ -121,7 +123,9 @@ func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
|
|||||||
activity.SetJSONLDId(id)
|
activity.SetJSONLDId(id)
|
||||||
|
|
||||||
// CC the public if we're not treating ActivityPub as "private".
|
// CC the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
public, _ := url.Parse(PUBLIC)
|
public, _ := url.Parse(PUBLIC)
|
||||||
cc := streams.NewActivityStreamsCcProperty()
|
cc := streams.NewActivityStreamsCcProperty()
|
||||||
cc.AppendIRI(public)
|
cc.AppendIRI(public)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,11 +101,13 @@ func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty
|
|||||||
|
|
||||||
// MakeServiceForAccount will create a new local actor service with the the provided username.
|
// MakeServiceForAccount will create a new local actor service with the the provided username.
|
||||||
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
actorIRI := MakeLocalIRIForAccount(accountName)
|
actorIRI := MakeLocalIRIForAccount(accountName)
|
||||||
|
|
||||||
person := streams.NewActivityStreamsService()
|
person := streams.NewActivityStreamsService()
|
||||||
nameProperty := streams.NewActivityStreamsNameProperty()
|
nameProperty := streams.NewActivityStreamsNameProperty()
|
||||||
nameProperty.AppendXMLSchemaString(data.GetServerName())
|
nameProperty.AppendXMLSchemaString(configRepository.GetServerName())
|
||||||
person.SetActivityStreamsName(nameProperty)
|
person.SetActivityStreamsName(nameProperty)
|
||||||
|
|
||||||
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
@@ -119,7 +121,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
person.SetActivityStreamsInbox(inboxProp)
|
person.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
|
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
|
||||||
needsFollowApprovalProperty.Set(data.GetFederationIsPrivate())
|
needsFollowApprovalProperty.Set(configRepository.GetFederationIsPrivate())
|
||||||
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
|
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
|
||||||
|
|
||||||
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
|
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
|
||||||
@@ -152,7 +154,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
|
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
|
||||||
person.SetW3IDSecurityV1PublicKey(publicKeyProp)
|
person.SetW3IDSecurityV1PublicKey(publicKeyProp)
|
||||||
|
|
||||||
if t, err := data.GetServerInitTime(); t != nil {
|
if t, err := configRepository.GetServerInitTime(); t != nil {
|
||||||
publishedDateProp := streams.NewActivityStreamsPublishedProperty()
|
publishedDateProp := streams.NewActivityStreamsPublishedProperty()
|
||||||
publishedDateProp.Set(t.Time)
|
publishedDateProp.Set(t.Time)
|
||||||
person.SetActivityStreamsPublished(publishedDateProp)
|
person.SetActivityStreamsPublished(publishedDateProp)
|
||||||
@@ -163,8 +165,8 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
// Profile properties
|
// Profile properties
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
uniquenessString := data.GetLogoUniquenessString()
|
uniquenessString := configRepository.GetLogoUniquenessString()
|
||||||
userAvatarURLString := data.GetServerURL() + "/logo/external"
|
userAvatarURLString := configRepository.GetServerURL() + "/logo/external"
|
||||||
userAvatarURL, err := url.Parse(userAvatarURLString)
|
userAvatarURL, err := url.Parse(userAvatarURLString)
|
||||||
userAvatarURL.RawQuery = "uc=" + uniquenessString
|
userAvatarURL.RawQuery = "uc=" + uniquenessString
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -195,14 +197,14 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
|
|
||||||
// Profile bio
|
// Profile bio
|
||||||
summaryProperty := streams.NewActivityStreamsSummaryProperty()
|
summaryProperty := streams.NewActivityStreamsSummaryProperty()
|
||||||
summaryProperty.AppendXMLSchemaString(data.GetServerSummary())
|
summaryProperty.AppendXMLSchemaString(configRepository.GetServerSummary())
|
||||||
person.SetActivityStreamsSummary(summaryProperty)
|
person.SetActivityStreamsSummary(summaryProperty)
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
if serverURL := data.GetServerURL(); serverURL != "" {
|
if serverURL := configRepository.GetServerURL(); serverURL != "" {
|
||||||
addMetadataLinkToProfile(person, "Stream", serverURL)
|
addMetadataLinkToProfile(person, "Stream", serverURL)
|
||||||
}
|
}
|
||||||
for _, link := range data.GetSocialHandles() {
|
for _, link := range configRepository.GetSocialHandles() {
|
||||||
addMetadataLinkToProfile(person, link.Platform, link.URL)
|
addMetadataLinkToProfile(person, link.Platform, link.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
tagProp := streams.NewActivityStreamsTagProperty()
|
tagProp := streams.NewActivityStreamsTagProperty()
|
||||||
for _, tagString := range data.GetServerMetadataTags() {
|
for _, tagString := range configRepository.GetServerMetadataTags() {
|
||||||
hashtag := MakeHashtag(tagString)
|
hashtag := MakeHashtag(tagString)
|
||||||
tagProp.AppendTootHashtag(hashtag)
|
tagProp.AppendTootHashtag(hashtag)
|
||||||
}
|
}
|
||||||
@@ -229,7 +231,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
|
|
||||||
// Work around an issue where a single attachment will not serialize
|
// Work around an issue where a single attachment will not serialize
|
||||||
// as an array, so add another item to the mix.
|
// as an array, so add another item to the mix.
|
||||||
if len(data.GetSocialHandles()) == 1 {
|
if len(configRepository.GetSocialHandles()) == 1 {
|
||||||
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
|
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeFakeService() vocab.ActivityStreamsService {
|
func makeFakeService() vocab.ActivityStreamsService {
|
||||||
@@ -55,9 +56,11 @@ func TestMain(m *testing.M) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.SetupPersistence(dbFile.Name())
|
data.SetupPersistence(dbFile.Name())
|
||||||
data.SetServerURL("https://my.cool.site.biz")
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
configRepository.SetServerURL("https://my.cool.site.biz")
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +27,9 @@ func MakeRemoteIRIForResource(resourcePath string, host string) (*url.URL, error
|
|||||||
|
|
||||||
// MakeLocalIRIForResource will create an IRI for the local server.
|
// MakeLocalIRIForResource will create an IRI for the local server.
|
||||||
func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI url", host, err)
|
log.Errorln("unable to parse local IRI url", host, err)
|
||||||
@@ -41,7 +43,9 @@ func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
|||||||
|
|
||||||
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
|
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
|
||||||
func MakeLocalIRIForAccount(account string) *url.URL {
|
func MakeLocalIRIForAccount(account string) *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI account server url", err)
|
log.Errorln("unable to parse local IRI account server url", err)
|
||||||
@@ -64,7 +68,9 @@ func Serialize(obj vocab.Type) ([]byte, error) {
|
|||||||
|
|
||||||
// MakeLocalIRIForStreamURL will return a full IRI for the local server stream url.
|
// MakeLocalIRIForStreamURL will return a full IRI for the local server stream url.
|
||||||
func MakeLocalIRIForStreamURL() *url.URL {
|
func MakeLocalIRIForStreamURL() *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI stream url", err)
|
log.Errorln("unable to parse local IRI stream url", err)
|
||||||
@@ -78,7 +84,9 @@ func MakeLocalIRIForStreamURL() *url.URL {
|
|||||||
|
|
||||||
// MakeLocalIRIforLogo will return a full IRI for the local server logo.
|
// MakeLocalIRIforLogo will return a full IRI for the local server logo.
|
||||||
func MakeLocalIRIforLogo() *url.URL {
|
func MakeLocalIRIforLogo() *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI stream url", err)
|
log.Errorln("unable to parse local IRI stream url", err)
|
||||||
@@ -93,7 +101,9 @@ func MakeLocalIRIforLogo() *url.URL {
|
|||||||
// GetLogoType will return the rel value for the webfinger response and
|
// GetLogoType will return the rel value for the webfinger response and
|
||||||
// the default static image is of type png.
|
// the default static image is of type png.
|
||||||
func GetLogoType() string {
|
func GetLogoType() string {
|
||||||
imageFilename := data.GetLogoPath()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
imageFilename := configRepository.GetLogoPath()
|
||||||
if imageFilename == "" {
|
if imageFilename == "" {
|
||||||
return "image/png"
|
return "image/png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActorHandler handles requests for a single actor.
|
// ActorHandler handles requests for a single actor.
|
||||||
func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,7 @@ func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
pathComponents := strings.Split(r.URL.Path, "/")
|
pathComponents := strings.Split(r.URL.Path, "/")
|
||||||
accountName := pathComponents[3]
|
accountName := pathComponents[3]
|
||||||
|
|
||||||
if _, valid := data.GetFederatedInboxMap()[accountName]; !valid {
|
if _, valid := configRepository.GetFederatedInboxMap()[accountName]; !valid {
|
||||||
// User is not valid
|
// User is not valid
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -145,7 +145,9 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createPageURL(r *http.Request, page *string) (*url.URL, error) {
|
func createPageURL(r *http.Request, page *string) (*url.URL, error) {
|
||||||
domain := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
domain := configRepository.GetServerURL()
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return nil, errors.New("unable to get server URL")
|
return nil, errors.New("unable to get server URL")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/inbox"
|
"github.com/owncast/owncast/activitypub/inbox"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -22,7 +22,9 @@ func InboxHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@ func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// The account this request is for must match the account name we have set
|
// The account this request is for must match the account name we have set
|
||||||
// for federation.
|
// for federation.
|
||||||
if forLocalAccount != data.GetFederationUsername() {
|
if forLocalAccount != configRepository.GetFederationUsername() {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,12 +25,14 @@ func NodeInfoController(w http.ResponseWriter, r *http.Request) {
|
|||||||
Links []links `json:"links"`
|
Links []links `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -89,7 +91,9 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
Metadata metadata `json:"metadata"`
|
Metadata metadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +121,7 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
OpenRegistrations: false,
|
OpenRegistrations: false,
|
||||||
Protocols: []string{"activitypub"},
|
Protocols: []string{"activitypub"},
|
||||||
Metadata: metadata{
|
Metadata: metadata{
|
||||||
ChatEnabled: !data.GetChatDisabled(),
|
ChatEnabled: !configRepository.GetChatDisabled(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +167,14 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
OpenRegistrations bool `json:"openRegistrations"`
|
OpenRegistrations bool `json:"openRegistrations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -178,7 +184,7 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res := &response{
|
res := &response{
|
||||||
Organization: Organization{
|
Organization: Organization{
|
||||||
Name: data.GetServerName(),
|
Name: configRepository.GetServerName(),
|
||||||
Contact: serverURL,
|
Contact: serverURL,
|
||||||
},
|
},
|
||||||
Server: Server{
|
Server: Server{
|
||||||
@@ -232,12 +238,14 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
InvitesEnabled bool `json:"invites_enabled"`
|
InvitesEnabled bool `json:"invites_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -254,9 +262,9 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res := response{
|
res := response{
|
||||||
URI: serverURL,
|
URI: serverURL,
|
||||||
Title: data.GetServerName(),
|
Title: configRepository.GetServerName(),
|
||||||
ShortDescription: data.GetServerSummary(),
|
ShortDescription: configRepository.GetServerSummary(),
|
||||||
Description: data.GetServerSummary(),
|
Description: configRepository.GetServerSummary(),
|
||||||
Version: config.GetReleaseString(),
|
Version: config.GetReleaseString(),
|
||||||
Stats: Stats{
|
Stats: Stats{
|
||||||
UserCount: 1,
|
UserCount: 1,
|
||||||
@@ -275,7 +283,9 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
||||||
accountName := data.GetDefaultFederationUsername()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
accountName := configRepository.GetDefaultFederationUsername()
|
||||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
||||||
publicKey := crypto.GetPublicKey(actorIRI)
|
publicKey := crypto.GetPublicKey(actorIRI)
|
||||||
|
|
||||||
@@ -284,7 +294,15 @@ func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
|||||||
|
|
||||||
// HostMetaController points to webfinger.
|
// HostMetaController points to webfinger.
|
||||||
func HostMetaController(w http.ResponseWriter, r *http.Request) {
|
func HostMetaController(w http.ResponseWriter, r *http.Request) {
|
||||||
serverURL := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
log.Debugln("host meta request rejected! Federation is not enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,31 +8,33 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectHandler handles requests for a single federated ActivityPub object.
|
// ObjectHandler handles requests for a single federated ActivityPub object.
|
||||||
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
|
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If private federation mode is enabled do not allow access to objects.
|
// If private federation mode is enabled do not allow access to objects.
|
||||||
if data.GetFederationIsPrivate() {
|
if configRepository.GetFederationIsPrivate() {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "")
|
iri := strings.Join([]string{strings.TrimSuffix(configRepository.GetServerURL(), "/"), r.URL.Path}, "")
|
||||||
object, _, _, err := persistence.GetObjectByIRI(iri)
|
object, _, _, err := persistence.GetObjectByIRI(iri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountName := data.GetDefaultFederationUsername()
|
accountName := configRepository.GetDefaultFederationUsername()
|
||||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
||||||
publicKey := crypto.GetPublicKey(actorIRI)
|
publicKey := crypto.GetPublicKey(actorIRI)
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebfingerHandler will handle webfinger lookup requests.
|
// WebfingerHandler will handle webfinger lookup requests.
|
||||||
func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
log.Debugln("webfinger request rejected! Federation is not enabled")
|
log.Debugln("webfinger request rejected! Federation is not enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceHostURL := data.GetServerURL()
|
instanceHostURL := configRepository.GetServerURL()
|
||||||
if instanceHostURL == "" {
|
if instanceHostURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Warnln("webfinger request rejected! Federation is enabled but server URL is empty.")
|
log.Warnln("webfinger request rejected! Federation is enabled but server URL is empty.")
|
||||||
@@ -29,7 +31,7 @@ func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
instanceHostString := utils.GetHostnameFromURLString(instanceHostURL)
|
instanceHostString := utils.GetHostnameFromURLString(instanceHostURL)
|
||||||
if instanceHostString == "" {
|
if instanceHostString == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Warnln("webfinger request rejected! Federation is enabled but server URL is not set properly. data.GetServerURL(): " + data.GetServerURL())
|
log.Warnln("webfinger request rejected! Federation is enabled but server URL is not set properly. data.GetServerURL(): " + configRepository.GetServerURL())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
host := userComponents[1]
|
host := userComponents[1]
|
||||||
user := userComponents[0]
|
user := userComponents[0]
|
||||||
|
|
||||||
if _, valid := data.GetFederatedInboxMap()[user]; !valid {
|
if _, valid := configRepository.GetFederatedInboxMap()[user]; !valid {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Debugln("webfinger request rejected! Invalid user: " + user)
|
log.Debugln("webfinger request rejected! Invalid user: " + user)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPublicKey will return the public key for the provided actor.
|
// GetPublicKey will return the public key for the provided actor.
|
||||||
func GetPublicKey(actorIRI *url.URL) PublicKey {
|
func GetPublicKey(actorIRI *url.URL) PublicKey {
|
||||||
key := data.GetPublicKey()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
key := configRepository.GetPublicKey()
|
||||||
idURL, err := url.Parse(actorIRI.String() + "#main-key")
|
idURL, err := url.Parse(actorIRI.String() + "#main-key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse actor iri string", idURL, err)
|
log.Errorln("unable to parse actor iri string", idURL, err)
|
||||||
@@ -29,7 +31,9 @@ func GetPublicKey(actorIRI *url.URL) PublicKey {
|
|||||||
|
|
||||||
// GetPrivateKey will return the internal server private key.
|
// GetPrivateKey will return the internal server private key.
|
||||||
func GetPrivateKey() *rsa.PrivateKey {
|
func GetPrivateKey() *rsa.PrivateKey {
|
||||||
key := data.GetPrivateKey()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
key := configRepository.GetPrivateKey()
|
||||||
|
|
||||||
block, _ := pem.Decode([]byte(key))
|
block, _ := pem.Decode([]byte(key))
|
||||||
if block == nil {
|
if block == nil {
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// Do nothing if displaying engagement actions has been turned off.
|
// Do nothing if displaying engagement actions has been turned off.
|
||||||
if !data.GetFederationShowEngagement() {
|
if !configRepository.GetFederationShowEngagement() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do nothing if chat is disabled
|
// Do nothing if chat is disabled
|
||||||
if data.GetChatDisabled() {
|
if configRepository.GetChatDisabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +38,11 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
|||||||
if isLiveNotification && action == events.FediverseEngagementLike {
|
if isLiveNotification && action == events.FediverseEngagementLike {
|
||||||
suffix = "liked that this stream went live."
|
suffix = "liked that this stream went live."
|
||||||
} else if action == events.FediverseEngagementLike {
|
} else if action == events.FediverseEngagementLike {
|
||||||
suffix = fmt.Sprintf("liked a post from %s.", data.GetServerName())
|
suffix = fmt.Sprintf("liked a post from %s.", configRepository.GetServerName())
|
||||||
} else if isLiveNotification && action == events.FediverseEngagementRepost {
|
} else if isLiveNotification && action == events.FediverseEngagementRepost {
|
||||||
suffix = "shared this stream with their followers."
|
suffix = "shared this stream with their followers."
|
||||||
} else if action == events.FediverseEngagementRepost {
|
} else if action == events.FediverseEngagementRepost {
|
||||||
suffix = fmt.Sprintf("shared a post from %s.", data.GetServerName())
|
suffix = fmt.Sprintf("shared a post from %s.", configRepository.GetServerName())
|
||||||
} else if action == events.FediverseEngagementFollow {
|
} else if action == events.FediverseEngagementFollow {
|
||||||
suffix = "followed this stream."
|
suffix = "followed this stream."
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error {
|
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
follow, err := resolvers.MakeFollowRequest(c, activity)
|
follow, err := resolvers.MakeFollowRequest(c, activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to create follow inbox request", err)
|
log.Errorln("unable to create follow inbox request", err)
|
||||||
@@ -27,7 +29,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
|||||||
return fmt.Errorf("unable to handle request")
|
return fmt.Errorf("unable to handle request")
|
||||||
}
|
}
|
||||||
|
|
||||||
approved := !data.GetFederationIsPrivate()
|
approved := !configRepository.GetFederationIsPrivate()
|
||||||
|
|
||||||
followRequest := *follow
|
followRequest := *follow
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
localAccountName := data.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
|
||||||
if approved {
|
if approved {
|
||||||
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -131,7 +131,9 @@ func Verify(request *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isBlockedDomain(domain string) bool {
|
func isBlockedDomain(domain string) bool {
|
||||||
blockedDomains := data.GetBlockedFederatedDomains()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
blockedDomains := configRepository.GetBlockedFederatedDomains()
|
||||||
|
|
||||||
for _, blockedDomain := range blockedDomains {
|
for _, blockedDomain := range blockedDomains {
|
||||||
if strings.Contains(domain, blockedDomain) {
|
if strings.Contains(domain, blockedDomain) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeFakePerson() vocab.ActivityStreamsPerson {
|
func makeFakePerson() vocab.ActivityStreamsPerson {
|
||||||
@@ -49,21 +50,24 @@ func makeFakePerson() vocab.ActivityStreamsPerson {
|
|||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
data.SetupPersistence(":memory:")
|
data.SetupPersistence(":memory:")
|
||||||
data.SetServerURL("https://my.cool.site.biz")
|
configRepository := configrepository.Get()
|
||||||
|
configRepository.SetServerURL("https://my.cool.site.biz")
|
||||||
persistence.Setup(data.GetDatastore())
|
persistence.Setup(data.GetDatastore())
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlockedDomains(t *testing.T) {
|
func TestBlockedDomains(t *testing.T) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
person := makeFakePerson()
|
person := makeFakePerson()
|
||||||
|
|
||||||
data.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
|
configRepository.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
|
||||||
|
|
||||||
if len(data.GetBlockedFederatedDomains()) != 2 {
|
if len(configRepository.GetBlockedFederatedDomains()) != 2 {
|
||||||
t.Error("Blocked federated domains is not set correctly")
|
t.Error("Blocked federated domains is not set correctly")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range data.GetBlockedFederatedDomains() {
|
for _, domain := range configRepository.GetBlockedFederatedDomains() {
|
||||||
if domain == person.GetJSONLDId().GetIRI().Host {
|
if domain == person.GetJSONLDId().GetIRI().Host {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/activitypub/webfinger"
|
"github.com/owncast/owncast/activitypub/webfinger"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
@@ -27,7 +27,9 @@ import (
|
|||||||
|
|
||||||
// SendLive will send all followers the message saying you started a live stream.
|
// SendLive will send all followers the message saying you started a live stream.
|
||||||
func SendLive() error {
|
func SendLive() error {
|
||||||
textContent := data.GetFederationGoLiveMessage()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
textContent := configRepository.GetFederationGoLiveMessage()
|
||||||
|
|
||||||
// If the message is empty then do not send it.
|
// If the message is empty then do not send it.
|
||||||
if textContent == "" {
|
if textContent == "" {
|
||||||
@@ -38,7 +40,7 @@ func SendLive() error {
|
|||||||
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
|
||||||
tagProp := streams.NewActivityStreamsTagProperty()
|
tagProp := streams.NewActivityStreamsTagProperty()
|
||||||
for _, tagString := range data.GetServerMetadataTags() {
|
for _, tagString := range configRepository.GetServerMetadataTags() {
|
||||||
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
|
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
|
||||||
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
|
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
|
||||||
tagProp.AppendTootHashtag(hashtag)
|
tagProp.AppendTootHashtag(hashtag)
|
||||||
@@ -57,15 +59,15 @@ func SendLive() error {
|
|||||||
tagsString := strings.Join(tagStrings, " ")
|
tagsString := strings.Join(tagStrings, " ")
|
||||||
|
|
||||||
var streamTitle string
|
var streamTitle string
|
||||||
if title := data.GetStreamTitle(); title != "" {
|
if title := configRepository.GetStreamTitle(); title != "" {
|
||||||
streamTitle = fmt.Sprintf("<p>%s</p>", title)
|
streamTitle = fmt.Sprintf("<p>%s</p>", title)
|
||||||
}
|
}
|
||||||
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
|
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, configRepository.GetServerURL(), configRepository.GetServerURL())
|
||||||
|
|
||||||
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
||||||
|
|
||||||
// To the public if we're not treating ActivityPub as "private".
|
// To the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
note = apmodels.MakeNotePublic(note)
|
note = apmodels.MakeNotePublic(note)
|
||||||
activity = apmodels.MakeActivityPublic(activity)
|
activity = apmodels.MakeActivityPublic(activity)
|
||||||
}
|
}
|
||||||
@@ -73,7 +75,7 @@ func SendLive() error {
|
|||||||
note.SetActivityStreamsTag(tagProp)
|
note.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
// Attach an image along with the Federated message.
|
// Attach an image along with the Federated message.
|
||||||
previewURL, err := url.Parse(data.GetServerURL())
|
previewURL, err := url.Parse(configRepository.GetServerURL())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var imageToAttach string
|
var imageToAttach string
|
||||||
var mediaType string
|
var mediaType string
|
||||||
@@ -94,7 +96,7 @@ func SendLive() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.GetNSFW() {
|
if configRepository.GetNSFW() {
|
||||||
// Mark content as sensitive.
|
// Mark content as sensitive.
|
||||||
sensitive := streams.NewActivityStreamsSensitiveProperty()
|
sensitive := streams.NewActivityStreamsSensitiveProperty()
|
||||||
sensitive.AppendXMLSchemaBoolean(true)
|
sensitive.AppendXMLSchemaBoolean(true)
|
||||||
@@ -151,6 +153,8 @@ func SendDirectMessageToAccount(textContent, account string) error {
|
|||||||
|
|
||||||
// SendPublicMessage will send a public message to all followers.
|
// SendPublicMessage will send a public message to all followers.
|
||||||
func SendPublicMessage(textContent string) error {
|
func SendPublicMessage(textContent string) error {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
originalContent := textContent
|
originalContent := textContent
|
||||||
textContent = utils.RenderSimpleMarkdown(textContent)
|
textContent = utils.RenderSimpleMarkdown(textContent)
|
||||||
|
|
||||||
@@ -173,7 +177,7 @@ func SendPublicMessage(textContent string) error {
|
|||||||
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
||||||
note.SetActivityStreamsTag(tagProp)
|
note.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
note = apmodels.MakeNotePublic(note)
|
note = apmodels.MakeNotePublic(note)
|
||||||
activity = apmodels.MakeActivityPublic(activity)
|
activity = apmodels.MakeActivityPublic(activity)
|
||||||
}
|
}
|
||||||
@@ -197,7 +201,8 @@ func SendPublicMessage(textContent string) error {
|
|||||||
|
|
||||||
// nolint: unparam
|
// nolint: unparam
|
||||||
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
|
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
configRepository := configrepository.Get()
|
||||||
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
noteID := shortid.MustGenerate()
|
noteID := shortid.MustGenerate()
|
||||||
noteIRI := apmodels.MakeLocalIRIForResource(noteID)
|
noteIRI := apmodels.MakeLocalIRIForResource(noteID)
|
||||||
id := shortid.MustGenerate()
|
id := shortid.MustGenerate()
|
||||||
@@ -218,7 +223,8 @@ func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
|
|||||||
|
|
||||||
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
||||||
func SendToFollowers(payload []byte) error {
|
func SendToFollowers(payload []byte) error {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
configRepository := configrepository.Get()
|
||||||
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
followers, _, err := persistence.GetFederationFollowers(-1, 0)
|
followers, _, err := persistence.GetFederationFollowers(-1, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,7 +247,8 @@ func SendToFollowers(payload []byte) error {
|
|||||||
|
|
||||||
// SendToUser will send a payload to a single specific inbox.
|
// SendToUser will send a payload to a single specific inbox.
|
||||||
func SendToUser(inbox *url.URL, payload []byte) error {
|
func SendToUser(inbox *url.URL, payload []byte) error {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
configRepository := configrepository.Get()
|
||||||
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
|
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -255,8 +262,10 @@ func SendToUser(inbox *url.URL, payload []byte) error {
|
|||||||
|
|
||||||
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
||||||
func UpdateFollowersWithAccountUpdates() error {
|
func UpdateFollowersWithAccountUpdates() error {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// Don't do anything if federation is disabled.
|
// Don't do anything if federation is disabled.
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +274,7 @@ func UpdateFollowersWithAccountUpdates() error {
|
|||||||
activity := apmodels.MakeUpdateActivity(objectID)
|
activity := apmodels.MakeUpdateActivity(objectID)
|
||||||
|
|
||||||
actor := streams.NewActivityStreamsPerson()
|
actor := streams.NewActivityStreamsPerson()
|
||||||
actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
actorID := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
actorIDProperty := streams.NewJSONLDIdProperty()
|
actorIDProperty := streams.NewJSONLDIdProperty()
|
||||||
actorIDProperty.Set(actorID)
|
actorIDProperty.Set(actorID)
|
||||||
actor.SetJSONLDId(actorIDProperty)
|
actor.SetJSONLDId(actorIDProperty)
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
||||||
Limit: int32(limit),
|
Limit: limit,
|
||||||
Offset: int32(offset),
|
Offset: offset,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, e
|
|||||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
||||||
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
db.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)},
|
db.GetOutboxWithOffsetParams{Limit: limit, Offset: offset},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return collection, err
|
return collection, err
|
||||||
@@ -309,8 +309,8 @@ func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType s
|
|||||||
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
||||||
Limit: int32(limit),
|
Limit: limit,
|
||||||
Offset: int32(offset),
|
Offset: offset,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -47,11 +47,12 @@ func Resolve(c context.Context, data []byte, callbacks ...interface{}) error {
|
|||||||
|
|
||||||
// ResolveIRI will resolve an IRI ahd call the correct callback for the resolved type.
|
// ResolveIRI will resolve an IRI ahd call the correct callback for the resolved type.
|
||||||
func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
|
func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
log.Debugln("Resolving", iri)
|
log.Debugln("Resolving", iri)
|
||||||
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, iri, nil)
|
req, _ := http.NewRequest(http.MethodGet, iri, nil)
|
||||||
|
|
||||||
actor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
actor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
if err := crypto.SignRequest(req, nil, actor); err != nil {
|
if err := crypto.SignRequest(req, nil, actor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -47,6 +47,8 @@ func setupExpiredRequestPruner() {
|
|||||||
|
|
||||||
// StartAuthFlow will begin the IndieAuth flow by generating an auth request.
|
// StartAuthFlow will begin the IndieAuth flow by generating an auth request.
|
||||||
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
|
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// Limit the number of pending requests
|
// Limit the number of pending requests
|
||||||
if len(pendingAuthRequests) >= maxPendingRequests {
|
if len(pendingAuthRequests) >= maxPendingRequests {
|
||||||
return nil, errors.New("Please try again later. Too many pending requests.")
|
return nil, errors.New("Please try again later. Too many pending requests.")
|
||||||
@@ -68,7 +70,7 @@ func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL,
|
|||||||
return nil, errors.New("only servers secured with https are supported")
|
return nil, errors.New("only servers secured with https are supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return nil, errors.New("Owncast server URL must be set when using auth")
|
return nil, errors.New("Owncast server URL must be set when using auth")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
@@ -70,6 +70,8 @@ func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*S
|
|||||||
// CompleteServerAuth will verify that the values provided in the final step
|
// CompleteServerAuth will verify that the values provided in the final step
|
||||||
// of the IndieAuth flow are correct, and return some basic profile info.
|
// of the IndieAuth flow are correct, and return some basic profile info.
|
||||||
func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) {
|
func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
request, pending := pendingServerAuthRequests[code]
|
request, pending := pendingServerAuthRequests[code]
|
||||||
if !pending {
|
if !pending {
|
||||||
return nil, errors.New("no pending authentication request")
|
return nil, errors.New("no pending authentication request")
|
||||||
@@ -89,11 +91,11 @@ func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := ServerProfileResponse{
|
response := ServerProfileResponse{
|
||||||
Me: data.GetServerURL(),
|
Me: configRepository.GetServerURL(),
|
||||||
Profile: ServerProfile{
|
Profile: ServerProfile{
|
||||||
Name: data.GetServerName(),
|
Name: configRepository.GetServerName(),
|
||||||
URL: data.GetServerURL(),
|
URL: configRepository.GetServerURL(),
|
||||||
Photo: fmt.Sprintf("%s/%s", data.GetServerURL(), data.GetLogoPath()),
|
Photo: fmt.Sprintf("%s/%s", configRepository.GetServerURL(), configRepository.GetLogoPath()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
38
build/gen-api.sh
Executable file
38
build/gen-api.sh
Executable file
@@ -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 redocly &>/dev/null; then
|
||||||
|
echo "Please install \`redocly cli\` before running this script: npm install -g @redocly/cli"
|
||||||
|
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
|
||||||
|
npx redocly lint $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
|
||||||
@@ -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,82 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
|
||||||
"github.com/owncast/owncast/controllers"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ApproveFollower will approve a federated follow request.
|
|
||||||
func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requirePOST(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type approveFollowerRequest struct {
|
|
||||||
ActorIRI string `json:"actorIRI"`
|
|
||||||
Approved bool `json:"approved"`
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var approval approveFollowerRequest
|
|
||||||
if err := decoder.Decode(&approval); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, "unable to handle follower state with provided values")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if approval.Approved {
|
|
||||||
// Approve a follower
|
|
||||||
if err := persistence.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
localAccountName := data.GetDefaultFederationUsername()
|
|
||||||
|
|
||||||
followRequest, err := persistence.GetFollower(approval.ActorIRI)
|
|
||||||
if err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the approval to the follow requestor.
|
|
||||||
if err := requests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove/block a follower
|
|
||||||
if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers.WriteSimpleResponse(w, true, "follower updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPendingFollowRequests will return a list of pending follow requests.
|
|
||||||
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
|
||||||
requests, err := persistence.GetPendingFollowRequests()
|
|
||||||
if err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers.WriteResponse(w, requests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
|
||||||
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rejections, err := persistence.GetBlockedAndRejectedFollowers()
|
|
||||||
if err != nil {
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers.WriteResponse(w, rejections)
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/core/transcoder"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/router/middleware"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetServerConfig gets the config details of the server.
|
|
||||||
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
|
||||||
usernameBlocklist := data.GetForbiddenUsernameList()
|
|
||||||
usernameSuggestions := data.GetSuggestedUsernamesList()
|
|
||||||
|
|
||||||
videoQualityVariants := make([]models.StreamOutputVariant, 0)
|
|
||||||
for _, variant := range data.GetStreamOutputVariants() {
|
|
||||||
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
|
|
||||||
Name: variant.GetName(),
|
|
||||||
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
|
|
||||||
IsVideoPassthrough: variant.IsVideoPassthrough,
|
|
||||||
Framerate: variant.GetFramerate(),
|
|
||||||
VideoBitrate: variant.VideoBitrate,
|
|
||||||
AudioBitrate: variant.AudioBitrate,
|
|
||||||
CPUUsageLevel: variant.CPUUsageLevel,
|
|
||||||
ScaledWidth: variant.ScaledWidth,
|
|
||||||
ScaledHeight: variant.ScaledHeight,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
response := serverConfigAdminResponse{
|
|
||||||
InstanceDetails: webConfigResponse{
|
|
||||||
Name: data.GetServerName(),
|
|
||||||
Summary: data.GetServerSummary(),
|
|
||||||
Tags: data.GetServerMetadataTags(),
|
|
||||||
ExtraPageContent: data.GetExtraPageBodyContent(),
|
|
||||||
StreamTitle: data.GetStreamTitle(),
|
|
||||||
WelcomeMessage: data.GetServerWelcomeMessage(),
|
|
||||||
OfflineMessage: data.GetCustomOfflineMessage(),
|
|
||||||
Logo: data.GetLogoPath(),
|
|
||||||
SocialHandles: data.GetSocialHandles(),
|
|
||||||
NSFW: data.GetNSFW(),
|
|
||||||
CustomStyles: data.GetCustomStyles(),
|
|
||||||
CustomJavascript: data.GetCustomJavascript(),
|
|
||||||
AppearanceVariables: data.GetCustomColorVariableValues(),
|
|
||||||
},
|
|
||||||
FFmpegPath: ffmpeg,
|
|
||||||
AdminPassword: data.GetAdminPassword(),
|
|
||||||
StreamKeys: data.GetStreamKeys(),
|
|
||||||
StreamKeyOverridden: config.TemporaryStreamKey != "",
|
|
||||||
WebServerPort: config.WebServerPort,
|
|
||||||
WebServerIP: config.WebServerIP,
|
|
||||||
RTMPServerPort: data.GetRTMPPortNumber(),
|
|
||||||
ChatDisabled: data.GetChatDisabled(),
|
|
||||||
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(),
|
|
||||||
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
|
||||||
VideoServingEndpoint: data.GetVideoServingEndpoint(),
|
|
||||||
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
|
|
||||||
HideViewerCount: data.GetHideViewerCount(),
|
|
||||||
DisableSearchIndexing: data.GetDisableSearchIndexing(),
|
|
||||||
VideoSettings: videoSettings{
|
|
||||||
VideoQualityVariants: videoQualityVariants,
|
|
||||||
LatencyLevel: data.GetStreamLatencyLevel().Level,
|
|
||||||
},
|
|
||||||
YP: yp{
|
|
||||||
Enabled: data.GetDirectoryEnabled(),
|
|
||||||
InstanceURL: data.GetServerURL(),
|
|
||||||
},
|
|
||||||
S3: data.GetS3Config(),
|
|
||||||
ExternalActions: data.GetExternalActions(),
|
|
||||||
SupportedCodecs: transcoder.GetCodecs(ffmpeg),
|
|
||||||
VideoCodec: data.GetVideoCodec(),
|
|
||||||
ForbiddenUsernames: usernameBlocklist,
|
|
||||||
SuggestedUsernames: usernameSuggestions,
|
|
||||||
Federation: federationConfigResponse{
|
|
||||||
Enabled: data.GetFederationEnabled(),
|
|
||||||
IsPrivate: data.GetFederationIsPrivate(),
|
|
||||||
Username: data.GetFederationUsername(),
|
|
||||||
GoLiveMessage: data.GetFederationGoLiveMessage(),
|
|
||||||
ShowEngagement: data.GetFederationShowEngagement(),
|
|
||||||
BlockedDomains: data.GetBlockedFederatedDomains(),
|
|
||||||
},
|
|
||||||
Notifications: notificationsConfigResponse{
|
|
||||||
Discord: data.GetDiscordConfig(),
|
|
||||||
Browser: data.GetBrowserPushConfig(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
middleware.DisableCache(w)
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type serverConfigAdminResponse struct {
|
|
||||||
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
|
||||||
Notifications notificationsConfigResponse `json:"notifications"`
|
|
||||||
YP yp `json:"yp"`
|
|
||||||
FFmpegPath string `json:"ffmpegPath"`
|
|
||||||
AdminPassword string `json:"adminPassword"`
|
|
||||||
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
|
||||||
WebServerIP string `json:"webServerIP"`
|
|
||||||
VideoCodec string `json:"videoCodec"`
|
|
||||||
VideoServingEndpoint string `json:"videoServingEndpoint"`
|
|
||||||
S3 models.S3 `json:"s3"`
|
|
||||||
Federation federationConfigResponse `json:"federation"`
|
|
||||||
SupportedCodecs []string `json:"supportedCodecs"`
|
|
||||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
|
||||||
ForbiddenUsernames []string `json:"forbiddenUsernames"`
|
|
||||||
SuggestedUsernames []string `json:"suggestedUsernames"`
|
|
||||||
StreamKeys []models.StreamKey `json:"streamKeys"`
|
|
||||||
VideoSettings videoSettings `json:"videoSettings"`
|
|
||||||
RTMPServerPort int `json:"rtmpServerPort"`
|
|
||||||
WebServerPort int `json:"webServerPort"`
|
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
|
||||||
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
|
|
||||||
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
|
|
||||||
DisableSearchIndexing bool `json:"disableSearchIndexing"`
|
|
||||||
StreamKeyOverridden bool `json:"streamKeyOverridden"`
|
|
||||||
HideViewerCount bool `json:"hideViewerCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type videoSettings struct {
|
|
||||||
VideoQualityVariants []models.StreamOutputVariant `json:"videoQualityVariants"`
|
|
||||||
LatencyLevel int `json:"latencyLevel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type webConfigResponse struct {
|
|
||||||
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
WelcomeMessage string `json:"welcomeMessage"`
|
|
||||||
OfflineMessage string `json:"offlineMessage"`
|
|
||||||
Logo string `json:"logo"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
ExtraPageContent string `json:"extraPageContent"`
|
|
||||||
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
|
|
||||||
CustomStyles string `json:"customStyles"`
|
|
||||||
CustomJavascript string `json:"customJavascript"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
|
||||||
NSFW bool `json:"nsfw"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type yp struct {
|
|
||||||
InstanceURL string `json:"instanceUrl"` // The public URL the directory should link to
|
|
||||||
YPServiceURL string `json:"-"` // The base URL to the YP API to register with (optional)
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type federationConfigResponse struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
GoLiveMessage string `json:"goLiveMessage"`
|
|
||||||
BlockedDomains []string `json:"blockedDomains"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
IsPrivate bool `json:"isPrivate"`
|
|
||||||
ShowEngagement bool `json:"showEngagement"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type notificationsConfigResponse struct {
|
|
||||||
Browser models.BrowserNotificationConfiguration `json:"browser"`
|
|
||||||
Discord models.DiscordConfiguration `json:"discord"`
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/controllers"
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResetYPRegistration will clear the YP protocol registration key.
|
|
||||||
func ResetYPRegistration(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Traceln("Resetting YP registration key")
|
|
||||||
if err := data.SetDirectoryRegistrationKey(""); err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
controllers.WriteSimpleResponse(w, true, "reset")
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,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/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -23,6 +23,8 @@ var (
|
|||||||
func Start(getStatusFunc func() models.Status) error {
|
func Start(getStatusFunc func() models.Status) error {
|
||||||
setupPersistence()
|
setupPersistence()
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
getStatus = getStatusFunc
|
getStatus = getStatusFunc
|
||||||
_server = NewChat()
|
_server = NewChat()
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ func Start(getStatusFunc func() models.Status) error {
|
|||||||
Help: "The number of chat messages incremented over time.",
|
Help: "The number of chat messages incremented over time.",
|
||||||
ConstLabels: map[string]string{
|
ConstLabels: map[string]string{
|
||||||
"version": config.VersionNumber,
|
"version": config.VersionNumber,
|
||||||
"host": data.GetServerURL(),
|
"host": configRepository.GetServerURL(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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/models"
|
||||||
"github.com/owncast/owncast/geoip"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
"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,14 @@ func (c *Client) readPump() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this message passes the optional language filter
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
if configRepository.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 +211,15 @@ func (c *Client) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) passesRateLimit() bool {
|
func (c *Client) passesRateLimit() bool {
|
||||||
return c.User.IsModerator() || (c.rateLimiter.Allow() && !c.inTimeout)
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
// If spam rate limiting is disabled, or the user is a moderator, always
|
||||||
|
// allow the message.
|
||||||
|
if !configRepository.GetChatSpamProtectionEnabled() || c.User.IsModerator() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (c.rateLimiter.Allow() && !c.inTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startChatRejectionTimeout() {
|
func (c *Client) startChatRejectionTimeout() {
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setSystemConcurrentConnectionLimit(limit int64) {
|
func setSystemConcurrentConnectionLimit(limit uint64) {
|
||||||
var rLimit syscall.Rlimit
|
var rLimit syscall.Rlimit
|
||||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
originalLimit := rLimit.Cur
|
originalLimit := rLimit.Cur
|
||||||
rLimit.Cur = uint64(limit)
|
rLimit.Cur = limit
|
||||||
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setSystemConcurrentConnectionLimit(limit int64) {
|
func setSystemConcurrentConnectionLimit(limit uint64) {
|
||||||
var rLimit syscall.Rlimit
|
var rLimit syscall.Rlimit
|
||||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
|
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
func setSystemConcurrentConnectionLimit(limit int64) {}
|
func setSystemConcurrentConnectionLimit(limit uint64) {}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ 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/user"
|
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
@@ -22,10 +22,12 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
proposedUsername := receivedEvent.NewName
|
proposedUsername := receivedEvent.NewName
|
||||||
|
|
||||||
// Check if name is on the blocklist
|
// Check if name is on the blocklist
|
||||||
blocklist := data.GetForbiddenUsernameList()
|
blocklist := configRepository.GetForbiddenUsernameList()
|
||||||
|
|
||||||
// Names have a max length
|
// Names have a max length
|
||||||
proposedUsername = utils.MakeSafeStringOfLength(proposedUsername, config.MaxChatDisplayNameLength)
|
proposedUsername = utils.MakeSafeStringOfLength(proposedUsername, config.MaxChatDisplayNameLength)
|
||||||
@@ -46,12 +48,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 +64,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 +74,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 +107,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 +122,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 +132,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 +156,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import (
|
|||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
"github.com/yuin/goldmark/renderer/html"
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
"mvdan.cc/xurls"
|
"mvdan.cc/xurls/v2"
|
||||||
|
|
||||||
"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.
|
||||||
@@ -220,7 +220,7 @@ func RenderMarkdown(raw string) string {
|
|||||||
[]byte("https:"),
|
[]byte("https:"),
|
||||||
}),
|
}),
|
||||||
extension.WithLinkifyURLRegexp(
|
extension.WithLinkifyURLRegexp(
|
||||||
xurls.Strict,
|
xurls.Strict(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
emoji.New(
|
emoji.New(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/owncast/owncast/core/data"
|
import (
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
)
|
||||||
|
|
||||||
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
|
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
|
||||||
type FediverseEngagementEvent struct {
|
type FediverseEngagementEvent struct {
|
||||||
@@ -13,6 +15,8 @@ type FediverseEngagementEvent struct {
|
|||||||
|
|
||||||
// GetBroadcastPayload will return the object to send to all chat users.
|
// GetBroadcastPayload will return the object to send to all chat users.
|
||||||
func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
return EventPayload{
|
return EventPayload{
|
||||||
"id": e.ID,
|
"id": e.ID,
|
||||||
"timestamp": e.Timestamp,
|
"timestamp": e.Timestamp,
|
||||||
@@ -22,7 +26,7 @@ func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
|||||||
"title": e.UserAccountName,
|
"title": e.UserAccountName,
|
||||||
"link": e.Link,
|
"link": e.Link,
|
||||||
"user": EventPayload{
|
"user": EventPayload{
|
||||||
"displayName": data.GetServerName(),
|
"displayName": configRepository.GetServerName(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/owncast/owncast/core/data"
|
import (
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
)
|
||||||
|
|
||||||
// SystemMessageEvent is a message displayed in chat on behalf of the server.
|
// SystemMessageEvent is a message displayed in chat on behalf of the server.
|
||||||
type SystemMessageEvent struct {
|
type SystemMessageEvent struct {
|
||||||
@@ -10,13 +12,15 @@ type SystemMessageEvent struct {
|
|||||||
|
|
||||||
// GetBroadcastPayload will return the object to send to all chat users.
|
// GetBroadcastPayload will return the object to send to all chat users.
|
||||||
func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload {
|
func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
return EventPayload{
|
return EventPayload{
|
||||||
"id": e.ID,
|
"id": e.ID,
|
||||||
"timestamp": e.Timestamp,
|
"timestamp": e.Timestamp,
|
||||||
"body": e.Body,
|
"body": e.Body,
|
||||||
"type": SystemMessageSent,
|
"type": SystemMessageSent,
|
||||||
"user": EventPayload{
|
"user": EventPayload{
|
||||||
"displayName": data.GetServerName(),
|
"displayName": configRepository.GetServerName(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
core/chat/messageFilter.go
Normal file
18
core/chat/messageFilter.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
39
core/chat/messageFilter_test.go
Normal file
39
core/chat/messageFilter_test.go
Normal file
@@ -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,10 @@ 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/authrepository"
|
||||||
|
"github.com/owncast/owncast/persistence/tables"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,8 +24,10 @@ const (
|
|||||||
|
|
||||||
func setupPersistence() {
|
func setupPersistence() {
|
||||||
_datastore = data.GetDatastore()
|
_datastore = data.GetDatastore()
|
||||||
data.CreateMessagesTable(_datastore.DB)
|
tables.CreateMessagesTable(_datastore.DB)
|
||||||
data.CreateBanIPTable(_datastore.DB)
|
|
||||||
|
authRepository := authrepository.Get()
|
||||||
|
authRepository.CreateBanIPTable(_datastore.DB)
|
||||||
|
|
||||||
chatDataPruner := time.NewTicker(5 * time.Minute)
|
chatDataPruner := time.NewTicker(5 * time.Minute)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -104,7 +108,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,
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ 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/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/authrepository"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
"github.com/owncast/owncast/persistence/userrepository"
|
||||||
|
"github.com/owncast/owncast/services/geoip"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ type Server struct {
|
|||||||
// a map of user IDs and timers that fire for chat part messages.
|
// a map of user IDs and timers that fire for chat part messages.
|
||||||
userPartedTimers map[string]*time.Ticker
|
userPartedTimers map[string]*time.Ticker
|
||||||
seq uint
|
seq uint
|
||||||
maxSocketConnectionLimit int64
|
maxSocketConnectionLimit uint64
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -82,7 +84,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,
|
||||||
@@ -94,7 +96,9 @@ func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken st
|
|||||||
ConnectedAt: time.Now(),
|
ConnectedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldSendJoinedMessages := data.GetChatJoinPartMessagesEnabled()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
shouldSendJoinedMessages := configRepository.GetChatJoinPartMessagesEnabled()
|
||||||
|
|
||||||
// If there are existing clients connected for this user do not send
|
// If there are existing clients connected for this user do not send
|
||||||
// a user joined message. Do not put this under a mutex, as
|
// a user joined message. Do not put this under a mutex, as
|
||||||
@@ -185,8 +189,10 @@ func (s *Server) sendUserPartedMessage(c *Client) {
|
|||||||
userPartEvent.User = c.User
|
userPartEvent.User = c.User
|
||||||
userPartEvent.ClientID = c.Id
|
userPartEvent.ClientID = c.Id
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// If part messages are disabled.
|
// If part messages are disabled.
|
||||||
if data.GetChatJoinPartMessagesEnabled() {
|
if configRepository.GetChatJoinPartMessagesEnabled() {
|
||||||
if err := s.Broadcast(userPartEvent.GetBroadcastPayload()); err != nil {
|
if err := s.Broadcast(userPartEvent.GetBroadcastPayload()); err != nil {
|
||||||
log.Errorln("error sending chat part message", err)
|
log.Errorln("error sending chat part message", err)
|
||||||
}
|
}
|
||||||
@@ -197,14 +203,17 @@ func (s *Server) sendUserPartedMessage(c *Client) {
|
|||||||
|
|
||||||
// HandleClientConnection is fired when a single client connects to the websocket.
|
// HandleClientConnection is fired when a single client connects to the websocket.
|
||||||
func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
if data.GetChatDisabled() {
|
configRepository := configrepository.Get()
|
||||||
|
authRepository := authrepository.Get()
|
||||||
|
|
||||||
|
if configRepository.GetChatDisabled() {
|
||||||
_, _ = w.Write([]byte(events.ChatDisabled))
|
_, _ = w.Write([]byte(events.ChatDisabled))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddress := utils.GetIPAddressFromRequest(r)
|
ipAddress := utils.GetIPAddressFromRequest(r)
|
||||||
// Check if this client's IP address is banned. If so send a rejection.
|
// Check if this client's IP address is banned. If so send a rejection.
|
||||||
if blocked, err := data.IsIPAddressBanned(ipAddress); blocked {
|
if blocked, err := authRepository.IsIPAddressBanned(ipAddress); blocked {
|
||||||
log.Debugln("Client ip address has been blocked. Rejecting.")
|
log.Debugln("Client ip address has been blocked. Rejecting.")
|
||||||
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
@@ -214,7 +223,7 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Limit concurrent chat connections
|
// Limit concurrent chat connections
|
||||||
if int64(len(s.clients)) >= s.maxSocketConnectionLimit {
|
if uint64(len(s.clients)) >= s.maxSocketConnectionLimit {
|
||||||
log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxSocketConnectionLimit)
|
log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxSocketConnectionLimit)
|
||||||
_, _ = w.Write([]byte(events.ErrorMaxConnectionsExceeded))
|
_, _ = w.Write([]byte(events.ErrorMaxConnectionsExceeded))
|
||||||
return
|
return
|
||||||
@@ -239,8 +248,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 +347,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")
|
||||||
}
|
}
|
||||||
@@ -371,12 +385,14 @@ func SendActionToUser(userID string, text string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) eventReceived(event chatClientEvent) {
|
func (s *Server) eventReceived(event chatClientEvent) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
c := event.client
|
c := event.client
|
||||||
u := c.User
|
u := c.User
|
||||||
|
|
||||||
// If established chat user only mode is enabled and the user is not old
|
// If established chat user only mode is enabled and the user is not old
|
||||||
// enough then reject this event and send them an informative message.
|
// enough then reject this event and send them an informative message.
|
||||||
if u != nil && data.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() {
|
if u != nil && configRepository.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() {
|
||||||
s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.")
|
s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -403,10 +419,12 @@ func (s *Server) eventReceived(event chatClientEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendWelcomeMessageToClient(c *Client) {
|
func (s *Server) sendWelcomeMessageToClient(c *Client) {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// Add an artificial delay so people notice this message come in.
|
// Add an artificial delay so people notice this message come in.
|
||||||
time.Sleep(7 * time.Second)
|
time.Sleep(7 * time.Second)
|
||||||
|
|
||||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage())
|
welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage())
|
||||||
|
|
||||||
if welcomeMessage != "" {
|
if welcomeMessage != "" {
|
||||||
s.sendSystemMessageToClient(c, welcomeMessage)
|
s.sendSystemMessageToClient(c, welcomeMessage)
|
||||||
@@ -414,7 +432,9 @@ func (s *Server) sendWelcomeMessageToClient(c *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendAllWelcomeMessage() {
|
func (s *Server) sendAllWelcomeMessage() {
|
||||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage())
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage())
|
||||||
|
|
||||||
if welcomeMessage != "" {
|
if welcomeMessage != "" {
|
||||||
clientMessage := events.SystemMessageEvent{
|
clientMessage := events.SystemMessageEvent{
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getMaximumConcurrentConnectionLimit() int64 {
|
func getMaximumConcurrentConnectionLimit() uint64 {
|
||||||
var rLimit syscall.Rlimit
|
var rLimit syscall.Rlimit
|
||||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
|
// Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
|
||||||
proposedLimit := int64(float32(rLimit.Max) * 0.7)
|
proposedLimit := uint64(float32(rLimit.Max) * 0.7)
|
||||||
|
|
||||||
return proposedLimit
|
return proposedLimit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
func getMaximumConcurrentConnectionLimit() int64 {
|
func getMaximumConcurrentConnectionLimit() uint64 {
|
||||||
// The maximum limit I can find for windows is 16,777,216
|
// The maximum limit I can find for windows is 16,777,216
|
||||||
// (essentially unlimited, but add the 0.7 multiplier as well to be
|
// (essentially unlimited, but add the 0.7 multiplier as well to be
|
||||||
// consistent with other systems)
|
// consistent with other systems)
|
||||||
|
|||||||
21
core/core.go
21
core/core.go
@@ -13,10 +13,11 @@ 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/configrepository"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
@@ -34,10 +35,10 @@ var (
|
|||||||
// Start starts up the core processing.
|
// Start starts up the core processing.
|
||||||
func Start() error {
|
func Start() error {
|
||||||
resetDirectories()
|
resetDirectories()
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
// configRepository.PopulateDefaults()
|
||||||
|
|
||||||
data.PopulateDefaults()
|
if err := configRepository.VerifySettings(); err != nil {
|
||||||
|
|
||||||
if err := data.VerifySettings(); err != nil {
|
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,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)
|
||||||
@@ -75,7 +76,7 @@ func Start() error {
|
|||||||
// start the rtmp server
|
// start the rtmp server
|
||||||
go rtmp.Start(setStreamAsConnected, setBroadcaster)
|
go rtmp.Start(setStreamAsConnected, setBroadcaster)
|
||||||
|
|
||||||
rtmpPort := data.GetRTMPPortNumber()
|
rtmpPort := configRepository.GetRTMPPortNumber()
|
||||||
if rtmpPort != 1935 {
|
if rtmpPort != 1935 {
|
||||||
log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort)
|
log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort)
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,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)
|
||||||
}
|
}
|
||||||
@@ -113,7 +114,8 @@ func transitionToOfflineVideoStreamContent() {
|
|||||||
go _transcoder.Start(false)
|
go _transcoder.Start(false)
|
||||||
|
|
||||||
// Copy the logo to be the thumbnail
|
// Copy the logo to be the thumbnail
|
||||||
logo := data.GetLogoPath()
|
configRepository := configrepository.Get()
|
||||||
|
logo := configRepository.GetLogoPath()
|
||||||
dst := filepath.Join(config.TempDir, "thumbnail.jpg")
|
dst := filepath.Join(config.TempDir, "thumbnail.jpg")
|
||||||
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
|
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
|
||||||
log.Warnln(err)
|
log.Warnln(err)
|
||||||
@@ -130,7 +132,8 @@ func resetDirectories() {
|
|||||||
utils.CleanupDirectory(config.HLSStoragePath)
|
utils.CleanupDirectory(config.HLSStoragePath)
|
||||||
|
|
||||||
// Remove the previous thumbnail
|
// Remove the previous thumbnail
|
||||||
logo := data.GetLogoPath()
|
configRepository := configrepository.Get()
|
||||||
|
logo := configRepository.GetLogoPath()
|
||||||
if utils.DoesFileExists(logo) {
|
if utils.DoesFileExists(logo) {
|
||||||
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
|
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
// GetFederatedInboxMap is a mapping between account names and their outbox.
|
|
||||||
func GetFederatedInboxMap() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
GetDefaultFederationUsername(): GetDefaultFederationUsername(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefaultFederationUsername will return the username used for sending federation activities.
|
|
||||||
func GetDefaultFederationUsername() string {
|
|
||||||
return GetFederationUsername()
|
|
||||||
}
|
|
||||||
@@ -1,991 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/static"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
extraContentKey = "extra_page_content"
|
|
||||||
streamTitleKey = "stream_title"
|
|
||||||
adminPasswordKey = "admin_password_key"
|
|
||||||
logoPathKey = "logo_path"
|
|
||||||
logoUniquenessKey = "logo_uniqueness"
|
|
||||||
serverSummaryKey = "server_summary"
|
|
||||||
serverWelcomeMessageKey = "server_welcome_message"
|
|
||||||
serverNameKey = "server_name"
|
|
||||||
serverURLKey = "server_url"
|
|
||||||
httpPortNumberKey = "http_port_number"
|
|
||||||
httpListenAddressKey = "http_listen_address"
|
|
||||||
websocketHostOverrideKey = "websocket_host_override"
|
|
||||||
rtmpPortNumberKey = "rtmp_port_number"
|
|
||||||
serverMetadataTagsKey = "server_metadata_tags"
|
|
||||||
directoryEnabledKey = "directory_enabled"
|
|
||||||
directoryRegistrationKeyKey = "directory_registration_key"
|
|
||||||
socialHandlesKey = "social_handles"
|
|
||||||
peakViewersSessionKey = "peak_viewers_session"
|
|
||||||
peakViewersOverallKey = "peak_viewers_overall"
|
|
||||||
lastDisconnectTimeKey = "last_disconnect_time"
|
|
||||||
ffmpegPathKey = "ffmpeg_path"
|
|
||||||
nsfwKey = "nsfw"
|
|
||||||
s3StorageConfigKey = "s3_storage_config"
|
|
||||||
videoLatencyLevel = "video_latency_level"
|
|
||||||
videoStreamOutputVariantsKey = "video_stream_output_variants"
|
|
||||||
chatDisabledKey = "chat_disabled"
|
|
||||||
externalActionsKey = "external_actions"
|
|
||||||
customStylesKey = "custom_styles"
|
|
||||||
customJavascriptKey = "custom_javascript"
|
|
||||||
videoCodecKey = "video_codec"
|
|
||||||
blockedUsernamesKey = "blocked_usernames"
|
|
||||||
publicKeyKey = "public_key"
|
|
||||||
privateKeyKey = "private_key"
|
|
||||||
serverInitDateKey = "server_init_date"
|
|
||||||
federationEnabledKey = "federation_enabled"
|
|
||||||
federationUsernameKey = "federation_username"
|
|
||||||
federationPrivateKey = "federation_private"
|
|
||||||
federationGoLiveMessageKey = "federation_go_live_message"
|
|
||||||
federationShowEngagementKey = "federation_show_engagement"
|
|
||||||
federationBlockedDomainsKey = "federation_blocked_domains"
|
|
||||||
suggestedUsernamesKey = "suggested_usernames"
|
|
||||||
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
|
|
||||||
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
|
|
||||||
notificationsEnabledKey = "notifications_enabled"
|
|
||||||
discordConfigurationKey = "discord_configuration"
|
|
||||||
browserPushConfigurationKey = "browser_push_configuration"
|
|
||||||
browserPushPublicKeyKey = "browser_push_public_key"
|
|
||||||
// nolint:gosec
|
|
||||||
browserPushPrivateKeyKey = "browser_push_private_key"
|
|
||||||
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
|
|
||||||
hideViewerCountKey = "hide_viewer_count"
|
|
||||||
customOfflineMessageKey = "custom_offline_message"
|
|
||||||
customColorVariableValuesKey = "custom_color_variable_values"
|
|
||||||
streamKeysKey = "stream_keys"
|
|
||||||
disableSearchIndexingKey = "disable_search_indexing"
|
|
||||||
videoServingEndpointKey = "video_serving_endpoint"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
|
||||||
func GetExtraPageBodyContent() string {
|
|
||||||
content, err := _datastore.GetString(extraContentKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(extraContentKey, err)
|
|
||||||
return config.GetDefaults().PageBodyContent
|
|
||||||
}
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExtraPageBodyContent will set the user-supplied body content.
|
|
||||||
func SetExtraPageBodyContent(content string) error {
|
|
||||||
return _datastore.SetString(extraContentKey, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStreamTitle will return the name of the current stream.
|
|
||||||
func GetStreamTitle() string {
|
|
||||||
title, err := _datastore.GetString(streamTitleKey)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return title
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStreamTitle will set the name of the current stream.
|
|
||||||
func SetStreamTitle(title string) error {
|
|
||||||
return _datastore.SetString(streamTitleKey, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAdminPassword will return the admin password.
|
|
||||||
func GetAdminPassword() string {
|
|
||||||
key, _ := _datastore.GetString(adminPasswordKey)
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAdminPassword will set the admin password.
|
|
||||||
func SetAdminPassword(key string) error {
|
|
||||||
return _datastore.SetString(adminPasswordKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogoPath will return the path for the logo, relative to webroot.
|
|
||||||
func GetLogoPath() string {
|
|
||||||
logo, err := _datastore.GetString(logoPathKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(logoPathKey, err)
|
|
||||||
return config.GetDefaults().Logo
|
|
||||||
}
|
|
||||||
|
|
||||||
if logo == "" {
|
|
||||||
return config.GetDefaults().Logo
|
|
||||||
}
|
|
||||||
|
|
||||||
return logo
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogoPath will set the path for the logo, relative to webroot.
|
|
||||||
func SetLogoPath(logo string) error {
|
|
||||||
return _datastore.SetString(logoPathKey, logo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogoUniquenessString will set the logo cache busting string.
|
|
||||||
func SetLogoUniquenessString(uniqueness string) error {
|
|
||||||
return _datastore.SetString(logoUniquenessKey, uniqueness)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogoUniquenessString will return the logo cache busting string.
|
|
||||||
func GetLogoUniquenessString() string {
|
|
||||||
uniqueness, err := _datastore.GetString(logoUniquenessKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(logoUniquenessKey, err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueness
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerSummary will return the server summary text.
|
|
||||||
func GetServerSummary() string {
|
|
||||||
summary, err := _datastore.GetString(serverSummaryKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(serverSummaryKey, err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerSummary will set the server summary text.
|
|
||||||
func SetServerSummary(summary string) error {
|
|
||||||
return _datastore.SetString(serverSummaryKey, summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerWelcomeMessage will return the server welcome message text.
|
|
||||||
func GetServerWelcomeMessage() string {
|
|
||||||
welcomeMessage, err := _datastore.GetString(serverWelcomeMessageKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(serverWelcomeMessageKey, err)
|
|
||||||
return config.GetDefaults().ServerWelcomeMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
return welcomeMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerWelcomeMessage will set the server welcome message text.
|
|
||||||
func SetServerWelcomeMessage(welcomeMessage string) error {
|
|
||||||
return _datastore.SetString(serverWelcomeMessageKey, welcomeMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerName will return the server name text.
|
|
||||||
func GetServerName() string {
|
|
||||||
name, err := _datastore.GetString(serverNameKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(serverNameKey, err)
|
|
||||||
return config.GetDefaults().Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerName will set the server name text.
|
|
||||||
func SetServerName(name string) error {
|
|
||||||
return _datastore.SetString(serverNameKey, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerURL will return the server URL.
|
|
||||||
func GetServerURL() string {
|
|
||||||
url, err := _datastore.GetString(serverURLKey)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerURL will set the server URL.
|
|
||||||
func SetServerURL(url string) error {
|
|
||||||
return _datastore.SetString(serverURLKey, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHTTPPortNumber will return the server HTTP port.
|
|
||||||
func GetHTTPPortNumber() int {
|
|
||||||
port, err := _datastore.GetNumber(httpPortNumberKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(httpPortNumberKey, err)
|
|
||||||
return config.GetDefaults().WebServerPort
|
|
||||||
}
|
|
||||||
|
|
||||||
if port == 0 {
|
|
||||||
return config.GetDefaults().WebServerPort
|
|
||||||
}
|
|
||||||
return int(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWebsocketOverrideHost will set the host override for websockets.
|
|
||||||
func SetWebsocketOverrideHost(host string) error {
|
|
||||||
return _datastore.SetString(websocketHostOverrideKey, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWebsocketOverrideHost will return the host override for websockets.
|
|
||||||
func GetWebsocketOverrideHost() string {
|
|
||||||
host, _ := _datastore.GetString(websocketHostOverrideKey)
|
|
||||||
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHTTPPortNumber will set the server HTTP port.
|
|
||||||
func SetHTTPPortNumber(port float64) error {
|
|
||||||
return _datastore.SetNumber(httpPortNumberKey, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHTTPListenAddress will return the HTTP listen address.
|
|
||||||
func GetHTTPListenAddress() string {
|
|
||||||
address, err := _datastore.GetString(httpListenAddressKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(httpListenAddressKey, err)
|
|
||||||
return config.GetDefaults().WebServerIP
|
|
||||||
}
|
|
||||||
return address
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHTTPListenAddress will set the server HTTP listen address.
|
|
||||||
func SetHTTPListenAddress(address string) error {
|
|
||||||
return _datastore.SetString(httpListenAddressKey, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRTMPPortNumber will return the server RTMP port.
|
|
||||||
func GetRTMPPortNumber() int {
|
|
||||||
port, err := _datastore.GetNumber(rtmpPortNumberKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(rtmpPortNumberKey, err)
|
|
||||||
return config.GetDefaults().RTMPServerPort
|
|
||||||
}
|
|
||||||
|
|
||||||
if port == 0 {
|
|
||||||
return config.GetDefaults().RTMPServerPort
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRTMPPortNumber will set the server RTMP port.
|
|
||||||
func SetRTMPPortNumber(port float64) error {
|
|
||||||
return _datastore.SetNumber(rtmpPortNumberKey, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerMetadataTags will return the metadata tags.
|
|
||||||
func GetServerMetadataTags() []string {
|
|
||||||
tagsString, err := _datastore.GetString(serverMetadataTagsKey)
|
|
||||||
if tagsString == "" {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(serverMetadataTagsKey, err)
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(tagsString, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerMetadataTags will return the metadata tags.
|
|
||||||
func SetServerMetadataTags(tags []string) error {
|
|
||||||
tagString := strings.Join(tags, ",")
|
|
||||||
return _datastore.SetString(serverMetadataTagsKey, tagString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDirectoryEnabled will return if this server should register to YP.
|
|
||||||
func GetDirectoryEnabled() bool {
|
|
||||||
enabled, err := _datastore.GetBool(directoryEnabledKey)
|
|
||||||
if err != nil {
|
|
||||||
return config.GetDefaults().YPEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDirectoryEnabled will set if this server should register to YP.
|
|
||||||
func SetDirectoryEnabled(enabled bool) error {
|
|
||||||
return _datastore.SetBool(directoryEnabledKey, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDirectoryRegistrationKey will set the YP protocol registration key.
|
|
||||||
func SetDirectoryRegistrationKey(key string) error {
|
|
||||||
return _datastore.SetString(directoryRegistrationKeyKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDirectoryRegistrationKey will return the YP protocol registration key.
|
|
||||||
func GetDirectoryRegistrationKey() string {
|
|
||||||
key, _ := _datastore.GetString(directoryRegistrationKeyKey)
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSocialHandles will return the external social links.
|
|
||||||
func GetSocialHandles() []models.SocialHandle {
|
|
||||||
var socialHandles []models.SocialHandle
|
|
||||||
|
|
||||||
configEntry, err := _datastore.Get(socialHandlesKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln(socialHandlesKey, err)
|
|
||||||
return socialHandles
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := configEntry.getObject(&socialHandles); err != nil {
|
|
||||||
log.Traceln(err)
|
|
||||||
return socialHandles
|
|
||||||
}
|
|
||||||
|
|
||||||
return socialHandles
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSocialHandles will set the external social links.
|
|
||||||
func SetSocialHandles(socialHandles []models.SocialHandle) error {
|
|
||||||
configEntry := ConfigEntry{Key: socialHandlesKey, Value: socialHandles}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeakSessionViewerCount will return the max number of viewers for this stream.
|
|
||||||
func GetPeakSessionViewerCount() int {
|
|
||||||
count, err := _datastore.GetNumber(peakViewersSessionKey)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPeakSessionViewerCount will set the max number of viewers for this stream.
|
|
||||||
func SetPeakSessionViewerCount(count int) error {
|
|
||||||
return _datastore.SetNumber(peakViewersSessionKey, float64(count))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeakOverallViewerCount will return the overall max number of viewers.
|
|
||||||
func GetPeakOverallViewerCount() int {
|
|
||||||
count, err := _datastore.GetNumber(peakViewersOverallKey)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPeakOverallViewerCount will set the overall max number of viewers.
|
|
||||||
func SetPeakOverallViewerCount(count int) error {
|
|
||||||
return _datastore.SetNumber(peakViewersOverallKey, float64(count))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastDisconnectTime will return the time the last stream ended.
|
|
||||||
func GetLastDisconnectTime() (*utils.NullTime, error) {
|
|
||||||
var disconnectTime utils.NullTime
|
|
||||||
|
|
||||||
configEntry, err := _datastore.Get(lastDisconnectTimeKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := configEntry.getObject(&disconnectTime); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !disconnectTime.Valid || disconnectTime.Time.IsZero() {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &disconnectTime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLastDisconnectTime will set the time the last stream ended.
|
|
||||||
func SetLastDisconnectTime(disconnectTime time.Time) error {
|
|
||||||
savedDisconnectTime := utils.NullTime{Time: disconnectTime, Valid: true}
|
|
||||||
configEntry := ConfigEntry{Key: lastDisconnectTimeKey, Value: savedDisconnectTime}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNSFW will set if this stream has NSFW content.
|
|
||||||
func SetNSFW(isNSFW bool) error {
|
|
||||||
return _datastore.SetBool(nsfwKey, isNSFW)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNSFW will return if this stream has NSFW content.
|
|
||||||
func GetNSFW() bool {
|
|
||||||
nsfw, err := _datastore.GetBool(nsfwKey)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return nsfw
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFfmpegPath will set the custom ffmpeg path.
|
|
||||||
func SetFfmpegPath(path string) error {
|
|
||||||
return _datastore.SetString(ffmpegPathKey, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFfMpegPath will return the ffmpeg path.
|
|
||||||
func GetFfMpegPath() string {
|
|
||||||
path, err := _datastore.GetString(ffmpegPathKey)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetS3Config will return the external storage configuration.
|
|
||||||
func GetS3Config() models.S3 {
|
|
||||||
configEntry, err := _datastore.Get(s3StorageConfigKey)
|
|
||||||
if err != nil {
|
|
||||||
return models.S3{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
var s3Config models.S3
|
|
||||||
if err := configEntry.getObject(&s3Config); err != nil {
|
|
||||||
return models.S3{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s3Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetS3Config will set the external storage configuration.
|
|
||||||
func SetS3Config(config models.S3) error {
|
|
||||||
configEntry := ConfigEntry{Key: s3StorageConfigKey, Value: config}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStreamLatencyLevel will return the stream latency level.
|
|
||||||
func GetStreamLatencyLevel() models.LatencyLevel {
|
|
||||||
level, err := _datastore.GetNumber(videoLatencyLevel)
|
|
||||||
if err != nil {
|
|
||||||
level = 2 // default
|
|
||||||
} else if level > 4 {
|
|
||||||
level = 4 // highest
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.GetLatencyLevel(int(level))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStreamLatencyLevel will set the stream latency level.
|
|
||||||
func SetStreamLatencyLevel(level float64) error {
|
|
||||||
return _datastore.SetNumber(videoLatencyLevel, level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStreamOutputVariants will return all of the stream output variants.
|
|
||||||
func GetStreamOutputVariants() []models.StreamOutputVariant {
|
|
||||||
configEntry, err := _datastore.Get(videoStreamOutputVariantsKey)
|
|
||||||
if err != nil {
|
|
||||||
return config.GetDefaults().StreamVariants
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamOutputVariants []models.StreamOutputVariant
|
|
||||||
if err := configEntry.getObject(&streamOutputVariants); err != nil {
|
|
||||||
return config.GetDefaults().StreamVariants
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(streamOutputVariants) == 0 {
|
|
||||||
return config.GetDefaults().StreamVariants
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamOutputVariants
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStreamOutputVariants will set the stream output variants.
|
|
||||||
func SetStreamOutputVariants(variants []models.StreamOutputVariant) error {
|
|
||||||
configEntry := ConfigEntry{Key: videoStreamOutputVariantsKey, Value: variants}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChatDisabled will disable chat if set to true.
|
|
||||||
func SetChatDisabled(disabled bool) error {
|
|
||||||
return _datastore.SetBool(chatDisabledKey, disabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChatDisabled will return if chat is disabled.
|
|
||||||
func GetChatDisabled() bool {
|
|
||||||
disabled, err := _datastore.GetBool(chatDisabledKey)
|
|
||||||
if err == nil {
|
|
||||||
return disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChatEstablishedUsersOnlyMode sets the state of established user only mode.
|
|
||||||
func SetChatEstablishedUsersOnlyMode(enabled bool) error {
|
|
||||||
return _datastore.SetBool(chatEstablishedUsersOnlyModeKey, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChatEstbalishedUsersOnlyMode returns the state of established user only mode.
|
|
||||||
func GetChatEstbalishedUsersOnlyMode() bool {
|
|
||||||
enabled, err := _datastore.GetBool(chatEstablishedUsersOnlyModeKey)
|
|
||||||
if err == nil {
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalActions will return the registered external actions.
|
|
||||||
func GetExternalActions() []models.ExternalAction {
|
|
||||||
configEntry, err := _datastore.Get(externalActionsKey)
|
|
||||||
if err != nil {
|
|
||||||
return []models.ExternalAction{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var externalActions []models.ExternalAction
|
|
||||||
if err := configEntry.getObject(&externalActions); err != nil {
|
|
||||||
return []models.ExternalAction{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return externalActions
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExternalActions will save external actions.
|
|
||||||
func SetExternalActions(actions []models.ExternalAction) error {
|
|
||||||
configEntry := ConfigEntry{Key: externalActionsKey, Value: actions}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCustomStyles will save a string with CSS to insert into the page.
|
|
||||||
func SetCustomStyles(styles string) error {
|
|
||||||
return _datastore.SetString(customStylesKey, styles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomStyles will return a string with CSS to insert into the page.
|
|
||||||
func GetCustomStyles() string {
|
|
||||||
style, err := _datastore.GetString(customStylesKey)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCustomJavascript will save a string with Javascript to insert into the page.
|
|
||||||
func SetCustomJavascript(styles string) error {
|
|
||||||
return _datastore.SetString(customJavascriptKey, styles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomJavascript will return a string with Javascript to insert into the page.
|
|
||||||
func GetCustomJavascript() string {
|
|
||||||
style, err := _datastore.GetString(customJavascriptKey)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetVideoCodec will set the codec used for video encoding.
|
|
||||||
func SetVideoCodec(codec string) error {
|
|
||||||
return _datastore.SetString(videoCodecKey, codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVideoCodec returns the codec to use for transcoding video.
|
|
||||||
func GetVideoCodec() string {
|
|
||||||
codec, err := _datastore.GetString(videoCodecKey)
|
|
||||||
if codec == "" || err != nil {
|
|
||||||
return "libx264" // Default value
|
|
||||||
}
|
|
||||||
|
|
||||||
return codec
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifySettings will perform a sanity check for specific settings values.
|
|
||||||
func VerifySettings() error {
|
|
||||||
if len(GetStreamKeys()) == 0 && config.TemporaryStreamKey == "" {
|
|
||||||
log.Errorln("No stream key set. Streaming is disabled. Please set one via the admin or command line arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if GetAdminPassword() == "" {
|
|
||||||
return errors.New("no admin password set. Please set one via the admin or command line arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
logoPath := GetLogoPath()
|
|
||||||
if !utils.DoesFileExists(filepath.Join(config.DataDirectory, logoPath)) {
|
|
||||||
log.Traceln(logoPath, "not found in the data directory. copying a default logo.")
|
|
||||||
logo := static.GetLogo()
|
|
||||||
if err := os.WriteFile(filepath.Join(config.DataDirectory, "logo.png"), logo, 0o600); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to write logo to disk")
|
|
||||||
}
|
|
||||||
if err := SetLogoPath("logo.png"); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to save logo filename")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindHighestVideoQualityIndex will return the highest quality from a slice of variants.
|
|
||||||
func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int, bool) {
|
|
||||||
type IndexedQuality struct {
|
|
||||||
quality models.StreamOutputVariant
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(qualities) < 2 {
|
|
||||||
return 0, qualities[0].IsVideoPassthrough
|
|
||||||
}
|
|
||||||
|
|
||||||
indexedQualities := make([]IndexedQuality, 0)
|
|
||||||
for index, quality := range qualities {
|
|
||||||
indexedQuality := IndexedQuality{quality, index}
|
|
||||||
indexedQualities = append(indexedQualities, indexedQuality)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(indexedQualities, func(a, b int) bool {
|
|
||||||
if indexedQualities[a].quality.IsVideoPassthrough && !indexedQualities[b].quality.IsVideoPassthrough {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !indexedQualities[a].quality.IsVideoPassthrough && indexedQualities[b].quality.IsVideoPassthrough {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
|
|
||||||
})
|
|
||||||
|
|
||||||
// nolint:gosec
|
|
||||||
selectedQuality := indexedQualities[0]
|
|
||||||
return selectedQuality.index, selectedQuality.quality.IsVideoPassthrough
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetForbiddenUsernameList will return the blocked usernames as a comma separated string.
|
|
||||||
func GetForbiddenUsernameList() []string {
|
|
||||||
usernames, err := _datastore.GetStringSlice(blockedUsernamesKey)
|
|
||||||
if err != nil {
|
|
||||||
return config.DefaultForbiddenUsernames
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(usernames) == 0 {
|
|
||||||
return config.DefaultForbiddenUsernames
|
|
||||||
}
|
|
||||||
|
|
||||||
return usernames
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetForbiddenUsernameList set the username blocklist as a comma separated string.
|
|
||||||
func SetForbiddenUsernameList(usernames []string) error {
|
|
||||||
return _datastore.SetStringSlice(blockedUsernamesKey, usernames)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSuggestedUsernamesList will return the suggested usernames.
|
|
||||||
// If the number of suggested usernames is smaller than 10, the number pool is
|
|
||||||
// not used (see code in the CreateAnonymousUser function).
|
|
||||||
func GetSuggestedUsernamesList() []string {
|
|
||||||
usernames, err := _datastore.GetStringSlice(suggestedUsernamesKey)
|
|
||||||
|
|
||||||
if err != nil || len(usernames) == 0 {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usernames
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSuggestedUsernamesList sets the username suggestion list.
|
|
||||||
func SetSuggestedUsernamesList(usernames []string) error {
|
|
||||||
return _datastore.SetStringSlice(suggestedUsernamesKey, usernames)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServerInitTime will return when the server was first setup.
|
|
||||||
func GetServerInitTime() (*utils.NullTime, error) {
|
|
||||||
var t utils.NullTime
|
|
||||||
|
|
||||||
configEntry, err := _datastore.Get(serverInitDateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := configEntry.getObject(&t); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !t.Valid {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServerInitTime will set when the server was first created.
|
|
||||||
func SetServerInitTime(t time.Time) error {
|
|
||||||
nt := utils.NullTime{Time: t, Valid: true}
|
|
||||||
configEntry := ConfigEntry{Key: serverInitDateKey, Value: nt}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFederationEnabled will enable federation if set to true.
|
|
||||||
func SetFederationEnabled(enabled bool) error {
|
|
||||||
return _datastore.SetBool(federationEnabledKey, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationEnabled will return if federation is enabled.
|
|
||||||
func GetFederationEnabled() bool {
|
|
||||||
enabled, err := _datastore.GetBool(federationEnabledKey)
|
|
||||||
if err == nil {
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFederationUsername will set the username used in federated activities.
|
|
||||||
func SetFederationUsername(username string) error {
|
|
||||||
return _datastore.SetString(federationUsernameKey, username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationUsername will return the username used in federated activities.
|
|
||||||
func GetFederationUsername() string {
|
|
||||||
username, err := _datastore.GetString(federationUsernameKey)
|
|
||||||
if username == "" || err != nil {
|
|
||||||
return config.GetDefaults().FederationUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
return username
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFederationGoLiveMessage will set the message sent when going live.
|
|
||||||
func SetFederationGoLiveMessage(message string) error {
|
|
||||||
return _datastore.SetString(federationGoLiveMessageKey, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationGoLiveMessage will return the message sent when going live.
|
|
||||||
func GetFederationGoLiveMessage() string {
|
|
||||||
// Empty message means it's disabled.
|
|
||||||
message, err := _datastore.GetString(federationGoLiveMessageKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Traceln("unable to fetch go live message.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFederationIsPrivate will set if federation activity is private.
|
|
||||||
func SetFederationIsPrivate(isPrivate bool) error {
|
|
||||||
return _datastore.SetBool(federationPrivateKey, isPrivate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationIsPrivate will return if federation is private.
|
|
||||||
func GetFederationIsPrivate() bool {
|
|
||||||
isPrivate, err := _datastore.GetBool(federationPrivateKey)
|
|
||||||
if err == nil {
|
|
||||||
return isPrivate
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFederationShowEngagement will set if fediverse engagement shows in chat.
|
|
||||||
func SetFederationShowEngagement(showEngagement bool) error {
|
|
||||||
return _datastore.SetBool(federationShowEngagementKey, showEngagement)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationShowEngagement will return if fediverse engagement shows in chat.
|
|
||||||
func GetFederationShowEngagement() bool {
|
|
||||||
showEngagement, err := _datastore.GetBool(federationShowEngagementKey)
|
|
||||||
if err == nil {
|
|
||||||
return showEngagement
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBlockedFederatedDomains will set the blocked federated domains.
|
|
||||||
func SetBlockedFederatedDomains(domains []string) error {
|
|
||||||
return _datastore.SetString(federationBlockedDomainsKey, strings.Join(domains, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockedFederatedDomains will return a list of blocked federated domains.
|
|
||||||
func GetBlockedFederatedDomains() []string {
|
|
||||||
domains, err := _datastore.GetString(federationBlockedDomainsKey)
|
|
||||||
if err != nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domains == "" {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(domains, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChatJoinMessagesEnabled will set if chat join messages are enabled.
|
|
||||||
func SetChatJoinMessagesEnabled(enabled bool) error {
|
|
||||||
return _datastore.SetBool(chatJoinMessagesEnabledKey, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChatJoinPartMessagesEnabled will return if chat join messages are enabled.
|
|
||||||
func GetChatJoinPartMessagesEnabled() bool {
|
|
||||||
enabled, err := _datastore.GetBool(chatJoinMessagesEnabledKey)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNotificationsEnabled will save the enabled state of notifications.
|
|
||||||
func SetNotificationsEnabled(enabled bool) error {
|
|
||||||
return _datastore.SetBool(notificationsEnabledKey, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNotificationsEnabled will return the enabled state of notifications.
|
|
||||||
func GetNotificationsEnabled() bool {
|
|
||||||
enabled, _ := _datastore.GetBool(notificationsEnabledKey)
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDiscordConfig will return the Discord configuration.
|
|
||||||
func GetDiscordConfig() models.DiscordConfiguration {
|
|
||||||
configEntry, err := _datastore.Get(discordConfigurationKey)
|
|
||||||
if err != nil {
|
|
||||||
return models.DiscordConfiguration{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
var config models.DiscordConfiguration
|
|
||||||
if err := configEntry.getObject(&config); err != nil {
|
|
||||||
return models.DiscordConfiguration{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDiscordConfig will set the Discord configuration.
|
|
||||||
func SetDiscordConfig(config models.DiscordConfiguration) error {
|
|
||||||
configEntry := ConfigEntry{Key: discordConfigurationKey, Value: config}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBrowserPushConfig will return the browser push configuration.
|
|
||||||
func GetBrowserPushConfig() models.BrowserNotificationConfiguration {
|
|
||||||
configEntry, err := _datastore.Get(browserPushConfigurationKey)
|
|
||||||
if err != nil {
|
|
||||||
return models.BrowserNotificationConfiguration{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
var config models.BrowserNotificationConfiguration
|
|
||||||
if err := configEntry.getObject(&config); err != nil {
|
|
||||||
return models.BrowserNotificationConfiguration{Enabled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBrowserPushConfig will set the browser push configuration.
|
|
||||||
func SetBrowserPushConfig(config models.BrowserNotificationConfiguration) error {
|
|
||||||
configEntry := ConfigEntry{Key: browserPushConfigurationKey, Value: config}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBrowserPushPublicKey will set the public key for browser pushes.
|
|
||||||
func SetBrowserPushPublicKey(key string) error {
|
|
||||||
return _datastore.SetString(browserPushPublicKeyKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBrowserPushPublicKey will return the public key for browser pushes.
|
|
||||||
func GetBrowserPushPublicKey() (string, error) {
|
|
||||||
return _datastore.GetString(browserPushPublicKeyKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBrowserPushPrivateKey will set the private key for browser pushes.
|
|
||||||
func SetBrowserPushPrivateKey(key string) error {
|
|
||||||
return _datastore.SetString(browserPushPrivateKeyKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBrowserPushPrivateKey will return the private key for browser pushes.
|
|
||||||
func GetBrowserPushPrivateKey() (string, error) {
|
|
||||||
return _datastore.GetString(browserPushPrivateKeyKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHasPerformedInitialNotificationsConfig sets when performed initial setup.
|
|
||||||
func SetHasPerformedInitialNotificationsConfig(hasConfigured bool) error {
|
|
||||||
return _datastore.SetBool(hasConfiguredInitialNotificationsKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHasPerformedInitialNotificationsConfig gets when performed initial setup.
|
|
||||||
func GetHasPerformedInitialNotificationsConfig() bool {
|
|
||||||
configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey)
|
|
||||||
return configured
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHideViewerCount will return if the viewer count shold be hidden.
|
|
||||||
func GetHideViewerCount() bool {
|
|
||||||
hide, _ := _datastore.GetBool(hideViewerCountKey)
|
|
||||||
return hide
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHideViewerCount will set if the viewer count should be hidden.
|
|
||||||
func SetHideViewerCount(hide bool) error {
|
|
||||||
return _datastore.SetBool(hideViewerCountKey, hide)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomOfflineMessage will return the custom offline message.
|
|
||||||
func GetCustomOfflineMessage() string {
|
|
||||||
message, _ := _datastore.GetString(customOfflineMessageKey)
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCustomOfflineMessage will set the custom offline message.
|
|
||||||
func SetCustomOfflineMessage(message string) error {
|
|
||||||
return _datastore.SetString(customOfflineMessageKey, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCustomColorVariableValues sets CSS variable names and values.
|
|
||||||
func SetCustomColorVariableValues(variables map[string]string) error {
|
|
||||||
return _datastore.SetStringMap(customColorVariableValuesKey, variables)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomColorVariableValues gets CSS variable names and values.
|
|
||||||
func GetCustomColorVariableValues() map[string]string {
|
|
||||||
values, _ := _datastore.GetStringMap(customColorVariableValuesKey)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStreamKeys will return valid stream keys.
|
|
||||||
func GetStreamKeys() []models.StreamKey {
|
|
||||||
configEntry, err := _datastore.Get(streamKeysKey)
|
|
||||||
if err != nil {
|
|
||||||
return []models.StreamKey{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamKeys []models.StreamKey
|
|
||||||
if err := configEntry.getObject(&streamKeys); err != nil {
|
|
||||||
return []models.StreamKey{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStreamKeys will set valid stream keys.
|
|
||||||
func SetStreamKeys(actions []models.StreamKey) error {
|
|
||||||
configEntry := ConfigEntry{Key: streamKeysKey, Value: actions}
|
|
||||||
return _datastore.Save(configEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDisableSearchIndexing will set if the web server should be indexable.
|
|
||||||
func SetDisableSearchIndexing(disableSearchIndexing bool) error {
|
|
||||||
return _datastore.SetBool(disableSearchIndexingKey, disableSearchIndexing)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisableSearchIndexing will return if the web server should be indexable.
|
|
||||||
func GetDisableSearchIndexing() bool {
|
|
||||||
disableSearchIndexing, err := _datastore.GetBool(disableSearchIndexingKey)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return disableSearchIndexing
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVideoServingEndpoint returns the custom video endpont.
|
|
||||||
func GetVideoServingEndpoint() string {
|
|
||||||
message, _ := _datastore.GetString(videoServingEndpointKey)
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetVideoServingEndpoint sets the custom video endpoint.
|
|
||||||
func SetVideoServingEndpoint(message string) error {
|
|
||||||
return _datastore.SetString(videoServingEndpointKey, message)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
// GetPublicKey will return the public key.
|
|
||||||
func GetPublicKey() string {
|
|
||||||
value, _ := _datastore.GetString(publicKeyKey)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPublicKey will save the public key.
|
|
||||||
func SetPublicKey(key string) error {
|
|
||||||
return _datastore.SetString(publicKeyKey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrivateKey will return the private key.
|
|
||||||
func GetPrivateKey() string {
|
|
||||||
value, _ := _datastore.GetString(privateKeyKey)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPrivateKey will save the private key.
|
|
||||||
func SetPrivateKey(key string) error {
|
|
||||||
return _datastore.SetString(privateKeyKey, key)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -89,7 +91,7 @@ func TestCustomType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save config entry to the database
|
// Save config entry to the database
|
||||||
if err := _datastore.Save(ConfigEntry{&testStruct, testKey}); err != nil {
|
if err := _datastore.Save(models.ConfigEntry{&testStruct, testKey}); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ func TestCustomType(t *testing.T) {
|
|||||||
|
|
||||||
// Get a typed struct out of it
|
// Get a typed struct out of it
|
||||||
var testResult TestStruct
|
var testResult TestStruct
|
||||||
if err := entryResult.getObject(&testResult); err != nil {
|
if err := entryResult.GetObject(&testResult); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ func TestStringMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save config entry to the database
|
// Save config entry to the database
|
||||||
if err := _datastore.Save(ConfigEntry{&testMap, testKey}); err != nil {
|
if err := _datastore.Save(models.ConfigEntry{Value: &testMap, Key: testKey}); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ func TestStringMap(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testResult, err := entryResult.getStringMap()
|
testResult, err := entryResult.GetStringMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
// sqlite requires a blank import.
|
// sqlite requires a blank import.
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/db"
|
"github.com/owncast/owncast/db"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +20,8 @@ type Datastore struct {
|
|||||||
DbLock *sync.Mutex
|
DbLock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Datastore) warmCache() {
|
// WarmCache pre-caches all configuration values in memory.
|
||||||
|
func (ds *Datastore) WarmCache() {
|
||||||
log.Traceln("Warming config value cache")
|
log.Traceln("Warming config value cache")
|
||||||
|
|
||||||
res, err := ds.DB.Query("SELECT key, value FROM datastore")
|
res, err := ds.DB.Query("SELECT key, value FROM datastore")
|
||||||
@@ -46,10 +46,10 @@ func (ds *Datastore) GetQueries() *db.Queries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get will query the database for the key and return the entry.
|
// Get will query the database for the key and return the entry.
|
||||||
func (ds *Datastore) Get(key string) (ConfigEntry, error) {
|
func (ds *Datastore) Get(key string) (models.ConfigEntry, error) {
|
||||||
cachedValue, err := ds.GetCachedValue(key)
|
cachedValue, err := ds.GetCachedValue(key)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ConfigEntry{
|
return models.ConfigEntry{
|
||||||
Key: key,
|
Key: key,
|
||||||
Value: cachedValue,
|
Value: cachedValue,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -60,10 +60,10 @@ func (ds *Datastore) Get(key string) (ConfigEntry, error) {
|
|||||||
|
|
||||||
row := ds.DB.QueryRow("SELECT key, value FROM datastore WHERE key = ? LIMIT 1", key)
|
row := ds.DB.QueryRow("SELECT key, value FROM datastore WHERE key = ? LIMIT 1", key)
|
||||||
if err := row.Scan(&resultKey, &resultValue); err != nil {
|
if err := row.Scan(&resultKey, &resultValue); err != nil {
|
||||||
return ConfigEntry{}, err
|
return models.ConfigEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := ConfigEntry{
|
result := models.ConfigEntry{
|
||||||
Key: resultKey,
|
Key: resultKey,
|
||||||
Value: resultValue,
|
Value: resultValue,
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func (ds *Datastore) Get(key string) (ConfigEntry, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save will save the ConfigEntry to the database.
|
// Save will save the ConfigEntry to the database.
|
||||||
func (ds *Datastore) Save(e ConfigEntry) error {
|
func (ds *Datastore) Save(e models.ConfigEntry) error {
|
||||||
ds.DbLock.Lock()
|
ds.DbLock.Lock()
|
||||||
defer ds.DbLock.Unlock()
|
defer ds.DbLock.Unlock()
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ func (ds *Datastore) Save(e ConfigEntry) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = stmt.Exec(e.Key, dataGob.Bytes())
|
_, err = stmt.Exec(e.Key, dataGob.Bytes())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -121,26 +120,6 @@ func (ds *Datastore) Setup() {
|
|||||||
);`
|
);`
|
||||||
|
|
||||||
ds.MustExec(createTableSQL)
|
ds.MustExec(createTableSQL)
|
||||||
|
|
||||||
if !HasPopulatedDefaults() {
|
|
||||||
PopulateDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasPopulatedFederationDefaults() {
|
|
||||||
if err := SetFederationGoLiveMessage(config.GetDefaults().FederationGoLiveMessage); err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
}
|
|
||||||
if err := _datastore.SetBool("HAS_POPULATED_FEDERATION_DEFAULTS", true); err != nil {
|
|
||||||
log.Errorln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the server initialization date if needed.
|
|
||||||
if hasSetInitDate, _ := GetServerInitTime(); hasSetInitDate == nil || !hasSetInitDate.Valid {
|
|
||||||
_ = SetServerInitTime(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
migrateDatastoreValues(_datastore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset will delete all config entries in the datastore and start over.
|
// Reset will delete all config entries in the datastore and start over.
|
||||||
@@ -156,8 +135,6 @@ func (ds *Datastore) Reset() {
|
|||||||
if _, err = stmt.Exec(); err != nil {
|
if _, err = stmt.Exec(); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
PopulateDefaults()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDatastore returns the shared instance of the owncast datastore.
|
// GetDatastore returns the shared instance of the owncast datastore.
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/owncast/owncast/config"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HasPopulatedDefaults will determine if the defaults have been inserted into the database.
|
|
||||||
func HasPopulatedDefaults() bool {
|
|
||||||
hasPopulated, err := _datastore.GetBool("HAS_POPULATED_DEFAULTS")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasPopulated
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasPopulatedFederationDefaults() bool {
|
|
||||||
hasPopulated, err := _datastore.GetBool("HAS_POPULATED_FEDERATION_DEFAULTS")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasPopulated
|
|
||||||
}
|
|
||||||
|
|
||||||
// PopulateDefaults will set default values in the database.
|
|
||||||
func PopulateDefaults() {
|
|
||||||
_datastore.warmCache()
|
|
||||||
|
|
||||||
defaults := config.GetDefaults()
|
|
||||||
|
|
||||||
if HasPopulatedDefaults() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = SetAdminPassword(defaults.AdminPassword)
|
|
||||||
_ = SetStreamKeys(defaults.StreamKeys)
|
|
||||||
_ = SetHTTPPortNumber(float64(defaults.WebServerPort))
|
|
||||||
_ = SetRTMPPortNumber(float64(defaults.RTMPServerPort))
|
|
||||||
_ = SetLogoPath(defaults.Logo)
|
|
||||||
_ = SetServerMetadataTags([]string{"owncast", "streaming"})
|
|
||||||
_ = SetServerSummary(defaults.Summary)
|
|
||||||
_ = SetServerWelcomeMessage("")
|
|
||||||
_ = SetServerName(defaults.Name)
|
|
||||||
_ = SetExtraPageBodyContent(defaults.PageBodyContent)
|
|
||||||
_ = SetFederationGoLiveMessage(defaults.FederationGoLiveMessage)
|
|
||||||
_ = SetSocialHandles([]models.SocialHandle{
|
|
||||||
{
|
|
||||||
Platform: "github",
|
|
||||||
URL: "https://github.com/owncast/owncast",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_ = _datastore.SetBool("HAS_POPULATED_DEFAULTS", true)
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,5 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
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`
|
||||||
@@ -51,58 +16,3 @@ func GetMessagesCount() int64 {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBanIPTable will create the IP ban table if needed.
|
|
||||||
func CreateBanIPTable(db *sql.DB) {
|
|
||||||
createTableSQL := ` CREATE TABLE IF NOT EXISTS ip_bans (
|
|
||||||
"ip_address" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"notes" TEXT,
|
|
||||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
stmt, err := db.Prepare(createTableSQL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("error creating ip ban table", err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
if _, err := stmt.Exec(); err != nil {
|
|
||||||
log.Fatal("error creating ip ban table", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BanIPAddress will persist a new IP address ban to the datastore.
|
|
||||||
func BanIPAddress(address, note string) error {
|
|
||||||
return _datastore.GetQueries().BanIPAddress(context.Background(), db.BanIPAddressParams{
|
|
||||||
IpAddress: address,
|
|
||||||
Notes: sql.NullString{String: note, Valid: true},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsIPAddressBanned will return if an IP address has been previously blocked.
|
|
||||||
func IsIPAddressBanned(address string) (bool, error) {
|
|
||||||
blocked, error := _datastore.GetQueries().IsIPAddressBlocked(context.Background(), address)
|
|
||||||
return blocked > 0, error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIPAddressBans will return all the banned IP addresses.
|
|
||||||
func GetIPAddressBans() ([]models.IPAddress, error) {
|
|
||||||
result, err := _datastore.GetQueries().GetIPAddressBans(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response := []models.IPAddress{}
|
|
||||||
for _, ip := range result {
|
|
||||||
response = append(response, models.IPAddress{
|
|
||||||
IPAddress: ip.IpAddress,
|
|
||||||
Notes: ip.Notes.String,
|
|
||||||
CreatedAt: ip.CreatedAt.Time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveIPAddressBan will remove a previously banned IP address.
|
|
||||||
func RemoveIPAddressBan(address string) error {
|
|
||||||
return _datastore.GetQueries().RemoveIPAddressBan(context.Background(), address)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
|
import "github.com/owncast/owncast/models"
|
||||||
|
|
||||||
// GetStringSlice will return the string slice value for a key.
|
// GetStringSlice will return the string slice value for a key.
|
||||||
func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
|
func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
return configEntry.getStringSlice()
|
return configEntry.GetStringSlice()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStringSlice will set the string slice value for a key.
|
// SetStringSlice will set the string slice value for a key.
|
||||||
func (ds *Datastore) SetStringSlice(key string, value []string) error {
|
func (ds *Datastore) SetStringSlice(key string, value []string) error {
|
||||||
configEntry := ConfigEntry{value, key}
|
configEntry := models.ConfigEntry{Value: value, Key: key}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,12 +23,12 @@ func (ds *Datastore) GetString(key string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return configEntry.getString()
|
return configEntry.GetString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetString will set the string value for a key.
|
// SetString will set the string value for a key.
|
||||||
func (ds *Datastore) SetString(key string, value string) error {
|
func (ds *Datastore) SetString(key string, value string) error {
|
||||||
configEntry := ConfigEntry{value, key}
|
configEntry := models.ConfigEntry{Value: value, Key: key}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +38,12 @@ func (ds *Datastore) GetNumber(key string) (float64, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return configEntry.getNumber()
|
return configEntry.GetNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNumber will set the numeric value for a key.
|
// SetNumber will set the numeric value for a key.
|
||||||
func (ds *Datastore) SetNumber(key string, value float64) error {
|
func (ds *Datastore) SetNumber(key string, value float64) error {
|
||||||
configEntry := ConfigEntry{value, key}
|
configEntry := models.ConfigEntry{Value: value, Key: key}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +53,12 @@ func (ds *Datastore) GetBool(key string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return configEntry.getBool()
|
return configEntry.GetBool()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBool will set the boolean value for a key.
|
// SetBool will set the boolean value for a key.
|
||||||
func (ds *Datastore) SetBool(key string, value bool) error {
|
func (ds *Datastore) SetBool(key string, value bool) error {
|
||||||
configEntry := ConfigEntry{value, key}
|
configEntry := models.ConfigEntry{Value: value, Key: key}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +68,11 @@ func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]string{}, err
|
return map[string]string{}, err
|
||||||
}
|
}
|
||||||
return configEntry.getStringMap()
|
return configEntry.GetStringMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStringMap will set the string map value for a key.
|
// SetStringMap will set the string map value for a key.
|
||||||
func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
|
func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
|
||||||
configEntry := ConfigEntry{value, key}
|
configEntry := models.ConfigEntry{Value: value, Key: key}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
|
|
||||||
"github.com/nareix/joy5/format/rtmp"
|
"github.com/nareix/joy5/format/rtmp"
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _hasInboundRTMPConnection = false
|
var _hasInboundRTMPConnection = false
|
||||||
@@ -33,7 +33,9 @@ func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models
|
|||||||
_setStreamAsConnected = setStreamAsConnected
|
_setStreamAsConnected = setStreamAsConnected
|
||||||
_setBroadcaster = setBroadcaster
|
_setBroadcaster = setBroadcaster
|
||||||
|
|
||||||
port := data.GetRTMPPortNumber()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
port := configRepository.GetRTMPPortNumber()
|
||||||
s := rtmp.NewServer()
|
s := rtmp.NewServer()
|
||||||
var lis net.Listener
|
var lis net.Listener
|
||||||
var err error
|
var err error
|
||||||
@@ -78,8 +80,10 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
accessGranted := false
|
accessGranted := false
|
||||||
validStreamingKeys := data.GetStreamKeys()
|
validStreamingKeys := configRepository.GetStreamKeys()
|
||||||
|
|
||||||
// If a stream key override was specified then use that instead.
|
// If a stream key override was specified then use that instead.
|
||||||
if config.TemporaryStreamKey != "" {
|
if config.TemporaryStreamKey != "" {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"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/persistence/configrepository"
|
||||||
|
"github.com/owncast/owncast/services/geoip"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -48,7 +48,8 @@ func IsStreamConnected() bool {
|
|||||||
// Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available.
|
// Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available.
|
||||||
// So account for that with an artificial buffer of four segments.
|
// So account for that with an artificial buffer of four segments.
|
||||||
timeSinceLastConnected := time.Since(_stats.LastConnectTime.Time).Seconds()
|
timeSinceLastConnected := time.Since(_stats.LastConnectTime.Time).Seconds()
|
||||||
waitTime := math.Max(float64(data.GetStreamLatencyLevel().SecondsPerSegment)*3.0, 7)
|
configRepository := configrepository.Get()
|
||||||
|
waitTime := math.Max(float64(configRepository.GetStreamLatencyLevel().SecondsPerSegment)*3.0, 7)
|
||||||
if timeSinceLastConnected < waitTime {
|
if timeSinceLastConnected < waitTime {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ func SetViewerActive(viewer *models.Viewer) {
|
|||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
|
|
||||||
// Asynchronously, optionally, fetch GeoIP data.
|
// Asynchronously, optionally, fetch GeoIP configRepository.
|
||||||
go func(viewer *models.Viewer) {
|
go func(viewer *models.Viewer) {
|
||||||
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
|
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
|
||||||
}(viewer)
|
}(viewer)
|
||||||
@@ -111,27 +112,29 @@ func pruneViewerCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveStats() {
|
func saveStats() {
|
||||||
if err := data.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
|
configRepository := configrepository.Get()
|
||||||
|
if err := configRepository.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
|
||||||
log.Errorln("error saving viewer count", err)
|
log.Errorln("error saving viewer count", err)
|
||||||
}
|
}
|
||||||
if err := data.SetPeakSessionViewerCount(_stats.SessionMaxViewerCount); err != nil {
|
if err := configRepository.SetPeakSessionViewerCount(_stats.SessionMaxViewerCount); err != nil {
|
||||||
log.Errorln("error saving viewer count", err)
|
log.Errorln("error saving viewer count", err)
|
||||||
}
|
}
|
||||||
if _stats.LastDisconnectTime != nil && _stats.LastDisconnectTime.Valid {
|
if _stats.LastDisconnectTime != nil && _stats.LastDisconnectTime.Valid {
|
||||||
if err := data.SetLastDisconnectTime(_stats.LastDisconnectTime.Time); err != nil {
|
if err := configRepository.SetLastDisconnectTime(_stats.LastDisconnectTime.Time); err != nil {
|
||||||
log.Errorln("error saving disconnect time", err)
|
log.Errorln("error saving disconnect time", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSavedStats() models.Stats {
|
func getSavedStats() models.Stats {
|
||||||
savedLastDisconnectTime, _ := data.GetLastDisconnectTime()
|
configRepository := configrepository.Get()
|
||||||
|
savedLastDisconnectTime, _ := configRepository.GetLastDisconnectTime()
|
||||||
|
|
||||||
result := models.Stats{
|
result := models.Stats{
|
||||||
ChatClients: make(map[string]models.Client),
|
ChatClients: make(map[string]models.Client),
|
||||||
Viewers: make(map[string]*models.Viewer),
|
Viewers: make(map[string]*models.Viewer),
|
||||||
SessionMaxViewerCount: data.GetPeakSessionViewerCount(),
|
SessionMaxViewerCount: configRepository.GetPeakSessionViewerCount(),
|
||||||
OverallMaxViewerCount: data.GetPeakOverallViewerCount(),
|
OverallMaxViewerCount: configRepository.GetPeakOverallViewerCount(),
|
||||||
LastDisconnectTime: savedLastDisconnectTime,
|
LastDisconnectTime: savedLastDisconnectTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetStatus gets the status of the system.
|
// GetStatus gets the status of the system.
|
||||||
@@ -17,6 +17,7 @@ func GetStatus() models.Status {
|
|||||||
viewerCount = len(_stats.Viewers)
|
viewerCount = len(_stats.Viewers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
return models.Status{
|
return models.Status{
|
||||||
Online: IsStreamConnected(),
|
Online: IsStreamConnected(),
|
||||||
ViewerCount: viewerCount,
|
ViewerCount: viewerCount,
|
||||||
@@ -25,7 +26,7 @@ func GetStatus() models.Status {
|
|||||||
LastDisconnectTime: _stats.LastDisconnectTime,
|
LastDisconnectTime: _stats.LastDisconnectTime,
|
||||||
LastConnectTime: _stats.LastConnectTime,
|
LastConnectTime: _stats.LastConnectTime,
|
||||||
VersionNumber: config.VersionNumber,
|
VersionNumber: config.VersionNumber,
|
||||||
StreamTitle: data.GetStreamTitle(),
|
StreamTitle: configRepository.GetStreamTitle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/core/storageproviders"
|
"github.com/owncast/owncast/core/storageproviders"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupStorage() error {
|
func setupStorage() error {
|
||||||
s3Config := data.GetS3Config()
|
configRepository := configrepository.Get()
|
||||||
|
s3Config := configRepository.GetS3Config()
|
||||||
|
|
||||||
if s3Config.Enabled {
|
if s3Config.Enabled {
|
||||||
_storage = storageproviders.NewS3Storage()
|
_storage = storageproviders.NewS3Storage()
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalStorage represents an instance of the local storage provider for HLS video.
|
// LocalStorage represents an instance of the local storage provider for HLS video.
|
||||||
@@ -22,7 +21,8 @@ func NewLocalStorage() *LocalStorage {
|
|||||||
|
|
||||||
// Setup configures this storage provider.
|
// Setup configures this storage provider.
|
||||||
func (s *LocalStorage) Setup() error {
|
func (s *LocalStorage) Setup() error {
|
||||||
s.host = data.GetVideoServingEndpoint()
|
configRepository := configrepository.Get()
|
||||||
|
s.host = configRepository.GetVideoServingEndpoint()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,8 @@ func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) {
|
|||||||
// Cleanup will remove old files from the storage provider.
|
// Cleanup will remove old files from the storage provider.
|
||||||
func (s *LocalStorage) Cleanup() error {
|
func (s *LocalStorage) Cleanup() error {
|
||||||
// Determine how many files we should keep on disk
|
// Determine how many files we should keep on disk
|
||||||
maxNumber := data.GetStreamLatencyLevel().SegmentCount
|
configRepository := configrepository.Get()
|
||||||
|
maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount
|
||||||
buffer := 10
|
buffer := 10
|
||||||
return localCleanup(maxNumber + buffer)
|
return localCleanup(maxNumber + buffer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -64,9 +64,9 @@ func NewS3Storage() *S3Storage {
|
|||||||
// Setup sets up the s3 storage for saving the video to s3.
|
// Setup sets up the s3 storage for saving the video to s3.
|
||||||
func (s *S3Storage) Setup() error {
|
func (s *S3Storage) Setup() error {
|
||||||
log.Trace("Setting up S3 for external storage of video...")
|
log.Trace("Setting up S3 for external storage of video...")
|
||||||
|
configRepository := configrepository.Get()
|
||||||
s3Config := data.GetS3Config()
|
s3Config := configRepository.GetS3Config()
|
||||||
customVideoServingEndpoint := data.GetVideoServingEndpoint()
|
customVideoServingEndpoint := configRepository.GetVideoServingEndpoint()
|
||||||
|
|
||||||
if customVideoServingEndpoint != "" {
|
if customVideoServingEndpoint != "" {
|
||||||
s.host = customVideoServingEndpoint
|
s.host = customVideoServingEndpoint
|
||||||
@@ -106,8 +106,9 @@ func (s *S3Storage) SegmentWritten(localFilePath string) {
|
|||||||
averagePerformance := utils.GetAveragePerformance(performanceMonitorKey)
|
averagePerformance := utils.GetAveragePerformance(performanceMonitorKey)
|
||||||
|
|
||||||
// Warn the user about long-running save operations
|
// Warn the user about long-running save operations
|
||||||
|
configRepository := configrepository.Get()
|
||||||
if averagePerformance != 0 {
|
if averagePerformance != 0 {
|
||||||
if averagePerformance > float64(data.GetStreamLatencyLevel().SecondsPerSegment)*0.9 {
|
if averagePerformance > float64(configRepository.GetStreamLatencyLevel().SecondsPerSegment)*0.9 {
|
||||||
log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/")
|
log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +221,8 @@ func (s *S3Storage) Cleanup() error {
|
|||||||
// RemoteCleanup will remove old files from the remote storage provider.
|
// RemoteCleanup will remove old files from the remote storage provider.
|
||||||
func (s *S3Storage) RemoteCleanup() error {
|
func (s *S3Storage) RemoteCleanup() error {
|
||||||
// Determine how many files we should keep on S3 storage
|
// Determine how many files we should keep on S3 storage
|
||||||
maxNumber := data.GetStreamLatencyLevel().SegmentCount
|
configRepository := configrepository.Get()
|
||||||
|
maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount
|
||||||
buffer := 20
|
buffer := 20
|
||||||
|
|
||||||
keys, err := s.getDeletableVideoSegmentsWithOffset(maxNumber + buffer)
|
keys, err := s.getDeletableVideoSegmentsWithOffset(maxNumber + buffer)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"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/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,9 +40,11 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
|
|||||||
_stats.LastConnectTime = &now
|
_stats.LastConnectTime = &now
|
||||||
_stats.SessionMaxViewerCount = 0
|
_stats.SessionMaxViewerCount = 0
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
_currentBroadcast = &models.CurrentBroadcast{
|
_currentBroadcast = &models.CurrentBroadcast{
|
||||||
LatencyLevel: data.GetStreamLatencyLevel(),
|
LatencyLevel: configRepository.GetStreamLatencyLevel(),
|
||||||
OutputSettings: data.GetStreamOutputVariants(),
|
OutputSettings: configRepository.GetStreamOutputVariants(),
|
||||||
}
|
}
|
||||||
|
|
||||||
StopOfflineCleanupTimer()
|
StopOfflineCleanupTimer()
|
||||||
@@ -69,7 +72,7 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go webhooks.SendStreamStatusEvent(models.StreamStarted)
|
go webhooks.SendStreamStatusEvent(models.StreamStarted)
|
||||||
selectedThumbnailVideoQualityIndex, isVideoPassthrough := data.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings)
|
selectedThumbnailVideoQualityIndex, isVideoPassthrough := configRepository.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings)
|
||||||
transcoder.StartThumbnailGenerator(segmentPath, selectedThumbnailVideoQualityIndex, isVideoPassthrough)
|
transcoder.StartThumbnailGenerator(segmentPath, selectedThumbnailVideoQualityIndex, isVideoPassthrough)
|
||||||
|
|
||||||
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
|
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
|
||||||
@@ -93,7 +96,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 {
|
||||||
@@ -176,8 +179,9 @@ func startLiveStreamNotificationsTimer() context.CancelFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
// Send Fediverse message.
|
// Send Fediverse message.
|
||||||
if data.GetFederationEnabled() {
|
if configRepository.GetFederationEnabled() {
|
||||||
log.Traceln("Sending Federated Go Live message.")
|
log.Traceln("Sending Federated Go Live message.")
|
||||||
if err := activitypub.SendLive(); err != nil {
|
if err := activitypub.SendLive(); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,9 +88,9 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
|
|||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
configRepository := configrepository.Get()
|
||||||
mostRecentFile := path.Join(framePath, names[0])
|
mostRecentFile := path.Join(framePath, names[0])
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg")
|
outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg")
|
||||||
|
|
||||||
thumbnailCmdFlags := []string{
|
thumbnailCmdFlags := []string{
|
||||||
@@ -120,7 +120,8 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeAnimatedGifPreview(sourceFile string, outputFile string) {
|
func makeAnimatedGifPreview(sourceFile string, outputFile string) {
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
configRepository := configrepository.Get()
|
||||||
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
outputFileTemp := path.Join(config.TempDir, "temppreview.gif")
|
outputFileTemp := path.Join(config.TempDir, "temppreview.gif")
|
||||||
|
|
||||||
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
|
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
|
|
||||||
"github.com/owncast/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/logging"
|
"github.com/owncast/owncast/logging"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -229,7 +229,9 @@ func (t *Transcoder) getString() string {
|
|||||||
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP
|
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP
|
||||||
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
||||||
|
|
||||||
"-method PUT", // HLS results sent back to us will be over PUTs
|
"-method PUT", // HLS results sent back to us will be over PUTs
|
||||||
|
"-http_persistent", "1", // Ensures persistent HTTP connections
|
||||||
|
|
||||||
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
|
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,15 +275,16 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int)
|
|||||||
|
|
||||||
// NewTranscoder will return a new Transcoder, populated by the config.
|
// NewTranscoder will return a new Transcoder, populated by the config.
|
||||||
func NewTranscoder() *Transcoder {
|
func NewTranscoder() *Transcoder {
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
configRepository := configrepository.Get()
|
||||||
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
|
|
||||||
transcoder := new(Transcoder)
|
transcoder := new(Transcoder)
|
||||||
transcoder.ffmpegPath = ffmpegPath
|
transcoder.ffmpegPath = ffmpegPath
|
||||||
transcoder.internalListenerPort = config.InternalHLSListenerPort
|
transcoder.internalListenerPort = config.InternalHLSListenerPort
|
||||||
|
|
||||||
transcoder.currentStreamOutputSettings = data.GetStreamOutputVariants()
|
transcoder.currentStreamOutputSettings = configRepository.GetStreamOutputVariants()
|
||||||
transcoder.currentLatencyLevel = data.GetStreamLatencyLevel()
|
transcoder.currentLatencyLevel = configRepository.GetStreamLatencyLevel()
|
||||||
transcoder.codec = getCodec(data.GetVideoCodec())
|
transcoder.codec = getCodec(configRepository.GetVideoCodec())
|
||||||
transcoder.segmentOutputPath = config.HLSStoragePath
|
transcoder.segmentOutputPath = config.HLSStoragePath
|
||||||
transcoder.playlistOutputPath = config.HLSStoragePath
|
transcoder.playlistOutputPath = config.HLSStoragePath
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user