mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-06-02 13:39:21 +00:00
Compare commits
4417 commits
Author | SHA1 | Date | |
---|---|---|---|
69b4c6647b | |||
f7fe0d46fc | |||
609a6411a7 | |||
44666a34a2 | |||
6db878f761 | |||
6031709fcf | |||
4d9e38d875 | |||
6a6c915014 | |||
96b7afc0b1 | |||
d2083f79d9 | |||
e8a7bb07a3 | |||
91e57ff954 | |||
7d80a3c7d6 | |||
abcfa266af | |||
51970ffc81 | |||
fd6a1283a5 | |||
af034f3b5e | |||
0d5db29bc9 | |||
ec77c00ef8 | |||
69bdcb3069 | |||
6a6108ac55 | |||
b2c1a14234 | |||
d8dc38eb06 | |||
c96017c009 | |||
9aa565b559 | |||
7d7cd8ded4 | |||
943c31cc72 | |||
973f39601c | |||
a39c19c9db | |||
55f84dd38a | |||
6b46a70535 | |||
4ffaa93431 | |||
a0ad7806cb | |||
99aac07714 | |||
1a4aa3eaba | |||
93c9a5f2b1 | |||
9a9d518153 | |||
7fb03c502e | |||
49bb17b583 | |||
723cb549d4 | |||
8b6a4c060e | |||
cb80980027 | |||
c4fc3a8ede | |||
b4f9ef24a5 | |||
866d752a3c | |||
e0b1d0553d | |||
7c146272c3 | |||
cfdc732d3a | |||
522f974e30 | |||
b152be7951 | |||
485b0f1a54 | |||
7540b02723 | |||
7746db4169 | |||
db2ce81fc4 | |||
4175a1af80 | |||
563280456e | |||
2fecb7ecdf | |||
2c6f9c7fd5 | |||
e338e59868 | |||
b0caa85ed4 | |||
ad60d91f5c | |||
6423d2dde5 | |||
12163701e7 | |||
5c35e97a75 | |||
b05f221565 | |||
beec080274 | |||
492d8f1b01 | |||
d3737d4453 | |||
b459949f57 | |||
93f5df2d2a | |||
cf426493e1 | |||
8e3ff0408e | |||
66e06b3952 | |||
6b9d9dfaa5 | |||
0eaf8d33e7 | |||
c31a29ec7f | |||
80635c9e24 | |||
95d75e07b2 | |||
efbfdc9340 | |||
1ae3aab764 | |||
f68881c552 | |||
2ba1ba88b8 | |||
079fa0b8f6 | |||
b0a740d5c5 | |||
ee46242a43 | |||
4ba6221e04 | |||
31829b6c05 | |||
b0370ae2fd | |||
6efab9aab1 | |||
d075acce43 | |||
3a0c1dca90 | |||
0f6b13a4ec | |||
64760ec960 | |||
555f789269 | |||
5dea21d531 | |||
d5622a65f8 | |||
9059de8569 | |||
0203b62a6d | |||
99d585b7be | |||
b4670988b5 | |||
1d0a6ac08f | |||
8e54a4a6cc | |||
a14ebefd24 | |||
705e86eb4c | |||
a1d632e582 | |||
087684658a | |||
aaaa362b98 | |||
5237233f97 | |||
94438a8516 | |||
4d9c16a336 | |||
ae9f82b452 | |||
007e9b7aab | |||
60f9a97dfa | |||
067332553d | |||
a4b79ca610 | |||
a632a86852 | |||
85ee89f4e8 | |||
6bfbb9332d | |||
945064726f | |||
7929e77602 | |||
95069d7648 | |||
e4356a7701 | |||
846848c4f6 | |||
d06ef2c47e | |||
99d01e186a | |||
ef4bb3cc40 | |||
21547dedf7 | |||
baf5921d2c | |||
6679b2559f | |||
38c22d9453 | |||
78581bd696 | |||
2fd81067c7 | |||
0f77951e05 | |||
9d4299aaac | |||
43378c5bb3 | |||
19a1a077c5 | |||
0e7080337b | |||
835d329134 | |||
f1de7b7590 | |||
255e695633 | |||
15f02f00a9 | |||
45c56df4e8 | |||
5d361d63ef | |||
10bf7464b1 | |||
5859502a2a | |||
00f7778485 | |||
bc2e75d5a3 | |||
f228f9d7a9 | |||
fed6b61eaf | |||
bdabc5e827 | |||
97e2dbdfd7 | |||
22c2e20fc5 | |||
6778279bb6 | |||
1ad9a211c9 | |||
157378b4c9 | |||
7f9950fe85 | |||
52155c74cb | |||
36ad1868b3 | |||
65da4e7dbd | |||
3c358e5b0b | |||
eb1245bceb | |||
7eec8714d7 | |||
08b01a377d | |||
c5e54a318a | |||
a7fa075e8c | |||
c895e57086 | |||
e1b26897be | |||
87b577467b | |||
6d815db375 | |||
328a48c9f5 | |||
ab4deaa49a | |||
e01ea32928 | |||
f3d48f2c2c | |||
7316dd281a | |||
80bfd23b4d | |||
f42420809b | |||
f56b84615c | |||
d79502dff3 | |||
ae62ef2b7e | |||
39345466da | |||
5d551e6da5 | |||
ffcf415cac | |||
86b44c2a4d | |||
a3bf2f1cf1 | |||
890565ca14 | |||
3f7cc07b02 | |||
33989f5518 | |||
8a6a86c1bb | |||
677d54ae57 | |||
300869d397 | |||
609de3e9e2 | |||
3c5b1ac6dd | |||
f631f43024 | |||
9367cbdb00 | |||
3647a46e86 | |||
328d48ef7e | |||
a09027c4c0 | |||
0e9924a2b3 | |||
eb0dc2fda4 | |||
9a2fb8e7c2 | |||
f481a607d0 | |||
ade1cb1495 | |||
0f414a95d5 | |||
8cde452fca | |||
dadf8f28f9 | |||
e8a52d3a5c | |||
1782aafd10 | |||
d8f9e8a64c | |||
759f6d8a9a | |||
8670403a67 | |||
922ec7c2cd | |||
f43d2eca70 | |||
20fd4b5869 | |||
eb56d9253c | |||
4b4b99aa78 | |||
2133bcea4e | |||
0868910570 | |||
e78fe5a34c | |||
f652513030 | |||
df11d77a0d | |||
3d6f7ff911 | |||
516db012bf | |||
b58da11fb7 | |||
1be7dbde33 | |||
9240a653c0 | |||
0d35c247f9 | |||
143fdb62b1 | |||
2d16d12cb7 | |||
3b717cfc88 | |||
ea0b856f1e | |||
92b49dea3a | |||
1ef90773e0 | |||
3d9dda4677 | |||
2f09ad8e5b | |||
e3b715002b | |||
4ca63c5641 | |||
346ff11795 | |||
747ab89e87 | |||
d3efebfa4e | |||
1856e7c0ca | |||
0e6669f617 | |||
4a740ee80a | |||
7d9b59c467 | |||
38e64825e6 | |||
3cad3b2119 | |||
023c9f4fcd | |||
952c162dac | |||
a5289dd4cf | |||
009a45dffb | |||
024ab7d530 | |||
53147596b4 | |||
4163e0465e | |||
abe8b18ea8 | |||
35db0dc8e7 | |||
5f603985c0 | |||
2f3a7abe6b | |||
f223eb94d5 | |||
4ef00e068f | |||
2b9d89057d | |||
08b7e0f03d | |||
7f780376bc | |||
a507a39336 | |||
2899ba0131 | |||
8583a85607 | |||
6790b54d4d | |||
dcb89f52d6 | |||
bc32b408b5 | |||
aab3ca4eb0 | |||
a7ba5c9dd3 | |||
7a182c982b | |||
719b76a6e7 | |||
246e38a45b | |||
e4b97ad8a9 | |||
70530a8ad2 | |||
af4d008ce4 | |||
ac209d58b4 | |||
59eef85bb7 | |||
442ec0b9f8 | |||
4a55d4f871 | |||
7353be5b68 | |||
32afc32bc0 | |||
01aa17f38e | |||
0b2df3980f | |||
6626d35b98 | |||
60ffa2a599 | |||
5e589004a5 | |||
a0ef56b9b7 | |||
93d123b46e | |||
5ad881f3df | |||
3e2393993e | |||
8d52c7e7c7 | |||
30d58865b8 | |||
e0e74e50ae | |||
c7ee53026f | |||
b2ae69f96c | |||
95130370f0 | |||
f764996745 | |||
e317947b75 | |||
cef3f220a2 | |||
60849355db | |||
2d4037ba61 | |||
c85e680aba | |||
203ca9d617 | |||
a790a24c4d | |||
de85e51fac | |||
a5386187e3 | |||
3f79eacb53 | |||
16ac893e15 | |||
d7376d9541 | |||
809fc05cb3 | |||
70003407a7 | |||
170b3ec45f | |||
7ef6476520 | |||
e84f8f55a2 | |||
86990d5138 | |||
23b266ec12 | |||
a986db1a00 | |||
6fa3b59d25 | |||
d95df3a46f | |||
7972dd0fcf | |||
fc07ba2d3b | |||
8a05c8f8be | |||
934fe7d1cb | |||
7d489f1e3f | |||
2b5a31b411 | |||
d795c54a7a | |||
6e4278b1c9 | |||
26297b4035 | |||
cafeb14f1c | |||
58258414cc | |||
e58a167d64 | |||
375e232217 | |||
9d4d6ce881 | |||
3e1f7f2efa | |||
28d779a960 | |||
7ba1d98915 | |||
49377c195e | |||
30f7135f32 | |||
62357f4444 | |||
82b30b5467 | |||
1c03a86c17 | |||
f786df151f | |||
25450ea090 | |||
2070381e81 | |||
525359f7c5 | |||
7cb20200d8 | |||
fc56d0aa05 | |||
e573010202 | |||
a00313e680 | |||
a53892d2bb | |||
e2bf2ce530 | |||
9a94a86363 | |||
28c30cc502 | |||
907e0ce726 | |||
1d23df37d8 | |||
8e2cbc9a0f | |||
1dc6c60760 | |||
e00b0c9da2 | |||
aaaf17486d | |||
7a30a75905 | |||
98ed0e51cc | |||
cb01427dcf | |||
97a4fb9a72 | |||
b9b65c9c18 | |||
8c85f35b19 | |||
cf788334aa | |||
adef3479c0 | |||
eeec56c22d | |||
1e59e7ab47 | |||
df53d2a0e8 | |||
9007d49324 | |||
c11e9446c6 | |||
08739e2925 | |||
b63836b31b | |||
1b751a8cac | |||
64b00ee850 | |||
568233b062 | |||
45bed71c36 | |||
766ca99fd5 | |||
bd9739b5b7 | |||
0b41ddca58 | |||
c38dfdcd64 | |||
c0aee244e7 | |||
221365bc9a | |||
2df21b9d83 | |||
d55bd2f2bc | |||
722d9efc13 | |||
09a67472e7 | |||
053dcf8c10 | |||
5d48ee3dc8 | |||
5540257b36 | |||
1596aee724 | |||
5bfa4e9358 | |||
6235ff45b5 | |||
ec0a707110 | |||
dfc74835b1 | |||
6bcb12b14f | |||
236c7e24fd | |||
cae25486e4 | |||
c1db65c6e5 | |||
8deb4e5752 | |||
9ef28eb53b | |||
a14657d124 | |||
a675fecacd | |||
6d27bfed08 | |||
d827af725a | |||
3a19af5215 | |||
3f62135083 | |||
cf1c32d2ab | |||
56322c75f0 | |||
332e698336 | |||
6cfbb8fc3b | |||
256ee61908 | |||
3be56ef2e0 | |||
9e099726e6 | |||
608bb6b1b4 | |||
645bf21d54 | |||
3a1be8c041 | |||
291ff19718 | |||
6d7b38f4de | |||
6d1a7c8ae0 | |||
6470ea81a7 | |||
dc327652a5 | |||
b7d570cf35 | |||
a5b8583aab | |||
4bf9947153 | |||
0115613f90 | |||
5ef4807d09 | |||
6950dd90e5 | |||
626c7ebc85 | |||
50b7322ff3 | |||
2dad29106a | |||
d45a2a6441 | |||
210c470ebd | |||
9bb99bec5e | |||
9275041f42 | |||
50589115e0 | |||
9a9ece8fa4 | |||
1032cce065 | |||
bee6b7a1bb | |||
13b20b0502 | |||
ed8dae99f1 | |||
e181f4f41d | |||
6387ac07b3 | |||
3a76a10931 | |||
37655f0f18 | |||
519e800b33 | |||
b16661a17d | |||
24c98a726a | |||
9bcadadede | |||
fe40adfc39 | |||
31f3677270 | |||
5941dc2924 | |||
07ffdc3d53 | |||
2c283d90ca | |||
dec25f9ee8 | |||
6d519ef376 | |||
459d5a3726 | |||
f858d8cbce | |||
7e8b973128 | |||
f16aa9228d | |||
1b7ab96887 | |||
bb552d7659 | |||
ac0c0c8856 | |||
2dbaf67b7c | |||
ae37ca0137 | |||
a808d3208a | |||
22608ae983 | |||
3d649e1d3e | |||
1a0c866b51 | |||
77b2d236b9 | |||
a866b3424d | |||
a61d564732 | |||
a59a94492d | |||
78a8a7b8b7 | |||
c792f46c59 | |||
ae3b8f4f76 | |||
2e7d2d1956 | |||
50f81cf157 | |||
89b7c981f5 | |||
a57b116431 | |||
553c5bda88 | |||
3812c8bf81 | |||
dc6dee61b9 | |||
671676d7e4 | |||
c471692949 | |||
be961075c7 | |||
c25734e4ca | |||
8c419103b2 | |||
4f85adc393 | |||
f93d08a147 | |||
990445bf84 | |||
a19b1d2606 | |||
afde8e7576 | |||
dff54d5e39 | |||
c05458adcd | |||
5fff7504e5 | |||
7dd857e00b | |||
2f0ad53b60 | |||
9868065089 | |||
d58e2e9db7 | |||
007f4f5319 | |||
954b3f443d | |||
9e886fba4a | |||
88931227b0 | |||
943b960c32 | |||
48496599b2 | |||
27141553b6 | |||
08401fc85f | |||
b431c9bdf9 | |||
544d30f0d4 | |||
7118200cab | |||
6d02a9914c | |||
05b1ebdc7c | |||
eab1862e9a | |||
fc1f9c1ec3 | |||
c86173577d | |||
7ee707e78b | |||
b09ffa7197 | |||
dca43dcfd9 | |||
4f1240487e | |||
6735a98d35 | |||
375d9a2a3c | |||
3b67642ec2 | |||
9785b20843 | |||
4121fc4d56 | |||
722027ce2e | |||
71d61138bc | |||
fe3ebea95a | |||
797d26fdf4 | |||
a0ea8dbc00 | |||
366d9d1e2e | |||
a1a9c3e4c0 | |||
5b5ac0f37d | |||
a57658d99c | |||
15930cbf4d | |||
bd3f39973f | |||
56e26fc3d4 | |||
fed6542055 | |||
384e55f0e4 | |||
c93bde9799 | |||
b2aee565f3 | |||
7fd14b3d2a | |||
26d125cc63 | |||
514f2222e0 | |||
6047257bfc | |||
51ccf318e8 | |||
28324ad2c8 | |||
ab828b81e4 | |||
ee7b35a04a | |||
c8063f3267 | |||
9b710a2ed3 | |||
654bc513ea | |||
29869b132c | |||
f7f6766650 | |||
ae438e148e | |||
33b2d67be6 | |||
969f8b2ce9 | |||
a47b12bbde | |||
2ad3450004 | |||
f9c2ba1cfa | |||
91c024fd98 | |||
48466b6826 | |||
d81fb987aa | |||
bed9474cf0 | |||
5e9930bdcc | |||
57cb5637fc | |||
66ac8100d9 | |||
2bb24c2859 | |||
da031a4ce2 | |||
70fae9d68d | |||
2d0f77af59 | |||
27be1efb74 | |||
be1389420b | |||
b4380cb548 | |||
58388f2ce8 | |||
7bc64ab91a | |||
91834d0d21 | |||
d82194cfe9 | |||
05a7fced65 | |||
55e383ae38 | |||
6ed2ddf76d | |||
963d04b352 | |||
543bd99900 | |||
39752fa096 | |||
e365d48769 | |||
37998b3398 | |||
db76c5b7ff | |||
a34e0d477e | |||
0e6fb08284 | |||
9a5a13c734 | |||
e315092ee3 | |||
1253a2a0d5 | |||
d909f3455d | |||
21a87ebaf2 | |||
2d7b416652 | |||
9bfa86d162 | |||
3471f3533c | |||
2de994797e | |||
c890797b37 | |||
dc4572460e | |||
afac3ee7ff | |||
f9351b6512 | |||
9b123f45ec | |||
cf2229d665 | |||
9a1f9aad45 | |||
95e758e707 | |||
3b09d8c882 | |||
13a866aeb0 | |||
102124b6d2 | |||
ea7f83c4dc | |||
e17f0097ff | |||
417c4d2311 | |||
623d81139e | |||
ccc122100e | |||
1a164a649e | |||
3753a3bf54 | |||
b511c2e6cb | |||
ced3aa5bd8 | |||
047db9ac85 | |||
d7051c40f8 | |||
5cd4c6c586 | |||
5d23ef960e | |||
77a8e3b897 | |||
1f21bdb2f9 | |||
2adf7d5008 | |||
38c6210912 | |||
0ada7dc889 | |||
9256895635 | |||
88215bfbc9 | |||
6688a8a5d4 | |||
e9e76549a8 | |||
2938b50908 | |||
164f4b93d9 | |||
39f28c1a40 | |||
7d8cb93b53 | |||
c0b7865896 | |||
40ff77eee2 | |||
8992965c0c | |||
6310f55984 | |||
9b5e765364 | |||
c060546ffa | |||
1d38aad9d3 | |||
73492af4b0 | |||
ef9dc5d0b6 | |||
9c2490d4f2 | |||
651f2747ee | |||
93225e5ddf | |||
b50634e2cf | |||
ff26bc21af | |||
934f72511e | |||
3dd2fd0f7f | |||
9d52d8bd0b | |||
e7e60e0514 | |||
b35757b429 | |||
0c82f4e660 | |||
c208faf276 | |||
aa70325c11 | |||
b3dfc3721f | |||
2a088cf023 | |||
22cdcc6b60 | |||
922ee6a230 | |||
00f9f79a44 | |||
c12fedaf1b | |||
084f603745 | |||
6840fd64f9 | |||
ef11a6ca37 | |||
ce1ffebeb0 | |||
2158621bda | |||
45b1a0d4fb | |||
ebaf69bd70 | |||
1e99e8b9dc | |||
657c2e37c0 | |||
ff47d97bd3 | |||
85dab149a9 | |||
cdc5b47886 | |||
8bcf2ea7c9 | |||
bf62fbe644 | |||
f0e487f18a | |||
7a04971ecd | |||
bbca6ef6dc | |||
696cca4ce4 | |||
935b0bf048 | |||
e1494d4683 | |||
b8ee9315bc | |||
efe98158c0 | |||
050216eed9 | |||
cb91eedd24 | |||
6405761891 | |||
fc60b82f82 | |||
3578dab67f | |||
682ca55e0c | |||
cb28af508d | |||
810762762f | |||
fcc010b5dc | |||
7d3894d5dd | |||
3159eedd99 | |||
6c64cb5233 | |||
0464c46d26 | |||
c216153dfb | |||
ec18fd9869 | |||
ffc049078e | |||
0f91759e4d | |||
bef76630c5 | |||
e4b739320c | |||
ad6f244b61 | |||
e63aa80c3a | |||
d1d90af0eb | |||
2aef6a5a33 | |||
76a4513774 | |||
98482b1564 | |||
211e76dc27 | |||
f5209fffc1 | |||
50efb1d519 | |||
8fbc630ce1 | |||
c9e9ff46fa | |||
62c8ac1db5 | |||
b2a9d4a335 | |||
73d2faa9f5 | |||
9406c3ad2b | |||
a39e948b52 | |||
83ff9adee5 | |||
3d7d6b2530 | |||
01dc1efe77 | |||
2f3d60a63b | |||
ddfa112e0b | |||
c5886404ef | |||
21d5349785 | |||
203e35899e | |||
206789af67 | |||
ce0cf0e41b | |||
6b28f8c616 | |||
d6b580a530 | |||
e4d78b0974 | |||
3f6e9a7f23 | |||
37a47de3a8 | |||
b6cd1bde8e | |||
aea5f6a38b | |||
9537878519 | |||
6d67f88603 | |||
d7da911a48 | |||
63d3759c48 | |||
cb3b9b1a66 | |||
1f1c0ad17a | |||
dce79b83bf | |||
39572e1c3e | |||
8a086c8240 | |||
418bca78e1 | |||
60baebcb14 | |||
ffce66abb8 | |||
4f2fb68d6b | |||
08e25d0e63 | |||
434beadb58 | |||
862ceefc56 | |||
985359918f | |||
0d576a8f23 | |||
ef3544bac4 | |||
c9d1fec117 | |||
da3e3b6d8a | |||
289052f261 | |||
4db65c191c | |||
d4f52b78b3 | |||
4cdb9583e9 | |||
3775418952 | |||
8fb4760e02 | |||
e3eda68147 | |||
1ef095dd96 | |||
a1beccf353 | |||
45818fb4c5 | |||
b214d3dc00 | |||
8179b0daf2 | |||
5ff221a2e4 | |||
8ed3f3530e | |||
b37c30ffae | |||
154a85cc36 | |||
4d160461ce | |||
dc36d9ed89 | |||
bfc0c0e7d3 | |||
06a3f9c5aa | |||
981c111567 | |||
56de68c77a | |||
e23621c2cd | |||
d97ff65fe1 | |||
bb27339e68 | |||
007c7d5812 | |||
becf75d1f9 | |||
b5dba17426 | |||
744153eee6 | |||
68d814b9b1 | |||
5af831c6fa | |||
ebd8c86f0e | |||
14b990e006 | |||
e0af3a292e | |||
53fe93db47 | |||
558035b128 | |||
f43bb454c3 | |||
1c7bfd6be8 | |||
f3f95e5d2f | |||
87aa0acaf1 | |||
91271a7ce4 | |||
c32585b034 | |||
932e65c16d | |||
5f92125e44 | |||
25275b79ee | |||
87634ca206 | |||
b47ce4c2e6 | |||
f50325e78a | |||
3a6f8877c9 | |||
ecc9469a02 | |||
14c18dbdae | |||
8cb5939f50 | |||
4e5798852f | |||
f98476c5c0 | |||
15c84e2f7b | |||
491e197529 | |||
980df7545d | |||
2423b89ced | |||
1c9f0c2bed | |||
9419dd7c05 | |||
24323e17b2 | |||
af03dcfeac | |||
e97c30b930 | |||
09099e7b90 | |||
3565ad984a | |||
ef1aa18fd2 | |||
bb625c3671 | |||
ae88f618fd | |||
7248c4a6f4 | |||
dc9ca5768b | |||
d439bdf8b4 | |||
1b4f995706 | |||
e363c25521 | |||
af3eb6e27b | |||
d30839bea1 | |||
4240af86ca | |||
a5707328cf | |||
4a70502007 | |||
82d93da26b | |||
a9d708f494 | |||
436a293f38 | |||
df57795595 | |||
8410a9696e | |||
e856eda74d | |||
69f179bd33 | |||
ce5161d997 | |||
0379c9e4c1 | |||
af5175a282 | |||
32a5567cbf | |||
d63b1ba935 | |||
014c0ad87f | |||
88a0d2feec | |||
1b5437cbe3 | |||
17527d0e7e | |||
a25faf3e97 | |||
a2fb4b8cd0 | |||
eb40aeb89b | |||
1e26709cb4 | |||
85ef507ac7 | |||
d8722b6e91 | |||
f5511cfd25 | |||
922b50fc7c | |||
33ba1fc623 | |||
90c6dc2732 | |||
3fa713f414 | |||
148f33def5 | |||
38d4429ae7 | |||
e7a2080f3f | |||
63f54a3103 | |||
844cde9db3 | |||
9d26ac2c6f | |||
d18bc95b7b | |||
3a526b0b7d | |||
165b19e75c | |||
fdaa2bcf09 | |||
6f513793cb | |||
6bc49bdd70 | |||
66b0ddbbc5 | |||
1dba94c9cb | |||
985fe24669 | |||
48f187188b | |||
d9e7f0100a | |||
3844ac76c3 | |||
f0e1627824 | |||
209c8a9185 | |||
8c0c1628e0 | |||
4c77c84b60 | |||
80571567e1 | |||
155af88582 | |||
675353d4e9 | |||
cd5c79527a | |||
a42f7271e6 | |||
62663a9f2e | |||
8409e50f8c | |||
1917e3d495 | |||
3735c6fabf | |||
47f4aa3550 | |||
9b0428bc50 | |||
993489a80f | |||
36aaa50644 | |||
8a27978ee1 | |||
956d0aa52f | |||
9d7009c772 | |||
25e98064b6 | |||
ddb6268164 | |||
166854b37e | |||
3bb98fcc64 | |||
3bfa8ab4ff | |||
ab9b60e86d | |||
7c1a37d584 | |||
2891856b48 | |||
eb78af9b02 | |||
bc8845391f | |||
0c8fd240a6 | |||
4dddc3f717 | |||
5ae409d4e4 | |||
ed95c61bca | |||
b1e121b5ba | |||
d0c94a4f49 | |||
93f2eb4df0 | |||
4dfe4c5579 | |||
41010328c1 | |||
2754bc1d10 | |||
36ac1b54c1 | |||
43b94e5f16 | |||
d5368cbba9 | |||
53bc0311b1 | |||
a9905bf999 | |||
a610211557 | |||
a8232fe3d6 | |||
c416e4ce14 | |||
70c549dad8 | |||
1eaf2c8a03 | |||
6eb5ed343c | |||
7e3d3839b6 | |||
ac56504291 | |||
b5d45fa826 | |||
2ec8bb877c | |||
ceff2ec686 | |||
0630d214e3 | |||
b7d5b37ac9 | |||
e567151ec3 | |||
dab532f743 | |||
270f3b781a | |||
0d4cdb579c | |||
9f52d4ae53 | |||
c3fbb7702f | |||
76e0ab934f | |||
9dfd819691 | |||
0ecf256ce3 | |||
2732a5bf07 | |||
f02892b23b | |||
c6c52ab9cc | |||
201fa97769 | |||
904d7bec2f | |||
030afbc2e7 | |||
70e3feb174 | |||
ccb6435c1d | |||
bcf5c91f81 | |||
41d4852efc | |||
96a2bec691 | |||
d39bc14c43 | |||
c3c4508a72 | |||
d20d2b9218 | |||
4ddca46228 | |||
87c7ee5490 | |||
bb1e3bc891 | |||
bc19d94e3c | |||
872c60a013 | |||
65041a20bb | |||
c27d2a5687 | |||
6f3bf4634b | |||
a39cf31466 | |||
23d0f3ed3c | |||
df7809fbbb | |||
63c237d6df | |||
24756af84b | |||
b16df59373 | |||
0b86ffbdbd | |||
a0fed24cee | |||
e3bb43542c | |||
83d2959b4b | |||
42674b7abd | |||
5d837780f5 | |||
ceae0f5993 | |||
a0a84d91ce | |||
50a2233b52 | |||
2207fed0f5 | |||
9393195827 | |||
5e871ca7ba | |||
b5cd732372 | |||
cb753b045f | |||
28eade6168 | |||
ed9e91345a | |||
7aa6d6b3e1 | |||
6aa9bdebae | |||
3b0be52e67 | |||
b0c3185663 | |||
235cc8b228 | |||
276a8c2bd3 | |||
9ac13a0891 | |||
aaaea2006c | |||
74bc1198ce | |||
0a60bcb8ee | |||
04f0f3af0b | |||
6c3e984ad1 | |||
cb559178bd | |||
ec5e63b5a9 | |||
d2608bb279 | |||
2ef0f8f5f8 | |||
7bb941e546 | |||
290acf6acc | |||
3f8c28b26f | |||
ee41654394 | |||
ae95f5928e | |||
0aeb78b8f3 | |||
f2537ba7db | |||
3c5368d8b1 | |||
2dca80d678 | |||
3adb293541 | |||
9ce15ed071 | |||
c9f1407429 | |||
4e6409f325 | |||
004efd5d94 | |||
09246a20fb | |||
71aed94a00 | |||
553654b8e8 | |||
a93095ed3f | |||
2f0560ca3f | |||
eacd4e44cd | |||
3f2a689edf | |||
265dc54627 | |||
0a14b17747 | |||
3295784ebb | |||
095591ef69 | |||
5e8a37ac86 | |||
d058e2217a | |||
bc7450ae3e | |||
f5b1ee6c34 | |||
3d0d8796ad | |||
a6dc6804aa | |||
e4a49b6eab | |||
c89006c94a | |||
870abf8442 | |||
583ceb2506 | |||
f878151648 | |||
21455d6b73 | |||
08a797c986 | |||
2f9d8776ac | |||
8a4d9cc1ba | |||
7094a0c0a4 | |||
9c3efe32e7 | |||
becb8b4f66 | |||
a85334c675 | |||
8bfeb4b627 | |||
42eac1560f | |||
3b86e15399 | |||
b78826c2c8 | |||
c62671116c | |||
b9f1fc0518 | |||
eee8f467b5 | |||
767f1aa0de | |||
16fe149a6d | |||
d0a3d99636 | |||
63fff96275 | |||
e33736ae47 | |||
67a34adf4b | |||
ff026dc3ff | |||
3ef812660c | |||
ddc9763050 | |||
b7a2677b4d | |||
587a0de8f7 | |||
a745fa6f43 | |||
8af913f583 | |||
40609549d8 | |||
92d17639cd | |||
dc35ab9c2b | |||
cd4d235171 | |||
c78de25652 | |||
4e12e25c59 | |||
5b7376512f | |||
a29cf04092 | |||
fcaf7a084c | |||
7bc2f9fd6d | |||
c050945b1f | |||
23819d44f3 | |||
7804a9d893 | |||
916c53ccec | |||
0a3b69a259 | |||
780832256c | |||
5387c262c1 | |||
339eab01fd | |||
9a458d2e4b | |||
13ca1111c4 | |||
a2a594b763 | |||
1bd57ee5ca | |||
9b9b7b16c8 | |||
09454f3e49 | |||
205b4a22a8 | |||
69106589ba | |||
eafb3869f2 | |||
3af4a27a88 | |||
3c111b3062 | |||
d4ee171b08 | |||
763dc9668c | |||
e6fe9d0325 | |||
ed9b66541e | |||
48f5a2ee5e | |||
7566f325cd | |||
9ef0efa420 | |||
60e9586530 | |||
be246425f0 | |||
426e1b1e51 | |||
144336b8b1 | |||
562b88da9e | |||
26d5b38115 | |||
91d1d5f7ff | |||
5eb0dfdc73 | |||
bd9df834f5 | |||
80684de7da | |||
f0ec3015cd | |||
99f3141841 | |||
ef12cd9588 | |||
559e43075a | |||
7b86441bab | |||
3053e14be7 | |||
3aa3d75a1e | |||
dd5835fb6e | |||
7058cfa1cf | |||
9c1b9ff3cf | |||
c718882479 | |||
2e01e8e42c | |||
56b7030901 | |||
3488b9aeb5 | |||
ae84258c41 | |||
7e13406979 | |||
00b0b26dfa | |||
24be9f2cd5 | |||
0a36b16e29 | |||
3951a16447 | |||
ce4682caa0 | |||
0372029e8d | |||
e0381df88a | |||
3d8709780a | |||
0d9b756b75 | |||
2985e88a49 | |||
bbd739af9b | |||
2180bd0369 | |||
b47a474863 | |||
fed73a72c1 | |||
0bcdfa8be4 | |||
a7540bd59f | |||
c232564fda | |||
4878848955 | |||
4052900d30 | |||
8337eaefdd | |||
9ac1f46a2b | |||
f9d563d80a | |||
b41f7f3eca | |||
1e2d2ecbae | |||
b5d0a9322a | |||
ef08870ce5 | |||
37c834725c | |||
65cac21713 | |||
c7f5337099 | |||
bfae246734 | |||
56b275acd4 | |||
589d952a95 | |||
4597ea2017 | |||
4cf0da7b60 | |||
3ad172e8ed | |||
ad7e6d99ed | |||
2e65b31aa6 | |||
d5a213760d | |||
11b7525c41 | |||
f7fa1f10f9 | |||
7bb8069224 | |||
610120067c | |||
e16821116e | |||
f6b543ae0f | |||
052e509954 | |||
d49565e030 | |||
f21f2fd7b0 | |||
a11b5b107d | |||
f2a0841586 | |||
483e7ab168 | |||
a5ff629b24 | |||
c681bb79b1 | |||
e208f0cb4d | |||
2da0684e95 | |||
ed9f70276d | |||
dfb0938738 | |||
1e9f609cdb | |||
83e996111e | |||
b406342a14 | |||
cb44b14717 | |||
ecd157d4a7 | |||
01cdac012f | |||
6616e89bcf | |||
8112816e99 | |||
0c63dbafb6 | |||
0d0772a48a | |||
cda93e08d8 | |||
7be5fa4a47 | |||
500ca15893 | |||
5698dd7771 | |||
4e6d901649 | |||
a10bf7d410 | |||
5a245659c8 | |||
5265f6100e | |||
bcf7ec6109 | |||
611d336f94 | |||
166ec196b0 | |||
d3d4e76a19 | |||
514e2301b5 | |||
9fd6a48e3c | |||
7d61616dff | |||
550a93aed9 | |||
f8fb3b41f0 | |||
71237f1b53 | |||
05f20da51f | |||
82dec1911f | |||
660efd1549 | |||
15ff832c4a | |||
3c5275638f | |||
ffd2ba5d90 | |||
fbadb2d7bd | |||
aef2abb962 | |||
df757f28b4 | |||
ef1e164cc5 | |||
fa29ffade1 | |||
46e937d6e6 | |||
a5433ebf77 | |||
0d957a2a27 | |||
e3153df74d | |||
b06f3eb32e | |||
1b5037e92d | |||
bc3927a9bd | |||
63565712ad | |||
fb4c47d602 | |||
348077c3de | |||
9f5183fe98 | |||
7f9b55e793 | |||
762b85b27e | |||
e094989a4c | |||
81aa39ba68 | |||
5a8c894359 | |||
788924d7ff | |||
dd865c5af5 | |||
f1aef63149 | |||
51fd3e7f77 | |||
6b500f6cf2 | |||
0f8caac689 | |||
14b27b8377 | |||
1546e5fd0d | |||
609d2b99f7 | |||
2e5746b74f | |||
6058b11f6e | |||
6b1894faf0 | |||
2ac6e1a463 | |||
d71e1c06e2 | |||
2fa47aaef9 | |||
7560e0ec8e | |||
545fe8d0dd | |||
a8d17c7847 | |||
18f63f0f01 | |||
4a23ee4d8b | |||
d83a53e905 | |||
1c5c02e1bf | |||
e36ad9d984 | |||
1372827b41 | |||
2544341e47 | |||
16f98261f8 | |||
16271b0a4c | |||
f23fed70bc | |||
272dc3e7a6 | |||
19ccaf767c | |||
a212f6b780 | |||
eea3308906 | |||
bc8ed1e6da | |||
55bb68f6f9 | |||
f53902ecff | |||
46e6affd54 | |||
bccb4f068d | |||
a860e22b85 | |||
a24676e629 | |||
e65c45f152 | |||
41b90bb162 | |||
3032a2745b | |||
661f97a073 | |||
e864685759 | |||
bd31475dba | |||
b733df2903 | |||
1943bcd7f5 | |||
f6c13cf313 | |||
3065154f4e | |||
9f64872d5a | |||
ad4715c2a3 | |||
c883a49a40 | |||
1410c5659c | |||
1880d64235 | |||
4a46dc805f | |||
161d41999d | |||
c6ac606f60 | |||
0ed3856ef1 | |||
7d07bc16ae | |||
5b3d707d8f | |||
2cf3278724 | |||
97ebf2f6f3 | |||
35cbae61bc | |||
4a541a25ab | |||
8ce0c6d618 | |||
4e9ecb2632 | |||
040770d7ba | |||
8bf0f31b0f | |||
e2baed9a6d | |||
a91c0c8feb | |||
ad76c75821 | |||
be65b6869f | |||
e765b42d46 | |||
27a687bcd0 | |||
97b8b9c255 | |||
7363b19f01 | |||
f6b96370b8 | |||
7f01d8cde6 | |||
b1f7bad0a3 | |||
46c7429429 | |||
d2e28e5f38 | |||
451818749b | |||
86b2901e8c | |||
1579ee566f | |||
3fea5645f8 | |||
88c7b95d03 | |||
3e062a9959 | |||
e88106cef4 | |||
3d08e6c1fc | |||
76c4378011 | |||
05d43150bc | |||
209829e1f3 | |||
afccd5cf69 | |||
88e81dce6b | |||
ae153dc243 | |||
d53d229c49 | |||
e58c2048ed | |||
ffc898a7b8 | |||
3e4fe89a45 | |||
6d35508f08 | |||
4ce01f8bb4 | |||
712dabd16b | |||
e756e85da7 | |||
da89ea22fb | |||
aeb34199f5 | |||
a2707e1c56 | |||
1f03a2d957 | |||
a56853be05 | |||
f07aaf417a | |||
7fe7062c47 | |||
8fc252ec55 | |||
66ba91de8c | |||
f4616789a7 | |||
8000ee92e1 | |||
4166f30fb4 | |||
0845d61d2b | |||
8b45ca0da2 | |||
a24683b40c | |||
c29366328e | |||
dc3858c82d | |||
5f6419ff76 | |||
db631005d3 | |||
3249060f67 | |||
20cddf5e81 | |||
7f4a773b88 | |||
c1f1b8aa0f | |||
dc416bf255 | |||
60f72b2785 | |||
579ca37ddf | |||
3914c6c875 | |||
ec40b4f7fb | |||
23641fcd06 | |||
09cd32034b | |||
497564359b | |||
3bd827c73a | |||
1b9414f292 | |||
dbc92d6ee0 | |||
135fd6864b | |||
8d4c4fdf6c | |||
8a108bccae | |||
9a1fe154db | |||
2ed58aeda6 | |||
f035af3738 | |||
5b3be5c102 | |||
76220a4523 | |||
c03689ed4c | |||
5319046dc7 | |||
5d321949e6 | |||
c21c142a9a | |||
252d87d332 | |||
888e683856 | |||
a5a674a270 | |||
02ce7bdc7d | |||
8ea21c39b7 | |||
c725514841 | |||
44b5fe0631 | |||
2edf8ba157 | |||
bd3352423a | |||
969a7f2d1b | |||
a83113935d | |||
1bec551945 | |||
41f7bcc0d2 | |||
b396344eae | |||
1c6f74cad5 | |||
d4753049d7 | |||
87bc2763ce | |||
0bde2d595e | |||
aaaf039779 | |||
03d8ac75ef | |||
5ff044346f | |||
358ef99ea2 | |||
d89156810d | |||
2d04ff93f5 | |||
271785b7fb | |||
74523fb534 | |||
03b8ae7215 | |||
614490d29b | |||
0af047dd83 | |||
6792e376b4 | |||
bb085189e0 | |||
b18ea3e0cc | |||
f1f7c45017 | |||
90a5febd38 | |||
61189efe72 | |||
d9ecabee87 | |||
f4bac6a17f | |||
7bfe59a89f | |||
40264c17a0 | |||
149a4e0de8 | |||
23731cd49c | |||
dd0ba10b44 | |||
21304e6751 | |||
9e0fa99c69 | |||
1aa0e1997b | |||
f24999027e | |||
d206aad282 | |||
e9d9bc82d8 | |||
aef9786fa7 | |||
97aa7268ae | |||
18badcfdb4 | |||
ccb8e4b414 | |||
2402515fcc | |||
f052e5f1ab | |||
f526bce618 | |||
8067244765 | |||
d26255957b | |||
f4c783cba5 | |||
864598908d | |||
35d0aba9e6 | |||
454d3987a5 | |||
53a2b6d013 | |||
e06cd9c0ac | |||
b96ce81f89 | |||
fe7c1b300b | |||
a03727e28a | |||
b6b045bf76 | |||
a99ba2de24 | |||
9c50cbf431 | |||
5a281445a3 | |||
aee48f3f5d | |||
37bd9e0e6c | |||
b18c744f63 | |||
240de006db | |||
329a282aac | |||
8708ad1b44 | |||
cf214ff583 | |||
527eefbe92 | |||
21346eb786 | |||
1e7f3a4b23 | |||
0fbc342fe3 | |||
05e91ab9a2 | |||
721962589c | |||
1033995801 | |||
e84e1193fc | |||
007a43c012 | |||
f3af561404 | |||
2cb0b6756c | |||
81c21a5328 | |||
9a2dc53097 | |||
e9891977ee | |||
a9a65a0f4f | |||
52e6dcc51c | |||
9376c67ade | |||
0940fb5f00 | |||
8b2491ea23 | |||
0b236a2fc2 | |||
df997dff49 | |||
83a035082a | |||
e19349343a | |||
ed2744201b | |||
c654aa3416 | |||
c28c1b4bb3 | |||
46b223e3c5 | |||
2822f5a126 | |||
2c8003a9fa | |||
0a91a28b27 | |||
da5fdae006 | |||
a685e33f20 | |||
af225a42d0 | |||
815b271084 | |||
73449c09a7 | |||
e413350be3 | |||
c23e7cc20d | |||
1ee0c0c291 | |||
3b37ea6c8b | |||
251e0d3b82 | |||
f6f169b4eb | |||
2016afc9db | |||
6af75492a9 | |||
8158966af6 | |||
d8fafc1b03 | |||
b241bb9cb7 | |||
6910e18a83 | |||
1f64db6a33 | |||
28d6370c35 | |||
6fbf6a684c | |||
2966203653 | |||
32874163a8 | |||
fb66b77d67 | |||
8a1af056e2 | |||
353a1fe0a0 | |||
44716c2f24 | |||
a44b5c9c36 | |||
af6aa3c0bf | |||
9d936a3d85 | |||
b2937223df | |||
7b8cbbba85 | |||
b8d7f00d58 | |||
1664cb326a | |||
6c9e04583d | |||
abd26aeab2 | |||
896ba8451c | |||
224592c547 | |||
6b57d716e1 | |||
dc363c8f35 | |||
3eb46868ff | |||
43ad99bbe8 | |||
bb7750d8ee | |||
520436cd9a | |||
433ab1e78b | |||
57b6ecaf40 | |||
ff265c7ebc | |||
15a11c13a2 | |||
8d2dc76be3 | |||
d85f0d087e | |||
6ffa291ec9 | |||
fcae2189eb | |||
7d3b90d711 | |||
5fe7de2bff | |||
f18139d06c | |||
c1db86925f | |||
c7de1fcf24 | |||
f4c7c2bf28 | |||
904709e3f1 | |||
9840f5c1c1 | |||
f54209c451 | |||
8c957ca939 | |||
1b1ebd2f70 | |||
469a2b5c90 | |||
f289374e70 | |||
79ee271a5e | |||
3022c00a0b | |||
1cdc5d662c | |||
12d50e42b4 | |||
7fb4508fa1 | |||
ab2b8df1cb | |||
f196f05f20 | |||
b95bbe2fae | |||
7008848f9c | |||
100a56e8ef | |||
f67b0e9ff8 | |||
6eed6cf362 | |||
c1cbcdc253 | |||
c48c620048 | |||
0d9dceff88 | |||
96488684e6 | |||
c864dfadf6 | |||
1eb7e41674 | |||
880c51687a | |||
df0a38e0ba | |||
6d3778cafe | |||
dc1a0dcd5e | |||
5b2be6f9df | |||
ed31deab00 | |||
ef7fe7586b | |||
51e181c529 | |||
f8cd6fd445 | |||
c86f5472fb | |||
a42ac3e58a | |||
db1abff857 | |||
efee2062dd | |||
9dc5605663 | |||
8bb3ba4a16 | |||
78eee7dc9e | |||
65a11a7239 | |||
03c18ecfbe | |||
c9d461ea93 | |||
b0cef3c76d | |||
c572dc0cc6 | |||
0d01461de4 | |||
d1b57501bd | |||
dee02e6642 | |||
198f3073f9 | |||
b7d1d91309 | |||
aa79c5131f | |||
89dd0f2925 | |||
2e16b11db8 | |||
b8a6592369 | |||
c3d64f996e | |||
1a70477fc7 | |||
3a01949f81 | |||
0c17668844 | |||
04a3cb1ee8 | |||
831ab76152 | |||
c30d38829d | |||
727fa610d4 | |||
2452bf8f01 | |||
7bd474a843 | |||
5fff927dc4 | |||
8df102ff4b | |||
d97129e7ae | |||
ccdf117c8f | |||
a4c2421e74 | |||
bcdbced0dd | |||
98216d8a3b | |||
7daee06383 | |||
df7fe705eb | |||
89b96511bf | |||
74a63804dc | |||
458767e3a8 | |||
f0c96c28bf | |||
ebb980eaaf | |||
a617a6a6d6 | |||
ba517249e8 | |||
f33f763278 | |||
b893085d1f | |||
799ab94af3 | |||
bf7558830f | |||
1ba570092e | |||
fac024b90c | |||
2f63fdbde0 | |||
1a3a215f59 | |||
722cdb78ba | |||
36a79e18cd | |||
4da6e42fc1 | |||
4c8f2e976e | |||
01fc1228d5 | |||
2309088b03 | |||
c2f8695c9a | |||
249fcc5066 | |||
6ee76e9280 | |||
0b5438d3fd | |||
34bdf4c7c5 | |||
af2f84805a | |||
3ca2453fc6 | |||
8a10a9079f | |||
c7524d924b | |||
7051207495 | |||
8543092b9f | |||
6758922a63 | |||
baab2e88a5 | |||
3949b35360 | |||
b4276341af | |||
e4ead96d3a | |||
eb45bf2d0a | |||
d6bd072ea1 | |||
a5a0d90349 | |||
e726d1e184 | |||
459b95bc7e | |||
6b5354fb11 | |||
8b2b3eef73 | |||
115b986edc | |||
c14a505456 | |||
c884510173 | |||
53ea383b4e | |||
1745b64ceb | |||
77f8dc9675 | |||
4b92e35edc | |||
e9eca35816 | |||
ae635c752a | |||
05b07b8cbb | |||
4f54108a9c | |||
f55ef1d7ef | |||
14bc9f0946 | |||
493598c1ba | |||
96efe302ce | |||
e25bcb35d7 | |||
05b485b678 | |||
360d4ea8d1 | |||
c06d612432 | |||
c88722983e | |||
33a326854a | |||
9930c7288a | |||
8d9fab0389 | |||
c3efb9f7cf | |||
4f7dca7c2b | |||
5899b89ef2 | |||
99e5a4d1c3 | |||
b3a5b4eb82 | |||
71067a8cb5 | |||
be00f63fb2 | |||
db4fe8031c | |||
270ce539bf | |||
b9f483bc27 | |||
8ee624a542 | |||
621355b6ef | |||
72b5e0cab5 | |||
74272ed754 | |||
4426c3176d | |||
7b0a09e84e | |||
ab947f1f08 | |||
5998c83b2a | |||
434fb53dd1 | |||
75a95acf04 | |||
de14636e10 | |||
931a132161 | |||
0a7271a185 | |||
de39d57592 | |||
7c039340ed | |||
b1ca85b910 | |||
50559de6d2 | |||
7d04f371a5 | |||
5d8ccbafe4 | |||
803aad3b3e | |||
1a4c8c08ee | |||
a2698dea92 | |||
9cb4dad4b4 | |||
ddf4a667b1 | |||
fc74bfeb23 | |||
3ffae1f5b8 | |||
dcf40db225 | |||
9172eff65a | |||
0c484e8c76 | |||
beb8b9fe69 | |||
8f6b8895f4 | |||
a650312858 | |||
ff2c71a74a | |||
126c6a23bb | |||
e0c61c1334 | |||
f7aa97d45e | |||
a1c7584875 | |||
817b4ff08e | |||
ca3c1269f5 | |||
0a52396706 | |||
7c4969c92b | |||
45a94203f2 | |||
7189328f80 | |||
134fece36d | |||
985dbcaada | |||
e78ba38e94 | |||
7f56281c26 | |||
45e05dac30 | |||
66946117e1 | |||
462c4a2954 | |||
5ce8adcb13 | |||
3bdd78f341 | |||
b5aa4cf41a | |||
a5e2463097 | |||
a869a2823b | |||
ff3e26452a | |||
da5b27ecc6 | |||
c618b4efaa | |||
4cc341e4aa | |||
82d97cf6de | |||
600ae662a5 | |||
efc9047f87 | |||
aba32917bd | |||
ea3c0e1772 | |||
058052b46c | |||
7c87da012e | |||
bf1e859e72 | |||
289cef3101 | |||
085c307b8b | |||
72783edb17 | |||
3141ad31de | |||
40ceec9737 | |||
723ec65ac6 | |||
3ae62573b7 | |||
6499709221 | |||
813fcabe13 | |||
92ea9b97dd | |||
0c9b109bf7 | |||
2cbd158a11 | |||
7dc3ff4544 | |||
8d5e9f865c | |||
8096765f0e | |||
8eb81bb153 | |||
c81435c994 | |||
7548b44d1b | |||
bcc8dae16b | |||
b014ac44c8 | |||
a806493bc2 | |||
b593047fb1 | |||
9845366a36 | |||
15d0f54a88 | |||
8088055d38 | |||
0c4b57a6d0 | |||
d3707ad4ef | |||
13a949d9ec | |||
6f683682c3 | |||
a920bf768e | |||
a183815870 | |||
d0bd02eea0 | |||
37ea778776 | |||
f37fd0ecfd | |||
3d400ca21d | |||
71aa8f3670 | |||
37ad9e9a09 | |||
1af906c224 | |||
f899831ed3 | |||
08900b5b94 | |||
5a33fce8bd | |||
acadf0289e | |||
2e5ccaf7fe | |||
63d9c0ee46 | |||
68edda7bf5 | |||
999d9f4d6c | |||
ce00677880 | |||
5656db3e3d | |||
c213edf7ee | |||
a7540b4947 | |||
5f112aad44 | |||
f198f281cf | |||
bf751dc7ab | |||
6f364e60fa | |||
2b6df63aee | |||
def8af7d8a | |||
14465b91b1 | |||
d09df9c02b | |||
897263e5a3 | |||
4864f80656 | |||
105dfc93f1 | |||
d5d99fa3b9 | |||
8a7e50381f | |||
f45f2ec202 | |||
1a4e35eb50 | |||
ed8a12f96f | |||
525fdcf73c | |||
840ab7cba4 | |||
9415bec557 | |||
712d497a2d | |||
dbc94af51d | |||
86d8c9b18e | |||
1857f02af8 | |||
10f0b3b877 | |||
0be9b5bddb | |||
6bb4f0b41f | |||
f4d33389a5 | |||
c8254dc0a8 | |||
6b36bf772e | |||
d2ba2960dd | |||
cd08fdf76f | |||
aecb2411d8 | |||
9609bd99bb | |||
5102cdddc1 | |||
3a05817b41 | |||
51465bc0d7 | |||
2322534648 | |||
3f23e0e6b9 | |||
e6a16f08a3 | |||
0fd0279543 | |||
f5e58c8bf5 | |||
5f59d7ba5f | |||
62a145d8b3 | |||
c09c462a6e | |||
3d578f9df2 | |||
363ceea5c7 | |||
c51f750831 | |||
cf911c023d | |||
ea59cf16e8 | |||
91d210ce79 | |||
f0dcc3a104 | |||
b2d6b554e8 | |||
1addbe361a | |||
97617d699d | |||
ac969dc737 | |||
c014bef84d | |||
f2f9f5776c | |||
cb9c354c28 | |||
b1fb90ff6d | |||
ee03cf8ae9 | |||
a01af67948 | |||
3b64c58198 | |||
88284a999e | |||
856802ef35 | |||
24c78de5f0 | |||
1de8a4606a | |||
672d4507b2 | |||
25dd1a21e2 | |||
6f2954dffd | |||
8cfee9ca7d | |||
b124a29e05 | |||
b3163f99f4 | |||
fe4b516bd9 | |||
f0d928a433 | |||
edf0fd4381 | |||
1de737b7a3 | |||
29ec5d4855 | |||
381fd82c13 | |||
8f61a148f6 | |||
a4f6ca0c9c | |||
15710a0595 | |||
f06b71d961 | |||
ccd2b9eb75 | |||
c8a8670aec | |||
110167f085 | |||
66102fb2d4 | |||
4fdcb57753 | |||
8b4a16a3f3 | |||
5c198ea85d | |||
15c5e5c502 | |||
116d908002 | |||
0c932e3ace | |||
6a04aaca55 | |||
a10974ed6e | |||
cd19a72c41 | |||
36976acb2f | |||
4677d3d782 | |||
82227846af | |||
eafdf3033f | |||
d54be4ed7f | |||
a1e5d0fd00 | |||
d4e800175f | |||
39001af9a0 | |||
c6357f3c86 | |||
4eedc31893 | |||
3d4cc32525 | |||
7db754e94c | |||
e483b6b51f | |||
689f5c1306 | |||
fec77d583f | |||
7a97fc370b | |||
b9b51c2dfc | |||
ceae7eb47a | |||
1c113f915e | |||
514f4011ba | |||
a56977f4c5 | |||
86dfe456fd | |||
50e7275c3b | |||
1e0c32f7a3 | |||
d227000de3 | |||
61c6f33103 | |||
d300968ee8 | |||
7bc9434596 | |||
632d8f384a | |||
bcb0c381e5 | |||
dd069de519 | |||
418eb8025c | |||
9ab3a9d072 | |||
4c681eb48b | |||
58281208b9 | |||
5a16d43fef | |||
95e30f0e08 | |||
d5efebbf47 | |||
e5a65d5807 | |||
1a0d1f64f0 | |||
bd06dd53e3 | |||
5231666465 | |||
a7e231b35b | |||
e25436576a | |||
8e1f41f1e4 | |||
04ce64e9b6 | |||
2aa8de87b2 | |||
929f1d02b5 | |||
d767dd998e | |||
5af8257e19 | |||
a27b7f8d1f | |||
2d7d9cf7d8 | |||
f842bbff8d | |||
114f3cbfb5 | |||
3f36730dba | |||
089d812dc8 | |||
9d0709dfe8 | |||
2e5297e337 | |||
5c266302c5 | |||
6cc148f6a6 | |||
1607930d07 | |||
1a4e2f4770 | |||
caaf6b178b | |||
6d96f105c6 | |||
583808d5e7 | |||
5768a4eda7 | |||
4997d4b0b5 | |||
179709cc09 | |||
44b72ccbd6 | |||
5b2fb07e44 | |||
4c79e26078 | |||
4bf0ec94c8 | |||
4f5e51beb5 | |||
05c3e471ae | |||
1cf520254d | |||
313f0467c8 | |||
bd6a4a54a9 | |||
57c2f2ef1c | |||
db0a51de2a | |||
711db4c790 | |||
cbd02f2a87 | |||
036161cb38 | |||
c947539301 | |||
471abf7f29 | |||
e4714627a4 | |||
998e824bd8 | |||
e492cce206 | |||
d5955b60c0 | |||
f33577b317 | |||
a455e8c0ab | |||
79a960d8a5 | |||
f456f5da46 | |||
ccd26bfcd7 | |||
f6ba6d5590 | |||
dfe17662df | |||
28c217eb66 | |||
d594005d49 | |||
4410d2e44d | |||
5dff60adc5 | |||
9e5824df85 | |||
543be801ab | |||
ab6b28ee60 | |||
bdd264cd5e | |||
5ae3f59092 | |||
0c89e9c2d6 | |||
30a1a69850 | |||
e7a5eff061 | |||
446ae301f8 | |||
2dd3eee0dd | |||
9c7f2cb0c3 | |||
b61bfcefa7 | |||
9cb7680211 | |||
20115444b6 | |||
7c12b1026c | |||
35bf50ab15 | |||
5b376de5f5 | |||
2259b7ebc2 | |||
d859844fea | |||
a56db9a47c | |||
cbe5cf8ca0 | |||
6075f7d2f1 | |||
55e3f370fd | |||
b11b3f4fad | |||
135d654999 | |||
6d1053b8e5 | |||
71d3457b82 | |||
6fec2e56f6 | |||
d9f0aa223a | |||
8b0374cc4e | |||
08748bbede | |||
7e7e27a7eb | |||
77b7511235 | |||
df2ec0aa38 | |||
afc79ce0e3 | |||
eef93440d0 | |||
4fd6b5f5e1 | |||
405e7eff27 | |||
606bfa89b6 | |||
964332db12 | |||
f7cdadc9c2 | |||
e64f196c0d | |||
ed29e3d934 | |||
a7413723b4 | |||
11c9559ef8 | |||
7af4a60ec4 | |||
7b2c59bd98 | |||
861e38d157 | |||
580e397525 | |||
7717deda0e | |||
cdcbef088d | |||
fadb2b46f5 | |||
a30199d879 | |||
6ff7debbdf | |||
f8a196faaf | |||
94d6ceb4df | |||
200913f631 | |||
53c4aab6af | |||
dd6b539119 | |||
e0bbb58ec5 | |||
048ada462b | |||
e849b22d3c | |||
f76f742ba7 | |||
c939954f84 | |||
56bea54536 | |||
9793a0e521 | |||
a8e0eee7df | |||
ad75f9de4b | |||
2c60215156 | |||
def5276f84 | |||
6391ec16ed | |||
a94fd6aaf6 | |||
f9bd72e1ee | |||
841ad8476c | |||
4eced2518c | |||
331985f0a0 | |||
37fc1d721f | |||
82c3778082 | |||
2d0bf7d40d | |||
fc382e20e1 | |||
a2cd1ff367 | |||
2d88dfdaef | |||
b5b670b8b9 | |||
e02d0f39ec | |||
003852f884 | |||
69c2fe19e4 | |||
b79c10c122 | |||
94ab4f6164 | |||
af2a27935b | |||
5b34d2be6c | |||
ec13759ca6 | |||
b7563bfbf5 | |||
7aa686b650 | |||
d0e730fed4 | |||
58850a0b0c | |||
ef22f70e18 | |||
88cd8b2d74 | |||
84ac188cee | |||
cdc7df8625 | |||
2d011468b4 | |||
4557f2b03d | |||
b83cafc454 | |||
dabcfca67b | |||
09212bb6b7 | |||
d3a89a2a22 | |||
bfd306e9de | |||
cb9a06e249 | |||
f9f95a2b92 | |||
91be01fd63 | |||
04fe7c29cb | |||
df0f609cce | |||
e8ffa283f2 | |||
b04944d9a5 | |||
eb06bbfef7 | |||
0e364a4efe | |||
d3b2ce7b35 | |||
742b78523a | |||
a432f939b6 | |||
6908feb3ba | |||
46e38bf714 | |||
5010f693ba | |||
08ab85a9d5 | |||
16deec7443 | |||
9cc1cfc973 | |||
860f2f3855 | |||
55e03c4eb4 | |||
fc1d07fce6 | |||
e371ec1dc4 | |||
5e57f1bcad | |||
9884927b8a | |||
f5bef3980a | |||
36f7b20784 | |||
5e510e7a67 | |||
caedb7fcc4 | |||
0a12cb0281 | |||
2400a078d7 | |||
028d1d0efc | |||
efdcbc44c4 | |||
cf3a98afcc | |||
88d7b0a83c | |||
b92e7eb781 | |||
2d4099577f | |||
6d8f93d8a1 | |||
d66f4e8ac0 | |||
5d44dedfda | |||
eed7eac10b | |||
37e7f1a9a8 | |||
4a94d9182e | |||
7731479607 | |||
8a0336c2ff | |||
ca7224c086 | |||
df8a696bb5 | |||
d06b59c92b | |||
df913326bd | |||
b587e147b0 | |||
e8116f21cd | |||
2b5c69d678 | |||
4079235b0d | |||
45efa94ba4 | |||
cc8a6bea65 | |||
4aa3180027 | |||
7f1ab6a5cd | |||
3a90f69efc | |||
2e9164584b | |||
9435994405 | |||
8d12c77e26 | |||
8fc4e1ecfe | |||
7d7fe5962a | |||
961fc9d0ce | |||
3ceeecc63e | |||
5b66e4860c | |||
5e10cf69b7 | |||
050ca88085 | |||
ac330a3f7b | |||
01a14e3b3c | |||
a30be1ca5d | |||
68173914ca | |||
f070b1823d | |||
2b5feca806 | |||
d75f621152 | |||
8bdbda1db9 | |||
a2d80d8f2e | |||
8b5289d5c7 | |||
8fe578c958 | |||
ac75304a09 | |||
7fe4558bee | |||
f3eebb1dfc | |||
250dcc26be | |||
11fdef56db | |||
d6493f31d9 | |||
8b8d47f6f5 | |||
aaa3fc08af | |||
9707de4d4a | |||
a7b72ed5c4 | |||
b2288fcb9a | |||
e0e23c2f9d | |||
cd3f20e49b | |||
1e187ab4f8 | |||
aaeb852f23 | |||
d26a7ad337 | |||
911b26f163 | |||
8920f439ce | |||
405ea38959 | |||
bffc82f752 | |||
e2693a4192 | |||
c7740a7ac5 | |||
c038acea5b | |||
48f506277a | |||
b8dc3c11c1 | |||
f0e3303aeb | |||
1ba1e466f7 | |||
eb690fc39c | |||
14bf45d099 | |||
eee394cf85 | |||
8675fed49c | |||
0dcff2e647 | |||
1b8ce33aa4 | |||
a68cff51b1 | |||
9e604b4038 | |||
438414a64b | |||
30d784c27c | |||
070efe72af | |||
2cd2a4df45 | |||
e8e0890341 | |||
d6b1c8df2f | |||
6d43202efb | |||
08b8e9999b | |||
15c77f85c1 | |||
fb16f47f2f | |||
964d95de5c | |||
64ac4e382a | |||
4a61758bde | |||
ff8acfb8de | |||
06e82fe761 | |||
94dd335fac | |||
437809d337 | |||
e1fd614dd1 | |||
3b4c3ec074 | |||
8803e7834f | |||
b469b6d8d3 | |||
7e13970a4f | |||
7c51a36012 | |||
405eb15291 | |||
b7d2dac9bf | |||
76193d37da | |||
60517f8471 | |||
c6c74ab1e3 | |||
9aa700fb10 | |||
d2bea09a60 | |||
45dced3fcb | |||
5e2a5c0266 | |||
18d9811de7 | |||
cca0b4c0f9 | |||
eab9123b86 | |||
0ea4095238 | |||
b3035e21ef | |||
3d872b03e3 | |||
1ae335e058 | |||
09498d8bb3 | |||
bfd8f52497 | |||
fc36ae22c9 | |||
1fd5486def | |||
7ef044231f | |||
9e5e0eb9b7 | |||
d3b07c8131 | |||
3bf885329d | |||
e95ca66c9f | |||
9e24eda752 | |||
77b17c6737 | |||
91d073c2e8 | |||
4c0c558945 | |||
5b871d3dd8 | |||
2e922d602d | |||
3100e8bf21 | |||
b42b461418 | |||
604d125a55 | |||
ba0680f5e6 | |||
6eab24270b | |||
53c9094d46 | |||
60bc62932b | |||
99abc49040 | |||
6337762ec7 | |||
c33918c71f | |||
4f0059198e | |||
134d66924e | |||
295c209c67 | |||
5e0562e80e | |||
ad1ba85eb9 | |||
ac79496036 | |||
6d17d5ead2 | |||
cfa60dcb76 | |||
52c679a3cd | |||
73ccbb1bc8 | |||
de8d8542b4 | |||
a897ec48d2 | |||
3d5647b16f | |||
18111629fa | |||
e7d3905093 | |||
b08e0a6415 | |||
2527c59e55 | |||
1a3b96b054 | |||
3e22c99c96 | |||
a62c96f85e | |||
8a6bfa3816 | |||
e8379cb3f7 | |||
8bfd678d49 | |||
1b62e65a5d | |||
dd99e77881 | |||
18b3eab909 | |||
7673e60654 | |||
695272f980 | |||
e190ecbefb | |||
37f616bad2 | |||
06a6bab2c1 | |||
cead2a6303 | |||
0c4f99b161 | |||
815cf60f45 | |||
571c71392e | |||
24484ed801 | |||
3e35e4052f | |||
a2df2b12e2 | |||
fe15ff3c51 | |||
39cbe5f31f | |||
23793fe096 | |||
c87a009b37 | |||
9bbacd38f4 | |||
c9c3d94bf9 | |||
d55e38d4a6 | |||
3f9ede79ed | |||
ac0cd7bc68 | |||
56d361eeff | |||
9084a56e36 | |||
7cfcf0acec | |||
2ad60379e4 | |||
0dda2577e1 | |||
cc9bb56d2d | |||
f44612970a | |||
70f7dd876f | |||
b19ac26325 | |||
4010a944a4 | |||
863a662ec6 | |||
08588c873a | |||
a903cae00b | |||
907f8fff4c | |||
1c8913090d | |||
e61c3f0de3 | |||
cb4a3a03a2 | |||
c90c96fbf6 | |||
97fc51b0cd | |||
0b4ecdfc05 | |||
875b0e6f01 | |||
03b1821586 | |||
8d0580461b | |||
7fbad900d7 | |||
f5184ce749 | |||
e9ce14069e | |||
c08d891742 | |||
b4c730e537 | |||
fd257a6d39 | |||
299598f0c4 | |||
f00cfa005e | |||
9dc6294065 | |||
9fe3efcb32 | |||
30431199cf | |||
db7027a367 | |||
26883208cd | |||
60730e81d9 | |||
ca4868cefd | |||
9e84fe20e6 | |||
c6c107cad1 | |||
5e92fb47ed | |||
984f1ae7fb | |||
38f4c5f310 | |||
dc18ec533e | |||
07ddeef568 | |||
14c3d10ba9 | |||
4a925c9e9f | |||
5a56c08c91 | |||
75ace1192a | |||
c348e788e4 | |||
557d476001 | |||
9b69c446e2 | |||
4d0365bca3 | |||
990987b174 | |||
04a4624f14 | |||
7cc5f53cad | |||
389e929c8e | |||
048fe287c2 | |||
9fa2092a21 | |||
2e2b6eacd7 | |||
eef0a5c7e8 | |||
e596ea6e3a | |||
15adc21e1f | |||
f5b511ccce | |||
2ad137c280 | |||
3a24adc57f | |||
61f013e4cb | |||
6c8a5723f8 | |||
a4cb067130 | |||
b074a963fe | |||
0ebd830814 | |||
4772cbd23f | |||
1fc21aed1c | |||
c1db1042ad | |||
0aa0ea19fb | |||
ada82582ef | |||
927ab1f040 | |||
8bea13d651 | |||
10b3d9005f | |||
5dbb2b45cb | |||
bfed8a8be4 | |||
4de80dc29d | |||
442369a041 | |||
e8ea0664ef | |||
698fb20c12 | |||
c6888472dc | |||
ab17e0a9f3 | |||
b99b62a211 | |||
1dcf14289d | |||
5732fde4d9 | |||
440dba4372 | |||
2bee3ac33b | |||
2189d15e5b | |||
66cf9ff70a | |||
8e69801e24 | |||
b5860a4283 | |||
aece5e67b7 | |||
9395312079 | |||
8a7eb57c24 | |||
12af0f462f | |||
db3dcc6fdc | |||
9d4973829b | |||
7bdb1abbe6 | |||
fc525c8144 | |||
cdb02c992c | |||
9f4493a0b2 | |||
397fc74a56 | |||
93f681bf7a | |||
96535366c3 | |||
3fbc8b130d | |||
dd93ac49a3 | |||
cf4cda2434 | |||
98c086abb9 | |||
d0fefca6f9 | |||
5c6258390c | |||
b69524b498 | |||
1870dc8cd9 | |||
762c1347ed | |||
3bf86e4ca0 | |||
29355c749e | |||
26816b9366 | |||
c239b9af83 | |||
934a26f1ef | |||
fbc0c28f5e | |||
986dc3f52c | |||
8660caae9b | |||
1a6dc72898 | |||
87cd4712e0 | |||
361655dd42 | |||
234411ff9a | |||
88122e0e76 | |||
bd0e69b2bb | |||
af364e7fe0 | |||
b6eecfd39d | |||
17df0ee6b3 | |||
730cb6ce67 | |||
2aaf4228ac | |||
4a4629763e | |||
7101ac1b4b | |||
3a6982e7b2 | |||
88978077b5 | |||
8015f560d6 | |||
e3140235de | |||
353e2e027a | |||
b0b93ba1e6 | |||
1d156e3854 | |||
b0af949480 | |||
09535e294a | |||
dfa9cb57aa | |||
5d64dfd4dc | |||
dc1bc741b4 | |||
34e539cdc0 | |||
ad7dfb0181 | |||
c4dd28e252 | |||
147972273a | |||
b15c406924 | |||
d4dccd17ae | |||
18002bc837 | |||
8184c8021a | |||
d0ad8022d9 | |||
e8600447ed | |||
f587008d61 | |||
b762760e96 | |||
36e4ab00da | |||
1ef3591e17 | |||
bebdc851c9 | |||
0f8372c0fb | |||
4819bd5608 | |||
5e78479371 | |||
d3a00014c9 | |||
3f0b3d7361 | |||
2d5a50e80f | |||
2080534744 | |||
dbf231865d | |||
e007006daf | |||
14f2d190e5 | |||
53d449d5d7 | |||
fd2291c004 | |||
82faea7f85 | |||
ae39ddf031 | |||
3301e0ba50 | |||
ed72b096b9 | |||
c323ab5275 | |||
725e46da4a | |||
abadc79756 | |||
0a27432b65 | |||
adeba99d38 | |||
7f97279c48 | |||
dd0418af18 | |||
995e56677a | |||
2a3b475b99 | |||
6b4eb79237 | |||
164a9c29fe | |||
f566af0c45 | |||
8c7c1f204c | |||
1711eb98b4 | |||
91926b2b1c | |||
9a343cfe8b | |||
4981eb38fb | |||
c19dfdae64 | |||
a496d8af65 | |||
bb4da4da02 | |||
8ee6f852cb | |||
aace1bd71e | |||
8daeba450c | |||
d28e5245d2 | |||
1c3ed864e7 | |||
49892690ff | |||
bc523abd62 | |||
3da47352be | |||
c1f48d6a0e | |||
95cd37827d | |||
780e7f9497 | |||
0acd0bba3a | |||
51b410ce01 | |||
7bc560b2ec | |||
221db1bd1b | |||
52ac4f70e1 | |||
5e9ffde0b1 | |||
410edc1df3 | |||
71a0dd4cc9 | |||
9c6673495f | |||
992b3ca95b | |||
492e8ad655 | |||
1d8183fe5f | |||
ddefc06fbc | |||
84c700eecb | |||
9c3776d034 | |||
ded7650a60 | |||
4896e7d096 | |||
cabbbf0afd | |||
0cc49e6ca9 | |||
9b7be1afb6 | |||
3cd8ded9d7 | |||
ce8622b9cd | |||
2aa6879a44 | |||
ad77c3d2d5 | |||
3e5d5abd53 | |||
c5443b6e82 | |||
313f315896 | |||
7bb546c7c2 | |||
cc2c7db9fe | |||
6fa9dc599c | |||
e645386842 | |||
048271f59b | |||
88bd368660 | |||
0e3f03836a | |||
335ebadbf8 | |||
464ea862b1 | |||
c34fffc2c4 | |||
81d4922740 | |||
4fcbc3749c | |||
233aa34d54 | |||
37b438a77f | |||
1de22f0f10 | |||
e9e1497830 | |||
6f05d28c56 | |||
e336e5bcc0 | |||
bdb95db5be | |||
76cd6ac6bc | |||
97d2584a3b | |||
ce874e183b | |||
089aefc7af | |||
fbd9adb135 | |||
5b23595710 | |||
c76e72b747 | |||
dee5c302a8 | |||
942e75fa28 | |||
ad44b7da9c | |||
4acd3c5bd2 | |||
1a0f70450c | |||
701abfa548 | |||
7938e8f5c8 | |||
0fccb22cd7 | |||
e4d5614f3f | |||
f190b272b9 | |||
b6411aba7a | |||
9a004c4535 | |||
78603e7e31 | |||
1acb51105a | |||
1e75e6591a | |||
90ae426e24 | |||
e31f74c3ad | |||
dc4ac6345c | |||
f0ce7b0e0f | |||
c24a619594 | |||
25e61b276a | |||
58b2dff0cd | |||
d14bc121c0 | |||
9ddc6659ec | |||
d2074cb669 | |||
6d8e1a7ab5 | |||
678e1fa927 | |||
705e74f4b9 | |||
f5211aab2a | |||
cee72065e9 | |||
04cb1e0161 | |||
35e6aa0d94 | |||
19db9dea30 | |||
49bd28e2d4 | |||
8ad4378960 | |||
a85873d294 | |||
5dd0efb280 | |||
2d00b1d0fb | |||
ff7e0c72f9 | |||
ae78b3473f | |||
75fe38ab08 | |||
52b221e3c9 | |||
abeefc95bb | |||
b2d2553305 | |||
ced3cbde73 | |||
ceb18b528a | |||
28faf170e9 | |||
494fcfdb8f | |||
1982fd630d | |||
cafa164f60 | |||
4915c31b03 | |||
8d1ec88195 | |||
be2d9f0427 | |||
e605d58888 | |||
1ed63e99d9 | |||
1ed7c59491 | |||
aaa4361158 | |||
5ada39f39c | |||
6e4599411b | |||
80943a65e6 | |||
c066915b8e | |||
d1342afe93 | |||
571d0a6500 | |||
617d636432 | |||
978a86cdaa | |||
eb6df560d8 | |||
c335069310 | |||
0ff9af5ba5 | |||
cd01c7ebd7 | |||
edb2e40383 | |||
ceaa485f06 | |||
70aa9531ef | |||
394c8e8037 | |||
255068f52d | |||
d8005f455c | |||
036c76798b | |||
edfd10262f | |||
afea72133a | |||
76fec39670 | |||
01cc8654e2 | |||
f5f7325ce3 | |||
a49172877d | |||
db02e2b29d | |||
b364f4a1be | |||
227f397d5e | |||
59da2976ab | |||
b6a6d52a92 | |||
db0d213710 | |||
5d0e7663b8 | |||
3f00140f5d | |||
5e5063cbdd | |||
bbcb782b33 | |||
f81a7ad9ab | |||
4b6a762a56 | |||
55ce7b1339 | |||
cac4abbe1b | |||
44897067ea | |||
981efde3ff | |||
30a2a6662f | |||
f93f2fe03c | |||
662efbcb68 | |||
2eac037408 | |||
fd96dfdb5e | |||
020cb77613 | |||
cbed6547e2 | |||
cd0f1a8eb2 | |||
6fbfb69ee4 | |||
ca7d2feedb | |||
613b462662 | |||
ef35971dfb | |||
9bc6698f58 | |||
d2f015d5d0 | |||
a67f46bec5 | |||
1a421e96d7 | |||
52810aa626 | |||
0a28ffb9c4 | |||
dca38d10eb | |||
8940ce7dbb | |||
2ff7acf207 | |||
87202b4915 | |||
b03a2d7995 | |||
f0a223f337 | |||
d23a596c3d | |||
3342a6a910 | |||
598e23622e | |||
03758a4f92 | |||
77a2a5eb01 | |||
845482a623 | |||
57db25e4a6 | |||
8f781c4757 | |||
09f892a7fa | |||
3b6b2c274d | |||
7fdd41697e | |||
aaa850c6c9 | |||
2a206e1b4d | |||
c4d54fc427 | |||
a9b180bf6f | |||
a9de6b6d00 | |||
1e30fcf235 | |||
56741fcea5 | |||
3030d24f74 | |||
f2508753b1 | |||
5222f0a8a7 | |||
2d4c41d2be | |||
ef8118f40f | |||
f69f044aaa | |||
9f36fd50b4 | |||
fc15276c10 | |||
e440357bec | |||
78cb306c07 | |||
e04d014a3a | |||
2f5e191b2d | |||
2c0928efe0 | |||
7d6985647f | |||
70f15ffd6f | |||
da14612049 | |||
cc0ae6343c | |||
2f32d3e6df | |||
073ab0aac0 | |||
d6fdfe0b6d | |||
5ccf81349e | |||
29037b4995 | |||
ef62f4698a | |||
563a66b053 | |||
602993ac76 | |||
47548905ce | |||
195bf022bb | |||
5aa2a682ff | |||
af2141417a | |||
2640efb3f0 | |||
f1d01f4fa0 | |||
d71897620c | |||
52983907c4 | |||
23688f17bb | |||
abe7a41f58 | |||
22a33929e2 | |||
ac56b57faa | |||
e5d3e3a9c3 | |||
ea0881f87e | |||
1b7a4ac090 | |||
bb3e29e5c4 | |||
8f745b80d3 | |||
7556f8615f | |||
a7c1c472fe | |||
9ad0a8825a | |||
e06d9a620c | |||
f0357bc988 | |||
85fd1d6d04 | |||
82dcaa4545 | |||
c72acfd282 | |||
a5454fe82d | |||
b037ff922c | |||
ff2f70b1de | |||
74655de618 | |||
f3dbfcd37b | |||
f3119b5b3a | |||
60288b2d06 | |||
1b9f2fa5f7 | |||
f03161b7f9 | |||
4b124b9381 | |||
d63eb7c8f5 | |||
4b30b6d764 | |||
cb77339370 | |||
8952cf659b | |||
9c7c7b06df | |||
8d24659892 | |||
7a9a973c89 | |||
ea5276ad7f | |||
181374d2cc | |||
80aef61aed | |||
8e96507cb1 | |||
4d82e23861 | |||
b0dd8ec729 | |||
debbd316c2 | |||
1377238950 | |||
d271ae67aa | |||
961d65c0ee | |||
50e6d81d0b | |||
85c07e7154 | |||
d222c60cef | |||
5b719a03bf | |||
db09730d5f | |||
7c35fc546b | |||
10a946cab3 | |||
20f9bde88f | |||
558644b8b3 | |||
3663cb5ad5 | |||
75251f6e78 | |||
e7b9ab1b3a | |||
3f34e5dadf | |||
abdbd50f5e | |||
32d43b85b9 | |||
8fe8836bc2 | |||
1b351e08aa | |||
d720993141 | |||
c9338027f2 | |||
e7b7b0dee3 | |||
cd5f500d11 | |||
1271c3e7cd | |||
118f197961 | |||
2f01e15cae | |||
cd4e0ab3c2 | |||
68e9755e59 | |||
60a1bbeb70 | |||
4575cd4e58 | |||
b407e6fffe | |||
edf1aed369 | |||
fd28f1730e | |||
ab174dc678 | |||
c4e25c8d9c | |||
b6be84c909 | |||
8fda7d00d5 | |||
382325ca47 | |||
f4565d0603 | |||
aaa536b454 | |||
6122fb9f73 | |||
cf13f11107 | |||
b9d16d7cd0 | |||
d03c435563 | |||
cb73a7ae0e | |||
6a5e82ba3b | |||
3050183ea9 | |||
94b4c0b2de | |||
9ce4c26c75 | |||
ba348e5af4 | |||
bf69d595b4 | |||
ce8e22ae3b | |||
3e6508f484 | |||
6780855b1f | |||
de63ff6d2a | |||
fa0b0b68ca | |||
c51c5b3c44 | |||
bca62cbf0f | |||
a850b287b2 | |||
9530dba2c2 | |||
f8789689f8 | |||
cc438980d1 | |||
00870dfb5e | |||
1bd7218a7e | |||
e95992b704 | |||
8ca9025311 | |||
69df9cecca | |||
b1b755713e | |||
4822a53f06 | |||
aab68f0baf | |||
2f54532be0 | |||
1f1ea68f45 | |||
f074e23c80 | |||
e0b2789840 | |||
284f00249a | |||
8aa9cc11eb | |||
916592944a | |||
0350f4bbeb | |||
1710844a1b | |||
08b953c0aa | |||
3f87b55a52 | |||
a6ec9dc666 | |||
9e1e12f6ce | |||
27cb9c433d | |||
d22152eefd | |||
96b73cca51 | |||
605ded9c73 | |||
8d49dc958e | |||
c345489923 | |||
db8753631c | |||
2e886ce4e6 | |||
52fcda96e1 | |||
fee0a0c867 | |||
360e7aec42 | |||
42c5c9fd65 | |||
1c0cc78f3f | |||
dbeb8d8da4 | |||
cd007febef | |||
a074564458 | |||
fb2b8994c7 | |||
95f59a0272 | |||
5c03e9e9ee | |||
1d10b19176 | |||
e0aaa3b162 | |||
c239a5f0e5 | |||
0d53c8dd0e | |||
e8fe8d832d | |||
e41117c878 | |||
6d70067b1a | |||
587731583f | |||
09a05f72e3 | |||
afb7e055e1 | |||
92b0536520 | |||
c0b11d4fa3 | |||
eded51aab6 | |||
15ea264fcf | |||
80bca8610e | |||
c10a05cb68 | |||
47a66df802 | |||
97e088dcbf | |||
3aff621478 | |||
3d746fc99a | |||
f3e3847e0a | |||
22abcb79e4 | |||
05ae51291c | |||
4f40fa1ad8 | |||
0dedbc7256 | |||
6ece596135 | |||
f7dfe6abb8 | |||
e8e5d409f9 | |||
18a38b0b11 | |||
6fa358c093 | |||
97a42011bb | |||
81142d3ffe | |||
a106f1bcf9 | |||
f5cd97e07d | |||
a28788488f | |||
15af11da98 | |||
be6e95e564 | |||
f12a637cdb | |||
dcd664e61b | |||
485feecf10 | |||
5c8e2b5bae | |||
9cf58a74d1 | |||
82419a08c2 | |||
997e373bf9 | |||
effd08c98a | |||
c2f7db9f76 | |||
4a43dcc373 | |||
309620ebbc | |||
ace5b71536 | |||
022fd31b66 | |||
c1b3711090 | |||
d1f7363779 | |||
0112d0d955 | |||
6672cbc253 | |||
7d861404ce | |||
c293c278bf | |||
a804926c35 | |||
4c1cb5999c | |||
e517de5480 | |||
bd3051e058 | |||
b6fc1959e6 | |||
fab33d678b | |||
afe3b6b2fc | |||
da2bb724e9 | |||
10acb79040 | |||
208c17dcc8 | |||
86dc50f9f0 | |||
cbeb56adc6 | |||
74dac13399 | |||
0c89de8065 | |||
365e93125d | |||
6df13643c3 | |||
52e66d4819 | |||
3928556033 | |||
7d1c6e9a40 | |||
3c43f5af1c | |||
9306050b9a | |||
6a428da11d | |||
57c8e98292 | |||
fae52848e3 | |||
6bf5e573e3 | |||
96f133750d | |||
a774e0a2a1 | |||
6e736e1f1e | |||
2233c2a76d | |||
e3860b8e97 | |||
00e2d546bf | |||
d9cac4ce8e | |||
d3dc5bf7c1 | |||
254200f8dc | |||
a9d98fd09b | |||
b94d380729 | |||
dc94e58cbf | |||
790b944031 | |||
0680dd2398 | |||
41777ad51c | |||
4c582cf1b6 | |||
43905a041b | |||
e015424a83 | |||
84ed1ecdfd | |||
27f2fd352a | |||
eb6b7bca20 | |||
5d1212b83c | |||
416344b361 | |||
11f771469f | |||
e3484de3b8 | |||
bca76f1f2f | |||
54c735841e | |||
318b8c691a | |||
be3a375cae | |||
63fbf70eaa | |||
c1dbeb43c9 | |||
ef2adcc95d | |||
1777070b46 | |||
9dfdf78697 | |||
c9dcb2662c | |||
234be6fb09 | |||
32d1f42626 | |||
1b38e33bd3 | |||
132e3534de | |||
983a45e178 | |||
96c9f801a9 | |||
040bd4361f | |||
a4d2c2ab71 | |||
cbed185040 | |||
186e261fc0 | |||
1fa7d17e35 | |||
f21e694907 | |||
b161cad982 | |||
4332828dd7 | |||
5872658f8c | |||
cb128256ed | |||
8e1e9a521a | |||
4247df4295 | |||
ceb1284f27 | |||
207caeda5b | |||
294acfe412 | |||
0782377ae3 | |||
fd6a040568 | |||
22c6cb1f99 | |||
032af0f687 | |||
6aa65a917f | |||
13760647f0 | |||
68bcc26ff6 | |||
325ed2ec3b | |||
9f50432999 | |||
bda657b638 | |||
6f58be9493 | |||
6dea945a3a | |||
5594bed6a8 | |||
f647f2ae6c | |||
7cb7c1f0f9 | |||
da6de03536 | |||
4ea6c4ad5d | |||
cddc23494d | |||
5a6f39dae4 | |||
cfa40e482a | |||
1b0212377d | |||
74c5380975 | |||
80992aa5ba | |||
de0fb631df | |||
831680d27e | |||
4cf1f080bf | |||
2fbd44c59d | |||
be84c7b977 | |||
043f484693 | |||
36e8ce624c | |||
13771d56cd | |||
46bb3064ed | |||
def3bb4729 | |||
da2229a303 | |||
2e4c725647 | |||
bd26e4e9c1 | |||
7bc601ad3b | |||
d8859001a7 | |||
0f1a8ec928 | |||
e583e45d9a | |||
5c6601cb2a | |||
a13e9fe395 | |||
c1421c9c44 | |||
55f179bf33 | |||
c7b861e0ba | |||
fe56da1654 | |||
a3a55dd195 | |||
63ca6e1e24 | |||
9bf444e93a | |||
8257b040be | |||
7c0ea6cfa7 | |||
ee32072489 | |||
f40f74b20d | |||
c1ef766125 | |||
2e882f75f2 | |||
9a9a0b9735 | |||
361cf995da | |||
686f1d57cb | |||
2deb0b3d7f | |||
3999e0485e | |||
b2a7ce834d | |||
59d22ef775 | |||
b1b9568c96 | |||
1e88b815da | |||
998b9454b2 | |||
d5b97c74f8 | |||
a8160288ae | |||
21e913192a | |||
16144c7be6 | |||
865185efad | |||
4148280a8e | |||
1aa30d855e | |||
f3aba6da92 | |||
c34cc46c2d | |||
52206998aa | |||
0c2905fd2d | |||
445f38eb3c | |||
847e19f22d | |||
ef7d10262a | |||
d6e2119277 | |||
29fc3681b9 | |||
871f09d109 | |||
d9ec95ab5b | |||
dd5cedcfea | |||
8f9bd1fef7 | |||
ce0a37cdf1 | |||
cac7011d53 | |||
5753c4feaa | |||
b08574fd57 | |||
baa402f82f | |||
1b132b5780 | |||
80653a5317 | |||
a9af247f1e | |||
e54eac6227 | |||
4f518a5d92 | |||
0a167a7bc7 | |||
7f58ea2725 | |||
8ec2dcc633 | |||
67d3595880 | |||
6034303b24 | |||
ed553aa9f0 | |||
d1aca27126 | |||
18d0edd4eb | |||
679aa5d073 | |||
7bc9bc962a | |||
b5c1fec845 | |||
bccca4e710 | |||
e664d7f5b9 | |||
879048983c | |||
96f6c1913f | |||
106ce6096b | |||
99e4551cf6 | |||
f5e96d9372 | |||
6fe3d43049 | |||
6fe4184f72 | |||
f15c3b4e1e | |||
9e61c3be94 | |||
f88180650d | |||
020b9b8cdd | |||
940dc73f28 | |||
d2e38b1062 | |||
3a4973ad68 | |||
0fb8450e56 | |||
13ca47a3b4 | |||
11acc7225e | |||
a1ad21ec56 | |||
bb1b4ee33e | |||
155b99b64e | |||
4d02e4c021 | |||
f971e31171 | |||
913383ca75 | |||
c22310bdaf | |||
eb72ace854 | |||
cb88510964 | |||
88b86721c6 | |||
f7c947888f | |||
2719daffb0 | |||
d17316508c | |||
9155469e8a | |||
c76b91d595 | |||
ade1d35463 | |||
b83aae0b57 | |||
df23bf7141 | |||
a6d9a9e1a8 | |||
e6717c86ac | |||
9bc45def17 | |||
2db6f35b7c | |||
f3fa6c1fc0 | |||
c836fa6912 | |||
a267443777 | |||
2fab9b5b9b | |||
38381ba287 | |||
ffcbb7613e | |||
bc8ad12b04 | |||
243dda543c | |||
d96c66ba9f | |||
66142c546b | |||
15f1920b25 | |||
dfd6629a6f | |||
21407260a4 | |||
f1692a07fc | |||
7485f1a5b4 | |||
b177cbce1d | |||
b8b2398d32 | |||
fab22e3d8a | |||
88e1251d6b | |||
e7a33f4988 | |||
2f7a4ecdb2 | |||
dfc9637230 | |||
2c22e413eb | |||
67d4daa7a1 | |||
0d08cf36f8 | |||
211ef795e9 | |||
a09c818746 | |||
8c765b25bc | |||
51eb538631 | |||
915fe79d7a | |||
ec087ce28a | |||
43592c32d9 | |||
5366797a4b | |||
75c6c8521b | |||
8d4ab4555c | |||
d5f62e1355 | |||
2f1cd9976d | |||
6d73b16716 | |||
461114c143 | |||
38cdfdf7e0 | |||
9b93d4c098 | |||
770dcbdc49 | |||
ee4f923f60 | |||
8cd68f56aa | |||
c43f06124a | |||
0c0c683986 | |||
36d0e34668 | |||
59bba148ff | |||
b60c7bbae7 | |||
07a9d84ed0 | |||
3b62f58dd2 | |||
6eaa06ab02 | |||
9721b77317 | |||
4b741c3759 | |||
59852d6899 | |||
2f47ea2228 | |||
70060c27b2 | |||
09d4bf1d7a | |||
9c30b37d57 | |||
10877fd45f | |||
22abbebd41 | |||
66134823bf | |||
72433b37c4 | |||
82941001e8 | |||
3ce0618362 | |||
92e4edf9b7 | |||
f4b5c1f27b | |||
610cce4162 | |||
674e221335 | |||
079ac091eb | |||
b5a5b307a0 | |||
df9135f410 | |||
8a25f0f816 | |||
33c5c21a57 | |||
d846740839 | |||
c3ac1649f2 | |||
66a2c4a2c3 | |||
e5497edd5c | |||
ce800f75ad | |||
b8aaf5c1f1 | |||
28ca7a727c | |||
d448bc6f9a | |||
d0fa235e84 | |||
70816a4779 | |||
c22090c602 | |||
313d023eef | |||
b2a89cd217 | |||
b92b8a9e15 | |||
92e30311ce | |||
0425e8b114 | |||
18e570b021 | |||
4abca69328 | |||
6b58080067 | |||
809cc1049e | |||
957e4a2611 | |||
01b7ac5fb9 | |||
d98c936295 | |||
4e80543edb | |||
a90b16a72c | |||
f0026065f5 | |||
2f4b3a4f83 | |||
697c62fb64 | |||
1e7c3841b2 | |||
ec23de7e33 | |||
b98fa12741 | |||
bd12070467 | |||
217a9b00b8 | |||
c0f4d5260f | |||
0d4679a37b | |||
05b961a90a | |||
d11af012fc | |||
d058db95e5 | |||
4df2031ae0 | |||
904ebf34a5 | |||
db4454b52b | |||
c18fa5a38e | |||
7ce845cde6 | |||
dc5636c6c1 | |||
217efdd706 | |||
4712ae4eb0 | |||
ac43b86b60 | |||
41a168ff29 | |||
fa8fcb0f8b | |||
5e64df220c | |||
f300c67a4d | |||
7117b5ce32 | |||
5284dc0c52 | |||
8daf72278d | |||
0199b5f169 | |||
a49bd1d42a | |||
b1b97db11a | |||
c5ced6fa5e | |||
8908c8b184 | |||
9c974fbe50 | |||
86f172076b | |||
9a85f1b25f | |||
a941c20024 | |||
09836bffef | |||
be6a7876b4 | |||
a32935fbe6 | |||
7ba6ee8714 | |||
fcf1c65fc1 | |||
1336b4ed60 | |||
641e4c5d96 | |||
f040dac647 | |||
26ad37a8c0 | |||
16005a0c05 | |||
5c83cbc1ac | |||
9878a58452 | |||
9d2046d5a2 | |||
19c8461397 | |||
6d5518cbd4 | |||
b9aef5891f | |||
abbce23b18 | |||
6bbd238d25 | |||
13e6c98e47 | |||
567a9aa131 | |||
fdaf0b3364 | |||
fac1cc7e1d | |||
8192f69584 | |||
676690097f | |||
a26bff6921 | |||
0b8369be23 | |||
5afaab7083 | |||
aa5db37bda | |||
fd76c5d9ec | |||
d6d040f5da | |||
2096bacd34 | |||
fc951d9295 | |||
e9535cd01d | |||
c92201a674 | |||
61815bce2e | |||
bb287cbd07 | |||
450a53d552 | |||
7edfccee49 | |||
17d3d2492c | |||
5e3902a3bc | |||
509005fa0c | |||
492625f6d6 | |||
ed264aba3c | |||
483d11e772 | |||
0b617377df | |||
f5b58bcdaf | |||
5706b533fd | |||
6962b9c433 | |||
edd0ef5991 | |||
d2bad5f79e | |||
61c560c12c | |||
d3bd7771d2 | |||
b7103a7e14 | |||
1b0da74b57 | |||
095ccae616 | |||
17bf6baa25 | |||
56947e7710 | |||
4fadc4d072 | |||
221e35ec38 | |||
85ea1046f0 | |||
cb49a7e790 | |||
6e5729c660 | |||
522299ea9e | |||
0ca385deaf | |||
cb7059f832 | |||
a497a568db | |||
d1cda51e80 | |||
c16458b728 | |||
6a7a262912 | |||
96c3621a80 | |||
9197b39ed6 | |||
32b0275257 | |||
31f835db86 | |||
9ea104b165 | |||
1e0f5cfcea | |||
ef564b0a93 | |||
0b467cfeed | |||
0f8be8d6db | |||
d7b730d8e0 | |||
fb21f9c22f | |||
4aec451584 | |||
a006f498b1 | |||
3c2a2a0adf | |||
76bf71162e | |||
bd09fe9556 | |||
36312e91e4 | |||
aab556bf77 | |||
9becced985 | |||
1571a4acd4 | |||
9fd95a4415 | |||
a789e4db2a | |||
5ca466117d | |||
0d369e6019 | |||
bafc2fc7ac | |||
4796f890fb | |||
945fd8331b | |||
83c7d82fce | |||
ac6dc65342 | |||
a8ab7864ae | |||
3b632c9501 | |||
0414edeade | |||
ef9ae09c2b | |||
f1ddf09cae | |||
32c85599b6 | |||
1f0439badd | |||
a03b9d8f02 | |||
da36f1dacc | |||
adde3375e5 | |||
bde422ab30 | |||
0050ae74a4 | |||
4949ab973b | |||
f76c3af557 | |||
522649d9d3 | |||
4354f868fd | |||
b3066db08b | |||
bf52bc22e4 | |||
875545f7e1 | |||
672798e711 | |||
807f33f748 | |||
f96a1eaae5 | |||
da8756ed0a | |||
47dd8acf54 | |||
ed00fe46e2 | |||
10b0667764 | |||
dd54a4f728 | |||
d690e9a73b | |||
d2cf36bf02 | |||
f85242af28 | |||
dc14719b08 | |||
875ed79f3f | |||
20a06ce3f2 | |||
cfe0d9c9c2 | |||
231f782260 | |||
33cce05300 | |||
390b204272 | |||
bd030470b1 | |||
b131c5bf94 | |||
2f59a8027e | |||
cc8144ac3c | |||
5043a52b88 | |||
05735b31c0 | |||
ed0dd3b8c0 | |||
1a7c6fbd32 | |||
8ebcc7ac02 | |||
5896a9d251 | |||
b01f4f75d6 | |||
74eae88e37 | |||
054dd186d6 | |||
62ab68d317 | |||
8f67a3c634 | |||
063811cb60 | |||
ef6df1339f | |||
27c07f1f84 | |||
54172bd322 | |||
f36aa1f40d | |||
8867fa1d52 | |||
18be8b10f5 | |||
6c31a1f9cc | |||
4fbf55d79e | |||
0637ccc1bc | |||
87db5ab64d | |||
62679090a3 | |||
edb7ab2638 | |||
8989aa1714 | |||
450ebf4e38 | |||
d7ff72a538 | |||
7d7beca9dc | |||
5427c4a230 | |||
a823084947 | |||
be0c6819ab | |||
3246e4719e | |||
d1ee0d7af7 | |||
d333651b9d | |||
bf1db428f6 | |||
4a1f744211 | |||
134e8c1c94 | |||
cd99b1bc1e | |||
9c510adc31 | |||
2ac0b4f5ab | |||
f8ea6a527d | |||
cf516fcc83 | |||
2eb681413d | |||
559ccf0264 | |||
7bb18bf73c | |||
63051717d7 | |||
5b2b433613 | |||
54762b601d | |||
5396cb2f5c | |||
ac280782b2 | |||
667dfb5d69 | |||
496c4e523c | |||
a67a69f95e | |||
0708a6d665 | |||
970e0b5185 | |||
d5d77f1738 | |||
e0ac4ad040 | |||
a3b6118927 | |||
4e5d7b6e84 | |||
08af66ba28 | |||
ab2012d12c | |||
b9ab38bdab | |||
e902ecab36 | |||
548c6697d0 | |||
6af8186e77 | |||
15e91e4b4c | |||
876d311706 | |||
858366c57b | |||
25fa97a20a | |||
34a827a270 | |||
1a9c1677d3 | |||
91ae9a9d49 | |||
6fe4cc3fd0 | |||
2a775cf960 | |||
eeef752a5c | |||
c999579c05 | |||
d14504763a | |||
6495cd2f65 | |||
43f783db42 | |||
f601d6cd03 | |||
7c274fdd35 | |||
2c2918cc9e | |||
fb35518848 | |||
8855b91d0c | |||
d8bcfd7d44 | |||
5b2383fa50 | |||
107005c474 | |||
30165ebd15 | |||
a5331bce77 | |||
0aec31328d | |||
48594537c0 | |||
b3b4b79775 | |||
fe5bd0341a | |||
ade334b251 | |||
7d602c26f8 | |||
44bbc45973 | |||
5f6f51b549 | |||
a8303940df | |||
0bf4a6a6b5 | |||
aff1ed95e9 | |||
80f9003135 | |||
5705839861 | |||
9c50d5f073 | |||
76b10d01ff | |||
85dd01ab6d | |||
d71145bc6e | |||
67d1c72a59 | |||
063b0e6f16 | |||
b5d3859adc | |||
6c6e7aa872 | |||
e638aeb778 | |||
c15ac96053 | |||
ee10798178 | |||
dd5be6d7d2 | |||
59391b2369 | |||
399be2370d | |||
93cb0892ae | |||
7680971b89 | |||
f893fb1614 | |||
565524a80b | |||
f607e98565 | |||
adba6b056f | |||
4005fab006 | |||
300828808d | |||
b713ee0ec5 | |||
ee7949a0fc | |||
898971cf15 | |||
d7f49cea9a | |||
8ed1610f48 | |||
51653015a0 | |||
c0802c7fa2 | |||
434bf35a55 | |||
1f29e91796 | |||
7cdf167e4b | |||
b854d8f3a0 | |||
f9443dfbd3 | |||
a215076358 | |||
da7429f0f6 | |||
70040e186e | |||
3d649756ec | |||
eead117d6b | |||
862321aa39 | |||
72c5bdbf9e | |||
fb32acb1ed | |||
aa94f11a2c | |||
788ecf9055 | |||
62199492ba | |||
b684260926 | |||
2dec9b5b10 | |||
883c8decde | |||
f36f44df74 | |||
08be1c97db | |||
f15dcc54be | |||
b18c87b9c4 | |||
7d577f137f | |||
59ab341a28 | |||
2062d07eb7 | |||
29822c13af | |||
022d632e40 | |||
ed7208d04e | |||
dd28e6fbeb | |||
9ab038d273 | |||
fd82487ab5 | |||
05648173ae | |||
1e5d93d9bb | |||
2fbcb2b23f | |||
702b55d16b | |||
cf9d287b23 | |||
9147675e60 | |||
d26557fac3 | |||
50600938cb | |||
35e05d250b | |||
6c28851fe8 | |||
30e1456ac0 | |||
ce8fadae52 | |||
55f91ac5dc | |||
0dcb935161 | |||
ebff00e057 | |||
b6ce26023e | |||
4b0a55144f | |||
83d17b2643 | |||
845c2b70a5 | |||
f9bd19ae94 | |||
ed07e98bff | |||
91649d21ae | |||
177c462b40 | |||
a27badefba | |||
7275e127cc | |||
0b69974e49 | |||
4fa7392205 | |||
774518e4fe | |||
fbab5db85a | |||
1d824ee293 | |||
f1392dc9a7 | |||
9c8fe0379f | |||
be539e3353 | |||
fd8814677e | |||
d422da3f4b | |||
58c7f23621 | |||
56cd103209 | |||
a8126b33bb | |||
15ef4a7122 | |||
186ad57858 | |||
8871dcef7d | |||
633568da03 | |||
abee6b8b49 | |||
8baa483c89 | |||
4b0227c45e | |||
41d8642bf5 | |||
ecd10482a6 | |||
8130535af4 | |||
779a72581c | |||
b6fceec6dd | |||
214c1d3fd1 | |||
be77e30110 | |||
218653c52f | |||
c84a2d6b1e | |||
b7a332ff3c | |||
cd4001b2ef | |||
1c030d1fb6 | |||
94b1bbe5aa | |||
1369ad1fbc | |||
33563c3e9f | |||
9b6f8ec207 | |||
12c7633380 | |||
65a6e7731c | |||
898ea8d3ca | |||
d15212448e | |||
e4526e8eaf | |||
894a80eb88 | |||
e12f911352 | |||
1ffd7b33fa | |||
da6cdb7c26 | |||
f21c32d0df | |||
5338faf46f | |||
1132c9eeb1 | |||
35400aeb16 | |||
daafcc230e | |||
2654aab3d9 | |||
dc81d932ed | |||
307c4f25dc | |||
d2b5125004 | |||
d6b4ab8abb | |||
f247b28262 | |||
716ce0f779 | |||
ac4a62636b | |||
8405f0ee53 | |||
2968dfb49c | |||
5f93598bf1 | |||
02511eefb9 | |||
d932acad16 | |||
74e7870cde | |||
420fb5a847 | |||
52f54b92d8 | |||
d4bc70b61e | |||
2286394f79 | |||
ae6c6ea055 | |||
b0b50098a4 | |||
07db70f5f1 | |||
70ae5cde4e | |||
fbdb9edd5e | |||
6e99aa8637 | |||
e4dfa5e52f | |||
f42b7b0e9d | |||
bf9a45e2a1 | |||
45a8464dee | |||
5a71dc2e5b | |||
a28f8c7ac4 | |||
347b290747 | |||
7e64d033bf | |||
471c1179d3 | |||
032ebe7a9e | |||
5ab4d0257a | |||
ee2038a75a | |||
f2f9502ab6 | |||
c6c95204f8 | |||
a2267aa056 | |||
025d6dcd80 | |||
46727f72df | |||
cd57e612c1 | |||
caba049da1 | |||
6124133e85 | |||
ccda4d8453 | |||
ac943f8951 | |||
23daba54b4 | |||
606fb9d326 | |||
142b19bf86 | |||
96a7e99e57 | |||
cda1e6e741 | |||
4d95be06b5 | |||
e8a604d0ed | |||
8035976e40 | |||
5d84d474d0 | |||
1856ab62a4 | |||
12cbb20d28 | |||
ecb6d90f63 | |||
9f2b61708e | |||
81998d9f89 | |||
cb1acd2cf0 | |||
d9dde4c4f9 | |||
7c93e17fbb | |||
b08f27bb60 | |||
ecbf7e8f85 | |||
895d857825 | |||
76a9890bfa | |||
5d63ef0c59 | |||
09747ccb66 | |||
8ec104cb76 | |||
8036474dda | |||
6307a02f92 | |||
2f9a052f89 | |||
7b4bf68486 | |||
5188bddd4d | |||
26468ffce4 | |||
43c55027f9 | |||
8cbdba1da3 | |||
ebb77d4756 | |||
c3a4097421 | |||
c93a56a964 | |||
e5f1475541 | |||
8ad119f72c | |||
f857846b2f | |||
836a2ba059 | |||
d5e5b52583 | |||
711feb4d7e | |||
1910b2642f | |||
a4a08ee05b | |||
d5c6c770a5 | |||
59c02f06a9 | |||
64815aaefa | |||
4778f198e1 | |||
0e1e52d650 | |||
930cad41b4 | |||
f61c969f7a | |||
c62f475d9c | |||
8bcf191a1d | |||
91234db53b | |||
8849dc07cf | |||
451dfe679a | |||
a81df1537d | |||
e4713829df | |||
2e84441e66 | |||
8cfd5f9266 | |||
ef05f00983 | |||
3bee6f6e31 | |||
d13bdb2308 | |||
cc6c173828 | |||
f0051e6d89 | |||
3d1a85a3a4 | |||
8aac74a0a6 | |||
ca1cce35a6 | |||
6718e2eb23 | |||
a7f2b2927d | |||
fc0495894d | |||
65adb0d15d | |||
6f8e174d19 | |||
29d34eaf03 | |||
f4ecb53342 | |||
1da94464eb | |||
aa5a911edc | |||
691d05222b | |||
7e6842e63a | |||
ec042d1d61 | |||
d26f4c50d3 | |||
3fb7274ff3 | |||
7a0b656634 | |||
3e517a59b6 | |||
9c71f6b9df | |||
85264c484c | |||
26b067a0a6 | |||
d93f0e2d34 | |||
ed842dfb72 | |||
608856b6cd | |||
353891a3d1 | |||
8d60748938 | |||
619660978e | |||
2ee3d70eda | |||
a8d93d639e | |||
8b88a8e75b | |||
ac1d5f2b86 | |||
39d1fb4450 | |||
b45c83682c | |||
55a144699e | |||
2f21071606 | |||
3b4258096c | |||
66af9623d9 | |||
316be483de | |||
0d2b4e6f18 | |||
f1b5b50dc8 | |||
253bc3e0af | |||
9f499faa29 | |||
50056699ac | |||
7914fd949c | |||
a964b4ce21 | |||
86871d17ac | |||
222374583b | |||
21ebc8a574 | |||
7aa801b111 | |||
9c99234e8b | |||
ebc61bf936 | |||
e31090c9bd | |||
f020e89a8c | |||
96684ef2a0 | |||
ac7a4031a8 | |||
96c1fceb99 | |||
5579f4b482 | |||
e094ae0208 | |||
2d144b5a58 | |||
9a6d665f01 | |||
8bf5aa6bfb | |||
4b029865d7 | |||
bd34b2a083 | |||
dcd60228ed | |||
a044718066 | |||
882404c3a6 | |||
ccafbb39dc | |||
12e0694d1b | |||
05ee5a8b60 | |||
cde4a8a977 | |||
643189160e | |||
2c55292544 | |||
9121d74194 | |||
f1035dacc2 | |||
19afdf993e | |||
1e60e1e351 | |||
8604c1d257 | |||
5fb2af0a6e | |||
c37ee2ae1b | |||
bc257d36ed | |||
8a392fff9e | |||
0c21b65917 | |||
5c5650d123 | |||
5a34faadc4 | |||
ea479102f4 | |||
262f84d49a | |||
afa888dda6 | |||
432acf4bf7 | |||
7e7bb2dc34 | |||
d675025340 | |||
669b41aa39 | |||
81c6d276d5 | |||
9c15d8f45e | |||
9845fda05c | |||
e636f3bca3 | |||
02344635db | |||
0e34e9ea7e | |||
4c97e03e82 | |||
0bf4242324 | |||
d02eb031ba | |||
a93ab4bcd1 | |||
fea6f76f4d | |||
f7580f5337 | |||
1c4022456e | |||
2fae761e66 | |||
435ecf5cec | |||
03bbfc55a7 | |||
eaf548b5db | |||
afa33e278b | |||
57d44a7e12 | |||
c421a80102 | |||
ae4a57e035 | |||
0009026bdd | |||
eccd826088 | |||
1710cd340e | |||
f31c781693 | |||
e6f0dda4f7 | |||
7bf391957d | |||
498ec2d8e0 | |||
8f32b84b5a | |||
b300db475c | |||
e5a3dc04bf | |||
a7a0d35ed3 | |||
3f868f99e6 | |||
c0136a9f34 | |||
e603b52a5b | |||
e3328709ff | |||
b79fc265f1 | |||
146e405979 | |||
aeec25838a | |||
dff8b947bb | |||
677d716ae6 | |||
c2b236f5a0 | |||
f3dbedb5c5 | |||
bc804521e8 | |||
253f4aa6a1 | |||
4361f48b98 | |||
5f31900375 | |||
d8c84e2541 | |||
35489a706b | |||
6f2f9995f2 | |||
f67436792a | |||
02e37d58d7 | |||
82253f625b | |||
8c4e973409 | |||
206d9160de | |||
a68fd4f2aa | |||
13cd2e82de | |||
345cfcaedd | |||
29e6e47ef6 | |||
a670096d53 | |||
747a8f5b96 | |||
c252785632 | |||
f6f2e7144f | |||
86f42e2ab0 | |||
ba6d8d8720 | |||
aa59906e91 | |||
1de4899c6a | |||
5b42dc3393 | |||
e09a035373 | |||
576980bf64 | |||
dc331d5293 | |||
f0db3084d8 | |||
7be492d129 | |||
d08e09fbdc | |||
730e2b7f09 | |||
f05b50020f | |||
081d8ddd12 | |||
0f8965fac9 | |||
3d4412a704 | |||
f5a13717ea | |||
62ba018b19 | |||
4faa46ac29 | |||
6058e837fc | |||
a74d92af3d | |||
77847ab3e6 | |||
d80e8fc075 | |||
0b04aec602 | |||
9d1383906a | |||
cd9df4455e | |||
80f76f2efd | |||
581f36d6ef | |||
47d55d9d2b | |||
4ad836d690 | |||
020a7cd8cf | |||
e5a9ccaff1 | |||
dfd27ea85c | |||
43505bd235 | |||
a4428528e3 | |||
106aaf4f28 | |||
1f3a16b159 | |||
ecf792af3b | |||
d57b2d4865 | |||
a4b9e635ba | |||
8da44c343d | |||
bb3dbe041d | |||
cde471617a | |||
f060ffadcc | |||
38d6db6d81 | |||
92825f4c98 | |||
331aecbba6 | |||
2e81a0d57a | |||
3d9704f93b | |||
786be3fa66 | |||
a3065ed39f | |||
b7c24a372b | |||
4e952af00c | |||
cdbf260803 | |||
3d9f7d28d6 | |||
1f7546789a | |||
ddc913ba7c | |||
8589b6c0bd | |||
cf3ae15ee9 | |||
34def84d43 | |||
104ac9b950 | |||
ea3adad878 | |||
2346f30e0e | |||
f2b2f99a51 | |||
e4cea91ea8 | |||
cffbb7cd58 | |||
665dca80d0 | |||
4cbfe6d0fc | |||
fca8e6a0a9 | |||
e9f4765663 | |||
50c953be61 | |||
dd787dfcf6 | |||
c19ace7e6a | |||
b7d05c18a1 | |||
faef62e073 | |||
dbc62a1b32 | |||
f0808a1116 | |||
4c7da003a2 | |||
a128258fe0 | |||
d18f2c9eb1 | |||
13f8608e05 | |||
b4ef762087 | |||
7880e5272a | |||
a6810da992 | |||
9185a31945 | |||
3158c9d710 | |||
a51b7d95b2 | |||
2325690cb6 | |||
a29af98e4b | |||
9fc8aacc2d | |||
3123d308d0 | |||
bff3183a27 | |||
88402909ff | |||
43d32d516b | |||
b63878d306 | |||
ab53840324 | |||
6afcf32422 | |||
0ceed8fd9b | |||
279a901e4a | |||
9363be0f19 | |||
653654cd36 | |||
143f191887 | |||
e5f17f7bc7 | |||
fab840a759 | |||
32c1d56bc7 | |||
52ea7a32db | |||
8e5c02f967 | |||
cfbe8ac926 | |||
f47bdbc37a | |||
4b036bbeba | |||
942f6a05af | |||
de313fc977 | |||
bf584e525b | |||
587a9f7889 | |||
2cf117b3c5 | |||
0651ae5ec0 | |||
bf601b0458 | |||
b29f578e31 | |||
c237b06f90 | |||
c6b854bfa1 | |||
9b01520462 | |||
bcf994efe9 | |||
eebe5913aa | |||
08f3342bc7 | |||
002e8b9db9 | |||
9b75cfe519 | |||
9df278319d | |||
ed9caf61fd | |||
2bab0370fa | |||
dcad96170e | |||
64a127845e | |||
39565fecfb | |||
5d02299287 | |||
4e968e7ca5 | |||
6d2436afeb | |||
6c29570a89 | |||
73e5c38f19 | |||
9ac1beae82 | |||
7b5884b664 | |||
20966e085a | |||
2666ba6adf | |||
10976bd656 | |||
4f116dc758 | |||
fcf3252292 | |||
fa1c306d64 | |||
cd7d3e3fd7 | |||
7202491c7e | |||
408b0efd48 | |||
a4033baa4c | |||
3fb328ec1c | |||
f8252521a7 | |||
08b6660ae6 | |||
d10a94b9de | |||
036ca46431 | |||
3021d19e1f | |||
e8ce11cc2f | |||
e87ba75809 | |||
1b162a49bb | |||
222cd8325f | |||
edbb5612a2 | |||
3350599901 | |||
68e4b61808 | |||
0e197cfdfc | |||
9e21902a5f | |||
2c2a24d77e | |||
a47b9c3ab8 | |||
6dca5b6f1e | |||
449544b3db | |||
845f17f7af | |||
e4666cd95c | |||
9f35b33dc7 | |||
6ad39f1853 | |||
198b5f10dd | |||
f1ffb67d5c | |||
7cab5d7b96 | |||
b249921bd3 | |||
655b9129a6 | |||
3ad35e4328 | |||
b78175a91f | |||
23c60dff4e | |||
152aa6c8e5 | |||
3a1e210ce1 | |||
49e9631460 | |||
ddf05d7b36 | |||
bab917f1be | |||
678021a3f2 | |||
395ef2675a | |||
088f47c611 | |||
ef56b3dede | |||
90ce1f1a3f | |||
a7dedaf273 | |||
934b3945f8 | |||
3a4505aaab | |||
869013d6bf | |||
02dd9ac32a | |||
5547ecdeaf | |||
3dc8b83cd6 | |||
289f67155e | |||
4e55612833 | |||
82ea5ae918 | |||
412d447de2 | |||
7836e29c37 | |||
344cd58f32 | |||
3d7831f86d | |||
9fad242382 | |||
05d8e29579 | |||
84ed4c7274 | |||
d561e2995b | |||
5b47fa249f | |||
ebffae59c0 | |||
ccbf2b4aab | |||
605cee9084 | |||
5cd42cdcc9 | |||
89a4f186e7 | |||
5c9cff5a97 | |||
1f5485fae2 | |||
f7c9dc0b21 | |||
fc5cb86011 | |||
8a2fb128a9 | |||
7004451db4 | |||
96a2f0d92d | |||
4e3998c64c | |||
5127d1536c | |||
1899e11b00 | |||
97dc722320 | |||
b65f5528bd | |||
63708e6624 | |||
5343659657 | |||
d387a4b7f6 | |||
da682f6593 | |||
1a5a9363b5 | |||
344ba4dfae | |||
d1a01c38ed | |||
45a04070a3 | |||
7ea4eca599 | |||
fd7e4b3fe2 | |||
0e55ef7d9e | |||
536313889d | |||
903d73d665 | |||
c0821fcaa5 | |||
37d4f84787 | |||
663a601e11 | |||
2520f3a006 | |||
3d987210cd | |||
28b97abb27 | |||
c4bb6028c4 | |||
4a5957cc9f | |||
f4c3c5d1be | |||
35dfe31a39 | |||
484630c1a8 | |||
c38f8c9c00 | |||
f356a35884 | |||
ab139995bd | |||
753fa8829e | |||
d21e020f18 | |||
a5f3efa669 | |||
44cea637ad | |||
a0bc840c7a | |||
529531bd85 | |||
68b90ff76b | |||
53773e650f | |||
61f3524bd1 | |||
ea613d2fcf | |||
e24f4a2d33 | |||
c8da0a92ec | |||
88f7c1e4cc | |||
19a704b7ea | |||
b8971c0da9 | |||
183bb664a2 | |||
eee920c119 | |||
c2ddbec504 | |||
96db1081ca | |||
0eb7d6c304 | |||
b1f9950cda | |||
10448ebe2d | |||
41a74c50e6 | |||
b52e27a953 | |||
090a6ecfce | |||
c8131d304b | |||
0d42e74524 | |||
b5f3100406 | |||
3f6b2be2cc | |||
48a30089b2 | |||
bd40a784cf | |||
9771b976e5 | |||
170f1a38fe | |||
4c33b8d75e | |||
1a83ee4ab9 | |||
3eda1244e6 | |||
46dae9f872 | |||
c173a779d3 | |||
461ae5d790 | |||
a279cfad95 | |||
fa0b9c8826 | |||
074e457836 | |||
c39ae06bd4 | |||
d4792b8ae5 | |||
b6ed1b81d0 | |||
44442565aa | |||
1954d2b67f | |||
6488c87949 | |||
1aea8647d4 | |||
1ee86a0b21 | |||
72e2d3115f | |||
b875b74ce5 | |||
f4c5612a60 | |||
fe99763745 | |||
ee47c517f4 | |||
737d7c6cd1 | |||
4f2997779c | |||
d66c19825a | |||
d281872f95 | |||
aca2c1abec | |||
299285abb8 | |||
565fc0abdb | |||
beea10454b | |||
d16df598fa | |||
6cd93ef990 | |||
05fa6837c4 | |||
b973e700cb | |||
d7d7e9076f | |||
8c961dfd80 | |||
f651635af8 | |||
d82117241d | |||
7cdcc14d4c | |||
aec7679a07 | |||
177ceff63c | |||
f538b3fb22 | |||
d99fa5268d | |||
fc5fd3a58a | |||
951a3d0616 | |||
449586972a | |||
ba55f1cefb | |||
d53b105c02 | |||
6699c7112c | |||
55138b1dd2 | |||
ef70ea5799 | |||
9df0800b65 | |||
5aceb15965 | |||
172cff19b9 | |||
d524393169 | |||
f0dc7f8356 | |||
ed5043cd84 | |||
aa2344bc10 | |||
28d3207b23 | |||
ce9c0b4aa7 | |||
6c84c3b971 | |||
a47af02e1b | |||
4e982ccf51 | |||
4d329c8da5 | |||
2d112dbd55 | |||
4f427d0e61 | |||
988eef2e65 | |||
8d1db3c395 | |||
de6ddeeca4 | |||
e4c9335278 | |||
f474904a46 | |||
4a2adfc765 | |||
e196ccd47f | |||
14ae99000d | |||
68dbac1fc7 | |||
fb11f890b1 | |||
b50ae1dafc | |||
de8c2198af | |||
d8ad56c647 | |||
465f5080c0 | |||
f137c73885 | |||
8b416c467c | |||
ece80890bd | |||
bdc2cd5dc9 | |||
a753a2f5ba | |||
977a996920 | |||
8f02becf9a | |||
38edb60be9 | |||
25e23af555 | |||
630513540e | |||
02de461878 | |||
171d9472ca | |||
d5606d7726 | |||
ed10673250 | |||
4e9c071049 | |||
ccb58f6ff2 | |||
e4ee79fab2 | |||
61617ff5f3 | |||
2f52d0a14f | |||
b821f8e40c | |||
0a44ca81b1 | |||
ce90c3216a | |||
dcf0f13d71 | |||
cfa2ef3a05 | |||
ba9f357b14 | |||
3561ef93a0 | |||
d5b9ba724d | |||
d9bc02eca4 | |||
b1e1dcf163 | |||
9510612b44 | |||
e905615710 | |||
5cbbf5a4ac | |||
77bba69eab | |||
20fec100b5 | |||
23150946d8 | |||
bfc3bbc371 | |||
954c89edaf | |||
b7e73a5559 | |||
512cde82ef | |||
c86a1d339e | |||
bdb1c72153 | |||
e7c588cbbc | |||
431210025d | |||
06a9417671 | |||
b5fc4bca10 | |||
cc40916bf5 | |||
baf205e113 | |||
5e44ac207b | |||
ac28ed6875 | |||
cce330fa2d | |||
c45ad217c4 | |||
6fa0bfce32 | |||
46363811f0 | |||
9cb10f7258 |
|
@ -1,5 +1,8 @@
|
|||
ui/node_modules
|
||||
ui/dist
|
||||
server/target
|
||||
docs
|
||||
.git
|
||||
# build folders and similar which are not needed for the docker build
|
||||
target
|
||||
docker
|
||||
api_tests
|
||||
ansible
|
||||
tests
|
||||
*.sh
|
||||
pictrs
|
||||
|
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
3
.github/CODEOWNERS
vendored
Normal file
3
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917
|
||||
crates/apub/ @Nutomic
|
||||
migrations/ @dessalines @phiresky @dullbananas
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,3 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
patreon: dessalines
|
||||
liberapay: Lemmy
|
||||
|
|
70
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
70
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: "\U0001F41E Bug Report"
|
||||
description: Create a report to help us improve lemmy
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Found a bug? Please fill out the sections below. 👍
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
For front end issues, use [lemmy](https://github.com/LemmyNet/lemmy-ui)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
description: Before you create a bug report please do the following.
|
||||
options:
|
||||
- label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
||||
required: true
|
||||
- label: Did you check to see if this issue already exists?
|
||||
required: true
|
||||
- label: Is this only a single bug? Do not put multiple bugs in one issue.
|
||||
required: true
|
||||
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
|
||||
required: true
|
||||
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: A summary of the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Describe the steps to reproduce the bug.
|
||||
The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: technical
|
||||
attributes:
|
||||
label: Technical Details
|
||||
description: |
|
||||
- Please post your log: `sudo docker-compose logs > lemmy_log.out`.
|
||||
- What OS are you trying to install lemmy on?
|
||||
- Any browser console errors?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: lemmy-backend-version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which Lemmy backend version do you use? Displayed in the footer.
|
||||
placeholder: ex. BE 0.17.4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: lemmy-instance
|
||||
attributes:
|
||||
label: Lemmy Instance URL
|
||||
description: Which Lemmy instance do you use? The address
|
||||
placeholder: lemmy.ml, lemmy.world, etc
|
56
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: "\U0001F680 Feature request"
|
||||
description: Suggest an idea for improving Lemmy
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Have a suggestion about Lemmy's UI?
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
description: Before you create a bug report please do the following.
|
||||
options:
|
||||
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
||||
required: true
|
||||
- label: Did you check to see if this issue already exists?
|
||||
required: true
|
||||
- label: Is this only a feature request? Do not put multiple feature requests in one issue.
|
||||
required: true
|
||||
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
|
||||
required: true
|
||||
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your proposal related to a problem?
|
||||
description: |
|
||||
Provide a clear and concise description of what the problem is.
|
||||
For example, "I'm always frustrated when..."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like.
|
||||
description: |
|
||||
Provide a clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered.
|
||||
description: |
|
||||
Let us know about other solutions you've tried or researched.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Is there anything else you can add about the proposal?
|
||||
You might want to link to related issues here, if you haven't already.
|
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: "? Question"
|
||||
description: General questions about Lemmy
|
||||
title: "Question: "
|
||||
labels: ["question", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Have a question about Lemmy?
|
||||
Please check the docs first: https://join-lemmy.org/docs/en/index.html
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What's the question you have about Lemmy?
|
||||
validations:
|
||||
required: true
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# local ansible configuration
|
||||
ansible/inventory
|
||||
ansible/passwords/
|
||||
|
||||
# docker build files
|
||||
docker/lemmy_mine.hjson
|
||||
docker/dev/env_deploy.sh
|
||||
volumes
|
||||
|
||||
# ide config
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# local build files
|
||||
target
|
||||
env_setup.sh
|
||||
query_testing/**/reports/*.json
|
||||
|
||||
# API tests
|
||||
api_tests/node_modules
|
||||
api_tests/.yalc
|
||||
api_tests/yalc.lock
|
||||
api_tests/pict-rs
|
||||
|
||||
# pictrs data
|
||||
pictrs/
|
||||
|
||||
# The generated typescript bindings
|
||||
bindings
|
||||
|
||||
# Database cluster and sockets for testing
|
||||
dev_pgdata/
|
||||
*.PGSQL.*
|
||||
|
||||
# database dumps
|
||||
*.sqldump
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[submodule "crates/utils/translations"]
|
||||
path = crates/utils/translations
|
||||
url = https://github.com/LemmyNet/lemmy-translations.git
|
||||
branch = main
|
7
.rustfmt.toml
Normal file
7
.rustfmt.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
tab_spaces = 2
|
||||
edition = "2021"
|
||||
imports_layout = "HorizontalVertical"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "One"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
22
.travis.yml
22
.travis.yml
|
@ -1,22 +0,0 @@
|
|||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
fast_finish: true
|
||||
cache: cargo
|
||||
before_script:
|
||||
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
||||
- psql -c 'create database rrr with owner rrr;' -U postgres
|
||||
before_install:
|
||||
- cd server
|
||||
script:
|
||||
- cargo install --force diesel_cli --no-default-features --features postgres
|
||||
- diesel migration run
|
||||
- cargo build --all
|
||||
- cargo test --all
|
||||
env:
|
||||
- DATABASE_URL=postgres://rrr:rrr@localhost/rrr
|
||||
addons:
|
||||
postgresql: "9.4"
|
312
.woodpecker.yml
Normal file
312
.woodpecker.yml
Normal file
|
@ -0,0 +1,312 @@
|
|||
# TODO: The when: platform conditionals aren't working currently
|
||||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
||||
|
||||
variables:
|
||||
- &rust_image "rust:1.78"
|
||||
- &rust_nightly_image "rustlang/rust:nightly"
|
||||
- &install_pnpm "corepack enable pnpm"
|
||||
- &slow_check_paths
|
||||
- event: pull_request
|
||||
path:
|
||||
include: [
|
||||
# rust source code
|
||||
"crates/**",
|
||||
"src/**",
|
||||
"**/Cargo.toml",
|
||||
"Cargo.lock",
|
||||
# database migrations
|
||||
"migrations/**",
|
||||
# typescript tests
|
||||
"api_tests/**",
|
||||
# config files and scripts used by ci
|
||||
".woodpecker.yml",
|
||||
".rustfmt.toml",
|
||||
"scripts/update_config_defaults.sh",
|
||||
"diesel.toml",
|
||||
".gitmodules",
|
||||
]
|
||||
- install_binstall: &install_binstall
|
||||
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||
- cp cargo-binstall /usr/local/cargo/bin
|
||||
- install_diesel_cli: &install_diesel_cli
|
||||
- apt update && apt install -y lsb-release build-essential
|
||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||
- apt update && apt install -y postgresql-client-16
|
||||
- cargo install diesel_cli --no-default-features --features postgres
|
||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||
|
||||
steps:
|
||||
prepare_repo:
|
||||
image: alpine:3
|
||||
commands:
|
||||
- apk add git
|
||||
- git submodule init
|
||||
- git submodule update
|
||||
when:
|
||||
- event: [pull_request, tag]
|
||||
|
||||
prettier_check:
|
||||
image: tmknom/prettier:3.0.0
|
||||
commands:
|
||||
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
toml_fmt:
|
||||
image: tamasfe/taplo:0.8.1
|
||||
commands:
|
||||
- taplo format --check
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
sql_fmt:
|
||||
image: backplane/pgformatter
|
||||
commands:
|
||||
- ./scripts/sql_format_check.sh
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
cargo_fmt:
|
||||
image: *rust_nightly_image
|
||||
environment:
|
||||
# store cargo data in repo folder so that it gets cached between steps
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- rustup component add rustfmt
|
||||
- cargo +nightly fmt -- --check
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
cargo_machete:
|
||||
image: *rust_nightly_image
|
||||
commands:
|
||||
- <<: *install_binstall
|
||||
- cargo binstall -y cargo-machete
|
||||
- cargo machete
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
ignored_files:
|
||||
image: alpine:3
|
||||
commands:
|
||||
- apk add git
|
||||
- IGNORED=$(git ls-files --cached -i --exclude-standard)
|
||||
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
# make sure api builds with default features (used by other crates relying on lemmy api)
|
||||
check_api_common_default_features:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- cargo check --package lemmy_api_common
|
||||
when: *slow_check_paths
|
||||
|
||||
lemmy_api_common_doesnt_depend_on_diesel:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
|
||||
when: *slow_check_paths
|
||||
|
||||
lemmy_api_common_works_with_wasm:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- "rustup target add wasm32-unknown-unknown"
|
||||
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
|
||||
when: *slow_check_paths
|
||||
|
||||
check_defaults_hjson_updated:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- export LEMMY_CONFIG_LOCATION=./config/config.hjson
|
||||
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
|
||||
- diff config/defaults.hjson config/defaults_current.hjson
|
||||
when: *slow_check_paths
|
||||
|
||||
check_diesel_schema:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
commands:
|
||||
- <<: *install_diesel_cli
|
||||
- diesel migration run
|
||||
- diesel print-schema --config-file=diesel.toml > tmp.schema
|
||||
- diff tmp.schema crates/db_schema/src/schema.rs
|
||||
when: *slow_check_paths
|
||||
|
||||
check_db_perf_tool:
|
||||
image: *rust_image
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
# same as scripts/db_perf.sh but without creating a new database server
|
||||
- export LEMMY_CONFIG_LOCATION=config/config.hjson
|
||||
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_clippy:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- rustup component add clippy
|
||||
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_build:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- cargo build
|
||||
- mv target/debug/lemmy_server target/lemmy_server
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_test:
|
||||
image: *rust_image
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
|
||||
- cargo test --workspace --no-fail-fast
|
||||
when: *slow_check_paths
|
||||
|
||||
check_diesel_migration:
|
||||
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
|
||||
image: *rust_image
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo_home
|
||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
PGUSER: lemmy
|
||||
PGPASSWORD: password
|
||||
PGHOST: database
|
||||
PGDATABASE: lemmy
|
||||
commands:
|
||||
# Install diesel_cli
|
||||
- <<: *install_diesel_cli
|
||||
# Run all migrations
|
||||
- diesel migration run
|
||||
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
|
||||
- apt update && apt install -y lsb-release
|
||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||
- apt update && apt install -y postgresql-client-16
|
||||
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
||||
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
|
||||
# Make sure that the newest migration is revertable without the `r` schema
|
||||
- diesel migration redo
|
||||
# Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things
|
||||
- alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending"
|
||||
- lemmy_schema_setup
|
||||
- lemmy_schema_setup
|
||||
# Make sure that the newest migration is revertable with the `r` schema
|
||||
- diesel migration redo
|
||||
# Check for changes in the schema, which would be caused by an incorrect migration
|
||||
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
||||
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump
|
||||
- diff before.sqldump after.sqldump
|
||||
when: *slow_check_paths
|
||||
|
||||
run_federation_tests:
|
||||
image: node:20-bookworm-slim
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
|
||||
DO_WRITE_HOSTS_FILE: "1"
|
||||
commands:
|
||||
- *install_pnpm
|
||||
- apt update && apt install -y bash curl postgresql-client
|
||||
- bash api_tests/prepare-drone-federation-test.sh
|
||||
- cd api_tests/
|
||||
- pnpm i
|
||||
- pnpm api-test
|
||||
when: *slow_check_paths
|
||||
|
||||
federation_tests_server_output:
|
||||
image: alpine:3
|
||||
commands:
|
||||
# `|| true` prevents this step from appearing to fail if the server output files don't exist
|
||||
- cat target/log/lemmy_*.out || true
|
||||
- "# If you can't see all output, then use the download button"
|
||||
when:
|
||||
- event: pull_request
|
||||
status: failure
|
||||
|
||||
publish_release_docker:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy
|
||||
dockerfile: docker/Dockerfile
|
||||
platforms: linux/amd64, linux/arm64
|
||||
build_args:
|
||||
- RUST_RELEASE_MODE=release
|
||||
tag: ${CI_COMMIT_TAG}
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
nightly_build:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy
|
||||
dockerfile: docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build_args:
|
||||
- RUST_RELEASE_MODE=release
|
||||
tag: dev
|
||||
when:
|
||||
- event: cron
|
||||
|
||||
# using https://github.com/pksunkara/cargo-workspaces
|
||||
publish_to_crates_io:
|
||||
image: *rust_image
|
||||
commands:
|
||||
- <<: *install_binstall
|
||||
# Install cargo-workspaces
|
||||
- cargo binstall -y cargo-workspaces
|
||||
- cp -r migrations crates/db_schema/
|
||||
- cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
|
||||
secrets: [cargo_api_token]
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
notify_on_failure:
|
||||
image: alpine:3
|
||||
commands:
|
||||
- apk add curl
|
||||
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
|
||||
when:
|
||||
- event: [pull_request, tag]
|
||||
status: failure
|
||||
|
||||
notify_on_tag_deploy:
|
||||
image: alpine:3
|
||||
commands:
|
||||
- apk add curl
|
||||
- "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: lemmy
|
||||
POSTGRES_PASSWORD: password
|
6901
Cargo.lock
generated
Normal file
6901
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
210
Cargo.toml
Normal file
210
Cargo.toml
Normal file
|
@ -0,0 +1,210 @@
|
|||
[workspace.package]
|
||||
version = "0.19.4-rc.5"
|
||||
edition = "2021"
|
||||
description = "A link aggregator for the fediverse"
|
||||
license = "AGPL-3.0"
|
||||
homepage = "https://join-lemmy.org/"
|
||||
documentation = "https://join-lemmy.org/docs/en/index.html"
|
||||
repository = "https://github.com/LemmyNet/lemmy"
|
||||
|
||||
[package]
|
||||
name = "lemmy_server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[profile.release]
|
||||
debug = 0
|
||||
lto = "thin"
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
opt-level = "z" # Optimize for size.
|
||||
|
||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
||||
# out temporarily, but make sure to leave this in the main branch.
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
[features]
|
||||
embed-pictrs = ["pict-rs"]
|
||||
# This feature requires building with `tokio_unstable` flag, see documentation:
|
||||
# https://docs.rs/tokio/latest/tokio/#unstable-features
|
||||
console = [
|
||||
"console-subscriber",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"tracing-opentelemetry",
|
||||
"reqwest-tracing/opentelemetry_0_16",
|
||||
]
|
||||
json-log = ["tracing-subscriber/json"]
|
||||
default = []
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/api",
|
||||
"crates/api_crud",
|
||||
"crates/api_common",
|
||||
"crates/apub",
|
||||
"crates/utils",
|
||||
"crates/db_perf",
|
||||
"crates/db_schema",
|
||||
"crates/db_views",
|
||||
"crates/db_views_actor",
|
||||
"crates/db_views_actor",
|
||||
"crates/routes",
|
||||
"crates/federate",
|
||||
]
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
complexity = { level = "deny", priority = -1 }
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
dbg_macro = "deny"
|
||||
explicit_into_iter_loop = "deny"
|
||||
explicit_iter_loop = "deny"
|
||||
get_first = "deny"
|
||||
implicit_clone = "deny"
|
||||
indexing_slicing = "deny"
|
||||
inefficient_to_string = "deny"
|
||||
items-after-statements = "deny"
|
||||
manual_string_new = "deny"
|
||||
needless_collect = "deny"
|
||||
perf = { level = "deny", priority = -1 }
|
||||
redundant_closure_for_method_calls = "deny"
|
||||
style = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
uninlined_format_args = "allow"
|
||||
unused_self = "deny"
|
||||
unwrap_used = "deny"
|
||||
|
||||
[workspace.dependencies]
|
||||
lemmy_api = { version = "=0.19.4-rc.5", path = "./crates/api" }
|
||||
lemmy_api_crud = { version = "=0.19.4-rc.5", path = "./crates/api_crud" }
|
||||
lemmy_apub = { version = "=0.19.4-rc.5", path = "./crates/apub" }
|
||||
lemmy_utils = { version = "=0.19.4-rc.5", path = "./crates/utils", default-features = false }
|
||||
lemmy_db_schema = { version = "=0.19.4-rc.5", path = "./crates/db_schema" }
|
||||
lemmy_api_common = { version = "=0.19.4-rc.5", path = "./crates/api_common" }
|
||||
lemmy_routes = { version = "=0.19.4-rc.5", path = "./crates/routes" }
|
||||
lemmy_db_views = { version = "=0.19.4-rc.5", path = "./crates/db_views" }
|
||||
lemmy_db_views_actor = { version = "=0.19.4-rc.5", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.4-rc.5", path = "./crates/db_views_moderator" }
|
||||
lemmy_federate = { version = "=0.19.4-rc.5", path = "./crates/federate" }
|
||||
activitypub_federation = { version = "0.5.6", default-features = false, features = [
|
||||
"actix-web",
|
||||
] }
|
||||
diesel = "2.1.6"
|
||||
diesel_migrations = "2.1.0"
|
||||
diesel-async = "0.4.1"
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
serde_with = "3.8.1"
|
||||
actix-web = { version = "4.6.0", default-features = false, features = [
|
||||
"macros",
|
||||
"rustls-0_23",
|
||||
"compress-brotli",
|
||||
"compress-gzip",
|
||||
"compress-zstd",
|
||||
"cookies",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-actix-web = { version = "0.7.10", default-features = false }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-log = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
|
||||
reqwest-middleware = "0.2.5"
|
||||
reqwest-tracing = "0.4.8"
|
||||
clokwerk = "0.4.0"
|
||||
doku = { version = "0.21.1", features = ["url-2"] }
|
||||
bcrypt = "0.15.1"
|
||||
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
||||
serde_json = { version = "1.0.117", features = ["preserve_order"] }
|
||||
base64 = "0.22.1"
|
||||
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
||||
async-trait = "0.1.80"
|
||||
captcha = "0.0.9"
|
||||
anyhow = { version = "1.0.86", features = [
|
||||
"backtrace",
|
||||
] } # backtrace is on by default on nightly, but not stable rust
|
||||
diesel_ltree = "0.3.1"
|
||||
typed-builder = "0.18.2"
|
||||
serial_test = "3.1.1"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
regex = "1.10.4"
|
||||
once_cell = "1.19.0"
|
||||
diesel-derive-newtype = "2.1.2"
|
||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||
strum = "0.26.2"
|
||||
strum_macros = "0.26.2"
|
||||
itertools = "0.13.0"
|
||||
futures = "0.3.30"
|
||||
http = "0.2.12"
|
||||
rosetta-i18n = "0.1.3"
|
||||
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
|
||||
tracing-opentelemetry = { version = "0.19.0" }
|
||||
ts-rs = { version = "7.1.1", features = [
|
||||
"serde-compat",
|
||||
"chrono-impl",
|
||||
"no-serde-warnings",
|
||||
] }
|
||||
rustls = { version = "0.23.8", features = ["ring"] }
|
||||
futures-util = "0.3.30"
|
||||
tokio-postgres = "0.7.10"
|
||||
tokio-postgres-rustls = "0.12.0"
|
||||
urlencoding = "2.1.3"
|
||||
enum-map = "2.7"
|
||||
moka = { version = "0.12.7", features = ["future"] }
|
||||
i-love-jesus = { version = "0.1.0" }
|
||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[dependencies]
|
||||
lemmy_api = { workspace = true }
|
||||
lemmy_api_crud = { workspace = true }
|
||||
lemmy_apub = { workspace = true }
|
||||
lemmy_utils = { workspace = true }
|
||||
lemmy_db_schema = { workspace = true }
|
||||
lemmy_api_common = { workspace = true }
|
||||
lemmy_routes = { workspace = true }
|
||||
lemmy_federate = { workspace = true }
|
||||
activitypub_federation = { workspace = true }
|
||||
diesel = { workspace = true }
|
||||
diesel-async = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
tracing-log = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
reqwest-tracing = { workspace = true }
|
||||
clokwerk = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, optional = true }
|
||||
console-subscriber = { version = "0.2.0", optional = true }
|
||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
||||
pict-rs = { version = "0.5.14", optional = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.7.0"
|
||||
futures-util = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
prometheus = { version = "0.13.4", features = ["process"] }
|
||||
serial_test = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
actix-web-prom = "0.8.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
246
README.md
246
README.md
|
@ -1,157 +1,165 @@
|
|||
<h1><img src="ui/assets/favicon.svg" width="50px" height="50px" style="vertical-align:bottom" /><span>Lemmy</span></h1>
|
||||
<div align="center">
|
||||
|
||||
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
||||
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
||||
[![star this repo](http://githubbadges.com/star.svg?user=dessalines&repo=lemmy&style=flat)](https://github.com/dessalines/lemmy)
|
||||
[![fork this repo](http://githubbadges.com/fork.svg?user=dessalines&repo=lemmy&style=flat)](https://github.com/dessalines/lemmy/fork)
|
||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
|
||||
[![Build Status](https://woodpecker.join-lemmy.org/api/badges/LemmyNet/lemmy/status.svg)](https://woodpecker.join-lemmy.org/LemmyNet/lemmy)
|
||||
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
||||
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
|
||||
![GitHub repo size](https://img.shields.io/github/repo-size/dessalines/lemmy.svg)
|
||||
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dessalines/lemmy.svg)
|
||||
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
|
||||
[![Mastodon](https://img.shields.io/badge/Mastodon-@LemmyDev-lightgrey.svg)](https://mastodon.social/@LemmyDev)
|
||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||
[![Patreon](https://img.shields.io/badge/-Support%20on%20Patreon-blueviolet.svg)](https://www.patreon.com/dessalines)
|
||||
[![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/)
|
||||
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
|
||||
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
|
||||
<a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a>
|
||||
|
||||
A link aggregator / reddit clone for the fediverse.
|
||||
</div>
|
||||
|
||||
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
|
||||
<p align="center">
|
||||
<span>English</span> |
|
||||
<a href="readmes/README.es.md">Español</a> |
|
||||
<a href="readmes/README.ru.md">Русский</a> |
|
||||
<a href="readmes/README.zh.hans.md">汉语</a> |
|
||||
<a href="readmes/README.zh.hant.md">漢語</a> |
|
||||
<a href="readmes/README.ja.md">日本語</a>
|
||||
</p>
|
||||
|
||||
This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
|
||||
<p align="center">
|
||||
<a href="https://join-lemmy.org/" rel="noopener">
|
||||
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmyNet/lemmy-ui/main/src/assets/icons/favicon.svg"></a>
|
||||
|
||||
Front Page|Post
|
||||
---|---
|
||||
![main screen](https://i.imgur.com/y64BtXC.png)|![chat screen](https://i.imgur.com/vsOr87q.png)
|
||||
## Features
|
||||
<h3 align="center"><a href="https://join-lemmy.org">Lemmy</a></h3>
|
||||
<p align="center">
|
||||
A link aggregator and forum for the fediverse.
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://join-lemmy.org">Join Lemmy</a>
|
||||
·
|
||||
<a href="https://join-lemmy.org/docs/index.html">Documentation</a>
|
||||
·
|
||||
<a href="https://matrix.to/#/#lemmy-space:matrix.org">Matrix Chat</a>
|
||||
·
|
||||
<a href="https://github.com/LemmyNet/lemmy/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
|
||||
·
|
||||
<a href="https://github.com/LemmyNet/lemmy/blob/main/RELEASES.md">Releases</a>
|
||||
·
|
||||
<a href="https://join-lemmy.org/docs/code_of_conduct.html">Code of Conduct</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
- Open source, [AGPL License](/LICENSE).
|
||||
- Self hostable, easy to deploy.
|
||||
- Comes with [Docker](#docker).
|
||||
- Live-updating Comment threads.
|
||||
- Full vote scores `(+/-)` like old reddit.
|
||||
- Moderation abilities.
|
||||
- Public Moderation Logs.
|
||||
- Both site admins, and community moderators, who can appoint other moderators.
|
||||
- Can lock, remove, and restore posts and comments.
|
||||
- Can ban and unban users from communities and the site.
|
||||
- Clean, mobile-friendly interface.
|
||||
- i18n / internationalization support.
|
||||
- NSFW post / community support.
|
||||
- High performance.
|
||||
- Server is written in rust.
|
||||
- Front end is `~80kB` gzipped.
|
||||
## About The Project
|
||||
|
||||
## About
|
||||
| Desktop | Mobile |
|
||||
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
|
||||
|
||||
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||
|
||||
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||
It is an easily self-hostable, decentralized alternative to Reddit and other link aggregators, outside of their corporate control and meddling.
|
||||
|
||||
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||
Each Lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||
|
||||
## Why's it called Lemmy?
|
||||
### Why's it called Lemmy?
|
||||
|
||||
- Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=3mbvWn1EY6g).
|
||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||
|
||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
||||
### Built With
|
||||
|
||||
## Install
|
||||
- [Rust](https://www.rust-lang.org)
|
||||
- [Actix](https://actix.rs/)
|
||||
- [Diesel](http://diesel.rs/)
|
||||
- [Inferno](https://infernojs.org)
|
||||
- [Typescript](https://www.typescriptlang.org/)
|
||||
|
||||
### Docker
|
||||
## Features
|
||||
|
||||
Make sure you have both docker and docker-compose(>=`1.24.0`) installed.
|
||||
- Open source, [AGPL License](/LICENSE).
|
||||
- Self hostable, easy to deploy.
|
||||
- Comes with [Docker](https://join-lemmy.org/docs/administration/install_docker.html) and [Ansible](https://join-lemmy.org/docs/administration/install_ansible.html).
|
||||
- Clean, mobile-friendly interface.
|
||||
- Only a minimum of a username and password is required to sign up!
|
||||
- User avatar support.
|
||||
- Live-updating Comment threads.
|
||||
- Full vote scores `(+/-)` like old Reddit.
|
||||
- Themes, including light, dark, and solarized.
|
||||
- Emojis with autocomplete support. Start typing `:`
|
||||
- User tagging using `@`, Community tagging using `!`.
|
||||
- Integrated image uploading in both posts and comments.
|
||||
- A post can consist of a title and any combination of self text, a URL, or nothing else.
|
||||
- Notifications, on comment replies and when you're tagged.
|
||||
- Notifications can be sent via email.
|
||||
- Private messaging support.
|
||||
- i18n / internationalization support.
|
||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||
- Cross-posting support.
|
||||
- A _similar post search_ when creating new posts. Great for question / answer communities.
|
||||
- Moderation abilities.
|
||||
- Public Moderation Logs.
|
||||
- Can sticky posts to the top of communities.
|
||||
- Both site admins, and community moderators, who can appoint other moderators.
|
||||
- Can lock, remove, and restore posts and comments.
|
||||
- Can ban and unban users from communities and the site.
|
||||
- Can transfer site and communities to others.
|
||||
- Can fully erase your data, replacing all posts and comments.
|
||||
- NSFW post / community support.
|
||||
- High performance.
|
||||
- Server is written in rust.
|
||||
- Supports arm64 / Raspberry Pi.
|
||||
|
||||
```
|
||||
mkdir lemmy/
|
||||
cd lemmy/
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
## Installation
|
||||
|
||||
and goto http://localhost:8536
|
||||
- [Lemmy Administration Docs](https://join-lemmy.org/docs/administration/administration.html)
|
||||
|
||||
### Nginx Config
|
||||
```
|
||||
location / {
|
||||
rewrite (\/(user|u|inbox|post|community|c|login|search|sponsors|communities|modlog|home)+) /static/index.html break;
|
||||
proxy_pass http://0.0.0.0:8536;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
## Lemmy Projects
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
- [awesome-lemmy - A community driven list of apps and tools for lemmy](https://github.com/dbeley/awesome-lemmy)
|
||||
|
||||
## Develop
|
||||
|
||||
### Docker Development
|
||||
|
||||
```
|
||||
git clone https://github.com/dessalines/lemmy
|
||||
cd lemmy/docker
|
||||
./docker_update.sh # This pulls the newest version, builds and runs it
|
||||
```
|
||||
|
||||
and goto http://localhost:8536
|
||||
|
||||
### Local Development
|
||||
|
||||
#### Requirements
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [Yarn](https://yarnpkg.com/en/)
|
||||
- [Postgres](https://www.postgresql.org/)
|
||||
|
||||
#### Set up Postgres DB
|
||||
|
||||
```
|
||||
psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
||||
psql -c 'create database rrr with owner rrr;' -U postgres
|
||||
```
|
||||
|
||||
#### Running
|
||||
|
||||
```
|
||||
git clone https://github.com/dessalines/lemmy
|
||||
cd lemmy
|
||||
./install.sh
|
||||
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||
# cd ui && yarn start
|
||||
# cd server && cargo watch -x run
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Websocket API for App developers](docs/api.md)
|
||||
- [ActivityPub API.md](docs/apub_api_outline.md)
|
||||
- [Goals](docs/goals.md)
|
||||
- [Ranking Algorithm](docs/ranking.md)
|
||||
|
||||
## Support
|
||||
## Support / Donate
|
||||
|
||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||
|
||||
Lemmy is made possible by a generous grant from the [NLnet foundation](https://nlnet.nl/).
|
||||
|
||||
- [Support on Liberapay](https://liberapay.com/Lemmy).
|
||||
- [Support on Patreon](https://www.patreon.com/dessalines).
|
||||
- [Sponsor List](https://dev.lemmy.ml/sponsors).
|
||||
- [Support on OpenCollective](https://opencollective.com/lemmy).
|
||||
- [List of Sponsors](https://join-lemmy.org/donate).
|
||||
|
||||
### Crypto
|
||||
|
||||
- bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
|
||||
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
||||
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
|
||||
|
||||
## Translations
|
||||
## Contributing
|
||||
|
||||
If you'd like to add translations, take a look a look at the [english translation file](ui/src/translations/en.ts).
|
||||
Read the following documentation to setup the development environment and start coding:
|
||||
|
||||
- Languages supported: `en`, `zh`, `fr`, `sv`, `de`, `ru`, `de`
|
||||
- [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.html)
|
||||
- [Docker Development](https://join-lemmy.org/docs/contributors/03-docker-development.html)
|
||||
- [Local Development](https://join-lemmy.org/docs/contributors/02-local-development.html)
|
||||
|
||||
When working on an issue or pull request, you can comment with any questions you may have so that maintainers can answer them. You can also join the [Matrix Development Chat](https://matrix.to/#/#lemmydev:matrix.org) for general assistance.
|
||||
|
||||
### Translations
|
||||
|
||||
- If you want to help with translating, take a look at [Weblate](https://weblate.join-lemmy.org/projects/lemmy/). You can also help by [translating the documentation](https://github.com/LemmyNet/lemmy-docs#adding-a-new-language).
|
||||
|
||||
## Community
|
||||
|
||||
- [Matrix Space](https://matrix.to/#/#lemmy-space:matrix.org)
|
||||
- [Lemmy Forum](https://lemmy.ml/c/lemmy)
|
||||
- [Lemmy Support Forum](https://lemmy.ml/c/lemmy_support)
|
||||
|
||||
## Code Mirrors
|
||||
|
||||
- [GitHub](https://github.com/LemmyNet/lemmy)
|
||||
- [Gitea](https://git.join-lemmy.org/LemmyNet/lemmy)
|
||||
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
|
||||
|
||||
## Credits
|
||||
|
||||
Icon made by Andy Cuccaro (@andycuccaro).
|
||||
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.
|
||||
|
|
3
RELEASES.md
Normal file
3
RELEASES.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
[Lemmy Releases / news](https://join-lemmy.org/news)
|
||||
|
||||
[Github link](https://github.com/LemmyNet/joinlemmy-site/tree/main/src/assets/news)
|
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use [Github's security advisory issue system](https://github.com/LemmyNet/lemmy/security/advisories/new).
|
42
api_tests/.eslintrc.json
Normal file
42
api_tests/.eslintrc.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"warnOnUnsupportedTypeScriptVersion": false
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"arrow-body-style": 0,
|
||||
"curly": 0,
|
||||
"eol-last": 0,
|
||||
"eqeqeq": 0,
|
||||
"func-style": 0,
|
||||
"import/no-duplicates": 0,
|
||||
"max-statements": 0,
|
||||
"max-params": 0,
|
||||
"new-cap": 0,
|
||||
"no-console": 0,
|
||||
"no-duplicate-imports": 0,
|
||||
"no-extra-parens": 0,
|
||||
"no-return-assign": 0,
|
||||
"no-throw-literal": 0,
|
||||
"no-trailing-spaces": 0,
|
||||
"no-unused-expressions": 0,
|
||||
"no-useless-constructor": 0,
|
||||
"no-useless-escape": 0,
|
||||
"no-var": 0,
|
||||
"prefer-const": 0,
|
||||
"prefer-rest-params": 0,
|
||||
"quote-props": 0,
|
||||
"unicorn/filename-case": 0
|
||||
}
|
||||
}
|
1
api_tests/.npmrc
Normal file
1
api_tests/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-manager-strict=false
|
4
api_tests/.prettierrc.json
Normal file
4
api_tests/.prettierrc.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": true
|
||||
}
|
4
api_tests/jest.config.js
Normal file
4
api_tests/jest.config.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
36
api_tests/package.json
Normal file
36
api_tests/package.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "api_tests",
|
||||
"version": "0.0.1",
|
||||
"description": "API tests for lemmy backend",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/LemmyNet/lemmy",
|
||||
"author": "Dessalines",
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@9.1.4",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
|
||||
"fix": "prettier --write src && eslint --fix src",
|
||||
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
|
||||
"api-test-follow": "jest -i follow.spec.ts",
|
||||
"api-test-comment": "jest -i comment.spec.ts",
|
||||
"api-test-post": "jest -i post.spec.ts",
|
||||
"api-test-user": "jest -i user.spec.ts",
|
||||
"api-test-community": "jest -i community.spec.ts",
|
||||
"api-test-private-message": "jest -i private_message.spec.ts",
|
||||
"api-test-image": "jest -i image.spec.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"download-file-sync": "^1.0.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.5.0",
|
||||
"lemmy-js-client": "0.19.4-alpha.18",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.4.4"
|
||||
}
|
||||
}
|
3462
api_tests/pnpm-lock.yaml
Normal file
3462
api_tests/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
94
api_tests/prepare-drone-federation-test.sh
Executable file
94
api_tests/prepare-drone-federation-test.sh
Executable file
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env bash
|
||||
# IMPORTANT NOTE: this script does not use the normal LEMMY_DATABASE_URL format
|
||||
# it is expected that this script is called by run-federation-test.sh script.
|
||||
set -e
|
||||
|
||||
if [ -z "$LEMMY_LOG_LEVEL" ];
|
||||
then
|
||||
LEMMY_LOG_LEVEL=info
|
||||
fi
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
|
||||
|
||||
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
|
||||
|
||||
# pictrs setup
|
||||
if [ ! -f "api_tests/pict-rs" ]; then
|
||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs
|
||||
chmod +x api_tests/pict-rs
|
||||
fi
|
||||
./api_tests/pict-rs \
|
||||
run -a 0.0.0.0:8080 \
|
||||
--danger-dummy-mode \
|
||||
--api-key "my-pictrs-key" \
|
||||
filesystem -p /tmp/pictrs/files \
|
||||
sled -p /tmp/pictrs/sled-repo 2>&1 &
|
||||
|
||||
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
|
||||
echo "DB URL: ${LEMMY_DATABASE_URL} INSTANCE: $INSTANCE"
|
||||
psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
|
||||
echo "create database"
|
||||
psql "${LEMMY_DATABASE_URL}/lemmy" -c "CREATE DATABASE $INSTANCE"
|
||||
done
|
||||
|
||||
if [ -z "$DO_WRITE_HOSTS_FILE" ]; then
|
||||
if ! grep -q lemmy-alpha /etc/hosts; then
|
||||
echo "Please add the following to your /etc/hosts file, then press enter:
|
||||
|
||||
127.0.0.1 lemmy-alpha
|
||||
127.0.0.1 lemmy-beta
|
||||
127.0.0.1 lemmy-gamma
|
||||
127.0.0.1 lemmy-delta
|
||||
127.0.0.1 lemmy-epsilon"
|
||||
read -p ""
|
||||
fi
|
||||
else
|
||||
for INSTANCE in lemmy-alpha lemmy-beta lemmy-gamma lemmy-delta lemmy-epsilon; do
|
||||
echo "127.0.0.1 $INSTANCE" >>/etc/hosts
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$PWD"
|
||||
|
||||
LOG_DIR=target/log
|
||||
mkdir -p $LOG_DIR
|
||||
|
||||
echo "start alpha"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
|
||||
target/lemmy_server >$LOG_DIR/lemmy_alpha.out 2>&1 &
|
||||
|
||||
echo "start beta"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
|
||||
target/lemmy_server >$LOG_DIR/lemmy_beta.out 2>&1 &
|
||||
|
||||
echo "start gamma"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
|
||||
target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 &
|
||||
|
||||
echo "start delta"
|
||||
# An instance with only an allowlist for beta
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
|
||||
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 &
|
||||
|
||||
echo "start epsilon"
|
||||
# An instance who has a blocklist, with lemmy-alpha blocked
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
|
||||
target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 &
|
||||
|
||||
echo "wait for all instances to start"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
echo "alpha started"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
echo "beta started"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
echo "gamma started"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
echo "delta started"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
echo "epsilon started. All started"
|
21
api_tests/run-federation-test.sh
Executable file
21
api_tests/run-federation-test.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432
|
||||
pushd ..
|
||||
cargo build
|
||||
rm target/lemmy_server || true
|
||||
cp target/debug/lemmy_server target/lemmy_server
|
||||
killall -s1 lemmy_server || true
|
||||
./api_tests/prepare-drone-federation-test.sh
|
||||
popd
|
||||
|
||||
pnpm i
|
||||
pnpm api-test || true
|
||||
|
||||
killall -s1 lemmy_server || true
|
||||
killall -s1 pict-rs || true
|
||||
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
|
||||
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"
|
||||
done
|
||||
rm -r /tmp/pictrs
|
860
api_tests/src/comment.spec.ts
Normal file
860
api_tests/src/comment.spec.ts
Normal file
|
@ -0,0 +1,860 @@
|
|||
jest.setTimeout(180000);
|
||||
|
||||
import { PostResponse } from "lemmy-js-client/dist/types/PostResponse";
|
||||
import {
|
||||
alpha,
|
||||
beta,
|
||||
gamma,
|
||||
setupLogins,
|
||||
createPost,
|
||||
getPost,
|
||||
resolveComment,
|
||||
likeComment,
|
||||
followBeta,
|
||||
resolveBetaCommunity,
|
||||
createComment,
|
||||
editComment,
|
||||
deleteComment,
|
||||
removeComment,
|
||||
getMentions,
|
||||
resolvePost,
|
||||
unfollowRemotes,
|
||||
createCommunity,
|
||||
registerUser,
|
||||
reportComment,
|
||||
listCommentReports,
|
||||
randomString,
|
||||
unfollows,
|
||||
getComments,
|
||||
getCommentParentId,
|
||||
resolveCommunity,
|
||||
getPersonDetails,
|
||||
getReplies,
|
||||
getUnreadCount,
|
||||
waitUntil,
|
||||
waitForPost,
|
||||
alphaUrl,
|
||||
followCommunity,
|
||||
blockCommunity,
|
||||
delay,
|
||||
saveUserSettings,
|
||||
} from "./shared";
|
||||
import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client";
|
||||
|
||||
let betaCommunity: CommunityView | undefined;
|
||||
let postOnAlphaRes: PostResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
await Promise.all([followBeta(alpha), followBeta(gamma)]);
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
if (betaCommunity) {
|
||||
postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(unfollows);
|
||||
|
||||
function assertCommentFederation(
|
||||
commentOne?: CommentView,
|
||||
commentTwo?: CommentView,
|
||||
) {
|
||||
expect(commentOne?.comment.ap_id).toBe(commentTwo?.comment.ap_id);
|
||||
expect(commentOne?.comment.content).toBe(commentTwo?.comment.content);
|
||||
expect(commentOne?.creator.name).toBe(commentTwo?.creator.name);
|
||||
expect(commentOne?.community.actor_id).toBe(commentTwo?.community.actor_id);
|
||||
expect(commentOne?.comment.published).toBe(commentTwo?.comment.published);
|
||||
expect(commentOne?.comment.updated).toBe(commentOne?.comment.updated);
|
||||
expect(commentOne?.comment.deleted).toBe(commentOne?.comment.deleted);
|
||||
expect(commentOne?.comment.removed).toBe(commentOne?.comment.removed);
|
||||
}
|
||||
|
||||
test("Create a comment", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
||||
expect(commentRes.comment_view.community.local).toBe(false);
|
||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure that comment is liked on beta
|
||||
let betaComment = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
)
|
||||
).comment;
|
||||
expect(betaComment).toBeDefined();
|
||||
expect(betaComment?.community.local).toBe(true);
|
||||
expect(betaComment?.creator.local).toBe(false);
|
||||
expect(betaComment?.counts.score).toBe(1);
|
||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
||||
});
|
||||
|
||||
test("Create a comment in a non-existent post", async () => {
|
||||
await expect(createComment(alpha, -1)).rejects.toStrictEqual(
|
||||
Error("couldnt_find_post"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Update a comment", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
// Federate the comment first
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment;
|
||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
||||
|
||||
let updateCommentRes = await editComment(
|
||||
alpha,
|
||||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
expect(updateCommentRes.comment_view.comment.content).toBe(
|
||||
"A jest test federated comment update",
|
||||
);
|
||||
expect(updateCommentRes.comment_view.community.local).toBe(false);
|
||||
expect(updateCommentRes.comment_view.creator.local).toBe(true);
|
||||
|
||||
// Make sure that post is updated on beta
|
||||
let betaCommentUpdated = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c =>
|
||||
c.comment?.comment.content === "A jest test federated comment update",
|
||||
)
|
||||
).comment;
|
||||
assertCommentFederation(betaCommentUpdated, updateCommentRes.comment_view);
|
||||
});
|
||||
|
||||
test("Delete a comment", async () => {
|
||||
let post = await createPost(alpha, betaCommunity!.community.id);
|
||||
// creating a comment on alpha (remote from home of community)
|
||||
let commentRes = await createComment(alpha, post.post_view.post.id);
|
||||
|
||||
// Find the comment on beta (home of community)
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment;
|
||||
if (!betaComment) {
|
||||
throw "Missing beta comment before delete";
|
||||
}
|
||||
|
||||
// Find the comment on remote instance gamma
|
||||
let gammaComment = (
|
||||
await waitUntil(
|
||||
() =>
|
||||
resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
||||
r => r.message !== "couldnt_find_object",
|
||||
)
|
||||
).comment;
|
||||
if (!gammaComment) {
|
||||
throw "Missing gamma comment (remote-home-remote replication) before delete";
|
||||
}
|
||||
|
||||
let deleteCommentRes = await deleteComment(
|
||||
alpha,
|
||||
true,
|
||||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
|
||||
expect(deleteCommentRes.comment_view.comment.content).toBe("");
|
||||
|
||||
// Make sure that comment is undefined on beta
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message == "couldnt_find_object",
|
||||
);
|
||||
|
||||
// Make sure that comment is undefined on gamma after delete
|
||||
await waitUntil(
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message === "couldnt_find_object",
|
||||
);
|
||||
|
||||
// Test undeleting the comment
|
||||
let undeleteCommentRes = await deleteComment(
|
||||
alpha,
|
||||
false,
|
||||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false);
|
||||
|
||||
// Make sure that comment is undeleted on beta
|
||||
let betaComment2 = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message !== "couldnt_find_object",
|
||||
)
|
||||
).comment;
|
||||
expect(betaComment2?.comment.deleted).toBe(false);
|
||||
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
|
||||
});
|
||||
|
||||
test.skip("Remove a comment from admin and community on the same instance", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
|
||||
// Get the id for beta
|
||||
let betaCommentId = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment?.comment.id;
|
||||
|
||||
if (!betaCommentId) {
|
||||
throw "beta comment id is missing";
|
||||
}
|
||||
|
||||
// The beta admin removes it (the community lives on beta)
|
||||
let removeCommentRes = await removeComment(beta, true, betaCommentId);
|
||||
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
|
||||
|
||||
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
|
||||
let refetchedPostComments = await getPersonDetails(
|
||||
alpha,
|
||||
commentRes.comment_view.comment.creator_id,
|
||||
);
|
||||
expect(refetchedPostComments.comments[0].comment.removed).toBe(true);
|
||||
|
||||
// beta will unremove the comment
|
||||
let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
|
||||
expect(unremoveCommentRes.comment_view.comment.removed).toBe(false);
|
||||
|
||||
// Make sure that comment is unremoved on alpha
|
||||
let refetchedPostComments2 = await getComments(
|
||||
alpha,
|
||||
postOnAlphaRes.post_view.post.id,
|
||||
);
|
||||
expect(refetchedPostComments2.comments[0].comment.removed).toBe(false);
|
||||
assertCommentFederation(
|
||||
refetchedPostComments2.comments[0],
|
||||
unremoveCommentRes.comment_view,
|
||||
);
|
||||
});
|
||||
|
||||
test("Remove a comment from admin and community on different instance", async () => {
|
||||
let newAlphaApi = await registerUser(alpha, alphaUrl);
|
||||
|
||||
// New alpha user creates a community, post, and comment.
|
||||
let newCommunity = await createCommunity(newAlphaApi);
|
||||
let newPost = await createPost(
|
||||
newAlphaApi,
|
||||
newCommunity.community_view.community.id,
|
||||
);
|
||||
let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id);
|
||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
||||
|
||||
// Beta searches that to cache it, then removes it
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment;
|
||||
|
||||
if (!betaComment) {
|
||||
throw "beta comment missing";
|
||||
}
|
||||
|
||||
let removeCommentRes = await removeComment(
|
||||
beta,
|
||||
true,
|
||||
betaComment.comment.id,
|
||||
);
|
||||
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
|
||||
expect(removeCommentRes.comment_view.comment.content).toBe("");
|
||||
|
||||
// Comment text is also hidden from list
|
||||
let listComments = await getComments(
|
||||
beta,
|
||||
removeCommentRes.comment_view.post.id,
|
||||
);
|
||||
expect(listComments.comments.length).toBe(1);
|
||||
expect(listComments.comments[0].comment.removed).toBe(true);
|
||||
expect(listComments.comments[0].comment.content).toBe("");
|
||||
|
||||
// Make sure its not removed on alpha
|
||||
let refetchedPostComments = await getComments(
|
||||
alpha,
|
||||
newPost.post_view.post.id,
|
||||
);
|
||||
expect(refetchedPostComments.comments[0].comment.removed).toBe(false);
|
||||
assertCommentFederation(
|
||||
refetchedPostComments.comments[0],
|
||||
commentRes.comment_view,
|
||||
);
|
||||
});
|
||||
|
||||
test("Unlike a comment", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
|
||||
// Lemmy automatically creates 1 like (vote) by author of comment.
|
||||
// Make sure that comment is liked (voted up) on gamma, downstream peer
|
||||
// This is testing replication from remote-home-remote (alpha-beta-gamma)
|
||||
|
||||
let gammaComment1 = (
|
||||
await waitUntil(
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
)
|
||||
).comment;
|
||||
expect(gammaComment1).toBeDefined();
|
||||
expect(gammaComment1?.community.local).toBe(false);
|
||||
expect(gammaComment1?.creator.local).toBe(false);
|
||||
expect(gammaComment1?.counts.score).toBe(1);
|
||||
|
||||
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
|
||||
expect(unlike.comment_view.counts.score).toBe(0);
|
||||
|
||||
// Make sure that comment is unliked on beta
|
||||
let betaComment = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 0,
|
||||
)
|
||||
).comment;
|
||||
expect(betaComment).toBeDefined();
|
||||
expect(betaComment?.community.local).toBe(true);
|
||||
expect(betaComment?.creator.local).toBe(false);
|
||||
expect(betaComment?.counts.score).toBe(0);
|
||||
|
||||
// Make sure that comment is unliked on gamma, downstream peer
|
||||
// This is testing replication from remote-home-remote (alpha-beta-gamma)
|
||||
let gammaComment = (
|
||||
await waitUntil(
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 0,
|
||||
)
|
||||
).comment;
|
||||
expect(gammaComment).toBeDefined();
|
||||
expect(gammaComment?.community.local).toBe(false);
|
||||
expect(gammaComment?.creator.local).toBe(false);
|
||||
expect(gammaComment?.counts.score).toBe(0);
|
||||
});
|
||||
|
||||
test("Federated comment like", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
);
|
||||
// Find the comment on beta
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment;
|
||||
|
||||
if (!betaComment) {
|
||||
throw "Missing beta comment";
|
||||
}
|
||||
|
||||
let like = await likeComment(beta, 1, betaComment.comment);
|
||||
expect(like.comment_view.counts.score).toBe(2);
|
||||
|
||||
// Get the post from alpha, check the likes
|
||||
let postComments = await waitUntil(
|
||||
() => getComments(alpha, postOnAlphaRes.post_view.post.id),
|
||||
c => c.comments[0].counts.score === 2,
|
||||
);
|
||||
expect(postComments.comments[0].counts.score).toBe(2);
|
||||
});
|
||||
|
||||
test("Reply to a comment from another instance, get notification", async () => {
|
||||
await alpha.markAllAsRead();
|
||||
|
||||
let betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => !!c.community?.community.instance_id,
|
||||
)
|
||||
).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
||||
// Create a root-level trunk-branch comment on alpha
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
// find that comment id on beta
|
||||
let betaComment = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
)
|
||||
).comment;
|
||||
|
||||
if (!betaComment) {
|
||||
throw "Missing beta comment";
|
||||
}
|
||||
|
||||
// Reply from beta, extending the branch
|
||||
let replyRes = await createComment(
|
||||
beta,
|
||||
betaComment.post.id,
|
||||
betaComment.comment.id,
|
||||
);
|
||||
expect(replyRes.comment_view.comment.content).toBeDefined();
|
||||
expect(replyRes.comment_view.community.local).toBe(true);
|
||||
expect(replyRes.comment_view.creator.local).toBe(true);
|
||||
expect(getCommentParentId(replyRes.comment_view.comment)).toBe(
|
||||
betaComment.comment.id,
|
||||
);
|
||||
expect(replyRes.comment_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure that reply comment is seen on alpha
|
||||
let commentSearch = await waitUntil(
|
||||
() => resolveComment(alpha, replyRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
);
|
||||
let alphaComment = commentSearch.comment!;
|
||||
let postComments = await waitUntil(
|
||||
() => getComments(alpha, postOnAlphaRes.post_view.post.id),
|
||||
pc => pc.comments.length >= 2,
|
||||
);
|
||||
// Note: this test fails when run twice and this count will differ
|
||||
expect(postComments.comments.length).toBeGreaterThanOrEqual(2);
|
||||
expect(alphaComment.comment.content).toBeDefined();
|
||||
|
||||
expect(getCommentParentId(alphaComment.comment)).toBe(
|
||||
postComments.comments[1].comment.id,
|
||||
);
|
||||
expect(alphaComment.community.local).toBe(false);
|
||||
expect(alphaComment.creator.local).toBe(false);
|
||||
expect(alphaComment.counts.score).toBe(1);
|
||||
assertCommentFederation(alphaComment, replyRes.comment_view);
|
||||
|
||||
// Did alpha get notified of the reply from beta?
|
||||
let alphaUnreadCountRes = await waitUntil(
|
||||
() => getUnreadCount(alpha),
|
||||
e => e.replies >= 1,
|
||||
);
|
||||
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// check inbox of replies on alpha, fetching read/unread both
|
||||
let alphaRepliesRes = await waitUntil(
|
||||
() => getReplies(alpha),
|
||||
r => r.replies.length > 0,
|
||||
);
|
||||
const alphaReply = alphaRepliesRes.replies.find(
|
||||
r => r.comment.id === alphaComment.comment.id,
|
||||
);
|
||||
expect(alphaReply).toBeDefined();
|
||||
if (!alphaReply) throw Error();
|
||||
expect(alphaReply.comment.content).toBeDefined();
|
||||
expect(alphaReply.community.local).toBe(false);
|
||||
expect(alphaReply.creator.local).toBe(false);
|
||||
expect(alphaReply.counts.score).toBe(1);
|
||||
// ToDo: interesting alphaRepliesRes.replies[0].comment_reply.id is 1, meaning? how did that come about?
|
||||
expect(alphaReply.comment.id).toBe(alphaComment.comment.id);
|
||||
// this is a new notification, getReplies fetch was for read/unread both, confirm it is unread.
|
||||
expect(alphaReply.comment_reply.read).toBe(false);
|
||||
assertCommentFederation(alphaReply, replyRes.comment_view);
|
||||
});
|
||||
|
||||
test("Bot reply notifications are filtered when bots are hidden", async () => {
|
||||
const newAlphaBot = await registerUser(alpha, alphaUrl);
|
||||
let form: SaveUserSettings = {
|
||||
bot_account: true,
|
||||
};
|
||||
await saveUserSettings(newAlphaBot, form);
|
||||
|
||||
const alphaCommunity = (
|
||||
await resolveCommunity(alpha, "!main@lemmy-alpha:8541")
|
||||
).community;
|
||||
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
|
||||
await alpha.markAllAsRead();
|
||||
form = {
|
||||
show_bot_accounts: false,
|
||||
};
|
||||
await saveUserSettings(alpha, form);
|
||||
const postOnAlphaRes = await createPost(alpha, alphaCommunity.community.id);
|
||||
|
||||
// Bot reply to alpha's post
|
||||
let commentRes = await createComment(
|
||||
newAlphaBot,
|
||||
postOnAlphaRes.post_view.post.id,
|
||||
);
|
||||
expect(commentRes).toBeDefined();
|
||||
|
||||
let alphaUnreadCountRes = await getUnreadCount(alpha);
|
||||
expect(alphaUnreadCountRes.replies).toBe(0);
|
||||
|
||||
let alphaUnreadRepliesRes = await getReplies(alpha, true);
|
||||
expect(alphaUnreadRepliesRes.replies.length).toBe(0);
|
||||
|
||||
// This both restores the original state that may be expected by other tests
|
||||
// implicitly and is used by the next steps to ensure replies are still
|
||||
// returned when a user later decides to show bot accounts again.
|
||||
form = {
|
||||
show_bot_accounts: true,
|
||||
};
|
||||
await saveUserSettings(alpha, form);
|
||||
|
||||
alphaUnreadCountRes = await getUnreadCount(alpha);
|
||||
expect(alphaUnreadCountRes.replies).toBe(1);
|
||||
|
||||
alphaUnreadRepliesRes = await getReplies(alpha, true);
|
||||
expect(alphaUnreadRepliesRes.replies.length).toBe(1);
|
||||
expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe(
|
||||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
});
|
||||
|
||||
test("Mention beta from alpha", async () => {
|
||||
if (!betaCommunity) throw Error("no community");
|
||||
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
// Create a new branch, trunk-level comment branch, from alpha instance
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
// Create a reply comment to previous comment, this has a mention in body
|
||||
let mentionContent = "A test mention of @lemmy_beta@lemmy-beta:8551";
|
||||
let mentionRes = await createComment(
|
||||
alpha,
|
||||
postOnAlphaRes.post_view.post.id,
|
||||
commentRes.comment_view.comment.id,
|
||||
mentionContent,
|
||||
);
|
||||
expect(mentionRes.comment_view.comment.content).toBeDefined();
|
||||
expect(mentionRes.comment_view.community.local).toBe(false);
|
||||
expect(mentionRes.comment_view.creator.local).toBe(true);
|
||||
expect(mentionRes.comment_view.counts.score).toBe(1);
|
||||
|
||||
// get beta's localized copy of the alpha post
|
||||
let betaPost = await waitForPost(beta, postOnAlphaRes.post_view.post);
|
||||
if (!betaPost) {
|
||||
throw "unable to locate post on beta";
|
||||
}
|
||||
expect(betaPost.post.ap_id).toBe(postOnAlphaRes.post_view.post.ap_id);
|
||||
expect(betaPost.post.name).toBe(postOnAlphaRes.post_view.post.name);
|
||||
|
||||
// Make sure that both new comments are seen on beta and have parent/child relationship
|
||||
let betaPostComments = await waitUntil(
|
||||
() => getComments(beta, betaPost!.post.id),
|
||||
c => c.comments[1]?.counts.score === 1,
|
||||
);
|
||||
expect(betaPostComments.comments.length).toEqual(2);
|
||||
// the trunk-branch root comment will be older than the mention reply comment, so index 1
|
||||
let betaRootComment = betaPostComments.comments[1];
|
||||
// the trunk-branch root comment should not have a parent
|
||||
expect(getCommentParentId(betaRootComment.comment)).toBeUndefined();
|
||||
expect(betaRootComment.comment.content).toBeDefined();
|
||||
// the mention reply comment should have parent that points to the branch root level comment
|
||||
expect(getCommentParentId(betaPostComments.comments[0].comment)).toBe(
|
||||
betaPostComments.comments[1].comment.id,
|
||||
);
|
||||
expect(betaRootComment.community.local).toBe(true);
|
||||
expect(betaRootComment.creator.local).toBe(false);
|
||||
expect(betaRootComment.counts.score).toBe(1);
|
||||
assertCommentFederation(betaRootComment, commentRes.comment_view);
|
||||
|
||||
let mentionsRes = await waitUntil(
|
||||
() => getMentions(beta),
|
||||
m => !!m.mentions[0],
|
||||
);
|
||||
expect(mentionsRes.mentions[0].comment.content).toBeDefined();
|
||||
expect(mentionsRes.mentions[0].community.local).toBe(true);
|
||||
expect(mentionsRes.mentions[0].creator.local).toBe(false);
|
||||
expect(mentionsRes.mentions[0].counts.score).toBe(1);
|
||||
// the reply comment with mention should be the most fresh, newest, index 0
|
||||
expect(mentionsRes.mentions[0].person_mention.comment_id).toBe(
|
||||
betaPostComments.comments[0].comment.id,
|
||||
);
|
||||
});
|
||||
|
||||
test("Comment Search", async () => {
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
).comment;
|
||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
||||
});
|
||||
|
||||
test("A and G subscribe to B (center) A posts, G mentions B, it gets announced to A", async () => {
|
||||
// Create a local post
|
||||
let alphaCommunity = (await resolveCommunity(alpha, "!main@lemmy-alpha:8541"))
|
||||
.community;
|
||||
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
|
||||
// follow community from beta so that it accepts the mention
|
||||
let betaCommunity = await resolveCommunity(
|
||||
beta,
|
||||
alphaCommunity.community.actor_id,
|
||||
);
|
||||
await followCommunity(beta, true, betaCommunity.community!.community.id);
|
||||
|
||||
let alphaPost = await createPost(alpha, alphaCommunity.community.id);
|
||||
expect(alphaPost.post_view.community.local).toBe(true);
|
||||
|
||||
// Make sure gamma sees it
|
||||
let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post))!.post;
|
||||
|
||||
if (!gammaPost) {
|
||||
throw "Missing gamma post";
|
||||
}
|
||||
|
||||
let commentContent =
|
||||
"A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551";
|
||||
let commentRes = await createComment(
|
||||
gamma,
|
||||
gammaPost.post.id,
|
||||
undefined,
|
||||
commentContent,
|
||||
);
|
||||
expect(commentRes.comment_view.comment.content).toBe(commentContent);
|
||||
expect(commentRes.comment_view.community.local).toBe(false);
|
||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure alpha sees it
|
||||
let alphaPostComments2 = await waitUntil(
|
||||
() => getComments(alpha, alphaPost.post_view.post.id),
|
||||
e => e.comments[0]?.counts.score === 1,
|
||||
);
|
||||
expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
|
||||
expect(alphaPostComments2.comments[0].community.local).toBe(true);
|
||||
expect(alphaPostComments2.comments[0].creator.local).toBe(false);
|
||||
expect(alphaPostComments2.comments[0].counts.score).toBe(1);
|
||||
assertCommentFederation(
|
||||
alphaPostComments2.comments[0],
|
||||
commentRes.comment_view,
|
||||
);
|
||||
|
||||
// Make sure beta has mentions
|
||||
let relevantMention = await waitUntil(
|
||||
() =>
|
||||
getMentions(beta).then(m =>
|
||||
m.mentions.find(
|
||||
m => m.comment.ap_id === commentRes.comment_view.comment.ap_id,
|
||||
),
|
||||
),
|
||||
e => !!e,
|
||||
);
|
||||
if (!relevantMention) throw Error("could not find mention");
|
||||
expect(relevantMention.comment.content).toBe(commentContent);
|
||||
expect(relevantMention.community.local).toBe(false);
|
||||
expect(relevantMention.creator.local).toBe(false);
|
||||
// TODO this is failing because fetchInReplyTos aren't getting score
|
||||
// expect(mentionsRes.mentions[0].score).toBe(1);
|
||||
});
|
||||
|
||||
test("Check that activity from another instance is sent to third instance", async () => {
|
||||
// Alpha and gamma users follow beta community
|
||||
let alphaFollow = await followBeta(alpha);
|
||||
expect(alphaFollow.community_view.community.local).toBe(false);
|
||||
expect(alphaFollow.community_view.community.name).toBe("main");
|
||||
|
||||
let gammaFollow = await followBeta(gamma);
|
||||
expect(gammaFollow.community_view.community.local).toBe(false);
|
||||
expect(gammaFollow.community_view.community.name).toBe("main");
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
);
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(gamma),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
);
|
||||
|
||||
// Create a post on beta
|
||||
let betaPost = await createPost(beta, 2);
|
||||
expect(betaPost.post_view.community.local).toBe(true);
|
||||
|
||||
// Make sure gamma and alpha see it
|
||||
let gammaPost = await waitForPost(gamma, betaPost.post_view.post);
|
||||
if (!gammaPost) {
|
||||
throw "Missing gamma post";
|
||||
}
|
||||
expect(gammaPost.post).toBeDefined();
|
||||
|
||||
let alphaPost = await waitForPost(alpha, betaPost.post_view.post);
|
||||
if (!alphaPost) {
|
||||
throw "Missing alpha post";
|
||||
}
|
||||
expect(alphaPost.post).toBeDefined();
|
||||
|
||||
// The bug: gamma comments, and alpha should see it.
|
||||
let commentContent = "Comment from gamma";
|
||||
let commentRes = await createComment(
|
||||
gamma,
|
||||
gammaPost.post.id,
|
||||
undefined,
|
||||
commentContent,
|
||||
);
|
||||
expect(commentRes.comment_view.comment.content).toBe(commentContent);
|
||||
expect(commentRes.comment_view.community.local).toBe(false);
|
||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure alpha sees it
|
||||
let alphaPostComments2 = await waitUntil(
|
||||
() => getComments(alpha, alphaPost!.post.id),
|
||||
e => e.comments[0]?.counts.score === 1,
|
||||
);
|
||||
expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
|
||||
expect(alphaPostComments2.comments[0].community.local).toBe(false);
|
||||
expect(alphaPostComments2.comments[0].creator.local).toBe(false);
|
||||
expect(alphaPostComments2.comments[0].counts.score).toBe(1);
|
||||
assertCommentFederation(
|
||||
alphaPostComments2.comments[0],
|
||||
commentRes.comment_view,
|
||||
);
|
||||
|
||||
await Promise.all([unfollowRemotes(alpha), unfollowRemotes(gamma)]);
|
||||
});
|
||||
|
||||
test("Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.", async () => {
|
||||
// Unfollow all remote communities
|
||||
let site = await unfollowRemotes(alpha);
|
||||
expect(
|
||||
site.my_user?.follows.filter(c => c.community.local == false).length,
|
||||
).toBe(0);
|
||||
|
||||
// B creates a post, and two comments, should be invisible to A
|
||||
let postOnBetaRes = await createPost(beta, 2);
|
||||
expect(postOnBetaRes.post_view.post.name).toBeDefined();
|
||||
|
||||
let parentCommentContent = "An invisible top level comment from beta";
|
||||
let parentCommentRes = await createComment(
|
||||
beta,
|
||||
postOnBetaRes.post_view.post.id,
|
||||
undefined,
|
||||
parentCommentContent,
|
||||
);
|
||||
expect(parentCommentRes.comment_view.comment.content).toBe(
|
||||
parentCommentContent,
|
||||
);
|
||||
|
||||
// B creates a comment, then a child one of that.
|
||||
let childCommentContent = "An invisible child comment from beta";
|
||||
let childCommentRes = await createComment(
|
||||
beta,
|
||||
postOnBetaRes.post_view.post.id,
|
||||
parentCommentRes.comment_view.comment.id,
|
||||
childCommentContent,
|
||||
);
|
||||
expect(childCommentRes.comment_view.comment.content).toBe(
|
||||
childCommentContent,
|
||||
);
|
||||
|
||||
// Follow beta again
|
||||
let follow = await followBeta(alpha);
|
||||
expect(follow.community_view.community.local).toBe(false);
|
||||
expect(follow.community_view.community.name).toBe("main");
|
||||
|
||||
// An update to the child comment on beta, should push the post, parent, and child to alpha now
|
||||
let updatedCommentContent = "An update child comment from beta";
|
||||
let updateRes = await editComment(
|
||||
beta,
|
||||
childCommentRes.comment_view.comment.id,
|
||||
updatedCommentContent,
|
||||
);
|
||||
expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent);
|
||||
|
||||
// Get the post from alpha
|
||||
let alphaPostB = await waitForPost(alpha, postOnBetaRes.post_view.post);
|
||||
|
||||
if (!alphaPostB) {
|
||||
throw "Missing alpha post B";
|
||||
}
|
||||
|
||||
let alphaPost = await getPost(alpha, alphaPostB.post.id);
|
||||
let alphaPostComments = await waitUntil(
|
||||
() => getComments(alpha, alphaPostB!.post.id),
|
||||
c =>
|
||||
c.comments[1]?.comment.content ===
|
||||
parentCommentRes.comment_view.comment.content &&
|
||||
c.comments[0]?.comment.content === updateRes.comment_view.comment.content,
|
||||
);
|
||||
expect(alphaPost.post_view.post.name).toBeDefined();
|
||||
assertCommentFederation(
|
||||
alphaPostComments.comments[1],
|
||||
parentCommentRes.comment_view,
|
||||
);
|
||||
assertCommentFederation(
|
||||
alphaPostComments.comments[0],
|
||||
updateRes.comment_view,
|
||||
);
|
||||
expect(alphaPost.post_view.community.local).toBe(false);
|
||||
expect(alphaPost.post_view.creator.local).toBe(false);
|
||||
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Report a comment", async () => {
|
||||
let betaCommunity = (await resolveBetaCommunity(beta)).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postOnBetaRes = (await createPost(beta, betaCommunity.community.id))
|
||||
.post_view.post;
|
||||
expect(postOnBetaRes).toBeDefined();
|
||||
let commentRes = (await createComment(beta, postOnBetaRes.id)).comment_view
|
||||
.comment;
|
||||
expect(commentRes).toBeDefined();
|
||||
|
||||
let alphaComment = (await resolveComment(alpha, commentRes)).comment?.comment;
|
||||
if (!alphaComment) {
|
||||
throw "Missing alpha comment";
|
||||
}
|
||||
|
||||
const reason = randomString(10);
|
||||
let alphaReport = (await reportComment(alpha, alphaComment.id, reason))
|
||||
.comment_report_view.comment_report;
|
||||
|
||||
let betaReport = (await waitUntil(
|
||||
() =>
|
||||
listCommentReports(beta).then(r =>
|
||||
r.comment_reports.find(rep => rep.comment_report.reason === reason),
|
||||
),
|
||||
e => !!e,
|
||||
))!.comment_report;
|
||||
expect(betaReport).toBeDefined();
|
||||
expect(betaReport.resolved).toBe(false);
|
||||
expect(betaReport.original_comment_text).toBe(
|
||||
alphaReport.original_comment_text,
|
||||
);
|
||||
expect(betaReport.reason).toBe(alphaReport.reason);
|
||||
});
|
||||
|
||||
test("Dont send a comment reply to a blocked community", async () => {
|
||||
let newCommunity = await createCommunity(beta);
|
||||
let newCommunityId = newCommunity.community_view.community.id;
|
||||
|
||||
// Create a post on beta
|
||||
let betaPost = await createPost(beta, newCommunityId);
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, betaPost.post_view.post))!.post;
|
||||
if (!alphaPost) {
|
||||
throw "unable to locate post on alpha";
|
||||
}
|
||||
|
||||
// Check beta's inbox count
|
||||
let unreadCount = await getUnreadCount(beta);
|
||||
expect(unreadCount.replies).toBe(1);
|
||||
|
||||
// Beta blocks the new beta community
|
||||
let blockRes = await blockCommunity(beta, newCommunityId, true);
|
||||
expect(blockRes.blocked).toBe(true);
|
||||
delay();
|
||||
|
||||
// Alpha creates a comment
|
||||
let commentRes = await createComment(alpha, alphaPost.post.id);
|
||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
||||
let alphaComment = await resolveComment(
|
||||
beta,
|
||||
commentRes.comment_view.comment,
|
||||
);
|
||||
if (!alphaComment) {
|
||||
throw "Missing alpha comment before block";
|
||||
}
|
||||
|
||||
// Check beta's inbox count, make sure it stays the same
|
||||
unreadCount = await getUnreadCount(beta);
|
||||
expect(unreadCount.replies).toBe(1);
|
||||
|
||||
let replies = await getReplies(beta);
|
||||
expect(replies.replies.length).toBe(1);
|
||||
|
||||
// Unblock the community
|
||||
blockRes = await blockCommunity(beta, newCommunityId, false);
|
||||
expect(blockRes.blocked).toBe(false);
|
||||
});
|
535
api_tests/src/community.spec.ts
Normal file
535
api_tests/src/community.spec.ts
Normal file
|
@ -0,0 +1,535 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
||||
import {
|
||||
alpha,
|
||||
beta,
|
||||
gamma,
|
||||
setupLogins,
|
||||
resolveCommunity,
|
||||
createCommunity,
|
||||
deleteCommunity,
|
||||
removeCommunity,
|
||||
getCommunity,
|
||||
followCommunity,
|
||||
banPersonFromCommunity,
|
||||
resolvePerson,
|
||||
getSite,
|
||||
createPost,
|
||||
getPost,
|
||||
resolvePost,
|
||||
registerUser,
|
||||
getPosts,
|
||||
getComments,
|
||||
createComment,
|
||||
getCommunityByName,
|
||||
blockInstance,
|
||||
waitUntil,
|
||||
alphaUrl,
|
||||
delta,
|
||||
betaAllowedInstances,
|
||||
searchPostLocal,
|
||||
longDelay,
|
||||
editCommunity,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
import { EditCommunity, EditSite } from "lemmy-js-client";
|
||||
|
||||
beforeAll(setupLogins);
|
||||
afterAll(unfollows);
|
||||
|
||||
function assertCommunityFederation(
|
||||
communityOne?: CommunityView,
|
||||
communityTwo?: CommunityView,
|
||||
) {
|
||||
expect(communityOne?.community.actor_id).toBe(
|
||||
communityTwo?.community.actor_id,
|
||||
);
|
||||
expect(communityOne?.community.name).toBe(communityTwo?.community.name);
|
||||
expect(communityOne?.community.title).toBe(communityTwo?.community.title);
|
||||
expect(communityOne?.community.description).toBe(
|
||||
communityTwo?.community.description,
|
||||
);
|
||||
expect(communityOne?.community.icon).toBe(communityTwo?.community.icon);
|
||||
expect(communityOne?.community.banner).toBe(communityTwo?.community.banner);
|
||||
expect(communityOne?.community.published).toBe(
|
||||
communityTwo?.community.published,
|
||||
);
|
||||
expect(communityOne?.community.nsfw).toBe(communityTwo?.community.nsfw);
|
||||
expect(communityOne?.community.removed).toBe(communityTwo?.community.removed);
|
||||
expect(communityOne?.community.deleted).toBe(communityTwo?.community.deleted);
|
||||
}
|
||||
|
||||
test("Create community", async () => {
|
||||
let communityRes = await createCommunity(alpha);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
|
||||
// A dupe check
|
||||
let prevName = communityRes.community_view.community.name;
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
||||
Error("community_already_exists"),
|
||||
);
|
||||
|
||||
// Cache the community on beta, make sure it has the other fields
|
||||
let searchShort = `!${prevName}@lemmy-alpha:8541`;
|
||||
let betaCommunity = (await resolveCommunity(beta, searchShort)).community;
|
||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
||||
});
|
||||
|
||||
test("Delete community", async () => {
|
||||
let communityRes = await createCommunity(beta);
|
||||
|
||||
// Cache the community on Alpha
|
||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
||||
|
||||
// Follow the community from alpha
|
||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(follow.community_view.community.local).toBe(false);
|
||||
|
||||
let deleteCommunityRes = await deleteCommunity(
|
||||
beta,
|
||||
true,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
|
||||
expect(deleteCommunityRes.community_view.community.title).toBe(
|
||||
communityRes.community_view.community.title,
|
||||
);
|
||||
|
||||
// Make sure it got deleted on A
|
||||
let communityOnAlphaDeleted = await waitUntil(
|
||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
||||
g => g.community_view.community.deleted,
|
||||
);
|
||||
expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true);
|
||||
|
||||
// Undelete
|
||||
let undeleteCommunityRes = await deleteCommunity(
|
||||
beta,
|
||||
false,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(undeleteCommunityRes.community_view.community.deleted).toBe(false);
|
||||
|
||||
// Make sure it got undeleted on A
|
||||
let communityOnAlphaUnDeleted = await waitUntil(
|
||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
||||
g => !g.community_view.community.deleted,
|
||||
);
|
||||
expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("Remove community", async () => {
|
||||
let communityRes = await createCommunity(beta);
|
||||
|
||||
// Cache the community on Alpha
|
||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
||||
|
||||
// Follow the community from alpha
|
||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(follow.community_view.community.local).toBe(false);
|
||||
|
||||
let removeCommunityRes = await removeCommunity(
|
||||
beta,
|
||||
true,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(removeCommunityRes.community_view.community.removed).toBe(true);
|
||||
expect(removeCommunityRes.community_view.community.title).toBe(
|
||||
communityRes.community_view.community.title,
|
||||
);
|
||||
|
||||
// Make sure it got Removed on A
|
||||
let communityOnAlphaRemoved = await waitUntil(
|
||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
||||
g => g.community_view.community.removed,
|
||||
);
|
||||
expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true);
|
||||
|
||||
// unremove
|
||||
let unremoveCommunityRes = await removeCommunity(
|
||||
beta,
|
||||
false,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(unremoveCommunityRes.community_view.community.removed).toBe(false);
|
||||
|
||||
// Make sure it got undeleted on A
|
||||
let communityOnAlphaUnRemoved = await waitUntil(
|
||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
||||
g => !g.community_view.community.removed,
|
||||
);
|
||||
expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("Search for beta community", async () => {
|
||||
let communityRes = await createCommunity(beta);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
|
||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
||||
});
|
||||
|
||||
test("Admin actions in remote community are not federated to origin", async () => {
|
||||
// create a community on alpha
|
||||
let communityRes = (await createCommunity(alpha)).community_view;
|
||||
expect(communityRes.community.name).toBeDefined();
|
||||
|
||||
// gamma follows community and posts in it
|
||||
let gammaCommunity = (
|
||||
await resolveCommunity(gamma, communityRes.community.actor_id)
|
||||
).community;
|
||||
if (!gammaCommunity) {
|
||||
throw "Missing gamma community";
|
||||
}
|
||||
await followCommunity(gamma, true, gammaCommunity.community.id);
|
||||
gammaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(gamma, communityRes.community.actor_id),
|
||||
g => g.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
if (!gammaCommunity) {
|
||||
throw "Missing gamma community";
|
||||
}
|
||||
expect(gammaCommunity.subscribed).toBe("Subscribed");
|
||||
let gammaPost = (await createPost(gamma, gammaCommunity.community.id))
|
||||
.post_view;
|
||||
expect(gammaPost.post.id).toBeDefined();
|
||||
expect(gammaPost.creator_banned_from_community).toBe(false);
|
||||
|
||||
// admin of beta decides to ban gamma from community
|
||||
let betaCommunity = (
|
||||
await resolveCommunity(beta, communityRes.community.actor_id)
|
||||
).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let bannedUserInfo1 = (await getSite(gamma)).my_user?.local_user_view.person;
|
||||
if (!bannedUserInfo1) {
|
||||
throw "Missing banned user 1";
|
||||
}
|
||||
let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.actor_id))
|
||||
.person;
|
||||
if (!bannedUserInfo2) {
|
||||
throw "Missing banned user 2";
|
||||
}
|
||||
let banRes = await banPersonFromCommunity(
|
||||
beta,
|
||||
bannedUserInfo2.person.id,
|
||||
betaCommunity.community.id,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(banRes.banned).toBe(true);
|
||||
|
||||
// ban doesn't federate to community's origin instance alpha
|
||||
let alphaPost = (await resolvePost(alpha, gammaPost.post)).post;
|
||||
expect(alphaPost?.creator_banned_from_community).toBe(false);
|
||||
|
||||
// and neither to gamma
|
||||
let gammaPost2 = await getPost(gamma, gammaPost.post.id);
|
||||
expect(gammaPost2.post_view.creator_banned_from_community).toBe(false);
|
||||
});
|
||||
|
||||
test("moderator view", async () => {
|
||||
// register a new user with their own community on alpha and post to it
|
||||
let otherUser = await registerUser(alpha, alphaUrl);
|
||||
|
||||
let otherCommunity = (await createCommunity(otherUser)).community_view;
|
||||
expect(otherCommunity.community.name).toBeDefined();
|
||||
let otherPost = (await createPost(otherUser, otherCommunity.community.id))
|
||||
.post_view;
|
||||
expect(otherPost.post.id).toBeDefined();
|
||||
|
||||
let otherComment = (await createComment(otherUser, otherPost.post.id))
|
||||
.comment_view;
|
||||
expect(otherComment.comment.id).toBeDefined();
|
||||
|
||||
// create a community and post on alpha
|
||||
let alphaCommunity = (await createCommunity(alpha)).community_view;
|
||||
expect(alphaCommunity.community.name).toBeDefined();
|
||||
let alphaPost = (await createPost(alpha, alphaCommunity.community.id))
|
||||
.post_view;
|
||||
expect(alphaPost.post.id).toBeDefined();
|
||||
|
||||
let alphaComment = (await createComment(otherUser, alphaPost.post.id))
|
||||
.comment_view;
|
||||
expect(alphaComment.comment.id).toBeDefined();
|
||||
|
||||
// other user also posts on alpha's community
|
||||
let otherAlphaPost = (
|
||||
await createPost(otherUser, alphaCommunity.community.id)
|
||||
).post_view;
|
||||
expect(otherAlphaPost.post.id).toBeDefined();
|
||||
|
||||
let otherAlphaComment = (
|
||||
await createComment(otherUser, otherAlphaPost.post.id)
|
||||
).comment_view;
|
||||
expect(otherAlphaComment.comment.id).toBeDefined();
|
||||
|
||||
// alpha lists posts and comments on home page, should contain all posts that were made
|
||||
let posts = (await getPosts(alpha, "All")).posts;
|
||||
expect(posts).toBeDefined();
|
||||
let postIds = posts.map(post => post.post.id);
|
||||
|
||||
let comments = (await getComments(alpha, undefined, "All")).comments;
|
||||
expect(comments).toBeDefined();
|
||||
let commentIds = comments.map(comment => comment.comment.id);
|
||||
|
||||
expect(postIds).toContain(otherPost.post.id);
|
||||
expect(commentIds).toContain(otherComment.comment.id);
|
||||
|
||||
expect(postIds).toContain(alphaPost.post.id);
|
||||
expect(commentIds).toContain(alphaComment.comment.id);
|
||||
|
||||
expect(postIds).toContain(otherAlphaPost.post.id);
|
||||
expect(commentIds).toContain(otherAlphaComment.comment.id);
|
||||
|
||||
// in moderator view, alpha should not see otherPost, wich was posted on a community alpha doesn't moderate
|
||||
posts = (await getPosts(alpha, "ModeratorView")).posts;
|
||||
expect(posts).toBeDefined();
|
||||
postIds = posts.map(post => post.post.id);
|
||||
|
||||
comments = (await getComments(alpha, undefined, "ModeratorView")).comments;
|
||||
expect(comments).toBeDefined();
|
||||
commentIds = comments.map(comment => comment.comment.id);
|
||||
|
||||
expect(postIds).not.toContain(otherPost.post.id);
|
||||
expect(commentIds).not.toContain(otherComment.comment.id);
|
||||
|
||||
expect(postIds).toContain(alphaPost.post.id);
|
||||
expect(commentIds).toContain(alphaComment.comment.id);
|
||||
|
||||
expect(postIds).toContain(otherAlphaPost.post.id);
|
||||
expect(commentIds).toContain(otherAlphaComment.comment.id);
|
||||
});
|
||||
|
||||
test("Get community for different casing on domain", async () => {
|
||||
let communityRes = await createCommunity(alpha);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
|
||||
// A dupe check
|
||||
let prevName = communityRes.community_view.community.name;
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
||||
Error("community_already_exists"),
|
||||
);
|
||||
|
||||
// Cache the community on beta, make sure it has the other fields
|
||||
let communityName = `${communityRes.community_view.community.name}@LEMMY-ALPHA:8541`;
|
||||
let betaCommunity = (await getCommunityByName(beta, communityName))
|
||||
.community_view;
|
||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
||||
});
|
||||
|
||||
test("User blocks instance, communities are hidden", async () => {
|
||||
// create community and post on beta
|
||||
let communityRes = await createCommunity(beta);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
let postRes = await createPost(
|
||||
beta,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(postRes.post_view.post.id).toBeDefined();
|
||||
|
||||
// fetch post to alpha
|
||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post!;
|
||||
expect(alphaPost.post).toBeDefined();
|
||||
|
||||
// post should be included in listing
|
||||
let listing = await getPosts(alpha, "All");
|
||||
let listing_ids = listing.posts.map(p => p.post.ap_id);
|
||||
expect(listing_ids).toContain(postRes.post_view.post.ap_id);
|
||||
|
||||
// block the beta instance
|
||||
await blockInstance(alpha, alphaPost.community.instance_id, true);
|
||||
|
||||
// after blocking, post should not be in listing
|
||||
let listing2 = await getPosts(alpha, "All");
|
||||
let listing_ids2 = listing2.posts.map(p => p.post.ap_id);
|
||||
expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1);
|
||||
|
||||
// unblock instance again
|
||||
await blockInstance(alpha, alphaPost.community.instance_id, false);
|
||||
|
||||
// post should be included in listing
|
||||
let listing3 = await getPosts(alpha, "All");
|
||||
let listing_ids3 = listing3.posts.map(p => p.post.ap_id);
|
||||
expect(listing_ids3).toContain(postRes.post_view.post.ap_id);
|
||||
});
|
||||
|
||||
test("Community follower count is federated", async () => {
|
||||
// Follow the beta community from alpha
|
||||
let community = await createCommunity(beta);
|
||||
let communityActorId = community.community_view.community.actor_id;
|
||||
let resolved = await resolveCommunity(alpha, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
await followCommunity(alpha, true, resolved.community.community.id);
|
||||
let followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(alpha, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
||||
// Make sure there is 1 subscriber
|
||||
expect(followed?.counts.subscribers).toBe(1);
|
||||
|
||||
// Follow the community from gamma
|
||||
resolved = await resolveCommunity(gamma, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
await followCommunity(gamma, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(gamma, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
||||
// Make sure there are 2 subscribers
|
||||
expect(followed?.counts?.subscribers).toBe(2);
|
||||
|
||||
// Follow the community from delta
|
||||
resolved = await resolveCommunity(delta, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
await followCommunity(delta, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(delta, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
||||
// Make sure there are 3 subscribers
|
||||
expect(followed?.counts?.subscribers).toBe(3);
|
||||
});
|
||||
|
||||
test("Dont receive community activities after unsubscribe", async () => {
|
||||
let communityRes = await createCommunity(alpha);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
expect(communityRes.community_view.counts.subscribers).toBe(1);
|
||||
|
||||
let betaCommunity = (
|
||||
await resolveCommunity(beta, communityRes.community_view.community.actor_id)
|
||||
).community;
|
||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
||||
|
||||
// follow alpha community from beta
|
||||
await followCommunity(beta, true, betaCommunity!.community.id);
|
||||
|
||||
// ensure that follower count was updated
|
||||
let communityRes1 = await getCommunity(
|
||||
alpha,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(communityRes1.community_view.counts.subscribers).toBe(2);
|
||||
|
||||
// temporarily block alpha, so that it doesn't know about unfollow
|
||||
let editSiteForm: EditSite = {};
|
||||
editSiteForm.allowed_instances = ["lemmy-epsilon"];
|
||||
await beta.editSite(editSiteForm);
|
||||
await longDelay();
|
||||
|
||||
// unfollow
|
||||
await followCommunity(beta, false, betaCommunity!.community.id);
|
||||
|
||||
// ensure that alpha still sees beta as follower
|
||||
let communityRes2 = await getCommunity(
|
||||
alpha,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(communityRes2.community_view.counts.subscribers).toBe(2);
|
||||
|
||||
// unblock alpha
|
||||
editSiteForm.allowed_instances = betaAllowedInstances;
|
||||
await beta.editSite(editSiteForm);
|
||||
await longDelay();
|
||||
|
||||
// create a post, it shouldnt reach beta
|
||||
let postRes = await createPost(
|
||||
alpha,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(postRes.post_view.post.id).toBeDefined();
|
||||
// await longDelay();
|
||||
|
||||
let postResBeta = searchPostLocal(beta, postRes.post_view.post);
|
||||
expect((await postResBeta).posts.length).toBe(0);
|
||||
});
|
||||
|
||||
test("Fetch community, includes posts", async () => {
|
||||
let communityRes = await createCommunity(alpha);
|
||||
expect(communityRes.community_view.community.name).toBeDefined();
|
||||
expect(communityRes.community_view.counts.subscribers).toBe(1);
|
||||
|
||||
let postRes = await createPost(
|
||||
alpha,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let resolvedCommunity = await waitUntil(
|
||||
() =>
|
||||
resolveCommunity(beta, communityRes.community_view.community.actor_id),
|
||||
c => c.community?.community.id != undefined,
|
||||
);
|
||||
let betaCommunity = resolvedCommunity.community;
|
||||
expect(betaCommunity?.community.actor_id).toBe(
|
||||
communityRes.community_view.community.actor_id,
|
||||
);
|
||||
|
||||
await longDelay();
|
||||
|
||||
let post_listing = await getPosts(beta, "All", betaCommunity?.community.id);
|
||||
expect(post_listing.posts.length).toBe(1);
|
||||
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
|
||||
});
|
||||
|
||||
test("Content in local-only community doesn't federate", async () => {
|
||||
// create a community and set it local-only
|
||||
let communityRes = (await createCommunity(alpha)).community_view.community;
|
||||
let form: EditCommunity = {
|
||||
community_id: communityRes.id,
|
||||
visibility: "LocalOnly",
|
||||
};
|
||||
await editCommunity(alpha, form);
|
||||
|
||||
// cant resolve the community from another instance
|
||||
await expect(
|
||||
resolveCommunity(beta, communityRes.actor_id),
|
||||
).rejects.toStrictEqual(Error("couldnt_find_object"));
|
||||
|
||||
// create a post, also cant resolve it
|
||||
let postRes = await createPost(alpha, communityRes.id);
|
||||
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
|
||||
Error("couldnt_find_object"),
|
||||
);
|
||||
});
|
98
api_tests/src/follow.spec.ts
Normal file
98
api_tests/src/follow.spec.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import {
|
||||
alpha,
|
||||
setupLogins,
|
||||
resolveBetaCommunity,
|
||||
followCommunity,
|
||||
getSite,
|
||||
waitUntil,
|
||||
beta,
|
||||
betaUrl,
|
||||
registerUser,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
|
||||
beforeAll(setupLogins);
|
||||
|
||||
afterAll(unfollows);
|
||||
|
||||
test("Follow local community", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
|
||||
let community = (await resolveBetaCommunity(user)).community!;
|
||||
expect(community.counts.subscribers).toBe(1);
|
||||
expect(community.counts.subscribers_local).toBe(1);
|
||||
let follow = await followCommunity(user, true, community.community.id);
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(follow.community_view.community.local).toBe(true);
|
||||
expect(follow.community_view.subscribed).toBe("Subscribed");
|
||||
expect(follow.community_view.counts.subscribers).toBe(2);
|
||||
expect(follow.community_view.counts.subscribers_local).toBe(2);
|
||||
|
||||
// Test an unfollow
|
||||
let unfollow = await followCommunity(user, false, community.community.id);
|
||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
||||
expect(unfollow.community_view.counts.subscribers).toBe(1);
|
||||
expect(unfollow.community_view.counts.subscribers_local).toBe(1);
|
||||
});
|
||||
|
||||
test("Follow federated community", async () => {
|
||||
// It takes about 1 second for the community aggregates to federate
|
||||
let betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c =>
|
||||
c.community?.counts.subscribers === 1 &&
|
||||
c.community.counts.subscribers_local === 0,
|
||||
)
|
||||
).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let follow = await followCommunity(alpha, true, betaCommunity.community.id);
|
||||
expect(follow.community_view.subscribed).toBe("Pending");
|
||||
betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(betaCommunity?.community.local).toBe(false);
|
||||
expect(betaCommunity?.community.name).toBe("main");
|
||||
expect(betaCommunity?.subscribed).toBe("Subscribed");
|
||||
expect(betaCommunity?.counts.subscribers_local).toBe(1);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta1 = await resolveBetaCommunity(beta);
|
||||
expect(communityOnBeta1.community?.counts.subscribers).toBe(2);
|
||||
expect(communityOnBeta1.community?.counts.subscribers_local).toBe(1);
|
||||
|
||||
// Check it from local
|
||||
let site = await getSite(alpha);
|
||||
let remoteCommunityId = site.my_user?.follows.find(
|
||||
c => c.community.local == false,
|
||||
)?.community.id;
|
||||
expect(remoteCommunityId).toBeDefined();
|
||||
expect(site.my_user?.follows.length).toBe(2);
|
||||
|
||||
if (!remoteCommunityId) {
|
||||
throw "Missing remote community id";
|
||||
}
|
||||
|
||||
// Test an unfollow
|
||||
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
|
||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
||||
|
||||
// Make sure you are unsubbed locally
|
||||
let siteUnfollowCheck = await getSite(alpha);
|
||||
expect(siteUnfollowCheck.my_user?.follows.length).toBe(1);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta2 = await resolveBetaCommunity(beta);
|
||||
expect(communityOnBeta2.community?.counts.subscribers).toBe(1);
|
||||
expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1);
|
||||
});
|
363
api_tests/src/image.spec.ts
Normal file
363
api_tests/src/image.spec.ts
Normal file
|
@ -0,0 +1,363 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
PurgePerson,
|
||||
PurgePost,
|
||||
} from "lemmy-js-client";
|
||||
import {
|
||||
alpha,
|
||||
alphaImage,
|
||||
alphaUrl,
|
||||
beta,
|
||||
betaUrl,
|
||||
createCommunity,
|
||||
createPost,
|
||||
deleteAllImages,
|
||||
epsilon,
|
||||
followCommunity,
|
||||
gamma,
|
||||
getSite,
|
||||
imageFetchLimit,
|
||||
registerUser,
|
||||
resolveBetaCommunity,
|
||||
resolveCommunity,
|
||||
resolvePost,
|
||||
setupLogins,
|
||||
waitForPost,
|
||||
unfollows,
|
||||
getPost,
|
||||
waitUntil,
|
||||
createPostWithThumbnail,
|
||||
sampleImage,
|
||||
sampleSite,
|
||||
} from "./shared";
|
||||
const downloadFileSync = require("download-file-sync");
|
||||
|
||||
beforeAll(setupLogins);
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([unfollows(), deleteAllImages(alpha)]);
|
||||
});
|
||||
|
||||
test("Upload image and delete it", async () => {
|
||||
// Before running this test, you need to delete all previous images in the DB
|
||||
await deleteAllImages(alpha);
|
||||
|
||||
// Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
|
||||
// in testing mode.
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await alphaImage.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const content = downloadFileSync(upload.url);
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
// Ensure that it comes back with the list_media endpoint
|
||||
const listMediaRes = await alphaImage.listMedia();
|
||||
expect(listMediaRes.images.length).toBe(1);
|
||||
|
||||
// Ensure that it also comes back with the admin all images
|
||||
const listAllMediaRes = await alphaImage.listAllMedia({
|
||||
limit: imageFetchLimit,
|
||||
});
|
||||
|
||||
// This number comes from all the previous thumbnails fetched in other tests.
|
||||
const previousThumbnails = 1;
|
||||
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
|
||||
|
||||
// The deleteUrl is a combination of the endpoint, delete token, and alias
|
||||
let firstImage = listMediaRes.images[0];
|
||||
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
|
||||
expect(deleteUrl).toBe(upload.delete_url);
|
||||
|
||||
// Make sure the uploader is correct
|
||||
expect(firstImage.person.actor_id).toBe(
|
||||
`http://lemmy-alpha:8541/u/lemmy_alpha`,
|
||||
);
|
||||
|
||||
// delete image
|
||||
const delete_form: DeleteImage = {
|
||||
token: upload.files![0].delete_token,
|
||||
filename: upload.files![0].file,
|
||||
};
|
||||
const delete_ = await alphaImage.deleteImage(delete_form);
|
||||
expect(delete_).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const content2 = downloadFileSync(upload.url);
|
||||
expect(content2).toBe("");
|
||||
|
||||
// Ensure that it shows the image is deleted
|
||||
const deletedListMediaRes = await alphaImage.listMedia();
|
||||
expect(deletedListMediaRes.images.length).toBe(0);
|
||||
|
||||
// Ensure that the admin shows its deleted
|
||||
const deletedListAllMediaRes = await alphaImage.listAllMedia({
|
||||
limit: imageFetchLimit,
|
||||
});
|
||||
expect(deletedListAllMediaRes.images.length).toBe(previousThumbnails - 1);
|
||||
});
|
||||
|
||||
test("Purge user, uploaded image removed", async () => {
|
||||
let user = await registerUser(alphaImage, alphaUrl);
|
||||
|
||||
// upload test image
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const content = downloadFileSync(upload.url);
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
// purge user
|
||||
let site = await getSite(user);
|
||||
const purgeForm: PurgePerson = {
|
||||
person_id: site.my_user!.local_user_view.person.id,
|
||||
};
|
||||
const delete_ = await alphaImage.purgePerson(purgeForm);
|
||||
expect(delete_.success).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const content2 = downloadFileSync(upload.url);
|
||||
expect(content2).toBe("");
|
||||
});
|
||||
|
||||
test("Purge post, linked image removed", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
|
||||
// upload test image
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const content = downloadFileSync(upload.url);
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
let community = await resolveBetaCommunity(user);
|
||||
let post = await createPost(
|
||||
user,
|
||||
community.community!.community.id,
|
||||
upload.url,
|
||||
);
|
||||
expect(post.post_view.post.url).toBe(upload.url);
|
||||
|
||||
// purge post
|
||||
const purgeForm: PurgePost = {
|
||||
post_id: post.post_view.post.id,
|
||||
};
|
||||
const delete_ = await beta.purgePost(purgeForm);
|
||||
expect(delete_.success).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const content2 = downloadFileSync(upload.url);
|
||||
expect(content2).toBe("");
|
||||
});
|
||||
|
||||
test("Images in remote image post are proxied if setting enabled", async () => {
|
||||
let community = await createCommunity(gamma);
|
||||
let postRes = await createPost(
|
||||
gamma,
|
||||
community.community_view.community.id,
|
||||
sampleImage,
|
||||
`![](${sampleImage})`,
|
||||
);
|
||||
const post = postRes.post_view.post;
|
||||
expect(post).toBeDefined();
|
||||
|
||||
// remote image gets proxied after upload
|
||||
expect(
|
||||
post.thumbnail_url?.startsWith(
|
||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with jpg, to be sure its an image
|
||||
expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
||||
|
||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
||||
expect(epsilonPostRes.post).toBeDefined();
|
||||
|
||||
// Fetch the post again, the metadata should be backgrounded now
|
||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
||||
let epsilonPostRes2 = await waitUntil(
|
||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
||||
|
||||
expect(
|
||||
epsilonPost.thumbnail_url?.startsWith(
|
||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
epsilonPost.body?.startsWith(
|
||||
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with jpg, to be sure its an image
|
||||
expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Thumbnail of remote image link is proxied if setting enabled", async () => {
|
||||
let community = await createCommunity(gamma);
|
||||
let postRes = await createPost(
|
||||
gamma,
|
||||
community.community_view.community.id,
|
||||
// The sample site metadata thumbnail ends in png
|
||||
sampleSite,
|
||||
);
|
||||
const post = postRes.post_view.post;
|
||||
expect(post).toBeDefined();
|
||||
|
||||
// remote image gets proxied after upload
|
||||
expect(
|
||||
post.thumbnail_url?.startsWith(
|
||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with png, to be sure its an image
|
||||
expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
||||
|
||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
||||
expect(epsilonPostRes.post).toBeDefined();
|
||||
|
||||
let epsilonPostRes2 = await waitUntil(
|
||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
||||
|
||||
expect(
|
||||
epsilonPost.thumbnail_url?.startsWith(
|
||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with png, to be sure its an image
|
||||
expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("No image proxying if setting is disabled", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
let community = await createCommunity(alpha);
|
||||
let betaCommunity = await resolveCommunity(
|
||||
beta,
|
||||
community.community_view.community.actor_id,
|
||||
);
|
||||
await followCommunity(beta, true, betaCommunity.community!.community.id);
|
||||
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
let post = await createPost(
|
||||
alpha,
|
||||
community.community_view.community.id,
|
||||
upload.url,
|
||||
`![](${sampleImage})`,
|
||||
);
|
||||
expect(post.post_view.post).toBeDefined();
|
||||
|
||||
// remote image doesn't get proxied after upload
|
||||
expect(
|
||||
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
||||
).toBeTruthy();
|
||||
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
|
||||
|
||||
let betaPost = await waitForPost(
|
||||
beta,
|
||||
post.post_view.post,
|
||||
res => res?.post.alt_text != null,
|
||||
);
|
||||
expect(betaPost.post).toBeDefined();
|
||||
|
||||
// remote image doesn't get proxied after federation
|
||||
expect(
|
||||
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
||||
).toBeTruthy();
|
||||
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
|
||||
// Make sure the alt text got federated
|
||||
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
|
||||
});
|
||||
|
||||
test("Make regular post, and give it a custom thumbnail", async () => {
|
||||
const uploadForm1: UploadImage = {
|
||||
image: Buffer.from("testRegular1"),
|
||||
};
|
||||
const upload1 = await alphaImage.uploadImage(uploadForm1);
|
||||
|
||||
const community = await createCommunity(alphaImage);
|
||||
|
||||
// Use wikipedia since it has an opengraph image
|
||||
const wikipediaUrl = "https://wikipedia.org/";
|
||||
|
||||
let post = await createPostWithThumbnail(
|
||||
alphaImage,
|
||||
community.community_view.community.id,
|
||||
wikipediaUrl,
|
||||
upload1.url!,
|
||||
);
|
||||
|
||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
||||
post = await waitUntil(
|
||||
() => getPost(alphaImage, post.post_view.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
expect(post.post_view.post.url).toBe(wikipediaUrl);
|
||||
// Make sure it uses custom thumbnail
|
||||
expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
|
||||
});
|
||||
|
||||
test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
|
||||
const uploadForm1: UploadImage = {
|
||||
image: Buffer.from("test1"),
|
||||
};
|
||||
const upload1 = await alphaImage.uploadImage(uploadForm1);
|
||||
|
||||
const uploadForm2: UploadImage = {
|
||||
image: Buffer.from("test2"),
|
||||
};
|
||||
const upload2 = await alphaImage.uploadImage(uploadForm2);
|
||||
|
||||
const community = await createCommunity(alphaImage);
|
||||
|
||||
let post = await createPostWithThumbnail(
|
||||
alphaImage,
|
||||
community.community_view.community.id,
|
||||
upload1.url!,
|
||||
upload2.url!,
|
||||
);
|
||||
post = await waitUntil(
|
||||
() => getPost(alphaImage, post.post_view.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
expect(post.post_view.post.url).toBe(upload1.url);
|
||||
// Make sure the custom thumbnail is ignored
|
||||
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
|
||||
});
|
781
api_tests/src/post.spec.ts
Normal file
781
api_tests/src/post.spec.ts
Normal file
|
@ -0,0 +1,781 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
||||
import {
|
||||
alpha,
|
||||
beta,
|
||||
gamma,
|
||||
delta,
|
||||
epsilon,
|
||||
setupLogins,
|
||||
createPost,
|
||||
editPost,
|
||||
featurePost,
|
||||
lockPost,
|
||||
resolvePost,
|
||||
likePost,
|
||||
followBeta,
|
||||
resolveBetaCommunity,
|
||||
createComment,
|
||||
deletePost,
|
||||
delay,
|
||||
removePost,
|
||||
getPost,
|
||||
unfollowRemotes,
|
||||
resolvePerson,
|
||||
banPersonFromSite,
|
||||
followCommunity,
|
||||
banPersonFromCommunity,
|
||||
reportPost,
|
||||
listPostReports,
|
||||
randomString,
|
||||
registerUser,
|
||||
getSite,
|
||||
unfollows,
|
||||
resolveCommunity,
|
||||
waitUntil,
|
||||
waitForPost,
|
||||
alphaUrl,
|
||||
loginUser,
|
||||
createCommunity,
|
||||
} from "./shared";
|
||||
import { PostView } from "lemmy-js-client/dist/types/PostView";
|
||||
import { EditSite, ResolveObject } from "lemmy-js-client";
|
||||
|
||||
let betaCommunity: CommunityView | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
expect(betaCommunity).toBeDefined();
|
||||
});
|
||||
|
||||
afterAll(unfollows);
|
||||
|
||||
async function assertPostFederation(postOne: PostView, postTwo: PostView) {
|
||||
// Link metadata is generated in background task and may not be ready yet at this time,
|
||||
// so wait for it explicitly. For removed posts we cant refetch anything.
|
||||
postOne = await waitForPost(beta, postOne.post, res => {
|
||||
return res === null || res?.post.embed_title !== null;
|
||||
});
|
||||
postTwo = await waitForPost(
|
||||
beta,
|
||||
postTwo.post,
|
||||
res => res === null || res?.post.embed_title !== null,
|
||||
);
|
||||
|
||||
expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id);
|
||||
expect(postOne?.post.name).toBe(postTwo?.post.name);
|
||||
expect(postOne?.post.body).toBe(postTwo?.post.body);
|
||||
// TODO url clears arent working
|
||||
// expect(postOne?.post.url).toBe(postTwo?.post.url);
|
||||
expect(postOne?.post.nsfw).toBe(postTwo?.post.nsfw);
|
||||
expect(postOne?.post.embed_title).toBe(postTwo?.post.embed_title);
|
||||
expect(postOne?.post.embed_description).toBe(postTwo?.post.embed_description);
|
||||
expect(postOne?.post.embed_video_url).toBe(postTwo?.post.embed_video_url);
|
||||
expect(postOne?.post.published).toBe(postTwo?.post.published);
|
||||
expect(postOne?.community.actor_id).toBe(postTwo?.community.actor_id);
|
||||
expect(postOne?.post.locked).toBe(postTwo?.post.locked);
|
||||
expect(postOne?.post.removed).toBe(postTwo?.post.removed);
|
||||
expect(postOne?.post.deleted).toBe(postTwo?.post.deleted);
|
||||
}
|
||||
|
||||
test("Create a post", async () => {
|
||||
// Setup some allowlists and blocklists
|
||||
const editSiteForm: EditSite = {};
|
||||
|
||||
editSiteForm.allowed_instances = [];
|
||||
editSiteForm.blocked_instances = ["lemmy-alpha"];
|
||||
await epsilon.editSite(editSiteForm);
|
||||
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
let postRes = await createPost(
|
||||
alpha,
|
||||
betaCommunity.community.id,
|
||||
"https://example.com/",
|
||||
"აშშ ითხოვს ირანს დაუყოვნებლივ გაანთავისუფლოს დაკავებული ნავთობის ტანკერი",
|
||||
);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
expect(postRes.post_view.community.local).toBe(false);
|
||||
expect(postRes.post_view.creator.local).toBe(true);
|
||||
expect(postRes.post_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure that post is liked on beta
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
postRes.post_view.post,
|
||||
res => res?.counts.score === 1,
|
||||
);
|
||||
|
||||
expect(betaPost).toBeDefined();
|
||||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.counts.score).toBe(1);
|
||||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
|
||||
// Delta only follows beta, so it should not see an alpha ap_id
|
||||
await expect(
|
||||
resolvePost(delta, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(Error("couldnt_find_object"));
|
||||
|
||||
// Epsilon has alpha blocked, it should not see the alpha post
|
||||
await expect(
|
||||
resolvePost(epsilon, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(Error("couldnt_find_object"));
|
||||
|
||||
// remove added allow/blocklists
|
||||
editSiteForm.allowed_instances = [];
|
||||
editSiteForm.blocked_instances = [];
|
||||
await delta.editSite(editSiteForm);
|
||||
await epsilon.editSite(editSiteForm);
|
||||
});
|
||||
|
||||
test("Create a post in a non-existent community", async () => {
|
||||
await expect(createPost(alpha, -2)).rejects.toStrictEqual(
|
||||
Error("couldnt_find_community"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Unlike a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
let unlike = await likePost(alpha, 0, postRes.post_view.post);
|
||||
expect(unlike.post_view.counts.score).toBe(0);
|
||||
|
||||
// Try to unlike it again, make sure it stays at 0
|
||||
let unlike2 = await likePost(alpha, 0, postRes.post_view.post);
|
||||
expect(unlike2.post_view.counts.score).toBe(0);
|
||||
|
||||
// Make sure that post is unliked on beta
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
postRes.post_view.post,
|
||||
post => post?.counts.score === 0,
|
||||
);
|
||||
|
||||
expect(betaPost).toBeDefined();
|
||||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.counts.score).toBe(0);
|
||||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
});
|
||||
|
||||
test("Update a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
await waitForPost(beta, postRes.post_view.post);
|
||||
|
||||
let updatedName = "A jest test federated post, updated";
|
||||
let updatedPost = await editPost(alpha, postRes.post_view.post);
|
||||
expect(updatedPost.post_view.post.name).toBe(updatedName);
|
||||
expect(updatedPost.post_view.community.local).toBe(false);
|
||||
expect(updatedPost.post_view.creator.local).toBe(true);
|
||||
|
||||
// Make sure that post is updated on beta
|
||||
let betaPost = await waitForPost(beta, updatedPost.post_view.post);
|
||||
expect(betaPost.community.local).toBe(true);
|
||||
expect(betaPost.creator.local).toBe(false);
|
||||
expect(betaPost.post.name).toBe(updatedName);
|
||||
await assertPostFederation(betaPost, updatedPost.post_view);
|
||||
|
||||
// Make sure lemmy beta cannot update the post
|
||||
await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual(
|
||||
Error("no_post_edit_allowed"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Sticky a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
||||
let betaPost1 = await waitForPost(beta, postRes.post_view.post);
|
||||
if (!betaPost1) {
|
||||
throw "Missing beta post1";
|
||||
}
|
||||
let stickiedPostRes = await featurePost(beta, true, betaPost1.post);
|
||||
expect(stickiedPostRes.post_view.post.featured_community).toBe(true);
|
||||
|
||||
// Make sure that post is stickied on beta
|
||||
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.post.featured_community).toBe(true);
|
||||
|
||||
// Unsticky a post
|
||||
let unstickiedPost = await featurePost(beta, false, betaPost1.post);
|
||||
expect(unstickiedPost.post_view.post.featured_community).toBe(false);
|
||||
|
||||
// Make sure that post is unstickied on beta
|
||||
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
expect(betaPost2?.community.local).toBe(true);
|
||||
expect(betaPost2?.creator.local).toBe(false);
|
||||
expect(betaPost2?.post.featured_community).toBe(false);
|
||||
|
||||
// Make sure that gamma cannot sticky the post on beta
|
||||
let gammaPost = (await resolvePost(gamma, postRes.post_view.post)).post;
|
||||
if (!gammaPost) {
|
||||
throw "Missing gamma post";
|
||||
}
|
||||
// This has been failing occasionally
|
||||
await featurePost(gamma, true, gammaPost.post);
|
||||
let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
// expect(gammaTrySticky.post_view.post.featured_community).toBe(true);
|
||||
expect(betaPost3?.post.featured_community).toBe(false);
|
||||
});
|
||||
|
||||
test("Collection of featured posts gets federated", async () => {
|
||||
// create a new community and feature a post
|
||||
let community = await createCommunity(alpha);
|
||||
let post = await createPost(alpha, community.community_view.community.id);
|
||||
let featuredPost = await featurePost(alpha, true, post.post_view.post);
|
||||
expect(featuredPost.post_view.post.featured_community).toBe(true);
|
||||
|
||||
// fetch the community, ensure that post is also fetched and marked as featured
|
||||
let betaCommunity = await resolveCommunity(
|
||||
beta,
|
||||
community.community_view.community.actor_id,
|
||||
);
|
||||
expect(betaCommunity).toBeDefined();
|
||||
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
post.post_view.post,
|
||||
post => post?.post.featured_community === true,
|
||||
);
|
||||
expect(betaPost).toBeDefined();
|
||||
});
|
||||
|
||||
test("Lock a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followCommunity(alpha, true, betaCommunity.community.id);
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
);
|
||||
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
let betaPost1 = await waitForPost(beta, postRes.post_view.post);
|
||||
// Lock the post
|
||||
let lockedPostRes = await lockPost(beta, true, betaPost1.post);
|
||||
expect(lockedPostRes.post_view.post.locked).toBe(true);
|
||||
|
||||
// Make sure that post is locked on alpha
|
||||
let alphaPost1 = await waitForPost(
|
||||
alpha,
|
||||
postRes.post_view.post,
|
||||
post => !!post && post.post.locked,
|
||||
);
|
||||
|
||||
// Try to make a new comment there, on alpha. For this we need to create a normal
|
||||
// user account because admins/mods can comment in locked posts.
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
await expect(createComment(user, alphaPost1.post.id)).rejects.toStrictEqual(
|
||||
Error("locked"),
|
||||
);
|
||||
|
||||
// Unlock a post
|
||||
let unlockedPost = await lockPost(beta, false, betaPost1.post);
|
||||
expect(unlockedPost.post_view.post.locked).toBe(false);
|
||||
|
||||
// Make sure that post is unlocked on alpha
|
||||
let alphaPost2 = await waitForPost(
|
||||
alpha,
|
||||
postRes.post_view.post,
|
||||
post => !!post && !post.post.locked,
|
||||
);
|
||||
expect(alphaPost2.community.local).toBe(false);
|
||||
expect(alphaPost2.creator.local).toBe(true);
|
||||
expect(alphaPost2.post.locked).toBe(false);
|
||||
|
||||
// Try to create a new comment, on alpha
|
||||
let commentAlpha = await createComment(user, alphaPost1.post.id);
|
||||
expect(commentAlpha).toBeDefined();
|
||||
});
|
||||
|
||||
test("Delete a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
await waitForPost(beta, postRes.post_view.post);
|
||||
|
||||
let deletedPost = await deletePost(alpha, true, postRes.post_view.post);
|
||||
expect(deletedPost.post_view.post.deleted).toBe(true);
|
||||
expect(deletedPost.post_view.post.name).toBe(postRes.post_view.post.name);
|
||||
|
||||
// Make sure lemmy beta sees post is deleted
|
||||
// This will be undefined because of the tombstone
|
||||
await waitForPost(beta, postRes.post_view.post, p => !p || p.post.deleted);
|
||||
|
||||
// Undelete
|
||||
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post);
|
||||
|
||||
// Make sure lemmy beta sees post is undeleted
|
||||
let betaPost2 = await waitForPost(
|
||||
beta,
|
||||
postRes.post_view.post,
|
||||
p => !!p && !p.post.deleted,
|
||||
);
|
||||
|
||||
if (!betaPost2) {
|
||||
throw "Missing beta post 2";
|
||||
}
|
||||
expect(betaPost2.post.deleted).toBe(false);
|
||||
await assertPostFederation(betaPost2, undeletedPost.post_view);
|
||||
|
||||
// Make sure lemmy beta cannot delete the post
|
||||
await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual(
|
||||
Error("no_post_edit_allowed"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Remove a post from admin and community on different instance", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
let gammaCommunity = (
|
||||
await resolveCommunity(gamma, betaCommunity.community.actor_id)
|
||||
).community?.community;
|
||||
if (!gammaCommunity) {
|
||||
throw "Missing gamma community";
|
||||
}
|
||||
let postRes = await createPost(gamma, gammaCommunity.id);
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
|
||||
if (!alphaPost) {
|
||||
throw "Missing alpha post";
|
||||
}
|
||||
let removedPost = await removePost(alpha, true, alphaPost.post);
|
||||
expect(removedPost.post_view.post.removed).toBe(true);
|
||||
expect(removedPost.post_view.post.name).toBe(postRes.post_view.post.name);
|
||||
|
||||
// Make sure lemmy beta sees post is NOT removed
|
||||
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
if (!betaPost) {
|
||||
throw "Missing beta post";
|
||||
}
|
||||
expect(betaPost.post.removed).toBe(false);
|
||||
|
||||
// Undelete
|
||||
let undeletedPost = await removePost(alpha, false, alphaPost.post);
|
||||
expect(undeletedPost.post_view.post.removed).toBe(false);
|
||||
|
||||
// Make sure lemmy beta sees post is undeleted
|
||||
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
expect(betaPost2?.post.removed).toBe(false);
|
||||
await assertPostFederation(betaPost2!, undeletedPost.post_view);
|
||||
});
|
||||
|
||||
test("Remove a post from admin and community on same instance", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followBeta(alpha);
|
||||
let gammaCommunity = await resolveCommunity(
|
||||
gamma,
|
||||
betaCommunity.community.actor_id,
|
||||
);
|
||||
let postRes = await createPost(gamma, gammaCommunity.community!.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
// Get the id for beta
|
||||
let betaPost = await waitForPost(beta, postRes.post_view.post);
|
||||
expect(betaPost).toBeDefined();
|
||||
|
||||
let alphaPost0 = await waitForPost(alpha, postRes.post_view.post);
|
||||
expect(alphaPost0).toBeDefined();
|
||||
|
||||
// The beta admin removes it (the community lives on beta)
|
||||
let removePostRes = await removePost(beta, true, betaPost.post);
|
||||
expect(removePostRes.post_view.post.removed).toBe(true);
|
||||
|
||||
// Make sure lemmy alpha sees post is removed
|
||||
let alphaPost = await waitUntil(
|
||||
() => getPost(alpha, alphaPost0.post.id),
|
||||
p => p?.post_view.post.removed ?? false,
|
||||
);
|
||||
expect(alphaPost?.post_view.post.removed).toBe(true);
|
||||
await assertPostFederation(alphaPost.post_view, removePostRes.post_view);
|
||||
|
||||
// Undelete
|
||||
let undeletedPost = await removePost(beta, false, betaPost.post);
|
||||
expect(undeletedPost.post_view.post.removed).toBe(false);
|
||||
|
||||
// Make sure lemmy alpha sees post is undeleted
|
||||
let alphaPost2 = await waitForPost(
|
||||
alpha,
|
||||
postRes.post_view.post,
|
||||
p => !!p && !p.post.removed,
|
||||
);
|
||||
expect(alphaPost2.post.removed).toBe(false);
|
||||
await assertPostFederation(alphaPost2, undeletedPost.post_view);
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Search for a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await unfollowRemotes(alpha);
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let betaPost = await waitForPost(beta, postRes.post_view.post);
|
||||
expect(betaPost?.post.name).toBeDefined();
|
||||
});
|
||||
|
||||
test("Enforce site ban federation for local user", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
// create a test user
|
||||
let alphaUserHttp = await registerUser(alpha, alphaUrl);
|
||||
let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view
|
||||
.person;
|
||||
let alphaUserActorId = alphaUserPerson?.actor_id;
|
||||
if (!alphaUserActorId) {
|
||||
throw "Missing alpha user actor id";
|
||||
}
|
||||
expect(alphaUserActorId).toBeDefined();
|
||||
await followBeta(alphaUserHttp);
|
||||
|
||||
let alphaPerson = (await resolvePerson(alphaUserHttp, alphaUserActorId!))
|
||||
.person;
|
||||
if (!alphaPerson) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
expect(alphaPerson).toBeDefined();
|
||||
|
||||
// alpha makes post in beta community, it federates to beta instance
|
||||
let postRes1 = await createPost(alphaUserHttp, betaCommunity.community.id);
|
||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
||||
|
||||
// ban alpha from its own instance
|
||||
let banAlpha = await banPersonFromSite(
|
||||
alpha,
|
||||
alphaPerson.person.id,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(banAlpha.banned).toBe(true);
|
||||
|
||||
// alpha ban should be federated to beta
|
||||
let alphaUserOnBeta1 = await waitUntil(
|
||||
() => resolvePerson(beta, alphaUserActorId!),
|
||||
res => res.person?.person.banned ?? false,
|
||||
);
|
||||
expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
|
||||
|
||||
// existing alpha post should be removed on beta
|
||||
let betaBanRes = await waitUntil(
|
||||
() => getPost(beta, searchBeta1.post.id),
|
||||
s => s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(true);
|
||||
|
||||
// Unban alpha
|
||||
let unBanAlpha = await banPersonFromSite(
|
||||
alpha,
|
||||
alphaPerson.person.id,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
expect(unBanAlpha.banned).toBe(false);
|
||||
|
||||
// Login gets invalidated by ban, need to login again
|
||||
if (!alphaUserPerson) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
|
||||
alphaUserHttp.setHeaders({
|
||||
Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "",
|
||||
});
|
||||
// alpha makes new post in beta community, it federates
|
||||
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id);
|
||||
await waitForPost(beta, postRes2.post_view.post);
|
||||
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Enforce site ban federation for federated user", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
// create a test user
|
||||
let alphaUserHttp = await registerUser(alpha, alphaUrl);
|
||||
let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view
|
||||
.person;
|
||||
let alphaUserActorId = alphaUserPerson?.actor_id;
|
||||
if (!alphaUserActorId) {
|
||||
throw "Missing alpha user actor id";
|
||||
}
|
||||
expect(alphaUserActorId).toBeDefined();
|
||||
await followBeta(alphaUserHttp);
|
||||
|
||||
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId!);
|
||||
expect(alphaUserOnBeta2.person?.person.banned).toBe(false);
|
||||
|
||||
if (!alphaUserOnBeta2.person) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
|
||||
// alpha makes post in beta community, it federates to beta instance
|
||||
let postRes1 = await createPost(alphaUserHttp, betaCommunity.community.id);
|
||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
||||
expect(searchBeta1.post).toBeDefined();
|
||||
|
||||
// Now ban and remove their data from beta
|
||||
let banAlphaOnBeta = await banPersonFromSite(
|
||||
beta,
|
||||
alphaUserOnBeta2.person.person.id,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(banAlphaOnBeta.banned).toBe(true);
|
||||
|
||||
// The beta site ban should NOT be federated to alpha
|
||||
let alphaPerson2 = (await getSite(alphaUserHttp)).my_user!.local_user_view
|
||||
.person;
|
||||
expect(alphaPerson2.banned).toBe(false);
|
||||
|
||||
// existing alpha post should be removed on beta
|
||||
let betaBanRes = await waitUntil(
|
||||
() => getPost(beta, searchBeta1.post.id),
|
||||
s => s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(true);
|
||||
|
||||
// existing alpha's post to the beta community should be removed on alpha
|
||||
let alphaPostAfterRemoveOnBeta = await waitUntil(
|
||||
() => getPost(alpha, postRes1.post_view.post.id),
|
||||
s => s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(true);
|
||||
expect(alphaPostAfterRemoveOnBeta.post_view.post.removed).toBe(true);
|
||||
expect(
|
||||
alphaPostAfterRemoveOnBeta.post_view.creator_banned_from_community,
|
||||
).toBe(true);
|
||||
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Enforce community ban for federated user", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followBeta(alpha);
|
||||
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
|
||||
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
|
||||
if (!alphaPerson) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
expect(alphaPerson).toBeDefined();
|
||||
|
||||
// make a post in beta, it goes through
|
||||
let postRes1 = await createPost(alpha, betaCommunity.community.id);
|
||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
||||
expect(searchBeta1.post).toBeDefined();
|
||||
|
||||
// ban alpha from beta community
|
||||
let banAlpha = await banPersonFromCommunity(
|
||||
beta,
|
||||
alphaPerson.person.id,
|
||||
searchBeta1.community.id,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(banAlpha.banned).toBe(true);
|
||||
|
||||
// ensure that the post by alpha got removed
|
||||
let removePostRes = await waitUntil(
|
||||
() => getPost(alpha, postRes1.post_view.post.id),
|
||||
s => s.post_view.post.removed,
|
||||
);
|
||||
expect(removePostRes.post_view.post.removed).toBe(true);
|
||||
expect(removePostRes.post_view.creator_banned_from_community).toBe(true);
|
||||
expect(removePostRes.community_view.banned_from_community).toBe(true);
|
||||
|
||||
// Alpha tries to make post on beta, but it fails because of ban
|
||||
await expect(
|
||||
createPost(alpha, betaCommunity.community.id),
|
||||
).rejects.toStrictEqual(Error("banned_from_community"));
|
||||
|
||||
// Unban alpha
|
||||
let unBanAlpha = await banPersonFromCommunity(
|
||||
beta,
|
||||
alphaPerson.person.id,
|
||||
searchBeta1.community.id,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
expect(unBanAlpha.banned).toBe(false);
|
||||
|
||||
// Need to re-follow the community
|
||||
await followBeta(alpha);
|
||||
|
||||
let postRes3 = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes3.post_view.post).toBeDefined();
|
||||
expect(postRes3.post_view.community.local).toBe(false);
|
||||
expect(postRes3.post_view.creator.local).toBe(true);
|
||||
expect(postRes3.post_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure that post makes it to beta community
|
||||
let postRes4 = await waitForPost(beta, postRes3.post_view.post);
|
||||
expect(postRes4.post).toBeDefined();
|
||||
expect(postRes4.creator_banned_from_community).toBe(false);
|
||||
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("A and G subscribe to B (center) A posts, it gets announced to G", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followBeta(alpha);
|
||||
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post;
|
||||
expect(betaPost?.post.name).toBeDefined();
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Report a post", async () => {
|
||||
// Create post from alpha
|
||||
let alphaCommunity = (await resolveBetaCommunity(alpha)).community!;
|
||||
await followBeta(alpha);
|
||||
let postRes = await createPost(alpha, alphaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
|
||||
if (!alphaPost) {
|
||||
throw "Missing alpha post";
|
||||
}
|
||||
|
||||
// Send report from gamma
|
||||
let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!;
|
||||
let gammaReport = (
|
||||
await reportPost(gamma, gammaPost.post.id, randomString(10))
|
||||
).post_report_view.post_report;
|
||||
expect(gammaReport).toBeDefined();
|
||||
|
||||
// Report was federated to community instance
|
||||
let betaReport = (await waitUntil(
|
||||
() =>
|
||||
listPostReports(beta).then(p =>
|
||||
p.post_reports.find(
|
||||
r =>
|
||||
r.post_report.original_post_name === gammaReport.original_post_name,
|
||||
),
|
||||
),
|
||||
res => !!res,
|
||||
))!.post_report;
|
||||
expect(betaReport).toBeDefined();
|
||||
expect(betaReport.resolved).toBe(false);
|
||||
expect(betaReport.original_post_name).toBe(gammaReport.original_post_name);
|
||||
//expect(betaReport.original_post_url).toBe(gammaReport.original_post_url);
|
||||
expect(betaReport.original_post_body).toBe(gammaReport.original_post_body);
|
||||
expect(betaReport.reason).toBe(gammaReport.reason);
|
||||
await unfollowRemotes(alpha);
|
||||
|
||||
// Report was federated to poster's instance
|
||||
let alphaReport = (await waitUntil(
|
||||
() =>
|
||||
listPostReports(alpha).then(p =>
|
||||
p.post_reports.find(
|
||||
r =>
|
||||
r.post_report.original_post_name === gammaReport.original_post_name,
|
||||
),
|
||||
),
|
||||
res => !!res,
|
||||
))!.post_report;
|
||||
expect(alphaReport).toBeDefined();
|
||||
expect(alphaReport.resolved).toBe(false);
|
||||
expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name);
|
||||
//expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url);
|
||||
expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body);
|
||||
expect(alphaReport.reason).toBe(gammaReport.reason);
|
||||
});
|
||||
|
||||
test("Fetch post via redirect", async () => {
|
||||
await followBeta(alpha);
|
||||
let alphaPost = await createPost(alpha, betaCommunity!.community.id);
|
||||
expect(alphaPost.post_view.post).toBeDefined();
|
||||
// Make sure that post is liked on beta
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
alphaPost.post_view.post,
|
||||
res => res?.counts.score === 1,
|
||||
);
|
||||
|
||||
expect(betaPost).toBeDefined();
|
||||
expect(betaPost.post?.ap_id).toBe(alphaPost.post_view.post.ap_id);
|
||||
|
||||
// Fetch post from url on beta instance instead of ap_id
|
||||
let q = `http://lemmy-beta:8551/post/${betaPost.post.id}`;
|
||||
let form: ResolveObject = {
|
||||
q,
|
||||
};
|
||||
let gammaPost = await gamma.resolveObject(form);
|
||||
expect(gammaPost).toBeDefined();
|
||||
expect(gammaPost.post?.post.ap_id).toBe(alphaPost.post_view.post.ap_id);
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Block post that contains banned URL", async () => {
|
||||
let editSiteForm: EditSite = {
|
||||
blocked_urls: ["https://evil.com/"],
|
||||
};
|
||||
|
||||
await epsilon.editSite(editSiteForm);
|
||||
|
||||
await delay();
|
||||
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
expect(
|
||||
createPost(epsilon, betaCommunity.community.id, "https://evil.com"),
|
||||
).rejects.toStrictEqual(Error("blocked_url"));
|
||||
|
||||
// Later tests need this to be empty
|
||||
editSiteForm.blocked_urls = [];
|
||||
await epsilon.editSite(editSiteForm);
|
||||
});
|
||||
|
||||
test("Fetch post with redirect", async () => {
|
||||
let alphaPost = await createPost(alpha, betaCommunity!.community.id);
|
||||
expect(alphaPost.post_view.post).toBeDefined();
|
||||
|
||||
// beta fetches from alpha as usual
|
||||
let betaPost = await resolvePost(beta, alphaPost.post_view.post);
|
||||
expect(betaPost.post).toBeDefined();
|
||||
|
||||
// gamma fetches from beta, and gets redirected to alpha
|
||||
let gammaPost = await resolvePost(gamma, betaPost.post!.post);
|
||||
expect(gammaPost.post).toBeDefined();
|
||||
|
||||
// fetch remote object from local url, which redirects to the original url
|
||||
let form: ResolveObject = {
|
||||
q: `http://lemmy-gamma:8561/post/${gammaPost.post!.post.id}`,
|
||||
};
|
||||
let gammaPost2 = await gamma.resolveObject(form);
|
||||
expect(gammaPost2.post).toBeDefined();
|
||||
});
|
149
api_tests/src/private_message.spec.ts
Normal file
149
api_tests/src/private_message.spec.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
jest.setTimeout(120000);
|
||||
import {
|
||||
alpha,
|
||||
beta,
|
||||
setupLogins,
|
||||
followBeta,
|
||||
createPrivateMessage,
|
||||
editPrivateMessage,
|
||||
listPrivateMessages,
|
||||
deletePrivateMessage,
|
||||
waitUntil,
|
||||
reportPrivateMessage,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
|
||||
let recipient_id: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
await followBeta(alpha);
|
||||
recipient_id = 3;
|
||||
});
|
||||
|
||||
afterAll(unfollows);
|
||||
|
||||
test("Create a private message", async () => {
|
||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||
expect(pmRes.private_message_view.private_message.content).toBeDefined();
|
||||
expect(pmRes.private_message_view.private_message.local).toBe(true);
|
||||
expect(pmRes.private_message_view.creator.local).toBe(true);
|
||||
expect(pmRes.private_message_view.recipient.local).toBe(false);
|
||||
|
||||
let betaPms = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
e => !!e.private_messages[0],
|
||||
);
|
||||
expect(betaPms.private_messages[0].private_message.content).toBeDefined();
|
||||
expect(betaPms.private_messages[0].private_message.local).toBe(false);
|
||||
expect(betaPms.private_messages[0].creator.local).toBe(false);
|
||||
expect(betaPms.private_messages[0].recipient.local).toBe(true);
|
||||
});
|
||||
|
||||
test("Update a private message", async () => {
|
||||
let updatedContent = "A jest test federated private message edited";
|
||||
|
||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||
let pmUpdated = await editPrivateMessage(
|
||||
alpha,
|
||||
pmRes.private_message_view.private_message.id,
|
||||
);
|
||||
expect(pmUpdated.private_message_view.private_message.content).toBe(
|
||||
updatedContent,
|
||||
);
|
||||
|
||||
let betaPms = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
p => p.private_messages[0].private_message.content === updatedContent,
|
||||
);
|
||||
expect(betaPms.private_messages[0].private_message.content).toBe(
|
||||
updatedContent,
|
||||
);
|
||||
});
|
||||
|
||||
test("Delete a private message", async () => {
|
||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||
let betaPms1 = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
m =>
|
||||
!!m.private_messages.find(
|
||||
e =>
|
||||
e.private_message.ap_id ===
|
||||
pmRes.private_message_view.private_message.ap_id,
|
||||
),
|
||||
);
|
||||
let deletedPmRes = await deletePrivateMessage(
|
||||
alpha,
|
||||
true,
|
||||
pmRes.private_message_view.private_message.id,
|
||||
);
|
||||
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
|
||||
|
||||
// The GetPrivateMessages filters out deleted,
|
||||
// even though they are in the actual database.
|
||||
// no reason to show them
|
||||
let betaPms2 = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
p => p.private_messages.length === betaPms1.private_messages.length - 1,
|
||||
);
|
||||
expect(betaPms2.private_messages.length).toBe(
|
||||
betaPms1.private_messages.length - 1,
|
||||
);
|
||||
|
||||
// Undelete
|
||||
let undeletedPmRes = await deletePrivateMessage(
|
||||
alpha,
|
||||
false,
|
||||
pmRes.private_message_view.private_message.id,
|
||||
);
|
||||
expect(undeletedPmRes.private_message_view.private_message.deleted).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
let betaPms3 = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
p => p.private_messages.length === betaPms1.private_messages.length,
|
||||
);
|
||||
expect(betaPms3.private_messages.length).toBe(
|
||||
betaPms1.private_messages.length,
|
||||
);
|
||||
});
|
||||
|
||||
test("Create a private message report", async () => {
|
||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||
let betaPms1 = await waitUntil(
|
||||
() => listPrivateMessages(beta),
|
||||
m =>
|
||||
!!m.private_messages.find(
|
||||
e =>
|
||||
e.private_message.ap_id ===
|
||||
pmRes.private_message_view.private_message.ap_id,
|
||||
),
|
||||
);
|
||||
let betaPm = betaPms1.private_messages[0];
|
||||
expect(betaPm).toBeDefined();
|
||||
|
||||
// Make sure that only the recipient can report it, so this should fail
|
||||
await expect(
|
||||
reportPrivateMessage(
|
||||
alpha,
|
||||
pmRes.private_message_view.private_message.id,
|
||||
"a reason",
|
||||
),
|
||||
).rejects.toStrictEqual(Error("couldnt_create_report"));
|
||||
|
||||
// This one should pass
|
||||
let reason = "another reason";
|
||||
let report = await reportPrivateMessage(
|
||||
beta,
|
||||
betaPm.private_message.id,
|
||||
reason,
|
||||
);
|
||||
|
||||
expect(report.private_message_report_view.private_message.id).toBe(
|
||||
betaPm.private_message.id,
|
||||
);
|
||||
expect(report.private_message_report_view.private_message_report.reason).toBe(
|
||||
reason,
|
||||
);
|
||||
});
|
976
api_tests/src/shared.ts
Normal file
976
api_tests/src/shared.ts
Normal file
|
@ -0,0 +1,976 @@
|
|||
import {
|
||||
BlockCommunity,
|
||||
BlockCommunityResponse,
|
||||
BlockInstance,
|
||||
BlockInstanceResponse,
|
||||
CommunityId,
|
||||
CreatePrivateMessageReport,
|
||||
DeleteImage,
|
||||
EditCommunity,
|
||||
GetReplies,
|
||||
GetRepliesResponse,
|
||||
GetUnreadCountResponse,
|
||||
InstanceId,
|
||||
LemmyHttp,
|
||||
PostView,
|
||||
PrivateMessageReportResponse,
|
||||
SuccessResponse,
|
||||
} from "lemmy-js-client";
|
||||
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
|
||||
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
|
||||
import { EditPost } from "lemmy-js-client/dist/types/EditPost";
|
||||
import { EditSite } from "lemmy-js-client/dist/types/EditSite";
|
||||
import { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost";
|
||||
import { GetComments } from "lemmy-js-client/dist/types/GetComments";
|
||||
import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse";
|
||||
import { GetPost } from "lemmy-js-client/dist/types/GetPost";
|
||||
import { GetPostResponse } from "lemmy-js-client/dist/types/GetPostResponse";
|
||||
import { LockPost } from "lemmy-js-client/dist/types/LockPost";
|
||||
import { Login } from "lemmy-js-client/dist/types/Login";
|
||||
import { Post } from "lemmy-js-client/dist/types/Post";
|
||||
import { PostResponse } from "lemmy-js-client/dist/types/PostResponse";
|
||||
import { RemovePost } from "lemmy-js-client/dist/types/RemovePost";
|
||||
import { ResolveObject } from "lemmy-js-client/dist/types/ResolveObject";
|
||||
import { ResolveObjectResponse } from "lemmy-js-client/dist/types/ResolveObjectResponse";
|
||||
import { Search } from "lemmy-js-client/dist/types/Search";
|
||||
import { SearchResponse } from "lemmy-js-client/dist/types/SearchResponse";
|
||||
import { Comment } from "lemmy-js-client/dist/types/Comment";
|
||||
import { BanPersonResponse } from "lemmy-js-client/dist/types/BanPersonResponse";
|
||||
import { BanPerson } from "lemmy-js-client/dist/types/BanPerson";
|
||||
import { BanFromCommunityResponse } from "lemmy-js-client/dist/types/BanFromCommunityResponse";
|
||||
import { BanFromCommunity } from "lemmy-js-client/dist/types/BanFromCommunity";
|
||||
import { CommunityResponse } from "lemmy-js-client/dist/types/CommunityResponse";
|
||||
import { FollowCommunity } from "lemmy-js-client/dist/types/FollowCommunity";
|
||||
import { CreatePostLike } from "lemmy-js-client/dist/types/CreatePostLike";
|
||||
import { CommentResponse } from "lemmy-js-client/dist/types/CommentResponse";
|
||||
import { CreateComment } from "lemmy-js-client/dist/types/CreateComment";
|
||||
import { EditComment } from "lemmy-js-client/dist/types/EditComment";
|
||||
import { DeleteComment } from "lemmy-js-client/dist/types/DeleteComment";
|
||||
import { RemoveComment } from "lemmy-js-client/dist/types/RemoveComment";
|
||||
import { GetPersonMentionsResponse } from "lemmy-js-client/dist/types/GetPersonMentionsResponse";
|
||||
import { GetPersonMentions } from "lemmy-js-client/dist/types/GetPersonMentions";
|
||||
import { CreateCommentLike } from "lemmy-js-client/dist/types/CreateCommentLike";
|
||||
import { CreateCommunity } from "lemmy-js-client/dist/types/CreateCommunity";
|
||||
import { GetCommunity } from "lemmy-js-client/dist/types/GetCommunity";
|
||||
import { DeleteCommunity } from "lemmy-js-client/dist/types/DeleteCommunity";
|
||||
import { RemoveCommunity } from "lemmy-js-client/dist/types/RemoveCommunity";
|
||||
import { PrivateMessageResponse } from "lemmy-js-client/dist/types/PrivateMessageResponse";
|
||||
import { CreatePrivateMessage } from "lemmy-js-client/dist/types/CreatePrivateMessage";
|
||||
import { EditPrivateMessage } from "lemmy-js-client/dist/types/EditPrivateMessage";
|
||||
import { DeletePrivateMessage } from "lemmy-js-client/dist/types/DeletePrivateMessage";
|
||||
import { LoginResponse } from "lemmy-js-client/dist/types/LoginResponse";
|
||||
import { Register } from "lemmy-js-client/dist/types/Register";
|
||||
import { SaveUserSettings } from "lemmy-js-client/dist/types/SaveUserSettings";
|
||||
import { DeleteAccount } from "lemmy-js-client/dist/types/DeleteAccount";
|
||||
import { GetSiteResponse } from "lemmy-js-client/dist/types/GetSiteResponse";
|
||||
import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessagesResponse";
|
||||
import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages";
|
||||
import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse";
|
||||
import { CreatePostReport } from "lemmy-js-client/dist/types/CreatePostReport";
|
||||
import { ListPostReportsResponse } from "lemmy-js-client/dist/types/ListPostReportsResponse";
|
||||
import { ListPostReports } from "lemmy-js-client/dist/types/ListPostReports";
|
||||
import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportResponse";
|
||||
import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport";
|
||||
import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse";
|
||||
import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports";
|
||||
import { GetPostsResponse } from "lemmy-js-client/dist/types/GetPostsResponse";
|
||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
||||
import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse";
|
||||
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
|
||||
import { ListingType } from "lemmy-js-client/dist/types/ListingType";
|
||||
|
||||
export const fetchFunction = fetch;
|
||||
export const imageFetchLimit = 50;
|
||||
export const sampleImage =
|
||||
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg";
|
||||
export const sampleSite = "https://yahoo.com";
|
||||
|
||||
export const alphaUrl = "http://127.0.0.1:8541";
|
||||
export const betaUrl = "http://127.0.0.1:8551";
|
||||
export const gammaUrl = "http://127.0.0.1:8561";
|
||||
export const deltaUrl = "http://127.0.0.1:8571";
|
||||
export const epsilonUrl = "http://127.0.0.1:8581";
|
||||
|
||||
export const alpha = new LemmyHttp(alphaUrl, { fetchFunction });
|
||||
export const alphaImage = new LemmyHttp(alphaUrl);
|
||||
export const beta = new LemmyHttp(betaUrl, { fetchFunction });
|
||||
export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
|
||||
export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
|
||||
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
|
||||
|
||||
export const betaAllowedInstances = [
|
||||
"lemmy-alpha",
|
||||
"lemmy-gamma",
|
||||
"lemmy-delta",
|
||||
"lemmy-epsilon",
|
||||
];
|
||||
|
||||
const password = "lemmylemmy";
|
||||
|
||||
export async function setupLogins() {
|
||||
let formAlpha: Login = {
|
||||
username_or_email: "lemmy_alpha",
|
||||
password,
|
||||
};
|
||||
let resAlpha = alpha.login(formAlpha);
|
||||
|
||||
let formBeta: Login = {
|
||||
username_or_email: "lemmy_beta",
|
||||
password,
|
||||
};
|
||||
let resBeta = beta.login(formBeta);
|
||||
|
||||
let formGamma: Login = {
|
||||
username_or_email: "lemmy_gamma",
|
||||
password,
|
||||
};
|
||||
let resGamma = gamma.login(formGamma);
|
||||
|
||||
let formDelta: Login = {
|
||||
username_or_email: "lemmy_delta",
|
||||
password,
|
||||
};
|
||||
let resDelta = delta.login(formDelta);
|
||||
|
||||
let formEpsilon: Login = {
|
||||
username_or_email: "lemmy_epsilon",
|
||||
password,
|
||||
};
|
||||
let resEpsilon = epsilon.login(formEpsilon);
|
||||
|
||||
let res = await Promise.all([
|
||||
resAlpha,
|
||||
resBeta,
|
||||
resGamma,
|
||||
resDelta,
|
||||
resEpsilon,
|
||||
]);
|
||||
alpha.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
|
||||
alphaImage.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
|
||||
beta.setHeaders({ Authorization: `Bearer ${res[1].jwt ?? ""}` });
|
||||
gamma.setHeaders({ Authorization: `Bearer ${res[2].jwt ?? ""}` });
|
||||
delta.setHeaders({ Authorization: `Bearer ${res[3].jwt ?? ""}` });
|
||||
epsilon.setHeaders({ Authorization: `Bearer ${res[4].jwt ?? ""}` });
|
||||
|
||||
// Registration applications are now enabled by default, need to disable them
|
||||
let editSiteForm: EditSite = {
|
||||
registration_mode: "Open",
|
||||
rate_limit_message: 999,
|
||||
rate_limit_post: 999,
|
||||
rate_limit_register: 999,
|
||||
rate_limit_image: 999,
|
||||
rate_limit_comment: 999,
|
||||
rate_limit_search: 999,
|
||||
};
|
||||
|
||||
// Set the blocks and auths for each
|
||||
editSiteForm.allowed_instances = [
|
||||
"lemmy-beta",
|
||||
"lemmy-gamma",
|
||||
"lemmy-delta",
|
||||
"lemmy-epsilon",
|
||||
];
|
||||
await alpha.editSite(editSiteForm);
|
||||
|
||||
editSiteForm.allowed_instances = betaAllowedInstances;
|
||||
await beta.editSite(editSiteForm);
|
||||
|
||||
editSiteForm.allowed_instances = [
|
||||
"lemmy-alpha",
|
||||
"lemmy-beta",
|
||||
"lemmy-delta",
|
||||
"lemmy-epsilon",
|
||||
];
|
||||
await gamma.editSite(editSiteForm);
|
||||
|
||||
// Setup delta allowed instance
|
||||
editSiteForm.allowed_instances = ["lemmy-beta"];
|
||||
await delta.editSite(editSiteForm);
|
||||
|
||||
// Create the main alpha/beta communities
|
||||
// Ignore thrown errors of duplicates
|
||||
try {
|
||||
await createCommunity(alpha, "main");
|
||||
await createCommunity(beta, "main");
|
||||
// wait for > INSTANCES_RECHECK_DELAY to ensure federation is initialized
|
||||
// otherwise the first few federated events may be missed
|
||||
// (because last_successful_id is set to current id when federation to an instance is first started)
|
||||
// only needed the first time so do in this try
|
||||
await delay(10_000);
|
||||
} catch (_) {
|
||||
console.log("Communities already exist");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPost(
|
||||
api: LemmyHttp,
|
||||
community_id: number,
|
||||
url: string = "https://example.com/",
|
||||
body = randomString(10),
|
||||
// use example.com for consistent title and embed description
|
||||
name: string = randomString(5),
|
||||
alt_text = randomString(10),
|
||||
custom_thumbnail: string | undefined = undefined,
|
||||
): Promise<PostResponse> {
|
||||
let form: CreatePost = {
|
||||
name,
|
||||
url,
|
||||
body,
|
||||
alt_text,
|
||||
community_id,
|
||||
custom_thumbnail,
|
||||
};
|
||||
return api.createPost(form);
|
||||
}
|
||||
|
||||
export async function editPost(
|
||||
api: LemmyHttp,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let name = "A jest test federated post, updated";
|
||||
let form: EditPost = {
|
||||
name,
|
||||
post_id: post.id,
|
||||
};
|
||||
return api.editPost(form);
|
||||
}
|
||||
|
||||
export async function createPostWithThumbnail(
|
||||
api: LemmyHttp,
|
||||
community_id: number,
|
||||
url: string,
|
||||
custom_thumbnail: string,
|
||||
): Promise<PostResponse> {
|
||||
let form: CreatePost = {
|
||||
name: randomString(10),
|
||||
url,
|
||||
community_id,
|
||||
custom_thumbnail,
|
||||
};
|
||||
return api.createPost(form);
|
||||
}
|
||||
|
||||
export async function deletePost(
|
||||
api: LemmyHttp,
|
||||
deleted: boolean,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let form: DeletePost = {
|
||||
post_id: post.id,
|
||||
deleted: deleted,
|
||||
};
|
||||
return api.deletePost(form);
|
||||
}
|
||||
|
||||
export async function removePost(
|
||||
api: LemmyHttp,
|
||||
removed: boolean,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let form: RemovePost = {
|
||||
post_id: post.id,
|
||||
removed,
|
||||
};
|
||||
return api.removePost(form);
|
||||
}
|
||||
|
||||
export async function featurePost(
|
||||
api: LemmyHttp,
|
||||
featured: boolean,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let form: FeaturePost = {
|
||||
post_id: post.id,
|
||||
featured,
|
||||
feature_type: "Community",
|
||||
};
|
||||
return api.featurePost(form);
|
||||
}
|
||||
|
||||
export async function lockPost(
|
||||
api: LemmyHttp,
|
||||
locked: boolean,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let form: LockPost = {
|
||||
post_id: post.id,
|
||||
locked,
|
||||
};
|
||||
return api.lockPost(form);
|
||||
}
|
||||
|
||||
export async function resolvePost(
|
||||
api: LemmyHttp,
|
||||
post: Post,
|
||||
): Promise<ResolveObjectResponse> {
|
||||
let form: ResolveObject = {
|
||||
q: post.ap_id,
|
||||
};
|
||||
return api.resolveObject(form);
|
||||
}
|
||||
|
||||
export async function searchPostLocal(
|
||||
api: LemmyHttp,
|
||||
post: Post,
|
||||
): Promise<SearchResponse> {
|
||||
let form: Search = {
|
||||
q: post.name,
|
||||
type_: "Posts",
|
||||
sort: "TopAll",
|
||||
listing_type: "All",
|
||||
};
|
||||
return api.search(form);
|
||||
}
|
||||
|
||||
/// wait for a post to appear locally without pulling it
|
||||
export async function waitForPost(
|
||||
api: LemmyHttp,
|
||||
post: Post,
|
||||
checker: (t: PostView | undefined) => boolean = p => !!p,
|
||||
) {
|
||||
return waitUntil<PostView>(
|
||||
() => searchPostLocal(api, post).then(p => p.posts[0]),
|
||||
checker,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPost(
|
||||
api: LemmyHttp,
|
||||
post_id: number,
|
||||
): Promise<GetPostResponse> {
|
||||
let form: GetPost = {
|
||||
id: post_id,
|
||||
};
|
||||
return api.getPost(form);
|
||||
}
|
||||
|
||||
export async function getComments(
|
||||
api: LemmyHttp,
|
||||
post_id?: number,
|
||||
listingType: ListingType = "All",
|
||||
): Promise<GetCommentsResponse> {
|
||||
let form: GetComments = {
|
||||
post_id: post_id,
|
||||
type_: listingType,
|
||||
sort: "New",
|
||||
limit: 50,
|
||||
};
|
||||
return api.getComments(form);
|
||||
}
|
||||
|
||||
export async function getUnreadCount(
|
||||
api: LemmyHttp,
|
||||
): Promise<GetUnreadCountResponse> {
|
||||
return api.getUnreadCount();
|
||||
}
|
||||
|
||||
export async function getReplies(
|
||||
api: LemmyHttp,
|
||||
unread_only: boolean = false,
|
||||
): Promise<GetRepliesResponse> {
|
||||
let form: GetReplies = {
|
||||
sort: "New",
|
||||
unread_only,
|
||||
};
|
||||
return api.getReplies(form);
|
||||
}
|
||||
|
||||
export async function resolveComment(
|
||||
api: LemmyHttp,
|
||||
comment: Comment,
|
||||
): Promise<ResolveObjectResponse> {
|
||||
let form: ResolveObject = {
|
||||
q: comment.ap_id,
|
||||
};
|
||||
return api.resolveObject(form);
|
||||
}
|
||||
|
||||
export async function resolveBetaCommunity(
|
||||
api: LemmyHttp,
|
||||
): Promise<ResolveObjectResponse> {
|
||||
// Use short-hand search url
|
||||
let form: ResolveObject = {
|
||||
q: "!main@lemmy-beta:8551",
|
||||
};
|
||||
return api.resolveObject(form);
|
||||
}
|
||||
|
||||
export async function resolveCommunity(
|
||||
api: LemmyHttp,
|
||||
q: string,
|
||||
): Promise<ResolveObjectResponse> {
|
||||
let form: ResolveObject = {
|
||||
q,
|
||||
};
|
||||
return api.resolveObject(form);
|
||||
}
|
||||
|
||||
export async function resolvePerson(
|
||||
api: LemmyHttp,
|
||||
apShortname: string,
|
||||
): Promise<ResolveObjectResponse> {
|
||||
let form: ResolveObject = {
|
||||
q: apShortname,
|
||||
};
|
||||
return api.resolveObject(form);
|
||||
}
|
||||
|
||||
export async function banPersonFromSite(
|
||||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
ban: boolean,
|
||||
remove_data: boolean,
|
||||
): Promise<BanPersonResponse> {
|
||||
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
||||
let form: BanPerson = {
|
||||
person_id,
|
||||
ban,
|
||||
remove_data,
|
||||
};
|
||||
return api.banPerson(form);
|
||||
}
|
||||
|
||||
export async function banPersonFromCommunity(
|
||||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
community_id: number,
|
||||
remove_data: boolean,
|
||||
ban: boolean,
|
||||
): Promise<BanFromCommunityResponse> {
|
||||
let form: BanFromCommunity = {
|
||||
person_id,
|
||||
community_id,
|
||||
remove_data: remove_data,
|
||||
ban,
|
||||
};
|
||||
return api.banFromCommunity(form);
|
||||
}
|
||||
|
||||
export async function followCommunity(
|
||||
api: LemmyHttp,
|
||||
follow: boolean,
|
||||
community_id: number,
|
||||
): Promise<CommunityResponse> {
|
||||
let form: FollowCommunity = {
|
||||
community_id,
|
||||
follow,
|
||||
};
|
||||
const res = await api.followCommunity(form);
|
||||
await waitUntil(
|
||||
() => getCommunity(api, res.community_view.community.id),
|
||||
g =>
|
||||
g.community_view.subscribed === (follow ? "Subscribed" : "NotSubscribed"),
|
||||
);
|
||||
// wait FOLLOW_ADDITIONS_RECHECK_DELAY (there's no API to wait for this currently)
|
||||
await delay(2000);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function likePost(
|
||||
api: LemmyHttp,
|
||||
score: number,
|
||||
post: Post,
|
||||
): Promise<PostResponse> {
|
||||
let form: CreatePostLike = {
|
||||
post_id: post.id,
|
||||
score: score,
|
||||
};
|
||||
|
||||
return api.likePost(form);
|
||||
}
|
||||
|
||||
export async function createComment(
|
||||
api: LemmyHttp,
|
||||
post_id: number,
|
||||
parent_id?: number,
|
||||
content = "a jest test comment",
|
||||
): Promise<CommentResponse> {
|
||||
let form: CreateComment = {
|
||||
content,
|
||||
post_id,
|
||||
parent_id,
|
||||
};
|
||||
return api.createComment(form);
|
||||
}
|
||||
|
||||
export async function editComment(
|
||||
api: LemmyHttp,
|
||||
comment_id: number,
|
||||
content = "A jest test federated comment update",
|
||||
): Promise<CommentResponse> {
|
||||
let form: EditComment = {
|
||||
content,
|
||||
comment_id,
|
||||
};
|
||||
return api.editComment(form);
|
||||
}
|
||||
|
||||
export async function deleteComment(
|
||||
api: LemmyHttp,
|
||||
deleted: boolean,
|
||||
comment_id: number,
|
||||
): Promise<CommentResponse> {
|
||||
let form: DeleteComment = {
|
||||
comment_id,
|
||||
deleted,
|
||||
};
|
||||
return api.deleteComment(form);
|
||||
}
|
||||
|
||||
export async function removeComment(
|
||||
api: LemmyHttp,
|
||||
removed: boolean,
|
||||
comment_id: number,
|
||||
): Promise<CommentResponse> {
|
||||
let form: RemoveComment = {
|
||||
comment_id,
|
||||
removed,
|
||||
};
|
||||
return api.removeComment(form);
|
||||
}
|
||||
|
||||
export async function getMentions(
|
||||
api: LemmyHttp,
|
||||
): Promise<GetPersonMentionsResponse> {
|
||||
let form: GetPersonMentions = {
|
||||
sort: "New",
|
||||
unread_only: false,
|
||||
};
|
||||
return api.getPersonMentions(form);
|
||||
}
|
||||
|
||||
export async function likeComment(
|
||||
api: LemmyHttp,
|
||||
score: number,
|
||||
comment: Comment,
|
||||
): Promise<CommentResponse> {
|
||||
let form: CreateCommentLike = {
|
||||
comment_id: comment.id,
|
||||
score,
|
||||
};
|
||||
return api.likeComment(form);
|
||||
}
|
||||
|
||||
export async function createCommunity(
|
||||
api: LemmyHttp,
|
||||
name_: string = randomString(10),
|
||||
): Promise<CommunityResponse> {
|
||||
let description = "a sample description";
|
||||
let form: CreateCommunity = {
|
||||
name: name_,
|
||||
title: name_,
|
||||
description,
|
||||
};
|
||||
return api.createCommunity(form);
|
||||
}
|
||||
|
||||
export async function editCommunity(
|
||||
api: LemmyHttp,
|
||||
form: EditCommunity,
|
||||
): Promise<CommunityResponse> {
|
||||
return api.editCommunity(form);
|
||||
}
|
||||
|
||||
export async function getCommunity(
|
||||
api: LemmyHttp,
|
||||
id: number,
|
||||
): Promise<CommunityResponse> {
|
||||
let form: GetCommunity = {
|
||||
id,
|
||||
};
|
||||
return api.getCommunity(form);
|
||||
}
|
||||
|
||||
export async function getCommunityByName(
|
||||
api: LemmyHttp,
|
||||
name: string,
|
||||
): Promise<CommunityResponse> {
|
||||
let form: GetCommunity = {
|
||||
name,
|
||||
};
|
||||
return api.getCommunity(form);
|
||||
}
|
||||
|
||||
export async function deleteCommunity(
|
||||
api: LemmyHttp,
|
||||
deleted: boolean,
|
||||
community_id: number,
|
||||
): Promise<CommunityResponse> {
|
||||
let form: DeleteCommunity = {
|
||||
community_id,
|
||||
deleted,
|
||||
};
|
||||
return api.deleteCommunity(form);
|
||||
}
|
||||
|
||||
export async function removeCommunity(
|
||||
api: LemmyHttp,
|
||||
removed: boolean,
|
||||
community_id: number,
|
||||
): Promise<CommunityResponse> {
|
||||
let form: RemoveCommunity = {
|
||||
community_id,
|
||||
removed,
|
||||
};
|
||||
return api.removeCommunity(form);
|
||||
}
|
||||
|
||||
export async function createPrivateMessage(
|
||||
api: LemmyHttp,
|
||||
recipient_id: number,
|
||||
): Promise<PrivateMessageResponse> {
|
||||
let content = "A jest test federated private message";
|
||||
let form: CreatePrivateMessage = {
|
||||
content,
|
||||
recipient_id,
|
||||
};
|
||||
return api.createPrivateMessage(form);
|
||||
}
|
||||
|
||||
export async function editPrivateMessage(
|
||||
api: LemmyHttp,
|
||||
private_message_id: number,
|
||||
): Promise<PrivateMessageResponse> {
|
||||
let updatedContent = "A jest test federated private message edited";
|
||||
let form: EditPrivateMessage = {
|
||||
content: updatedContent,
|
||||
private_message_id,
|
||||
};
|
||||
return api.editPrivateMessage(form);
|
||||
}
|
||||
|
||||
export async function deletePrivateMessage(
|
||||
api: LemmyHttp,
|
||||
deleted: boolean,
|
||||
private_message_id: number,
|
||||
): Promise<PrivateMessageResponse> {
|
||||
let form: DeletePrivateMessage = {
|
||||
deleted,
|
||||
private_message_id,
|
||||
};
|
||||
return api.deletePrivateMessage(form);
|
||||
}
|
||||
|
||||
export async function registerUser(
|
||||
api: LemmyHttp,
|
||||
url: string,
|
||||
username: string = randomString(5),
|
||||
): Promise<LemmyHttp> {
|
||||
let form: Register = {
|
||||
username,
|
||||
password,
|
||||
password_verify: password,
|
||||
show_nsfw: true,
|
||||
};
|
||||
let login_response = await api.register(form);
|
||||
|
||||
expect(login_response.jwt).toBeDefined();
|
||||
let lemmy_http = new LemmyHttp(url, {
|
||||
headers: { Authorization: `Bearer ${login_response.jwt ?? ""}` },
|
||||
});
|
||||
return lemmy_http;
|
||||
}
|
||||
|
||||
export async function loginUser(
|
||||
api: LemmyHttp,
|
||||
username: string,
|
||||
): Promise<LoginResponse> {
|
||||
let form: Login = {
|
||||
username_or_email: username,
|
||||
password: password,
|
||||
};
|
||||
return api.login(form);
|
||||
}
|
||||
|
||||
export async function saveUserSettingsBio(
|
||||
api: LemmyHttp,
|
||||
): Promise<SuccessResponse> {
|
||||
let form: SaveUserSettings = {
|
||||
show_nsfw: true,
|
||||
blur_nsfw: false,
|
||||
auto_expand: true,
|
||||
theme: "darkly",
|
||||
default_sort_type: "Active",
|
||||
default_listing_type: "All",
|
||||
interface_language: "en",
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
bio: "a changed bio",
|
||||
};
|
||||
return saveUserSettings(api, form);
|
||||
}
|
||||
|
||||
export async function saveUserSettingsFederated(
|
||||
api: LemmyHttp,
|
||||
): Promise<SuccessResponse> {
|
||||
let avatar = sampleImage;
|
||||
let banner = sampleImage;
|
||||
let bio = "a changed bio";
|
||||
let form: SaveUserSettings = {
|
||||
show_nsfw: false,
|
||||
blur_nsfw: true,
|
||||
auto_expand: false,
|
||||
default_sort_type: "Hot",
|
||||
default_listing_type: "All",
|
||||
interface_language: "",
|
||||
avatar,
|
||||
banner,
|
||||
display_name: "user321",
|
||||
show_avatars: false,
|
||||
send_notifications_to_email: false,
|
||||
bio,
|
||||
};
|
||||
return await saveUserSettings(api, form);
|
||||
}
|
||||
|
||||
export async function saveUserSettings(
|
||||
api: LemmyHttp,
|
||||
form: SaveUserSettings,
|
||||
): Promise<SuccessResponse> {
|
||||
return api.saveUserSettings(form);
|
||||
}
|
||||
export async function getPersonDetails(
|
||||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
): Promise<GetPersonDetailsResponse> {
|
||||
let form: GetPersonDetails = {
|
||||
person_id: person_id,
|
||||
};
|
||||
return api.getPersonDetails(form);
|
||||
}
|
||||
|
||||
export async function deleteUser(api: LemmyHttp): Promise<SuccessResponse> {
|
||||
let form: DeleteAccount = {
|
||||
delete_content: true,
|
||||
password,
|
||||
};
|
||||
return api.deleteAccount(form);
|
||||
}
|
||||
|
||||
export async function getSite(api: LemmyHttp): Promise<GetSiteResponse> {
|
||||
return api.getSite();
|
||||
}
|
||||
|
||||
export async function listPrivateMessages(
|
||||
api: LemmyHttp,
|
||||
): Promise<PrivateMessagesResponse> {
|
||||
let form: GetPrivateMessages = {
|
||||
unread_only: false,
|
||||
};
|
||||
return api.getPrivateMessages(form);
|
||||
}
|
||||
|
||||
export async function unfollowRemotes(
|
||||
api: LemmyHttp,
|
||||
): Promise<GetSiteResponse> {
|
||||
// Unfollow all remote communities
|
||||
let site = await getSite(api);
|
||||
let remoteFollowed =
|
||||
site.my_user?.follows.filter(c => c.community.local == false) ?? [];
|
||||
await Promise.all(
|
||||
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
|
||||
);
|
||||
|
||||
let siteRes = await getSite(api);
|
||||
return siteRes;
|
||||
}
|
||||
|
||||
export async function followBeta(api: LemmyHttp): Promise<CommunityResponse> {
|
||||
let betaCommunity = (await resolveBetaCommunity(api)).community;
|
||||
if (betaCommunity) {
|
||||
let follow = await followCommunity(api, true, betaCommunity.community.id);
|
||||
return follow;
|
||||
} else {
|
||||
return Promise.reject("no community worked");
|
||||
}
|
||||
}
|
||||
|
||||
export async function reportPost(
|
||||
api: LemmyHttp,
|
||||
post_id: number,
|
||||
reason: string,
|
||||
): Promise<PostReportResponse> {
|
||||
let form: CreatePostReport = {
|
||||
post_id,
|
||||
reason,
|
||||
};
|
||||
return api.createPostReport(form);
|
||||
}
|
||||
|
||||
export async function listPostReports(
|
||||
api: LemmyHttp,
|
||||
): Promise<ListPostReportsResponse> {
|
||||
let form: ListPostReports = {};
|
||||
return api.listPostReports(form);
|
||||
}
|
||||
|
||||
export async function reportComment(
|
||||
api: LemmyHttp,
|
||||
comment_id: number,
|
||||
reason: string,
|
||||
): Promise<CommentReportResponse> {
|
||||
let form: CreateCommentReport = {
|
||||
comment_id,
|
||||
reason,
|
||||
};
|
||||
return api.createCommentReport(form);
|
||||
}
|
||||
|
||||
export async function reportPrivateMessage(
|
||||
api: LemmyHttp,
|
||||
private_message_id: number,
|
||||
reason: string,
|
||||
): Promise<PrivateMessageReportResponse> {
|
||||
let form: CreatePrivateMessageReport = {
|
||||
private_message_id,
|
||||
reason,
|
||||
};
|
||||
return api.createPrivateMessageReport(form);
|
||||
}
|
||||
|
||||
export async function listCommentReports(
|
||||
api: LemmyHttp,
|
||||
): Promise<ListCommentReportsResponse> {
|
||||
let form: ListCommentReports = {};
|
||||
return api.listCommentReports(form);
|
||||
}
|
||||
|
||||
export function getPosts(
|
||||
api: LemmyHttp,
|
||||
listingType?: ListingType,
|
||||
community_id?: number,
|
||||
): Promise<GetPostsResponse> {
|
||||
let form: GetPosts = {
|
||||
type_: listingType,
|
||||
limit: 50,
|
||||
community_id,
|
||||
};
|
||||
return api.getPosts(form);
|
||||
}
|
||||
|
||||
export function blockInstance(
|
||||
api: LemmyHttp,
|
||||
instance_id: InstanceId,
|
||||
block: boolean,
|
||||
): Promise<BlockInstanceResponse> {
|
||||
let form: BlockInstance = {
|
||||
instance_id,
|
||||
block,
|
||||
};
|
||||
return api.blockInstance(form);
|
||||
}
|
||||
|
||||
export function blockCommunity(
|
||||
api: LemmyHttp,
|
||||
community_id: CommunityId,
|
||||
block: boolean,
|
||||
): Promise<BlockCommunityResponse> {
|
||||
let form: BlockCommunity = {
|
||||
community_id,
|
||||
block,
|
||||
};
|
||||
return api.blockCommunity(form);
|
||||
}
|
||||
|
||||
export function delay(millis = 500) {
|
||||
return new Promise(resolve => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
export function longDelay() {
|
||||
return delay(10000);
|
||||
}
|
||||
|
||||
export function wrapper(form: any): string {
|
||||
return JSON.stringify(form);
|
||||
}
|
||||
|
||||
export function randomString(length: number): string {
|
||||
var result = "";
|
||||
var characters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||
var charactersLength = characters.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteAllImages(api: LemmyHttp) {
|
||||
const imagesRes = await api.listAllMedia({
|
||||
limit: imageFetchLimit,
|
||||
});
|
||||
imagesRes.images;
|
||||
Promise.all(
|
||||
imagesRes.images
|
||||
.map(image => {
|
||||
const form: DeleteImage = {
|
||||
token: image.local_image.pictrs_delete_token,
|
||||
filename: image.local_image.pictrs_alias,
|
||||
};
|
||||
return form;
|
||||
})
|
||||
.map(form => api.deleteImage(form)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function unfollows() {
|
||||
await Promise.all([
|
||||
unfollowRemotes(alpha),
|
||||
unfollowRemotes(beta),
|
||||
unfollowRemotes(gamma),
|
||||
unfollowRemotes(delta),
|
||||
unfollowRemotes(epsilon),
|
||||
]);
|
||||
await Promise.all([
|
||||
purgeAllPosts(alpha),
|
||||
purgeAllPosts(beta),
|
||||
purgeAllPosts(gamma),
|
||||
purgeAllPosts(delta),
|
||||
purgeAllPosts(epsilon),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function purgeAllPosts(api: LemmyHttp) {
|
||||
// The best way to get all federated items, is to find the posts
|
||||
let res = await api.getPosts({ type_: "All", limit: 50 });
|
||||
await Promise.all(
|
||||
Array.from(new Set(res.posts.map(p => p.post.id)))
|
||||
.map(post_id => api.purgePost({ post_id }))
|
||||
// Ignore errors
|
||||
.map(p => p.catch(e => e)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCommentParentId(comment: Comment): number | undefined {
|
||||
let split = comment.path.split(".");
|
||||
// remove the 0
|
||||
split.shift();
|
||||
|
||||
if (split.length > 1) {
|
||||
return Number(split[split.length - 2]);
|
||||
} else {
|
||||
console.log(`Failed to extract comment parent id from ${comment.path}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitUntil<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
checker: (t: T) => boolean,
|
||||
retries = 10,
|
||||
delaySeconds = [0.2, 0.5, 1, 2, 3],
|
||||
) {
|
||||
let retry = 0;
|
||||
let result;
|
||||
while (retry++ < retries) {
|
||||
result = await fetcher();
|
||||
if (checker(result)) return result;
|
||||
await delay(
|
||||
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,
|
||||
);
|
||||
}
|
||||
console.error("result", result);
|
||||
throw Error(
|
||||
`Failed "${fetcher}": "${checker}" did not return true after ${retries} retries (delayed ${delaySeconds}s each)`,
|
||||
);
|
||||
}
|
195
api_tests/src/user.spec.ts
Normal file
195
api_tests/src/user.spec.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import { PersonView } from "lemmy-js-client/dist/types/PersonView";
|
||||
import {
|
||||
alpha,
|
||||
beta,
|
||||
registerUser,
|
||||
resolvePerson,
|
||||
getSite,
|
||||
createPost,
|
||||
resolveCommunity,
|
||||
createComment,
|
||||
resolveBetaCommunity,
|
||||
deleteUser,
|
||||
saveUserSettingsFederated,
|
||||
setupLogins,
|
||||
alphaUrl,
|
||||
saveUserSettings,
|
||||
getPost,
|
||||
getComments,
|
||||
fetchFunction,
|
||||
alphaImage,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
|
||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
||||
|
||||
beforeAll(setupLogins);
|
||||
afterAll(unfollows);
|
||||
|
||||
let apShortname: string;
|
||||
|
||||
function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) {
|
||||
expect(userOne?.person.name).toBe(userTwo?.person.name);
|
||||
expect(userOne?.person.display_name).toBe(userTwo?.person.display_name);
|
||||
expect(userOne?.person.bio).toBe(userTwo?.person.bio);
|
||||
expect(userOne?.person.actor_id).toBe(userTwo?.person.actor_id);
|
||||
expect(userOne?.person.avatar).toBe(userTwo?.person.avatar);
|
||||
expect(userOne?.person.banner).toBe(userTwo?.person.banner);
|
||||
expect(userOne?.person.published).toBe(userTwo?.person.published);
|
||||
}
|
||||
|
||||
test("Create user", async () => {
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
if (!site.my_user) {
|
||||
throw "Missing site user";
|
||||
}
|
||||
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
||||
});
|
||||
|
||||
test("Set some user settings, check that they are federated", async () => {
|
||||
await saveUserSettingsFederated(alpha);
|
||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
||||
let betaPerson = (await resolvePerson(beta, apShortname)).person;
|
||||
assertUserFederation(alphaPerson, betaPerson);
|
||||
|
||||
// Catches a bug where when only the person or local_user changed
|
||||
let form: SaveUserSettings = {
|
||||
theme: "test",
|
||||
};
|
||||
await saveUserSettings(beta, form);
|
||||
|
||||
let site = await getSite(beta);
|
||||
expect(site.my_user?.local_user_view.local_user.theme).toBe("test");
|
||||
});
|
||||
|
||||
test("Delete user", async () => {
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
|
||||
// make a local post and comment
|
||||
let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541"))
|
||||
.community;
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
let localPost = (await createPost(user, alphaCommunity.community.id))
|
||||
.post_view.post;
|
||||
expect(localPost).toBeDefined();
|
||||
let localComment = (await createComment(user, localPost.id)).comment_view
|
||||
.comment;
|
||||
expect(localComment).toBeDefined();
|
||||
|
||||
// make a remote post and comment
|
||||
let betaCommunity = (await resolveBetaCommunity(user)).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let remotePost = (await createPost(user, betaCommunity.community.id))
|
||||
.post_view.post;
|
||||
expect(remotePost).toBeDefined();
|
||||
let remoteComment = (await createComment(user, remotePost.id)).comment_view
|
||||
.comment;
|
||||
expect(remoteComment).toBeDefined();
|
||||
|
||||
await deleteUser(user);
|
||||
|
||||
// check that posts and comments are marked as deleted on other instances.
|
||||
// use get methods to avoid refetching from origin instance
|
||||
expect((await getPost(alpha, localPost.id)).post_view.post.deleted).toBe(
|
||||
true,
|
||||
);
|
||||
expect((await getPost(alpha, remotePost.id)).post_view.post.deleted).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await getComments(alpha, localComment.post_id)).comments[0].comment
|
||||
.deleted,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(await getComments(alpha, remoteComment.post_id)).comments[0].comment
|
||||
.deleted,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Requests with invalid auth should be treated as unauthenticated", async () => {
|
||||
let invalid_auth = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: "Bearer foobar" },
|
||||
fetchFunction,
|
||||
});
|
||||
let site = await getSite(invalid_auth);
|
||||
expect(site.my_user).toBeUndefined();
|
||||
expect(site.site_view).toBeDefined();
|
||||
|
||||
let form: GetPosts = {};
|
||||
let posts = invalid_auth.getPosts(form);
|
||||
expect((await posts).posts).toBeDefined();
|
||||
});
|
||||
|
||||
test("Create user with Arabic name", async () => {
|
||||
let user = await registerUser(alpha, alphaUrl, "تجريب");
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
if (!site.my_user) {
|
||||
throw "Missing site user";
|
||||
}
|
||||
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
||||
|
||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
||||
expect(alphaPerson).toBeDefined();
|
||||
});
|
||||
|
||||
test("Create user with accept-language", async () => {
|
||||
let lemmy_http = new LemmyHttp(alphaUrl, {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax
|
||||
headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" },
|
||||
});
|
||||
let user = await registerUser(lemmy_http, alphaUrl);
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
expect(site.my_user?.local_user_view.local_user.interface_language).toBe(
|
||||
"fr",
|
||||
);
|
||||
let langs = site.all_languages
|
||||
.filter(a => site.my_user?.discussion_languages.includes(a.id))
|
||||
.map(l => l.code);
|
||||
// should have languages from accept header, as well as "undetermined"
|
||||
// which is automatically enabled by backend
|
||||
expect(langs).toStrictEqual(["und", "de", "en", "fr"]);
|
||||
});
|
||||
|
||||
test("Set a new avatar, old avatar is deleted", async () => {
|
||||
const listMediaRes = await alphaImage.listMedia();
|
||||
expect(listMediaRes.images.length).toBe(0);
|
||||
const upload_form1: UploadImage = {
|
||||
image: Buffer.from("test1"),
|
||||
};
|
||||
const upload1 = await alphaImage.uploadImage(upload_form1);
|
||||
expect(upload1.url).toBeDefined();
|
||||
|
||||
let form1 = {
|
||||
avatar: upload1.url,
|
||||
};
|
||||
await saveUserSettings(alpha, form1);
|
||||
const listMediaRes1 = await alphaImage.listMedia();
|
||||
expect(listMediaRes1.images.length).toBe(1);
|
||||
|
||||
const upload_form2: UploadImage = {
|
||||
image: Buffer.from("test2"),
|
||||
};
|
||||
const upload2 = await alphaImage.uploadImage(upload_form2);
|
||||
expect(upload2.url).toBeDefined();
|
||||
|
||||
let form2 = {
|
||||
avatar: upload1.url,
|
||||
};
|
||||
await saveUserSettings(alpha, form2);
|
||||
// make sure only the new avatar is kept
|
||||
const listMediaRes2 = await alphaImage.listMedia();
|
||||
expect(listMediaRes2.images.length).toBe(1);
|
||||
});
|
15
api_tests/tsconfig.json
Normal file
15
api_tests/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"module": "CommonJS",
|
||||
"noImplicitAny": true,
|
||||
"lib": ["es2017", "es7", "es6", "dom"],
|
||||
"outDir": "./dist",
|
||||
"target": "ES2020",
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
86
cliff.toml
Normal file
86
cliff.toml
Normal file
|
@ -0,0 +1,86 @@
|
|||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "LemmyNet"
|
||||
repo = "lemmy"
|
||||
# token = ""
|
||||
|
||||
[changelog]
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
## What's Changed
|
||||
|
||||
{%- if version %} in {{ version }}{%- endif -%}
|
||||
{% for commit in commits %}
|
||||
{% if commit.github.pr_title -%}
|
||||
{%- set commit_message = commit.github.pr_title -%}
|
||||
{%- else -%}
|
||||
{%- set commit_message = commit.message -%}
|
||||
{%- endif -%}
|
||||
* {{ commit_message | split(pat="\n") | first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
|
||||
{% if commit.github.pr_number %} in \
|
||||
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
{% raw %}\n{% endraw -%}
|
||||
## New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# postprocessors
|
||||
postprocessors = []
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = false
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# remove issue numbers from commits
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# regex for matching git tags
|
||||
tag_pattern = "[0-9].*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "beta|alpha"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "rc"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
5
config/config.hjson
Normal file
5
config/config.hjson
Normal file
|
@ -0,0 +1,5 @@
|
|||
# See the documentation for available config fields and descriptions:
|
||||
# https://join-lemmy.org/docs/en/administration/configuration.html
|
||||
{
|
||||
hostname: lemmy-alpha
|
||||
}
|
122
config/defaults.hjson
Normal file
122
config/defaults.hjson
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
# settings related to the postgresql database
|
||||
database: {
|
||||
# Configure the database by specifying a URI
|
||||
#
|
||||
# This is the preferred method to specify database connection details since
|
||||
# it is the most flexible.
|
||||
# Connection URI pointing to a postgres instance
|
||||
#
|
||||
# This example uses peer authentication to obviate the need for creating,
|
||||
# configuring, and managing passwords.
|
||||
#
|
||||
# For an explanation of how to use connection URIs, see [here][0] in
|
||||
# PostgreSQL's documentation.
|
||||
#
|
||||
# [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6
|
||||
uri: "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql"
|
||||
|
||||
# or
|
||||
|
||||
# Configure the database by specifying parts of a URI
|
||||
#
|
||||
# Note that specifying the `uri` field should be preferred since it provides
|
||||
# greater control over how the connection is made. This merely exists for
|
||||
# backwards-compatibility.
|
||||
# Username to connect to postgres
|
||||
user: "string"
|
||||
# Password to connect to postgres
|
||||
password: "string"
|
||||
# Host where postgres is running
|
||||
host: "string"
|
||||
# Port where postgres can be accessed
|
||||
port: 123
|
||||
# Name of the postgres database for lemmy
|
||||
database: "string"
|
||||
# Maximum number of active sql connections
|
||||
pool_size: 30
|
||||
}
|
||||
# Pictrs image server configuration.
|
||||
pictrs: {
|
||||
# Address where pictrs is available (for image hosting)
|
||||
url: "http://localhost:8080/"
|
||||
# Set a custom pictrs API key. ( Required for deleting images )
|
||||
api_key: "string"
|
||||
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
|
||||
# equivalent to `image_mode: StoreLinkPreviews`.
|
||||
#
|
||||
# To be removed in 0.20
|
||||
cache_external_link_previews: true
|
||||
# Specifies how to handle remote images, so that users don't have to connect directly to remote
|
||||
# servers.
|
||||
image_mode:
|
||||
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
|
||||
# Opengraph image is directly returned as thumbnail
|
||||
"None"
|
||||
|
||||
# or
|
||||
|
||||
# Generate thumbnails for external post urls and store them persistently in pict-rs. This
|
||||
# ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
|
||||
# it also increases storage usage.
|
||||
#
|
||||
# This is the default behaviour, and also matches Lemmy 0.18.
|
||||
"StoreLinkPreviews"
|
||||
|
||||
# or
|
||||
|
||||
# If enabled, all images from remote domains are rewritten to pass through
|
||||
# `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
|
||||
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
||||
# servers, and decreases load on other servers. However it increases bandwidth use for the
|
||||
# local server.
|
||||
#
|
||||
# Requires pict-rs 0.5
|
||||
"ProxyAllImages"
|
||||
# Timeout for uploading images to pictrs (in seconds)
|
||||
upload_timeout: 30
|
||||
}
|
||||
# Email sending configuration. All options except login/password are mandatory
|
||||
email: {
|
||||
# Hostname and port of the smtp server
|
||||
smtp_server: "localhost:25"
|
||||
# Login name for smtp server
|
||||
smtp_login: "string"
|
||||
# Password to login to the smtp server
|
||||
smtp_password: "string"
|
||||
# Address to send emails from, eg "noreply@your-instance.com"
|
||||
smtp_from_address: "noreply@example.com"
|
||||
# Whether or not smtp connections should use tls. Can be none, tls, or starttls
|
||||
tls_type: "none"
|
||||
}
|
||||
# Parameters for automatic configuration of new instance (only used at first start)
|
||||
setup: {
|
||||
# Username for the admin user
|
||||
admin_username: "admin"
|
||||
# Password for the admin user. It must be between 10 and 60 characters.
|
||||
admin_password: "tf6HHDS4RolWfFhk4Rq9"
|
||||
# Name of the site, can be changed later. Maximum 20 characters.
|
||||
site_name: "My Lemmy Instance"
|
||||
# Email for the admin user (optional, can be omitted and set later through the website)
|
||||
admin_email: "user@example.com"
|
||||
}
|
||||
# the domain name of your instance (mandatory)
|
||||
hostname: "unset"
|
||||
# Address where lemmy should listen for incoming requests
|
||||
bind: "0.0.0.0"
|
||||
# Port where lemmy should listen for incoming requests
|
||||
port: 8536
|
||||
# Whether the site is available over TLS. Needs to be true for federation to work.
|
||||
tls_enabled: true
|
||||
# The number of activitypub federation workers that can be in-flight concurrently
|
||||
worker_count: 0
|
||||
# The number of activitypub federation retry workers that can be in-flight concurrently
|
||||
retry_count: 0
|
||||
prometheus: {
|
||||
bind: "127.0.0.1"
|
||||
port: 10002
|
||||
}
|
||||
# Sets a response Access-Control-Allow-Origin CORS header
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
||||
cors_origin: "*"
|
||||
}
|
45
crates/api/Cargo.toml
Normal file
45
crates/api/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
|||
[package]
|
||||
name = "lemmy_api"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "lemmy_api"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lemmy_utils = { workspace = true }
|
||||
lemmy_db_schema = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_moderator = { workspace = true, features = ["full"] }
|
||||
lemmy_db_views_actor = { workspace = true, features = ["full"] }
|
||||
lemmy_api_common = { workspace = true, features = ["full"] }
|
||||
activitypub_federation = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
captcha = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wav = "1.0.1"
|
||||
sitemap-rs = "0.2.1"
|
||||
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
|
||||
actix-web-httpauth = "0.8.1"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
elementtree = "1.2.3"
|
||||
pretty_assertions = { workspace = true }
|
66
crates/api/src/comment/distinguish.rs
Normal file
66
crates/api/src/comment/distinguish.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
comment::{CommentResponse, DistinguishComment},
|
||||
context::LemmyContext,
|
||||
utils::{check_community_mod_action, check_community_user_action},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::comment::{Comment, CommentUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn distinguish_comment(
|
||||
data: Json<DistinguishComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
orig_comment.community.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify that only the creator can distinguish
|
||||
if local_user_view.person.id != orig_comment.creator.id {
|
||||
Err(LemmyErrorType::NoCommentEditAllowed)?
|
||||
}
|
||||
|
||||
// Verify that only a mod or admin can distinguish a comment
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
orig_comment.community.id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update the Comment
|
||||
let form = CommentUpdateForm {
|
||||
distinguished: Some(data.distinguished),
|
||||
..Default::default()
|
||||
};
|
||||
Comment::update(&mut context.pool(), data.comment_id, &form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(local_user_view.person.id),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
Ok(Json(CommentResponse {
|
||||
comment_view,
|
||||
recipient_ids: Vec::new(),
|
||||
}))
|
||||
}
|
100
crates/api/src/comment/like.rs
Normal file
100
crates/api/src/comment/like.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
build_response::build_comment_response,
|
||||
comment::{CommentResponse, CreateCommentLike},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_bot_account, check_community_user_action, check_downvotes_enabled},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::LocalUserId,
|
||||
source::{
|
||||
comment::{CommentLike, CommentLikeForm},
|
||||
comment_reply::CommentReply,
|
||||
local_site::LocalSite,
|
||||
},
|
||||
traits::Likeable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn like_comment(
|
||||
data: Json<CreateCommentLike>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let mut recipient_ids = Vec::<LocalUserId>::new();
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
check_downvotes_enabled(data.score, &local_site)?;
|
||||
check_bot_account(&local_user_view.person)?;
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
orig_comment.community.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add parent poster or commenter to recipients
|
||||
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
|
||||
if let Ok(Some(reply)) = comment_reply {
|
||||
let recipient_id = reply.recipient_id;
|
||||
if let Ok(Some(local_recipient)) =
|
||||
LocalUserView::read_person(&mut context.pool(), recipient_id).await
|
||||
{
|
||||
recipient_ids.push(local_recipient.local_user.id);
|
||||
}
|
||||
}
|
||||
|
||||
let like_form = CommentLikeForm {
|
||||
comment_id: data.comment_id,
|
||||
post_id: orig_comment.post.id,
|
||||
person_id: local_user_view.person.id,
|
||||
score: data.score,
|
||||
};
|
||||
|
||||
// Remove any likes first
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
CommentLike::remove(&mut context.pool(), person_id, comment_id).await?;
|
||||
|
||||
// Only add the like if the score isnt 0
|
||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||
if do_add {
|
||||
CommentLike::like(&mut context.pool(), &like_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
|
||||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LikePostOrComment {
|
||||
object_id: orig_comment.comment.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community: orig_comment.community,
|
||||
score: data.score,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(
|
||||
build_comment_response(
|
||||
context.deref(),
|
||||
comment_id,
|
||||
Some(local_user_view),
|
||||
recipient_ids,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
36
crates/api/src/comment/list_comment_likes.rs
Normal file
36
crates/api/src/comment/list_comment_likes.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
comment::{ListCommentLikes, ListCommentLikesResponse},
|
||||
context::LemmyContext,
|
||||
utils::is_mod_or_admin,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
/// Lists likes for a comment
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_comment_likes(
|
||||
data: Query<ListCommentLikes>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListCommentLikesResponse>> {
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(local_user_view.person.id),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
is_mod_or_admin(
|
||||
&mut context.pool(),
|
||||
&local_user_view.person,
|
||||
comment_view.community.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let comment_likes =
|
||||
VoteView::list_for_comment(&mut context.pool(), data.comment_id, data.page, data.limit).await?;
|
||||
|
||||
Ok(Json(ListCommentLikesResponse { comment_likes }))
|
||||
}
|
4
crates/api/src/comment/mod.rs
Normal file
4
crates/api/src/comment/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod distinguish;
|
||||
pub mod like;
|
||||
pub mod list_comment_likes;
|
||||
pub mod save;
|
44
crates/api/src/comment/save.rs
Normal file
44
crates/api/src/comment/save.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
comment::{CommentResponse, SaveComment},
|
||||
context::LemmyContext,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::comment::{CommentSaved, CommentSavedForm},
|
||||
traits::Saveable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_comment(
|
||||
data: Json<SaveComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let comment_saved_form = CommentSavedForm {
|
||||
comment_id: data.comment_id,
|
||||
person_id: local_user_view.person.id,
|
||||
};
|
||||
|
||||
if data.save {
|
||||
CommentSaved::save(&mut context.pool(), &comment_saved_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
|
||||
} else {
|
||||
CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
|
||||
}
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
Ok(Json(CommentResponse {
|
||||
comment_view,
|
||||
recipient_ids: Vec::new(),
|
||||
}))
|
||||
}
|
92
crates/api/src/comment_report/create.rs
Normal file
92
crates/api/src/comment_report/create.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use crate::check_report_reason;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
comment::{CommentReportResponse, CreateCommentReport},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{
|
||||
check_comment_deleted_or_removed,
|
||||
check_community_user_action,
|
||||
send_new_report_email_to_admins,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
comment_report::{CommentReport, CommentReportForm},
|
||||
local_site::LocalSite,
|
||||
},
|
||||
traits::Reportable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Creates a comment report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn create_comment_report(
|
||||
data: Json<CreateCommentReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
check_report_reason(&reason, &local_site)?;
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_id = data.comment_id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
comment_view.community.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Don't allow creating reports for removed / deleted comments
|
||||
check_comment_deleted_or_removed(&comment_view.comment)?;
|
||||
|
||||
let report_form = CommentReportForm {
|
||||
creator_id: person_id,
|
||||
comment_id,
|
||||
original_comment_text: comment_view.comment.content,
|
||||
reason,
|
||||
};
|
||||
|
||||
let report = CommentReport::report(&mut context.pool(), &report_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
|
||||
|
||||
let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
||||
|
||||
// Email the admins
|
||||
if local_site.reports_email_admins {
|
||||
send_new_report_email_to_admins(
|
||||
&comment_report_view.creator.name,
|
||||
&comment_report_view.comment_creator.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::CreateReport {
|
||||
object_id: comment_view.comment.ap_id.inner().clone(),
|
||||
actor: local_user_view.person,
|
||||
community: comment_view.community,
|
||||
reason: data.reason.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(CommentReportResponse {
|
||||
comment_report_view,
|
||||
}))
|
||||
}
|
37
crates/api/src/comment_report/list.rs
Normal file
37
crates/api/src/comment_report/list.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
comment::{ListCommentReports, ListCommentReportsResponse},
|
||||
context::LemmyContext,
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Lists comment reports for a community if an id is supplied
|
||||
/// or returns all comment reports for communities a user moderates
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_comment_reports(
|
||||
data: Query<ListCommentReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListCommentReportsResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let comment_id = data.comment_id;
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let comment_reports = CommentReportQuery {
|
||||
community_id,
|
||||
comment_id,
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
.list(&mut context.pool(), &local_user_view)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ListCommentReportsResponse { comment_reports }))
|
||||
}
|
3
crates/api/src/comment_report/mod.rs
Normal file
3
crates/api/src/comment_report/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod create;
|
||||
pub mod list;
|
||||
pub mod resolve;
|
51
crates/api/src/comment_report/resolve.rs
Normal file
51
crates/api/src/comment_report/resolve.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
comment::{CommentReportResponse, ResolveCommentReport},
|
||||
context::LemmyContext,
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{CommentReportView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Resolves or unresolves a comment report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn resolve_comment_report(
|
||||
data: Json<ResolveCommentReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
||||
let report_id = data.report_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let report = CommentReportView::read(&mut context.pool(), report_id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
report.community.id,
|
||||
true,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if data.resolved {
|
||||
CommentReport::resolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
} else {
|
||||
CommentReport::unresolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
}
|
||||
|
||||
let report_id = data.report_id;
|
||||
let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
||||
|
||||
Ok(Json(CommentReportResponse {
|
||||
comment_report_view,
|
||||
}))
|
||||
}
|
97
crates/api/src/community/add_mod.rs
Normal file
97
crates/api/src/community/add_mod.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
community::{AddModToCommunity, AddModToCommunityResponse},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
||||
moderator::{ModAddCommunity, ModAddCommunityForm},
|
||||
},
|
||||
traits::{Crud, Joinable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn add_mod_to_community(
|
||||
data: Json<AddModToCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<AddModToCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
|
||||
// Verify that only mods or admins can add mod
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
community_id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
let community = Community::read(&mut context.pool(), community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
// If user is admin and community is remote, explicitly check that he is a
|
||||
// moderator. This is necessary because otherwise the action would be rejected
|
||||
// by the community's home instance.
|
||||
if local_user_view.local_user.admin && !community.local {
|
||||
let is_mod = CommunityModeratorView::is_community_moderator(
|
||||
&mut context.pool(),
|
||||
community.id,
|
||||
local_user_view.person.id,
|
||||
)
|
||||
.await?;
|
||||
if !is_mod {
|
||||
Err(LemmyErrorType::NotAModerator)?
|
||||
}
|
||||
}
|
||||
|
||||
// Update in local database
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
community_id: data.community_id,
|
||||
person_id: data.person_id,
|
||||
};
|
||||
if data.added {
|
||||
CommunityModerator::join(&mut context.pool(), &community_moderator_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
||||
} else {
|
||||
CommunityModerator::leave(&mut context.pool(), &community_moderator_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
let form = ModAddCommunityForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: data.person_id,
|
||||
community_id: data.community_id,
|
||||
removed: Some(!data.added),
|
||||
};
|
||||
|
||||
ModAddCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
// Note: in case a remote mod is added, this returns the old moderators list, it will only get
|
||||
// updated once we receive an activity from the community (like `Announce/Add/Moderator`)
|
||||
let community_id = data.community_id;
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::AddModToCommunity {
|
||||
moderator: local_user_view.person,
|
||||
community_id: data.community_id,
|
||||
target: data.person_id,
|
||||
added: data.added,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(AddModToCommunityResponse { moderators }))
|
||||
}
|
111
crates/api/src/community/ban.rs
Normal file
111
crates/api/src/community/ban.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
community::{BanFromCommunity, BanFromCommunityResponse},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{
|
||||
CommunityFollower,
|
||||
CommunityFollowerForm,
|
||||
CommunityPersonBan,
|
||||
CommunityPersonBanForm,
|
||||
},
|
||||
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
||||
},
|
||||
traits::{Bannable, Crud, Followable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::validation::is_valid_body_field,
|
||||
};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn ban_from_community(
|
||||
data: Json<BanFromCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BanFromCommunityResponse>> {
|
||||
let banned_person_id = data.person_id;
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
// Verify that only mods or admins can ban
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
data.community_id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
|
||||
let community_user_ban_form = CommunityPersonBanForm {
|
||||
community_id: data.community_id,
|
||||
person_id: data.person_id,
|
||||
expires: Some(expires),
|
||||
};
|
||||
|
||||
if data.ban {
|
||||
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
|
||||
|
||||
// Also unsubscribe them from the community, if they are subscribed
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: data.community_id,
|
||||
person_id: banned_person_id,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
|
||||
}
|
||||
|
||||
// Remove/Restore their data if that's desired
|
||||
if remove_data {
|
||||
remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?;
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanFromCommunityForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: data.person_id,
|
||||
community_id: data.community_id,
|
||||
reason: data.reason.clone(),
|
||||
banned: Some(data.ban),
|
||||
expires,
|
||||
};
|
||||
|
||||
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let person_view = PersonView::read(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromCommunity {
|
||||
moderator: local_user_view.person,
|
||||
community_id: data.community_id,
|
||||
target: person_view.person.clone(),
|
||||
data: data.0.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(BanFromCommunityResponse {
|
||||
person_view,
|
||||
banned: data.ban,
|
||||
}))
|
||||
}
|
72
crates/api/src/community/block.rs
Normal file
72
crates/api/src/community/block.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
community::{BlockCommunity, BlockCommunityResponse},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{CommunityFollower, CommunityFollowerForm},
|
||||
community_block::{CommunityBlock, CommunityBlockForm},
|
||||
},
|
||||
traits::{Blockable, Followable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn block_community(
|
||||
data: Json<BlockCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BlockCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_block_form = CommunityBlockForm {
|
||||
person_id,
|
||||
community_id,
|
||||
};
|
||||
|
||||
if data.block {
|
||||
CommunityBlock::block(&mut context.pool(), &community_block_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
||||
|
||||
// Also, unfollow the community, and send a federated unfollow
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: data.community_id,
|
||||
person_id,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
CommunityBlock::unblock(&mut context.pool(), &community_block_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
||||
}
|
||||
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FollowCommunity(
|
||||
community_view.community.clone(),
|
||||
local_user_view.person.clone(),
|
||||
false,
|
||||
),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(BlockCommunityResponse {
|
||||
blocked: data.block,
|
||||
community_view,
|
||||
}))
|
||||
}
|
77
crates/api/src/community/follow.rs
Normal file
77
crates/api/src/community/follow.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
community::{CommunityResponse, FollowCommunity},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::check_community_user_action,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
actor_language::CommunityLanguage,
|
||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
||||
},
|
||||
traits::{Crud, Followable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn follow_community(
|
||||
data: Json<FollowCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommunityResponse>> {
|
||||
let community = Community::read(&mut context.pool(), data.community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let mut community_follower_form = CommunityFollowerForm {
|
||||
community_id: community.id,
|
||||
person_id: local_user_view.person.id,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
if data.follow {
|
||||
if community.local {
|
||||
check_community_user_action(&local_user_view.person, community.id, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
||||
} else {
|
||||
// Mark as pending, the actual federation activity is sent via `SendActivity` handler
|
||||
community_follower_form.pending = true;
|
||||
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
||||
}
|
||||
} else {
|
||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
||||
}
|
||||
|
||||
if !community.local {
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
||||
|
||||
Ok(Json(CommunityResponse {
|
||||
community_view,
|
||||
discussion_languages,
|
||||
}))
|
||||
}
|
55
crates/api/src/community/hide.rs
Normal file
55
crates/api/src/community/hide.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
community::HideCommunity,
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::is_admin,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityUpdateForm},
|
||||
moderator::{ModHideCommunity, ModHideCommunityForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn hide_community(
|
||||
data: Json<HideCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Verify its a admin (only admin can hide or unhide it)
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let community_form = CommunityUpdateForm {
|
||||
hidden: Some(data.hidden),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mod_hide_community_form = ModHideCommunityForm {
|
||||
community_id: data.community_id,
|
||||
mod_person_id: local_user_view.person.id,
|
||||
reason: data.reason.clone(),
|
||||
hidden: Some(data.hidden),
|
||||
};
|
||||
|
||||
let community_id = data.community_id;
|
||||
let community = Community::update(&mut context.pool(), community_id, &community_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateCommunityHiddenStatus)?;
|
||||
|
||||
ModHideCommunity::create(&mut context.pool(), &mod_hide_community_form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
6
crates/api/src/community/mod.rs
Normal file
6
crates/api/src/community/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod add_mod;
|
||||
pub mod ban;
|
||||
pub mod block;
|
||||
pub mod follow;
|
||||
pub mod hide;
|
||||
pub mod transfer;
|
97
crates/api/src/community/transfer.rs
Normal file
97
crates/api/src/community/transfer.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use anyhow::Context;
|
||||
use lemmy_api_common::{
|
||||
community::{GetCommunityResponse, TransferCommunity},
|
||||
context::LemmyContext,
|
||||
utils::{check_community_user_action, is_admin, is_top_mod},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{CommunityModerator, CommunityModeratorForm},
|
||||
moderator::{ModTransferCommunity, ModTransferCommunityForm},
|
||||
},
|
||||
traits::{Crud, Joinable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
location_info,
|
||||
};
|
||||
|
||||
// TODO: we dont do anything for federation here, it should be updated the next time the community
|
||||
// gets fetched. i hope we can get rid of the community creator role soon.
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn transfer_community(
|
||||
data: Json<TransferCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let mut community_mods =
|
||||
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
|
||||
|
||||
// Make sure transferrer is either the top community mod, or an admin
|
||||
if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok())
|
||||
{
|
||||
Err(LemmyErrorType::NotAnAdmin)?
|
||||
}
|
||||
|
||||
// You have to re-do the community_moderator table, reordering it.
|
||||
// Add the transferee to the top
|
||||
let creator_index = community_mods
|
||||
.iter()
|
||||
.position(|r| r.moderator.id == data.person_id)
|
||||
.context(location_info!())?;
|
||||
let creator_person = community_mods.remove(creator_index);
|
||||
community_mods.insert(0, creator_person);
|
||||
|
||||
// Delete all the mods
|
||||
let community_id = data.community_id;
|
||||
|
||||
CommunityModerator::delete_for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
// TODO: this should probably be a bulk operation
|
||||
// Re-add the mods, in the new order
|
||||
for cmod in &community_mods {
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
community_id: cmod.community.id,
|
||||
person_id: cmod.moderator.id,
|
||||
};
|
||||
|
||||
CommunityModerator::join(&mut context.pool(), &community_moderator_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
let form = ModTransferCommunityForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: data.person_id,
|
||||
community_id: data.community_id,
|
||||
};
|
||||
|
||||
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(Json(GetCommunityResponse {
|
||||
community_view,
|
||||
site: None,
|
||||
moderators,
|
||||
discussion_languages: vec![],
|
||||
}))
|
||||
}
|
277
crates/api/src/lib.rs
Normal file
277
crates/api/src/lib.rs
Normal file
|
@ -0,0 +1,277 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::{http::header::Header, HttpRequest};
|
||||
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
|
||||
use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
|
||||
use captcha::Captcha;
|
||||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
community::BanFromCommunity,
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_expire_time, check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{
|
||||
CommunityFollower,
|
||||
CommunityFollowerForm,
|
||||
CommunityPersonBan,
|
||||
CommunityPersonBanForm,
|
||||
},
|
||||
local_site::LocalSite,
|
||||
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
||||
person::Person,
|
||||
},
|
||||
traits::{Bannable, Crud, Followable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
||||
utils::slurs::check_slurs,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use totp_rs::{Secret, TOTP};
|
||||
|
||||
pub mod comment;
|
||||
pub mod comment_report;
|
||||
pub mod community;
|
||||
pub mod local_user;
|
||||
pub mod post;
|
||||
pub mod post_report;
|
||||
pub mod private_message;
|
||||
pub mod private_message_report;
|
||||
pub mod site;
|
||||
pub mod sitemap;
|
||||
|
||||
/// Converts the captcha to a base64 encoded wav audio file
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
|
||||
let letters = captcha.as_wav();
|
||||
|
||||
// Decode each wav file, concatenate the samples
|
||||
let mut concat_samples: Vec<i16> = Vec::new();
|
||||
let mut any_header: Option<wav::Header> = None;
|
||||
for letter in letters {
|
||||
let mut cursor = Cursor::new(letter.unwrap_or_default());
|
||||
let (header, samples) = wav::read(&mut cursor)?;
|
||||
any_header = Some(header);
|
||||
if let Some(samples16) = samples.as_sixteen() {
|
||||
concat_samples.extend(samples16);
|
||||
} else {
|
||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the concatenated result as a wav file
|
||||
let mut output_buffer = Cursor::new(vec![]);
|
||||
if let Some(header) = any_header {
|
||||
wav::write(
|
||||
header,
|
||||
&wav::BitDepth::Sixteen(concat_samples),
|
||||
&mut output_buffer,
|
||||
)
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
|
||||
Ok(base64.encode(output_buffer.into_inner()))
|
||||
} else {
|
||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
||||
}
|
||||
}
|
||||
|
||||
/// Check size of report
|
||||
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> LemmyResult<()> {
|
||||
let slur_regex = &local_site_to_slur_regex(local_site);
|
||||
|
||||
check_slurs(reason, slur_regex)?;
|
||||
if reason.is_empty() {
|
||||
Err(LemmyErrorType::ReportReasonRequired)?
|
||||
} else if reason.chars().count() > 1000 {
|
||||
Err(LemmyErrorType::ReportTooLong)?
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_auth_token(req: &HttpRequest) -> LemmyResult<Option<String>> {
|
||||
// Try reading jwt from auth header
|
||||
if let Ok(header) = Authorization::<Bearer>::parse(req) {
|
||||
Ok(Some(header.as_ref().token().to_string()))
|
||||
}
|
||||
// If that fails, try to read from cookie
|
||||
else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) {
|
||||
Ok(Some(cookie.value().to_string()))
|
||||
}
|
||||
// Otherwise, there's no auth
|
||||
else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_totp_2fa_valid(
|
||||
local_user_view: &LocalUserView,
|
||||
totp_token: &Option<String>,
|
||||
site_name: &str,
|
||||
) -> LemmyResult<()> {
|
||||
// Throw an error if their token is missing
|
||||
let token = totp_token
|
||||
.as_deref()
|
||||
.ok_or(LemmyErrorType::MissingTotpToken)?;
|
||||
let secret = local_user_view
|
||||
.local_user
|
||||
.totp_2fa_secret
|
||||
.as_deref()
|
||||
.ok_or(LemmyErrorType::MissingTotpSecret)?;
|
||||
|
||||
let totp = build_totp_2fa(site_name, &local_user_view.person.name, secret)?;
|
||||
|
||||
let check_passed = totp.check_current(token)?;
|
||||
if !check_passed {
|
||||
return Err(LemmyErrorType::IncorrectTotpToken.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn generate_totp_2fa_secret() -> String {
|
||||
Secret::generate_secret().to_string()
|
||||
}
|
||||
|
||||
fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<TOTP> {
|
||||
let sec = Secret::Raw(secret.as_bytes().to_vec());
|
||||
let sec_bytes = sec
|
||||
.to_bytes()
|
||||
.map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?;
|
||||
|
||||
TOTP::new(
|
||||
totp_rs::Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
sec_bytes,
|
||||
Some(hostname.to_string()),
|
||||
username.to_string(),
|
||||
)
|
||||
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
|
||||
}
|
||||
|
||||
/// Site bans are only federated for local users.
|
||||
/// This is a problem, because site-banning non-local users will still leave content
|
||||
/// they've posted to our local communities, on other servers.
|
||||
///
|
||||
/// So when doing a site ban for a non-local user, you need to federate/send a
|
||||
/// community ban for every local community they've participated in.
|
||||
/// See https://github.com/LemmyNet/lemmy/issues/4118
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) async fn ban_nonlocal_user_from_local_communities(
|
||||
local_user_view: &LocalUserView,
|
||||
target: &Person,
|
||||
ban: bool,
|
||||
reason: &Option<String>,
|
||||
remove_data: &Option<bool>,
|
||||
expires: &Option<i64>,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
// Only run this code for federated users
|
||||
if !target.local {
|
||||
let ids = Person::list_local_community_ids(&mut context.pool(), target.id).await?;
|
||||
|
||||
for community_id in ids {
|
||||
let expires_dt = check_expire_time(*expires)?;
|
||||
|
||||
// Ban / unban them from our local communities
|
||||
let community_user_ban_form = CommunityPersonBanForm {
|
||||
community_id,
|
||||
person_id: target.id,
|
||||
expires: Some(expires_dt),
|
||||
};
|
||||
|
||||
if ban {
|
||||
// Ignore all errors for these
|
||||
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Also unsubscribe them from the community, if they are subscribed
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id,
|
||||
person_id: target.id,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanFromCommunityForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: target.id,
|
||||
community_id,
|
||||
reason: reason.clone(),
|
||||
banned: Some(ban),
|
||||
expires: expires_dt,
|
||||
};
|
||||
|
||||
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
// Federate the ban from community
|
||||
let ban_from_community = BanFromCommunity {
|
||||
community_id,
|
||||
person_id: target.id,
|
||||
ban,
|
||||
reason: reason.clone(),
|
||||
remove_data: *remove_data,
|
||||
expires: *expires,
|
||||
};
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromCommunity {
|
||||
moderator: local_user_view.person.clone(),
|
||||
community_id,
|
||||
target: target.clone(),
|
||||
data: ban_from_community,
|
||||
},
|
||||
context,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn local_user_view_from_jwt(
|
||||
jwt: &str,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<LocalUserView> {
|
||||
let local_user_id = Claims::validate(jwt, context)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
|
||||
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
|
||||
check_user_valid(&local_user_view.person)?;
|
||||
|
||||
Ok(local_user_view)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_totp() {
|
||||
let generated_secret = generate_totp_2fa_secret();
|
||||
let totp = build_totp_2fa("lemmy.ml", "my_name", &generated_secret);
|
||||
assert!(totp.is_ok());
|
||||
}
|
||||
}
|
55
crates/api/src/local_user/add_admin.rs
Normal file
55
crates/api/src/local_user/add_admin.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{AddAdmin, AddAdminResponse},
|
||||
utils::is_admin,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
moderator::{ModAdd, ModAddForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn add_admin(
|
||||
data: Json<AddAdmin>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<AddAdminResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Make sure that the person_id added is local
|
||||
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::ObjectNotLocal)?;
|
||||
|
||||
LocalUser::update(
|
||||
&mut context.pool(),
|
||||
added_local_user.local_user.id,
|
||||
&LocalUserUpdateForm {
|
||||
admin: Some(data.added),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
||||
|
||||
// Mod tables
|
||||
let form = ModAddForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: added_local_user.person.id,
|
||||
removed: Some(!data.added),
|
||||
};
|
||||
|
||||
ModAdd::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let admins = PersonView::admins(&mut context.pool()).await?;
|
||||
|
||||
Ok(Json(AddAdminResponse { admins }))
|
||||
}
|
105
crates/api/src/local_user/ban_person.rs
Normal file
105
crates/api/src/local_user/ban_person.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use crate::ban_nonlocal_user_from_local_communities;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{BanPerson, BanPersonResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_expire_time, is_admin, remove_user_data},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
login_token::LoginToken,
|
||||
moderator::{ModBan, ModBanForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::validation::is_valid_body_field,
|
||||
};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn ban_from_site(
|
||||
data: Json<BanPerson>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BanPersonResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
let person = Person::update(
|
||||
&mut context.pool(),
|
||||
data.person_id,
|
||||
&PersonUpdateForm {
|
||||
banned: Some(data.ban),
|
||||
ban_expires: Some(expires),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
||||
|
||||
// if its a local user, invalidate logins
|
||||
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
|
||||
if let Ok(Some(local_user)) = local_user {
|
||||
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
|
||||
}
|
||||
|
||||
// Remove their data if that's desired
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
if remove_data {
|
||||
remove_user_data(person.id, &context).await?;
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: person.id,
|
||||
reason: data.reason.clone(),
|
||||
banned: Some(data.ban),
|
||||
expires,
|
||||
};
|
||||
|
||||
ModBan::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let person_view = PersonView::read(&mut context.pool(), person.id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
ban_nonlocal_user_from_local_communities(
|
||||
&local_user_view,
|
||||
&person,
|
||||
data.ban,
|
||||
&data.reason,
|
||||
&data.remove_data,
|
||||
&data.expires,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromSite {
|
||||
moderator: local_user_view.person,
|
||||
banned_user: person_view.person.clone(),
|
||||
reason: data.reason.clone(),
|
||||
remove_data: data.remove_data,
|
||||
ban: data.ban,
|
||||
expires: data.expires,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(BanPersonResponse {
|
||||
person_view,
|
||||
banned: data.ban,
|
||||
}))
|
||||
}
|
59
crates/api/src/local_user/block.rs
Normal file
59
crates/api/src/local_user/block.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{BlockPerson, BlockPersonResponse},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::person_block::{PersonBlock, PersonBlockForm},
|
||||
traits::Blockable,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn block_person(
|
||||
data: Json<BlockPerson>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BlockPersonResponse>> {
|
||||
let target_id = data.person_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// Don't let a person block themselves
|
||||
if target_id == person_id {
|
||||
Err(LemmyErrorType::CantBlockYourself)?
|
||||
}
|
||||
|
||||
let person_block_form = PersonBlockForm {
|
||||
person_id,
|
||||
target_id,
|
||||
};
|
||||
|
||||
let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if target_user.is_some_and(|t| t.local_user.admin) {
|
||||
Err(LemmyErrorType::CantBlockAdmin)?
|
||||
}
|
||||
|
||||
if data.block {
|
||||
PersonBlock::block(&mut context.pool(), &person_block_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
|
||||
} else {
|
||||
PersonBlock::unblock(&mut context.pool(), &person_block_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
|
||||
}
|
||||
|
||||
let person_view = PersonView::read(&mut context.pool(), target_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
Ok(Json(BlockPersonResponse {
|
||||
person_view,
|
||||
blocked: data.block,
|
||||
}))
|
||||
}
|
53
crates/api/src/local_user/change_password.rs
Normal file
53
crates/api/src/local_user/change_password.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use actix_web::{
|
||||
web::{Data, Json},
|
||||
HttpRequest,
|
||||
};
|
||||
use bcrypt::verify;
|
||||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
context::LemmyContext,
|
||||
person::{ChangePassword, LoginResponse},
|
||||
utils::password_length_check,
|
||||
};
|
||||
use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn change_password(
|
||||
data: Json<ChangePassword>,
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
password_length_check(&data.new_password)?;
|
||||
|
||||
// Make sure passwords match
|
||||
if data.new_password != data.new_password_verify {
|
||||
Err(LemmyErrorType::PasswordsDoNotMatch)?
|
||||
}
|
||||
|
||||
// Check the old password
|
||||
let valid: bool = verify(
|
||||
&data.old_password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let new_password = data.new_password.clone();
|
||||
let updated_local_user =
|
||||
LocalUser::update_password(&mut context.pool(), local_user_id, &new_password).await?;
|
||||
|
||||
LoginToken::invalidate_all(&mut context.pool(), local_user_view.local_user.id).await?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(Json(LoginResponse {
|
||||
jwt: Some(Claims::generate(updated_local_user.id, req, &context).await?),
|
||||
verify_email_sent: false,
|
||||
registration_created: false,
|
||||
}))
|
||||
}
|
43
crates/api/src/local_user/change_password_after_reset.rs
Normal file
43
crates/api/src/local_user/change_password_after_reset.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::PasswordChangeAfterReset,
|
||||
utils::password_length_check,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::source::{
|
||||
local_user::LocalUser,
|
||||
login_token::LoginToken,
|
||||
password_reset_request::PasswordResetRequest,
|
||||
};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn change_password_after_reset(
|
||||
data: Json<PasswordChangeAfterReset>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Fetch the user_id from the token
|
||||
let token = data.token.clone();
|
||||
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::TokenNotFound)?
|
||||
.local_user_id;
|
||||
|
||||
password_length_check(&data.password)?;
|
||||
|
||||
// Make sure passwords match
|
||||
if data.password != data.password_verify {
|
||||
Err(LemmyErrorType::PasswordsDoNotMatch)?
|
||||
}
|
||||
|
||||
// Update the user with the new password
|
||||
let password = data.password.clone();
|
||||
LocalUser::update_password(&mut context.pool(), local_user_id, &password)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
||||
|
||||
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
42
crates/api/src/local_user/generate_totp_secret.rs
Normal file
42
crates/api/src/local_user/generate_totp_secret.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::{build_totp_2fa, generate_totp_2fa_secret};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
|
||||
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp]
|
||||
/// to enable it. This can only be called if 2FA is currently disabled.
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn generate_totp_secret(
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
|
||||
if local_user_view.local_user.totp_2fa_enabled {
|
||||
return Err(LemmyErrorType::TotpAlreadyEnabled)?;
|
||||
}
|
||||
|
||||
let secret = generate_totp_2fa_secret();
|
||||
let secret_url =
|
||||
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
|
||||
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
totp_2fa_secret: Some(Some(secret)),
|
||||
..Default::default()
|
||||
};
|
||||
LocalUser::update(
|
||||
&mut context.pool(),
|
||||
local_user_view.local_user.id,
|
||||
&local_user_form,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(GenerateTotpSecretResponse {
|
||||
totp_secret_url: secret_url.into(),
|
||||
}))
|
||||
}
|
56
crates/api/src/local_user/get_captcha.rs
Normal file
56
crates/api/src/local_user/get_captcha.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use crate::captcha_as_wav_base64;
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{CacheControl, CacheDirective},
|
||||
StatusCode,
|
||||
},
|
||||
web::{Data, Json},
|
||||
HttpResponse,
|
||||
HttpResponseBuilder,
|
||||
};
|
||||
use captcha::{gen, Difficulty};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{CaptchaResponse, GetCaptchaResponse},
|
||||
};
|
||||
use lemmy_db_schema::source::{
|
||||
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
|
||||
local_site::LocalSite,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpResponse> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
let mut res = HttpResponseBuilder::new(StatusCode::OK);
|
||||
res.insert_header(CacheControl(vec![CacheDirective::NoStore]));
|
||||
|
||||
if !local_site.captcha_enabled {
|
||||
return Ok(res.json(Json(GetCaptchaResponse { ok: None })));
|
||||
}
|
||||
|
||||
let captcha = gen(match local_site.captcha_difficulty.as_str() {
|
||||
"easy" => Difficulty::Easy,
|
||||
"hard" => Difficulty::Hard,
|
||||
_ => Difficulty::Medium,
|
||||
});
|
||||
|
||||
let answer = captcha.chars_as_string();
|
||||
|
||||
let png = captcha.as_base64().expect("failed to generate captcha");
|
||||
|
||||
let wav = captcha_as_wav_base64(&captcha)?;
|
||||
|
||||
let captcha_form: CaptchaAnswerForm = CaptchaAnswerForm { answer };
|
||||
// Stores the captcha item in the db
|
||||
let captcha = CaptchaAnswer::insert(&mut context.pool(), &captcha_form).await?;
|
||||
|
||||
let json = Json(GetCaptchaResponse {
|
||||
ok: Some(CaptchaResponse {
|
||||
png,
|
||||
wav,
|
||||
uuid: captcha.uuid.to_string(),
|
||||
}),
|
||||
});
|
||||
Ok(res.json(json))
|
||||
}
|
17
crates/api/src/local_user/list_banned.rs
Normal file
17
crates/api/src/local_user/list_banned.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
pub async fn list_banned_users(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<BannedPersonsResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let banned = PersonView::banned(&mut context.pool()).await?;
|
||||
|
||||
Ok(Json(BannedPersonsResponse { banned }))
|
||||
}
|
14
crates/api/src/local_user/list_logins.rs
Normal file
14
crates/api/src/local_user/list_logins.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_db_schema::source::login_token::LoginToken;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
pub async fn list_logins(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<Vec<LoginToken>>> {
|
||||
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
|
||||
|
||||
Ok(Json(logins))
|
||||
}
|
25
crates/api/src/local_user/list_media.rs
Normal file
25
crates/api/src/local_user/list_media.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{ListMedia, ListMediaResponse},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalImageView, LocalUserView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_media(
|
||||
data: Query<ListMedia>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListMediaResponse>> {
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let images = LocalImageView::get_all_paged_by_local_user_id(
|
||||
&mut context.pool(),
|
||||
local_user_view.local_user.id,
|
||||
page,
|
||||
limit,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ListMediaResponse { images }))
|
||||
}
|
94
crates/api/src/local_user/login.rs
Normal file
94
crates/api/src/local_user/login.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use crate::{check_totp_2fa_valid, local_user::check_email_verified};
|
||||
use actix_web::{
|
||||
web::{Data, Json},
|
||||
HttpRequest,
|
||||
};
|
||||
use bcrypt::verify;
|
||||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
context::LemmyContext,
|
||||
person::{Login, LoginResponse},
|
||||
utils::check_user_valid,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{local_site::LocalSite, registration_application::RegistrationApplication},
|
||||
utils::DbPool,
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn login(
|
||||
data: Json<Login>,
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
|
||||
// Fetch that username / email
|
||||
let username_or_email = data.username_or_email.clone();
|
||||
let local_user_view =
|
||||
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
// Verify the password
|
||||
let valid: bool = verify(
|
||||
&data.password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
check_user_valid(&local_user_view.person)?;
|
||||
check_email_verified(&local_user_view, &site_view)?;
|
||||
|
||||
check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
// Check the totp if enabled
|
||||
if local_user_view.local_user.totp_2fa_enabled {
|
||||
check_totp_2fa_valid(
|
||||
&local_user_view,
|
||||
&data.totp_2fa_token,
|
||||
&context.settings().hostname,
|
||||
)?;
|
||||
}
|
||||
|
||||
let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
jwt: Some(jwt.clone()),
|
||||
verify_email_sent: false,
|
||||
registration_created: false,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn check_registration_application(
|
||||
local_user_view: &LocalUserView,
|
||||
local_site: &LocalSite,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<()> {
|
||||
if (local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
|| local_site.registration_mode == RegistrationMode::Closed)
|
||||
&& !local_user_view.local_user.accepted_application
|
||||
&& !local_user_view.local_user.admin
|
||||
{
|
||||
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
|
||||
// was processed (either accepted or denied).
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
||||
if registration.admin_id.is_some() {
|
||||
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
|
||||
} else {
|
||||
Err(LemmyErrorType::RegistrationApplicationIsPending)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
23
crates/api/src/local_user/logout.rs
Normal file
23
crates/api/src/local_user/logout.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use crate::read_auth_token;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse};
|
||||
use lemmy_api_common::{context::LemmyContext, utils::AUTH_COOKIE_NAME, SuccessResponse};
|
||||
use lemmy_db_schema::source::login_token::LoginToken;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn logout(
|
||||
req: HttpRequest,
|
||||
// require login
|
||||
_local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<HttpResponse> {
|
||||
let jwt = read_auth_token(&req)?.ok_or(LemmyErrorType::NotLoggedIn)?;
|
||||
LoginToken::invalidate(&mut context.pool(), &jwt).await?;
|
||||
|
||||
let mut res = HttpResponse::Ok().json(SuccessResponse::default());
|
||||
let cookie = Cookie::new(AUTH_COOKIE_NAME, "");
|
||||
res.add_removal_cookie(&cookie)?;
|
||||
Ok(res)
|
||||
}
|
34
crates/api/src/local_user/mod.rs
Normal file
34
crates/api/src/local_user/mod.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
pub mod add_admin;
|
||||
pub mod ban_person;
|
||||
pub mod block;
|
||||
pub mod change_password;
|
||||
pub mod change_password_after_reset;
|
||||
pub mod generate_totp_secret;
|
||||
pub mod get_captcha;
|
||||
pub mod list_banned;
|
||||
pub mod list_logins;
|
||||
pub mod list_media;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod notifications;
|
||||
pub mod report_count;
|
||||
pub mod reset_password;
|
||||
pub mod save_settings;
|
||||
pub mod update_totp;
|
||||
pub mod validate_auth;
|
||||
pub mod verify_email;
|
||||
|
||||
/// Check if the user's email is verified if email verification is turned on
|
||||
/// However, skip checking verification if the user is an admin
|
||||
fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> {
|
||||
if !local_user_view.local_user.admin
|
||||
&& site_view.local_site.require_email_verification
|
||||
&& !local_user_view.local_user.email_verified
|
||||
{
|
||||
Err(LemmyErrorType::EmailNotVerified)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
36
crates/api/src/local_user/notifications/list_mentions.rs
Normal file
36
crates/api/src/local_user/notifications/list_mentions.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{GetPersonMentions, GetPersonMentionsResponse},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::person_mention_view::PersonMentionQuery;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_mentions(
|
||||
data: Query<GetPersonMentions>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetPersonMentionsResponse>> {
|
||||
let sort = data.sort;
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let unread_only = data.unread_only.unwrap_or_default();
|
||||
let person_id = Some(local_user_view.person.id);
|
||||
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
|
||||
|
||||
let mentions = PersonMentionQuery {
|
||||
recipient_id: person_id,
|
||||
my_person_id: person_id,
|
||||
sort,
|
||||
unread_only,
|
||||
show_bot_accounts,
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
|
||||
Ok(Json(GetPersonMentionsResponse { mentions }))
|
||||
}
|
36
crates/api/src/local_user/notifications/list_replies.rs
Normal file
36
crates/api/src/local_user/notifications/list_replies.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{GetReplies, GetRepliesResponse},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_replies(
|
||||
data: Query<GetReplies>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
||||
let sort = data.sort;
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let unread_only = data.unread_only.unwrap_or_default();
|
||||
let person_id = Some(local_user_view.person.id);
|
||||
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
|
||||
|
||||
let replies = CommentReplyQuery {
|
||||
recipient_id: person_id,
|
||||
my_person_id: person_id,
|
||||
sort,
|
||||
unread_only,
|
||||
show_bot_accounts,
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
|
||||
Ok(Json(GetRepliesResponse { replies }))
|
||||
}
|
34
crates/api/src/local_user/notifications/mark_all_read.rs
Normal file
34
crates/api/src/local_user/notifications/mark_all_read.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{context::LemmyContext, person::GetRepliesResponse};
|
||||
use lemmy_db_schema::source::{
|
||||
comment_reply::CommentReply,
|
||||
person_mention::PersonMention,
|
||||
private_message::PrivateMessage,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_all_notifications_read(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// Mark all comment_replies as read
|
||||
CommentReply::mark_all_as_read(&mut context.pool(), person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||
|
||||
// Mark all user mentions as read
|
||||
PersonMention::mark_all_as_read(&mut context.pool(), person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||
|
||||
// Mark all private_messages as read
|
||||
PrivateMessage::mark_all_as_read(&mut context.pool(), person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
|
||||
|
||||
Ok(Json(GetRepliesResponse { replies: vec![] }))
|
||||
}
|
49
crates/api/src/local_user/notifications/mark_mention_read.rs
Normal file
49
crates/api/src/local_user/notifications/mark_mention_read.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{MarkPersonMentionAsRead, PersonMentionResponse},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::person_mention::{PersonMention, PersonMentionUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonMentionView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_person_mention_as_read(
|
||||
data: Json<MarkPersonMentionAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PersonMentionResponse>> {
|
||||
let person_mention_id = data.person_mention_id;
|
||||
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
|
||||
|
||||
if local_user_view.person.id != read_person_mention.recipient_id {
|
||||
Err(LemmyErrorType::CouldntUpdateComment)?
|
||||
}
|
||||
|
||||
let person_mention_id = read_person_mention.id;
|
||||
let read = Some(data.read);
|
||||
PersonMention::update(
|
||||
&mut context.pool(),
|
||||
person_mention_id,
|
||||
&PersonMentionUpdateForm { read },
|
||||
)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||
|
||||
let person_mention_id = read_person_mention.id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let person_mention_view =
|
||||
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id))
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
|
||||
|
||||
Ok(Json(PersonMentionResponse {
|
||||
person_mention_view,
|
||||
}))
|
||||
}
|
48
crates/api/src/local_user/notifications/mark_reply_read.rs
Normal file
48
crates/api/src/local_user/notifications/mark_reply_read.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{CommentReplyResponse, MarkCommentReplyAsRead},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::comment_reply::{CommentReply, CommentReplyUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommentReplyView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_reply_as_read(
|
||||
data: Json<MarkCommentReplyAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentReplyResponse>> {
|
||||
let comment_reply_id = data.comment_reply_id;
|
||||
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
|
||||
|
||||
if local_user_view.person.id != read_comment_reply.recipient_id {
|
||||
Err(LemmyErrorType::CouldntUpdateComment)?
|
||||
}
|
||||
|
||||
let comment_reply_id = read_comment_reply.id;
|
||||
let read = Some(data.read);
|
||||
|
||||
CommentReply::update(
|
||||
&mut context.pool(),
|
||||
comment_reply_id,
|
||||
&CommentReplyUpdateForm { read },
|
||||
)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||
|
||||
let comment_reply_id = read_comment_reply.id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_reply_view =
|
||||
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id))
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
|
||||
|
||||
Ok(Json(CommentReplyResponse { comment_reply_view }))
|
||||
}
|
6
crates/api/src/local_user/notifications/mod.rs
Normal file
6
crates/api/src/local_user/notifications/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod list_mentions;
|
||||
pub mod list_replies;
|
||||
pub mod mark_all_read;
|
||||
pub mod mark_mention_read;
|
||||
pub mod mark_reply_read;
|
||||
pub mod unread_count;
|
29
crates/api/src/local_user/notifications/unread_count.rs
Normal file
29
crates/api/src/local_user/notifications/unread_count.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
|
||||
use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn unread_count(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetUnreadCountResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
let replies =
|
||||
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?;
|
||||
|
||||
let mentions =
|
||||
PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user)
|
||||
.await?;
|
||||
|
||||
let private_messages =
|
||||
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;
|
||||
|
||||
Ok(Json(GetUnreadCountResponse {
|
||||
replies,
|
||||
mentions,
|
||||
private_messages,
|
||||
}))
|
||||
}
|
46
crates/api/src/local_user/report_count.rs
Normal file
46
crates/api/src/local_user/report_count.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{GetReportCount, GetReportCountResponse},
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::structs::{
|
||||
CommentReportView,
|
||||
LocalUserView,
|
||||
PostReportView,
|
||||
PrivateMessageReportView,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn report_count(
|
||||
data: Query<GetReportCount>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetReportCountResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
let admin = local_user_view.local_user.admin;
|
||||
let community_id = data.community_id;
|
||||
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let comment_reports =
|
||||
CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id)
|
||||
.await?;
|
||||
|
||||
let post_reports =
|
||||
PostReportView::get_report_count(&mut context.pool(), person_id, admin, community_id).await?;
|
||||
|
||||
let private_message_reports = if admin && community_id.is_none() {
|
||||
Some(PrivateMessageReportView::get_report_count(&mut context.pool()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Json(GetReportCountResponse {
|
||||
community_id,
|
||||
comment_reports,
|
||||
post_reports,
|
||||
private_message_reports,
|
||||
}))
|
||||
}
|
31
crates/api/src/local_user/reset_password.rs
Normal file
31
crates/api/src/local_user/reset_password.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::local_user::check_email_verified;
|
||||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::PasswordReset,
|
||||
utils::send_password_reset_email,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn reset_password(
|
||||
data: Json<PasswordReset>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Fetch that email
|
||||
let email = data.email.to_lowercase();
|
||||
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
check_email_verified(&local_user_view, &site_view)?;
|
||||
|
||||
// Email the pure token to the user.
|
||||
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?;
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
159
crates/api/src/local_user/save_settings.rs
Normal file
159
crates/api/src/local_user/save_settings.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::SaveUserSettings,
|
||||
request::replace_image,
|
||||
utils::{
|
||||
get_url_blocklist,
|
||||
local_site_to_slur_regex,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_api,
|
||||
send_verification_email,
|
||||
},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
actor_language::LocalUserLanguage,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::diesel_option_overwrite,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorType, LemmyResult},
|
||||
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_user_settings(
|
||||
data: Json<SaveUserSettings>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
|
||||
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let bio = diesel_option_overwrite(
|
||||
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?,
|
||||
);
|
||||
replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?;
|
||||
replace_image(&data.banner, &local_user_view.person.banner, &context).await?;
|
||||
|
||||
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
||||
let display_name = diesel_option_overwrite(data.display_name.clone());
|
||||
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
|
||||
let email_deref = data.email.as_deref().map(str::to_lowercase);
|
||||
let email = diesel_option_overwrite(email_deref.clone());
|
||||
|
||||
if let Some(Some(email)) = &email {
|
||||
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
|
||||
// if email was changed, check that it is not taken and send verification mail
|
||||
if previous_email.deref() != email {
|
||||
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
|
||||
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
||||
}
|
||||
send_verification_email(
|
||||
&local_user_view,
|
||||
email,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None
|
||||
// value
|
||||
if let Some(email) = &email {
|
||||
if email.is_none() && site_view.local_site.require_email_verification {
|
||||
Err(LemmyErrorType::EmailRequired)?
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(bio)) = &bio {
|
||||
is_valid_bio_field(bio)?;
|
||||
}
|
||||
|
||||
if let Some(Some(display_name)) = &display_name {
|
||||
is_valid_display_name(
|
||||
display_name.trim(),
|
||||
site_view.local_site.actor_name_max_length as usize,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(Some(matrix_user_id)) = &matrix_user_id {
|
||||
is_valid_matrix_id(matrix_user_id)?;
|
||||
}
|
||||
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let default_listing_type = data.default_listing_type;
|
||||
let default_sort_type = data.default_sort_type;
|
||||
|
||||
let person_form = PersonUpdateForm {
|
||||
display_name,
|
||||
bio,
|
||||
matrix_user_id,
|
||||
bot_account: data.bot_account,
|
||||
avatar,
|
||||
banner,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Ignore errors, because 'no fields updated' will return an error.
|
||||
// https://github.com/LemmyNet/lemmy/issues/4076
|
||||
Person::update(&mut context.pool(), person_id, &person_form)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if let Some(discussion_languages) = data.discussion_languages.clone() {
|
||||
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
|
||||
}
|
||||
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
email,
|
||||
show_avatars: data.show_avatars,
|
||||
show_read_posts: data.show_read_posts,
|
||||
send_notifications_to_email: data.send_notifications_to_email,
|
||||
show_nsfw: data.show_nsfw,
|
||||
blur_nsfw: data.blur_nsfw,
|
||||
auto_expand: data.auto_expand,
|
||||
show_bot_accounts: data.show_bot_accounts,
|
||||
show_scores: data.show_scores,
|
||||
default_sort_type,
|
||||
default_listing_type,
|
||||
theme: data.theme.clone(),
|
||||
interface_language: data.interface_language.clone(),
|
||||
open_links_in_new_tab: data.open_links_in_new_tab,
|
||||
infinite_scroll_enabled: data.infinite_scroll_enabled,
|
||||
post_listing_mode: data.post_listing_mode,
|
||||
enable_keyboard_navigation: data.enable_keyboard_navigation,
|
||||
enable_animated_images: data.enable_animated_images,
|
||||
collapse_bot_comments: data.collapse_bot_comments,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
|
||||
|
||||
// Update the vote display modes
|
||||
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {
|
||||
score: data.show_scores,
|
||||
upvotes: data.show_upvotes,
|
||||
downvotes: data.show_downvotes,
|
||||
upvote_percentage: data.show_upvote_percentage,
|
||||
};
|
||||
LocalUserVoteDisplayMode::update(&mut context.pool(), local_user_id, &vote_display_modes_form)
|
||||
.await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
49
crates/api/src/local_user/update_totp.rs
Normal file
49
crates/api/src/local_user/update_totp.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::check_totp_2fa_valid;
|
||||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{UpdateTotp, UpdateTotpResponse},
|
||||
};
|
||||
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Enable or disable two-factor-authentication. The current setting is determined from
|
||||
/// [LocalUser.totp_2fa_enabled].
|
||||
///
|
||||
/// To enable, you need to first call [generate_totp_secret] and then pass a valid token to this
|
||||
/// function.
|
||||
///
|
||||
/// Disabling is only possible if 2FA was previously enabled. Again it is necessary to pass a valid
|
||||
/// token.
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn update_totp(
|
||||
data: Json<UpdateTotp>,
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<UpdateTotpResponse>> {
|
||||
check_totp_2fa_valid(
|
||||
&local_user_view,
|
||||
&Some(data.totp_token.clone()),
|
||||
&context.settings().hostname,
|
||||
)?;
|
||||
|
||||
// toggle the 2fa setting
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
totp_2fa_enabled: Some(data.enabled),
|
||||
// if totp is enabled, leave unchanged. otherwise clear secret
|
||||
totp_2fa_secret: if data.enabled { None } else { Some(None) },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
LocalUser::update(
|
||||
&mut context.pool(),
|
||||
local_user_view.local_user.id,
|
||||
&local_user_form,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(UpdateTotpResponse {
|
||||
enabled: data.enabled,
|
||||
}))
|
||||
}
|
23
crates/api/src/local_user/validate_auth.rs
Normal file
23
crates/api/src/local_user/validate_auth.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use crate::{local_user_view_from_jwt, read_auth_token};
|
||||
use actix_web::{
|
||||
web::{Data, Json},
|
||||
HttpRequest,
|
||||
};
|
||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Returns an error message if the auth token is invalid for any reason. Necessary because other
|
||||
/// endpoints silently treat any call with invalid auth as unauthenticated.
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn validate_auth(
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let jwt = read_auth_token(&req)?;
|
||||
if let Some(jwt) = jwt {
|
||||
local_user_view_from_jwt(&jwt, &context).await?;
|
||||
} else {
|
||||
Err(LemmyErrorType::NotLoggedIn)?;
|
||||
}
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
60
crates/api/src/local_user/verify_email.rs
Normal file
60
crates/api/src/local_user/verify_email.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::VerifyEmail,
|
||||
utils::send_new_applicant_email_to_admins,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
email_verification::EmailVerification,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
},
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
pub async fn verify_email(
|
||||
data: Json<VerifyEmail>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
let token = data.token.clone();
|
||||
let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::TokenNotFound)?;
|
||||
|
||||
let form = LocalUserUpdateForm {
|
||||
// necessary in case this is a new signup
|
||||
email_verified: Some(true),
|
||||
// necessary in case email of an existing user was changed
|
||||
email: Some(Some(verification.email)),
|
||||
..Default::default()
|
||||
};
|
||||
let local_user_id = verification.local_user_id;
|
||||
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
||||
|
||||
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
||||
|
||||
// send out notification about registration application to admins if enabled
|
||||
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
&& site_view.local_site.application_email_admins
|
||||
{
|
||||
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
send_new_applicant_email_to_admins(
|
||||
&local_user.person.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
82
crates/api/src/post/feature.rs
Normal file
82
crates/api/src/post/feature.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
build_response::build_post_response,
|
||||
context::LemmyContext,
|
||||
post::{FeaturePost, PostResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_mod_action, is_admin},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
moderator::{ModFeaturePost, ModFeaturePostForm},
|
||||
post::{Post, PostUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
PostFeatureType,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn feature_post(
|
||||
data: Json<FeaturePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_id = data.post_id;
|
||||
let orig_post = Post::read(&mut context.pool(), post_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
orig_post.community_id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if data.feature_type == PostFeatureType::Local {
|
||||
is_admin(&local_user_view)?;
|
||||
}
|
||||
|
||||
// Update the post
|
||||
let post_id = data.post_id;
|
||||
let new_post: PostUpdateForm = if data.feature_type == PostFeatureType::Community {
|
||||
PostUpdateForm {
|
||||
featured_community: Some(data.featured),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
PostUpdateForm {
|
||||
featured_local: Some(data.featured),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let post = Post::update(&mut context.pool(), post_id, &new_post).await?;
|
||||
|
||||
// Mod tables
|
||||
let form = ModFeaturePostForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
post_id: data.post_id,
|
||||
featured: data.featured,
|
||||
is_featured_community: data.feature_type == PostFeatureType::Community,
|
||||
};
|
||||
|
||||
ModFeaturePost::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
}
|
17
crates/api/src/post/get_link_metadata.rs
Normal file
17
crates/api/src/post/get_link_metadata.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
||||
request::fetch_link_metadata,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_link_metadata(
|
||||
data: Query<GetSiteMetadata>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
||||
let metadata = fetch_link_metadata(&data.url, &context).await?;
|
||||
|
||||
Ok(Json(GetSiteMetadataResponse { metadata }))
|
||||
}
|
34
crates/api/src/post/hide.rs
Normal file
34
crates/api/src/post/hide.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse};
|
||||
use lemmy_db_schema::source::post::PostHide;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn hide_post(
|
||||
data: Json<HidePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let post_ids = HashSet::from_iter(data.post_ids.clone());
|
||||
|
||||
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
|
||||
Err(LemmyErrorType::TooManyItems)?;
|
||||
}
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// Mark the post as hidden / unhidden
|
||||
if data.hide {
|
||||
PostHide::hide(&mut context.pool(), post_ids, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
||||
} else {
|
||||
PostHide::unhide(&mut context.pool(), post_ids, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
||||
}
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
95
crates/api/src/post/like.rs
Normal file
95
crates/api/src/post/like.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
build_response::build_post_response,
|
||||
context::LemmyContext,
|
||||
post::{CreatePostLike, PostResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{
|
||||
check_bot_account,
|
||||
check_community_user_action,
|
||||
check_downvotes_enabled,
|
||||
mark_post_as_read,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::Community,
|
||||
local_site::LocalSite,
|
||||
post::{Post, PostLike, PostLikeForm},
|
||||
},
|
||||
traits::{Crud, Likeable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn like_post(
|
||||
data: Json<CreatePostLike>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
check_downvotes_enabled(data.score, &local_site)?;
|
||||
check_bot_account(&local_user_view.person)?;
|
||||
|
||||
// Check for a community ban
|
||||
let post_id = data.post_id;
|
||||
let post = Post::read(&mut context.pool(), post_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
post.community_id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let like_form = PostLikeForm {
|
||||
post_id: data.post_id,
|
||||
person_id: local_user_view.person.id,
|
||||
score: data.score,
|
||||
};
|
||||
|
||||
// Remove any likes first
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
|
||||
|
||||
// Only add the like if the score isnt 0
|
||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||
if do_add {
|
||||
PostLike::like(&mut context.pool(), &like_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
||||
}
|
||||
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
let community = Community::read(&mut context.pool(), post.community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LikePostOrComment {
|
||||
object_id: post.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community,
|
||||
score: data.score,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
context.deref(),
|
||||
post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
}
|
32
crates/api/src/post/list_post_likes.rs
Normal file
32
crates/api/src/post/list_post_likes.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{ListPostLikes, ListPostLikesResponse},
|
||||
utils::is_mod_or_admin,
|
||||
};
|
||||
use lemmy_db_schema::{source::post::Post, traits::Crud};
|
||||
use lemmy_db_views::structs::{LocalUserView, VoteView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
/// Lists likes for a post
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_post_likes(
|
||||
data: Query<ListPostLikes>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListPostLikesResponse>> {
|
||||
let post = Post::read(&mut context.pool(), data.post_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
is_mod_or_admin(
|
||||
&mut context.pool(),
|
||||
&local_user_view.person,
|
||||
post.community_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let post_likes =
|
||||
VoteView::list_for_post(&mut context.pool(), data.post_id, data.page, data.limit).await?;
|
||||
|
||||
Ok(Json(ListPostLikesResponse { post_likes }))
|
||||
}
|
73
crates/api/src/post/lock.rs
Normal file
73
crates/api/src/post/lock.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
build_response::build_post_response,
|
||||
context::LemmyContext,
|
||||
post::{LockPost, PostResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
moderator::{ModLockPost, ModLockPostForm},
|
||||
post::{Post, PostUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn lock_post(
|
||||
data: Json<LockPost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_id = data.post_id;
|
||||
let orig_post = Post::read(&mut context.pool(), post_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
orig_post.community_id,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update the post
|
||||
let post_id = data.post_id;
|
||||
let locked = data.locked;
|
||||
let post = Post::update(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
&PostUpdateForm {
|
||||
locked: Some(locked),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mod tables
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
post_id: data.post_id,
|
||||
locked: Some(locked),
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
}
|
34
crates/api/src/post/mark_read.rs
Normal file
34
crates/api/src/post/mark_read.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
|
||||
use lemmy_db_schema::source::post::PostRead;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_post_as_read(
|
||||
data: Json<MarkPostAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let post_ids = HashSet::from_iter(data.post_ids.clone());
|
||||
|
||||
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
|
||||
Err(LemmyErrorType::TooManyItems)?;
|
||||
}
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// Mark the post as read / unread
|
||||
if data.read {
|
||||
PostRead::mark_as_read(&mut context.pool(), post_ids, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
|
||||
} else {
|
||||
PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
|
||||
}
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
8
crates/api/src/post/mod.rs
Normal file
8
crates/api/src/post/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
pub mod feature;
|
||||
pub mod get_link_metadata;
|
||||
pub mod hide;
|
||||
pub mod like;
|
||||
pub mod list_post_likes;
|
||||
pub mod lock;
|
||||
pub mod mark_read;
|
||||
pub mod save;
|
44
crates/api/src/post/save.rs
Normal file
44
crates/api/src/post/save.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{PostResponse, SavePost},
|
||||
utils::mark_post_as_read,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::post::{PostSaved, PostSavedForm},
|
||||
traits::Saveable,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_post(
|
||||
data: Json<SavePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_saved_form = PostSavedForm {
|
||||
post_id: data.post_id,
|
||||
person_id: local_user_view.person.id,
|
||||
};
|
||||
|
||||
if data.save {
|
||||
PostSaved::save(&mut context.pool(), &post_saved_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntSavePost)?;
|
||||
} else {
|
||||
PostSaved::unsave(&mut context.pool(), &post_saved_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntSavePost)?;
|
||||
}
|
||||
|
||||
let post_id = data.post_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
Ok(Json(PostResponse { post_view }))
|
||||
}
|
91
crates/api/src/post_report/create.rs
Normal file
91
crates/api/src/post_report/create.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use crate::check_report_reason;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{CreatePostReport, PostReportResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{
|
||||
check_community_user_action,
|
||||
check_post_deleted_or_removed,
|
||||
send_new_report_email_to_admins,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_site::LocalSite,
|
||||
post_report::{PostReport, PostReportForm},
|
||||
},
|
||||
traits::Reportable,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostReportView, PostView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Creates a post report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn create_post_report(
|
||||
data: Json<CreatePostReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
check_report_reason(&reason, &local_site)?;
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
let post_id = data.post_id;
|
||||
let post_view = PostView::read(&mut context.pool(), post_id, None, false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
post_view.community.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_post_deleted_or_removed(&post_view.post)?;
|
||||
|
||||
let report_form = PostReportForm {
|
||||
creator_id: person_id,
|
||||
post_id,
|
||||
original_post_name: post_view.post.name,
|
||||
original_post_url: post_view.post.url,
|
||||
original_post_body: post_view.post.body,
|
||||
reason,
|
||||
};
|
||||
|
||||
let report = PostReport::report(&mut context.pool(), &report_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
|
||||
|
||||
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
|
||||
|
||||
// Email the admins
|
||||
if local_site.reports_email_admins {
|
||||
send_new_report_email_to_admins(
|
||||
&post_report_view.creator.name,
|
||||
&post_report_view.post_creator.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::CreateReport {
|
||||
object_id: post_view.post.ap_id.inner().clone(),
|
||||
actor: local_user_view.person,
|
||||
community: post_view.community,
|
||||
reason: data.reason.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(PostReportResponse { post_report_view }))
|
||||
}
|
37
crates/api/src/post_report/list.rs
Normal file
37
crates/api/src/post_report/list.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{ListPostReports, ListPostReportsResponse},
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Lists post reports for a community if an id is supplied
|
||||
/// or returns all post reports for communities a user moderates
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_post_reports(
|
||||
data: Query<ListPostReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListPostReportsResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let post_id = data.post_id;
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let post_reports = PostReportQuery {
|
||||
community_id,
|
||||
post_id,
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
.list(&mut context.pool(), &local_user_view)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ListPostReportsResponse { post_reports }))
|
||||
}
|
3
crates/api/src/post_report/mod.rs
Normal file
3
crates/api/src/post_report/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod create;
|
||||
pub mod list;
|
||||
pub mod resolve;
|
48
crates/api/src/post_report/resolve.rs
Normal file
48
crates/api/src/post_report/resolve.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{PostReportResponse, ResolvePostReport},
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostReportView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Resolves or unresolves a post report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn resolve_post_report(
|
||||
data: Json<ResolvePostReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PostReportResponse>> {
|
||||
let report_id = data.report_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let report = PostReportView::read(&mut context.pool(), report_id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
report.community.id,
|
||||
true,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if data.resolved {
|
||||
PostReport::resolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
} else {
|
||||
PostReport::unresolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
}
|
||||
|
||||
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
|
||||
|
||||
Ok(Json(PostReportResponse { post_report_view }))
|
||||
}
|
48
crates/api/src/private_message/mark_read.rs
Normal file
48
crates/api/src/private_message/mark_read.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
private_message::{MarkPrivateMessageAsRead, PrivateMessageResponse},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::private_message::{PrivateMessage, PrivateMessageUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_pm_as_read(
|
||||
data: Json<MarkPrivateMessageAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PrivateMessageResponse>> {
|
||||
// Checking permissions
|
||||
let private_message_id = data.private_message_id;
|
||||
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
|
||||
if local_user_view.person.id != orig_private_message.recipient_id {
|
||||
Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
|
||||
}
|
||||
|
||||
// Doing the update
|
||||
let private_message_id = data.private_message_id;
|
||||
let read = data.read;
|
||||
PrivateMessage::update(
|
||||
&mut context.pool(),
|
||||
private_message_id,
|
||||
&PrivateMessageUpdateForm {
|
||||
read: Some(read),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
|
||||
|
||||
let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
|
||||
Ok(Json(PrivateMessageResponse {
|
||||
private_message_view: view,
|
||||
}))
|
||||
}
|
1
crates/api/src/private_message/mod.rs
Normal file
1
crates/api/src/private_message/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod mark_read;
|
72
crates/api/src/private_message_report/create.rs
Normal file
72
crates/api/src/private_message_report/create.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use crate::check_report_reason;
|
||||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse},
|
||||
utils::send_new_report_email_to_admins,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_site::LocalSite,
|
||||
private_message::PrivateMessage,
|
||||
private_message_report::{PrivateMessageReport, PrivateMessageReportForm},
|
||||
},
|
||||
traits::{Crud, Reportable},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn create_pm_report(
|
||||
data: Json<CreatePrivateMessageReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PrivateMessageReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
check_report_reason(&reason, &local_site)?;
|
||||
|
||||
let person_id = local_user_view.person.id;
|
||||
let private_message_id = data.private_message_id;
|
||||
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
|
||||
|
||||
// Make sure that only the recipient of the private message can create a report
|
||||
if person_id != private_message.recipient_id {
|
||||
Err(LemmyErrorType::CouldntCreateReport)?
|
||||
}
|
||||
|
||||
let report_form = PrivateMessageReportForm {
|
||||
creator_id: person_id,
|
||||
private_message_id,
|
||||
original_pm_text: private_message.content,
|
||||
reason,
|
||||
};
|
||||
|
||||
let report = PrivateMessageReport::report(&mut context.pool(), &report_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
|
||||
|
||||
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
|
||||
|
||||
// Email the admins
|
||||
if local_site.reports_email_admins {
|
||||
send_new_report_email_to_admins(
|
||||
&private_message_report_view.creator.name,
|
||||
&private_message_report_view.private_message_creator.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO: consider federating this
|
||||
|
||||
Ok(Json(PrivateMessageReportResponse {
|
||||
private_message_report_view,
|
||||
}))
|
||||
}
|
35
crates/api/src/private_message_report/list.rs
Normal file
35
crates/api/src/private_message_report/list.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse},
|
||||
utils::is_admin,
|
||||
};
|
||||
use lemmy_db_views::{
|
||||
private_message_report_view::PrivateMessageReportQuery,
|
||||
structs::LocalUserView,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_pm_reports(
|
||||
data: Query<ListPrivateMessageReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<ListPrivateMessageReportsResponse>> {
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let private_message_reports = PrivateMessageReportQuery {
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
.list(&mut context.pool())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ListPrivateMessageReportsResponse {
|
||||
private_message_reports,
|
||||
}))
|
||||
}
|
3
crates/api/src/private_message_report/mod.rs
Normal file
3
crates/api/src/private_message_report/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod create;
|
||||
pub mod list;
|
||||
pub mod resolve;
|
38
crates/api/src/private_message_report/resolve.rs
Normal file
38
crates/api/src/private_message_report/resolve.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport},
|
||||
utils::is_admin,
|
||||
};
|
||||
use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn resolve_pm_report(
|
||||
data: Json<ResolvePrivateMessageReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<PrivateMessageReportResponse>> {
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let report_id = data.report_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
if data.resolved {
|
||||
PrivateMessageReport::resolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
} else {
|
||||
PrivateMessageReport::unresolve(&mut context.pool(), report_id, person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
||||
}
|
||||
|
||||
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
|
||||
|
||||
Ok(Json(PrivateMessageReportResponse {
|
||||
private_message_report_view,
|
||||
}))
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue