aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--ARCHITECTURE.md12
-rw-r--r--CHANGELOG.md38
-rw-r--r--README.md10
-rw-r--r--client/package.json2
-rw-r--r--client/src/app/+accounts/accounts.component.html11
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/index.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html22
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts59
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html23
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts60
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts8
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts23
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts1
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html6
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts28
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html26
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts59
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html27
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts60
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts24
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts20
-rw-r--r--client/src/app/+my-account/my-account.component.html16
-rw-r--r--client/src/app/+my-account/my-account.component.scss2
-rw-r--r--client/src/app/+my-account/my-account.component.ts15
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/search/search.component.ts3
-rw-r--r--client/src/app/shared/account/account.model.ts9
-rw-r--r--client/src/app/shared/blocklist/account-block.model.ts14
-rw-r--r--client/src/app/shared/blocklist/blocklist.service.ts135
-rw-r--r--client/src/app/shared/blocklist/index.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html6
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss5
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.html7
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts224
-rw-r--r--client/src/app/shared/rest/rest-table.ts4
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/sass/include/_bootstrap-variables.scss4
-rw-r--r--client/tsconfig.json1
-rw-r--r--client/tslint.json16
-rw-r--r--package.json2
-rwxr-xr-xscripts/release.sh6
-rwxr-xr-xscripts/travis.sh3
-rw-r--r--server/controllers/api/accounts.ts5
-rw-r--r--server/controllers/api/search.ts2
-rw-r--r--server/controllers/api/server/index.ts2
-rw-r--r--server/controllers/api/server/server-blocklist.ts132
-rw-r--r--server/controllers/api/users/index.ts2
-rw-r--r--server/controllers/api/users/me.ts3
-rw-r--r--server/controllers/api/users/my-blocklist.ts125
-rw-r--r--server/controllers/api/video-channel.ts2
-rw-r--r--server/controllers/api/videos/comment.ts12
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts52
-rw-r--r--server/helpers/utils.ts5
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/initializers/database.ts6
-rw-r--r--server/lib/blocklist.ts40
-rw-r--r--server/lib/video-comment.ts6
-rw-r--r--server/middlewares/validators/blocklist.ts172
-rw-r--r--server/middlewares/validators/index.ts2
-rw-r--r--server/middlewares/validators/server.ts33
-rw-r--r--server/middlewares/validators/sort.ts8
-rw-r--r--server/models/account/account-blocklist.ts111
-rw-r--r--server/models/server/server-blocklist.ts121
-rw-r--r--server/models/server/server.ts6
-rw-r--r--server/models/utils.ts16
-rw-r--r--server/models/video/video-comment.ts95
-rw-r--r--server/models/video/video.ts46
-rw-r--r--server/tests/api/check-params/blocklist.ts494
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/index-4.ts1
-rw-r--r--server/tests/api/index.ts1
-rw-r--r--server/tests/api/redundancy/index.ts1
-rw-r--r--server/tests/api/redundancy/redundancy.ts483
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/server/redundancy.ts20
-rw-r--r--server/tests/api/users/blocklist.ts511
-rw-r--r--server/tests/api/users/index.ts1
-rw-r--r--server/tests/api/videos/single-server.ts2
-rw-r--r--server/tests/api/videos/video-imports.ts2
-rw-r--r--server/tests/utils/requests/requests.ts4
-rw-r--r--server/tests/utils/users/blocklist.ts198
-rw-r--r--server/tests/utils/videos/video-comments.ts14
-rw-r--r--server/tools/repl.ts79
-rw-r--r--shared/models/blocklist/account-block.model.ts7
-rw-r--r--shared/models/blocklist/index.ts2
-rw-r--r--shared/models/blocklist/server-block.model.ts9
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/users/user-right.enum.ts3
-rw-r--r--shared/models/users/user-role.ts4
-rw-r--r--shared/models/videos/video-resolution.enum.ts45
-rw-r--r--support/doc/tools.md174
-rw-r--r--tslint.json2
103 files changed, 3916 insertions, 202 deletions
diff --git a/.travis.yml b/.travis.yml
index 7670cb7c0..3a73e4fc0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -42,6 +42,7 @@ matrix:
42 - env: TEST_SUITE=api-1 42 - env: TEST_SUITE=api-1
43 - env: TEST_SUITE=api-2 43 - env: TEST_SUITE=api-2
44 - env: TEST_SUITE=api-3 44 - env: TEST_SUITE=api-3
45 - env: TEST_SUITE=api-4
45 - env: TEST_SUITE=cli 46 - env: TEST_SUITE=cli
46 - env: TEST_SUITE=lint 47 - env: TEST_SUITE=lint
47 - env: TEST_SUITE=jest 48 - env: TEST_SUITE=jest
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 160d6fc4f..f3254d2d6 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -2,10 +2,16 @@
2 2
3## Vocabulary 3## Vocabulary
4 4
5 - **Fediverse:** several servers following each others. 5 - **Fediverse:** several servers following one another, several users
6 following each other. Designates federated communities in general.
7 - **Vidiverse:** same as Fediverse, but federating videos specifically.
6 - **Instance:** a server which runs PeerTube in the fediverse. 8 - **Instance:** a server which runs PeerTube in the fediverse.
7 - **Origin instance:** the instance on which the video was uploaded and which 9 - **Origin instance:** the instance on which the video was uploaded and which
8 is seeding (through the WebSeed protocol) the video. 10 is seeding (through the WebSeed protocol) the video.
11 - **Cache instance:** an instance that decided to make available a WebSeed
12 of its own for a video originating from another instance. It sends a `ptCache`
13 activity to notify the origin instance, which will then update its list of
14 WebSeeds for the video.
9 - **Following:** the action of a PeerTube instance which will follow another 15 - **Following:** the action of a PeerTube instance which will follow another
10 instance (subscribe to its videos). 16 instance (subscribe to its videos).
11 17
@@ -22,8 +28,8 @@
22 * All the requests are retried several times if they fail. 28 * All the requests are retried several times if they fail.
23 29
24### Instance 30### Instance
25 * An instance has a websocket tracker which is responsible for all the video 31 * An instance has a websocket tracker which is responsible for all videos
26 uploaded in it. 32 uploaded by its users.
27 * An instance has an administrator that can follow other instances. 33 * An instance has an administrator that can follow other instances.
28 * An instance can be configured to follow back automatically. 34 * An instance can be configured to follow back automatically.
29 * An instance can blacklist other instances (only used in "follow back" 35 * An instance can blacklist other instances (only used in "follow back"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6716c7bc2..d2bd98f4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,44 @@
1# Changelog 1# Changelog
2 2
3## v1.1.0-alpha.1
4
5We released this alpha version because some admins/users need some moderation tools we implemented in recent weeks.
6This release could contain bugs. Don't expect a stable v1.1.0 until December :)
7
8### Scripts
9
10 * Use DB information from config/production.yaml in upgrade script ([@ldidry](https://github.com/ldidry))
11 * Add REPL script ([@McFlat](https://github.com/mcflat))
12
13### Docker
14
15 * Add search and import settings env settings env variables ([@kaiyou](https://github.com/kaiyou))
16 * Add docker dev image ([@am97](https://github.com/am97))
17
18### Features
19
20 * Automatically resume videos if the user is logged in
21 * Hide automatically the menu when the window is resized ([@BO41](https://github.com/BO41))
22 * Remove confirm modal for JavaScript/CSS injection ([@scanlime](https://github.com/scanlime))
23 * Set bitrate limits for transcoding ([@Nutomic](https://github.com/nutomic))
24 * Add moderation tools in the account page
25 * Add bulk actions in users table (Delete/Ban for now)
26 * Add search filter in admin users table
27 * Add search filter in admin following
28 * Add search filter in admin followers
29 * Add ability to list all local videos
30 * Add ability for users to mute an account or an instance
31 * Add ability for administrators to mute an account or an instance
32 * Rename "News" category to "News & Politics" ([@daker](https://github.com/daker))
33 * Add explicit error message when changing video ownership ([@lucas-dclrcq](https://github.com/lucas-dclrcq))
34 * Improve description of the HTTP video import feature ([@rigelk](https://github.com/rigelk))
35
36
3## v1.0.0 37## v1.0.0
4 38
5Announcement scheduled for october 15 39### SECURITY
40
41 * Add more headers to HTTP signature to avoid actor impersonation by replaying modified signed HTTP requests (thanks Thibaut Girka)
6 42
7### Bug fixes 43### Bug fixes
8 44
diff --git a/README.md b/README.md
index b06b9c002..ab811d276 100644
--- a/README.md
+++ b/README.md
@@ -17,10 +17,6 @@ directly in the web browser with <a href="https://github.com/feross/webtorrent">
17</p> 17</p>
18 18
19<p align="center"> 19<p align="center">
20<strong>We have run <a href="https://www.kisskissbankbank.com/en/projects/peertube-a-free-and-federated-video-platform">a crowdfunding campaign</a> to pave the road to version 1.0 of PeerTube. Thanks to everyone who pitched in and shared the news around. You can now check out <a href="https://github.com/Chocobozzz/PeerTube/milestone/1">the corresponding milestone</a> and help its development!</strong>
21</p>
22
23<p align="center">
24 <strong>Client</strong> 20 <strong>Client</strong>
25 21
26 <br /> 22 <br />
@@ -98,7 +94,7 @@ Be it as a user or an instance administrator, you can decide what your experienc
98In addition to visitors using WebTorrent to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our <a href="/support/doc/redundancy.md">redundancy guide</a>). 94In addition to visitors using WebTorrent to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our <a href="/support/doc/redundancy.md">redundancy guide</a>).
99</p> 95</p>
100<p align="right"> 96<p align="right">
101Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisments that hurt visitors and <strike>incentivize</strike> alter creativity (more about that in our <a href="./FAQ.md">FAQ</a>). 97Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and <strike>incentivize</strike> alter creativity (more about that in our <a href="./FAQ.md">FAQ</a>).
102</p> 98</p>
103 99
104--- 100---
@@ -109,7 +105,7 @@ Want to see it in action?
109 * [peertube.cpy.re](https://peertube.cpy.re) 105 * [peertube.cpy.re](https://peertube.cpy.re)
110 * [peertube2.cpy.re](https://peertube2.cpy.re) 106 * [peertube2.cpy.re](https://peertube2.cpy.re)
111 * [peertube3.cpy.re](https://peertube3.cpy.re) 107 * [peertube3.cpy.re](https://peertube3.cpy.re)
112 * [Video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) explaining what is PeerTube 108 * [Video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) explaining what PeerTube is
113 * [Video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) showing the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) 109 * [Video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) showing the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon)
114 110
115:question: Motivation 111:question: Motivation
@@ -212,6 +208,8 @@ Here are some simple schemes:
212:heart: Supports of our crowdfunding 208:heart: Supports of our crowdfunding
213---------------------------------------------------------------- 209----------------------------------------------------------------
214 210
211We have run [a crowdfunding campaign](https://www.kisskissbankbank.com/en/projects/peertube-a-free-and-federated-video-platform) to pave the road to the version 1.0.0 of PeerTube. Thanks to everyone who pitched in and shared the news around!
212
215Quonfucius, IP Solution, \_Laure\_, @lex666, 0x010C, 3dsman, 3rw4n-G3D, aallrd, Abel-Berger, Adam-Odell, adechambost, adim, adngdb, Adrien Thurotte, Adrien-BARAN, Adrien-Hamraoui, Adrien-Horcholle, Adrien-Luxey, Adrien-Polar, Adrien-Touminet, Agathe Begault, Agence-Différente, Ahmed-Al-Ahmed, aiprole, akpoptro, Al-Nimr, Alain-Delgrange, Alain-Fyon, Alain-Girard, Alain-MICHEL, Aleksandar-Aleksandrov, Alex-Chancellé, Alex-Dufournet, Alex-Gleason, Alexander-Murray-Watters, Alexandre-Alapetite, Alexandre-Badez, Alexandre-Giuliani, Alexandre-Mercier, Alexandre-Roux-2, Alexandre-SIMON, Alexandre29, Alexia-Monsavoir, Alexis-Frn, Alexis-Gros, Alexis-Kauffmann, alfajet, Alias, alinemont, Aliocha-Lang, Alllightlong, aloisdg, Amanda Hinault, André-Rabe, Anne-PROTAS, antoine, Antoine Derouin, Antoine-Beauvillain, Antoine-Deléron, antomoro, Antón López, Antonin-DENIS, Antonin-Segault, aokami, Apichat-Apichat, Ar-To, ARIAS-Frédéric-2, ariasuni, Aris-Papathéodorou, Arnaud -Vigoureux , Arnaud-Mounier, Arnaud-Risler, Arnaud-Vigouroux, Arnulf, Arthur-Bellier, arthur-bello, Arthur-Charron, Arthur-De Kimpe, Arthur.Ball, Arthur.Frin, Arvi-LEFEVRE, athanael.fr, auber38, Auguste Psqr, Aurélien-Tamisier, Avel-Musicavel, axel-guegant, Axel-Plat, Aymeric-Dlv, Ayst, Azenilion, Bandino, baptiste-lemoine, Baptiste-Rochez, baruica, Bastien-Dangin, batlab, bcourtine, Bea-Schaack-2, beaufils, beaumme, Belmont1, Ben-Geeraerts, Ben-Meijering, Benjamin-Baratta, Benjamin-Roussel, Benoît Joffre, Benoîtdd, Bernard-Legrand, Bernard-Vauquelin, Bernhard-Hayden, bertrand.arlabosse, bigsicret, bjg, bnjbvr, bob\_isat, bobstechsite, Bolton-Allan, Boov', Boris-ARGAUD, Brice.Francois, broz42, Bruno Lefèvre, Bruno-Douville, Bruno-Fortabat, Bruno-Gadaleta, Bruno-VASTA, Bumblebee, Butchcassidy, Cadiou-Christophe, calendros, Candy-Ming, cappitaine, Carmen-Drocourt, carrigns, case, Cathy-Barbet, CBach, ccazin, Cecile-Obernesser, Cecilia-:), Cédric-Bleschet, Cédric.Bayle, Cestdoncvrai, cgay, champ contrechamp, chapa, charlerlin, charles-jacquin, Charlie-Duclut, charlotte-cgondre78, Chris-Doe, chris-louba, Christel-Berthelot, Christian-FERRARIS, christiannavelot, Christophe-Bastin, christophe-beziers la fosse, Christophe-Pieret, Christophe-Verhaege, christophec, Christopher-Bero, chtfn, chud, Claire-C, clairezed, Claude-POUGHEON, Clément-Hubert, Clément-Morelle, clydeb, Comamanel, Côme Chilliet, Confederac.io, Consulting-AZAPTEC, Corentin3892, CryoGen, cyp, Cypher-Goat, Cyril, Cyril\_M\_, Cyril-MONMOUTON, Cyril-Waechter, Damien-Gabard, Damien-Garaud, Dams3132, Daniel Kuebler, Daniel Waxweiler, Daniel-Bartsch, Daniel-PIPALA, Daniel-Struck, Daniel-Thul, Danny-Joerger, DansLeRuSH, DantSu, Dany-Marcoux, Daouzli-Adel, Darfeld, Darth\_Judge, Dashcom, David-BADOIL, David-Benoist, David-Dormoy, David-Gil-2, David-Velasco, David-Wagner, David-writ, davlgd, davyg2, dbudo72300, de Folleville -Matthieu , DeBugs, Denis-Lecourtiller, Denis-Vannier, Desmu, Didier-Bove, Diego-Crespo, Dimitri-Stouney, dino, Dinosaure, Doc Skellington, Dominique-Brun, dr4Ke, DreamClassier, DRogueRonin, dussydelf, Dylan-Moonfire, Ealhad, Edouard-SCHWEISGUTH, Elanndelh--, ElodieEtJimmy, Éloi-Rivard, Elric-Noel, Elwan-Héry, Emilie-Wietzke, Emilien-Ghomi, eparth, Eric-Bouhana, Eric-Hendricks, Eric.Vales, Erwan-Moreau, Erzender, ESS\_Clem, Etienne-Baqué, Etienne-Botek, Etienne-Lmn, Ex-Serv, fabeveynes, Fabien BERINI ( Rehvaro ) , Fabien Freling, Fabien-Roualdes, Fabien.Abraini, Fabien.Bonneval, fabrice-simon, farlistener, Felix-ROBICHON, FelixDouet, FHE, Fiamoa-McBenson, flamwenco, Flopômpôm, FloraGC, Florent-Deschamps, Florent-Fayolle, Florent-Mallet, Florent-Vasseur, Florent.Duveau, Florestan Fournier, Florian Kohrt, Florian-Bellafont, Florian-Douay, Florian-LE GOFF, Florian-Siegenthaler, Florian.Freyss, fobrice, FOKUZA, Fol-De Dol, FP45, Francis.Moraud, François-Dambrine, François-Deguerry, Francois-Goer, François-Lecomte, François-Lemaire, François-Malterre, François-MORLET, François-Schoubben, François-Xavier-Davanne, François-Zajéga, francois.peyratout, Frathom, Fred-Fred-2, Frédéric GUÉLEN, Frédéric-Blumstein, Frédéric-Meurou, Frederic-Reynaud, Frédéric-Sagot, Frek, FrenchHope, freyja, FugazziPL, Funky-Whale, Gabriel-Devillers, Gabriel-Mirété, Galedas, GardoToF, Gaspard-Kemlin, GauthierPLM, Gauvain "GovanifY" Roussel-Tarbouriech, Gavy, gdquest, Geek Faëries, Geneviève-Perello, Geoffroy-MANAUD, Geojulien, Georges-Dutreix, Georges-Sempéré, Gerald-Vannier, Gérard-Brasquet, Gérard-Sensevy, Gerrit-Großkopf, GGBNM, Ghislain-Fabre, Gil-Felot, Gilles-Brossier, Gilles-Moisan, Gilles-SACLIER, Gilles-Trossevin, Gilou, GinGa, ginkgopr, glazzara, Glen-Lomax, Gof, Gonçalves-Daniel, goofy-goofy, grandlap, GRAP-Groupement Régional Alimentaire de Proximité, greg-chapuis, Grégoire-Delbeke, Grégory-Becq, Grégory-Goulaouic, Gregouw, Grizix, GrosCaillou, Grummfy, grumph, guiaug, Guillaume-Allart, Guillaume-Chambert, Guillaume-Chaslot, Guillaume-David, Guillaume-Duc, Guillaume-Gay, Guillaume-Lecoquierre, Guillaume007, guillaumefavre, Guiraud-Dominique, Guy-Torreilles, GwendalL, gwlolos, Hanna-E, Hanno-Wagner, Harald-Eilertsen, Harpocrate, Hebus82, Hellmut, Henri-ROS, hervelc, hguilbert, Hisham-Muhammad, Hoang-Mai-Lesaffre, Homerc, homosapienssapiens, hoper, Hoshin, Hugo-Lagouge, Hugo-SIMANCAS, Hugo-Simon, Hylm, IchbinRob, Ivan-Ogai, Ivan.D'halluin, Ivar-Troost, J-C-2, Jacques-Roos, James-Moore, James-Valleroy, Jan-Aagaard, Jan-Keromnes, Jancry, Janko-Mihelić, jano31coa, Jboot, jcgross, Jean CHARPENTIER, jean claude-skowron, Jean Dos, jean luc-PERROT, Jean-Baptiste-Maneyrol, Jean-charles-Surbayrole, Jean-claude-Jouanne, jean-dreyfus, jean-FISCHER, JEAN-FRANCOIS-BOUDEAU, Jean-Francois-Ducrot, Jean-François-PETITBON, Jean-François-Tomasi, Jean-Galland, Jean-louis-Bergamo, Jean-Luc-PIPO, Jean-Marie-Graïc, Jean-Martin Laval, Jean-Noel-Bruletout, Jean-Paul-GIBERT, Jean-Paul-Lescat, jean-philippe-bénétrix, Jean-Philippe-Eisenbarth, Jean-Philippe-Renaudet, Jean-Philippe-Rennard, Jean-Sébastien-Renaud, Jean-Yves Kiger, Jean-Yves-DUPARC, Jeanne-Corvellec, jeansebastien, Jelv, Jérémie -Wach, Jeremie-Lestel, Jérémy-Korwin, Jérôme-Avond, Jerome-Bu, Jerome-Denis, Jérôme-ISNARD, jerome-simonato, JeromeD, Jery, Jezza, Jim-McDoniel, jl-M-2, jlanca, jlcpuzzle, jn-m, jnthnctt, joakim.faiss, Joe-Riche, Joévin-SOULENQ, Johann-FONTAINE, John-Devor, John-Doe, Jojo-Boulix, Jonas-Aparicio, Jonathan-Dollé, Jonathan-Kohler, Jonathan-LAURENT, Jos-van den Oever, Joseph-Lawson, Jozef-Knaperek, jroger, ju, jubarbu, Julianoe-G, Julie-Bultez, Julien Loudet, Julien Maulny (alcalyn), Julien-AILHAUD, Julien-Aubin, Julien-Biaudet, Julien-Bréchet, Julien-Cochennec, Julien-Duroure, Julien-Huon, Julien-Lemaire, Julien-Weber, jyb, K-\_, KalambakA, Kanor, kari-kimber, Karim-Jouini, karl-bienfait, Kdecherf, Keplerpondorskell, kevin-Beranger, Kevin-Nguyen, King-Of Peons, Kioob, kloh, kokoklems, Konstantin-Kovar, Kriĉjo, Kyâne-PICHOU, L'elfe-Sylvain, La Gonz, Lara-Dufour, lareinedeselfes, Laurence-Giroud, laurent-fuentes, Laurent-HEINTZ, Laurent-PICQUENOT, ldubost, lebidibule, LeChi, LeDivinBueno, Legrave, Les Assortis, Leyokki-Tk, LibreEnFete-en Tregor, LilO. Moino, Liloumuloup, Linuxine-T, lionel-lachaud, Lionel-Schinckus, Loïc-L'Anton, Loïc.Guérin, Louis-Gatin, Louis-Marie-BAER, Louis-Rémi.Babé, Louis-Roche, Louisclement, Lu, ludovic-lainard, Ludovic-Pénet, Lukas-Steiblys, lusoheart, Mad Sugar, maguy-giorgi, mahen, maiido, Malphas, ManetteBE, Manon-Amalric, Manuel-Vazquez, ManuInzesky, Manumerique, Marc-BESSIERES, Marc-DUFOURNET, Marc-GASSER, Marc-Honnorat, marc-wilzius, marc.ribault.1, Marco-Heisig, Marie-PACHECO, Marien-Fressinaud, Marius-Lemonnier, Mark-O'Donovan, marliebo, marmat8951, mart1n, martensite, Mathdatech, Mathias-Bocquet, Mathieu-Amirault, Mathieu-B., Mathieu-Cornic, Mathieu-VIRAMAN, Matías-Pérez, Matilin-Torre, matt.faure, Mattéo-Delabre, Matthias-Devlamynck, Matthieu-Bollot, Matthieu-De Beule, Matthieu-DEVILLERS, Matthieu-Dupont de Dinechin, Matthieu-Gaudé, Matthieu-Sauboua-Beneluz, matthieublanco, MatthieuSchneider, Max-PENY, Maxime-de WYROW, Maxime-Desjardin, Maxime-Forest, maxime-haag, Maxime-Mangel, Maximilian Praeger, Mayeul-Cantan, Mayeul-Guiraud, mcg1712, metalvinze, Mewen, mheiber, Michael-Koppmann, Michael-Loew, Michael-Q. Bid, Michal-Herda, Michal-Noga, Michel-DUPONT, Michel-Le Lagadec, Michel-POUSSIER, Michel-Roux, Mickaël-Gauvin, Mickael-Liegard, MicMP3Man, Miguel-de la Cruz, Mike-Kasprzak, Mimon-Lapompe, Mister-Ocelot, mjhvc, Moutmout, MouTom, MP, mphdp, Mr-Tea, msellebulle, Mushussu, mylainos, nanouckd, Nasser-Debruyere, Nat-Tuck, Nathan.B, nayya, nazgulz666, Neal-Wilson, neeev, neodarz-neodarz, NepsKi, Nestorvep, NHenry, Nialix, NicoD, Nicolas-Auvray, nicolas-k, Nicolas-Pinault, Nicolas-Ruffel, NicolasCARPi, nicolaslegland, niconil, Niles, nitot, Nono1965, Norbert, Norde, Numcap, obergix, Obrow, Okki, Olivier-Calzi, Olivier-Ganneval, Olivier-Marouzé, Olivier-Mondoloni, olivier-pierret, Oncela-Petit Chat, Óskar-Sturluson, p3n15634n7, Paindesegle, Pas De-Panique, Pascal-BLEUSE, Pascal-Larramendy, Patrice-Jabeneau, patrice-maertens, patrick-bappel, PATRICK-GRANDIN, Patrick-MERCIER, Patrickl , Paul-Härle, Paul-Tardy, pbramy, Pedro-CADETE, Perrine-de Coëtlogon, Peter\_Fillgod, Petter-Joelson, Philippe-BATTMANN, Philippe-Cabaud, Philippe-Debar, philippe-giffard, Philippe-Lallemant, Philippe-Le Van, philippe-lhardy, Philippe-Thébault, Philippe-VINCENT-2, PhilOGM, Pierre 'catwell' Chapuis, Pierre Gros, Pierre-Antoine-Champin, Pierre-Bresson-2, Pierre-d'Alençon, Pierre-Equoy, Pierre-Girardeau, Pierre-Houmeau, Pierre-Marijon, Pierre-petch, Pierrick-Couturier, Pilou-CaraGk, Piotr-Miszczak, Pla, Plastic Yogi, PME2050, pmiossec, Pofilo, Polioman, Polios63, Poutchiny, PRALLET-Claude, PtrckVllnv, Pulov Yuran, queertube, Quentin-Dugne, Quentin-PAGÈS, ra-mon, Radhwan-Ben Madhkour, Raphaël-Brocq, Raphaël-Grolimund, Raphaël-Piédallu, raphane, Raphip, Raven, Raymond-Lutz, Razael, Rebecca-Breu, Remi-Durand, Rémi-Herrmann, Rémi-Verschelde, Remigho, Remix-the commons, Remy-Grauby, Rémy-Pradier, Renaud-Vincent, rgggn, rigelk, rip, Rivinbeg, Robert-Riemann, Robin Biechy, Roger-FRATTE, roipoussiere, Rolindes-Arroyo, Romain Théry-Hermain, Romain-Bouyé, Romain-Ortiz, RomainVENNE, Romuald-EYRAUD, royhome, Rudy-aparicio, Rusty-Dwyer, rverchere, sajous.net, Salah-ZERGUI, Sam-R, Samh, Samuel Tardieu, Samuel-FAYET, Samuel-Verschelde, Sanpi, Sascha-Brendel, Schwartz, Se7h, Sebastiaan-Glazenborg, Sebastian-Hugentobler, Sébastien Adam, Septie, Ser Eole, Severin-Suveren, severine-roger, shlagevuk-shlagevuk, Siegfried-Ehret, Simon-Hemery, Simon-Larcher, Simon-Reiser, Simounet, Siri-Louie, sissssou, skarab, Skurious, skynebula, Sohga-Sohga, Solène-Rapenne, solinux, Sophie-Imbach , Sosthen, Spiderweak, Stanislas-ANDRE, Stanislas-Michalak, starmatt, Steef, Stefan-Petrovski, Stéphane-Girardon, Stéphanie-Baltus, Stev-3d, Stoori, SuckyStrike, Sufflope, Sulfurax, SundownDEV, Swann-Fournial, Syk, Syluban, Sylv1c, Sylvain Bellone, Sylvain P, Sylvain\_M, Sylvain-Cazaux, Sylvain-GLAIZE, sylvain.arrachart, Sylvestre Ledru, sylvie-boutet, Sylvie-TORRES, tael67, tang35, tangi\_b, Tarulien, Taunya-Debolt, Tazimut-Khaelyor, terry-maire, Thanaen, Thatoo, Théophile-Noiré, Thibault-Vlieghe, Thierry-Chancé, Thierry-Fenasse, Thomas-Aurel, Thomas-CALVEZ, thomas-constans, Thomas-Kuntz, thomassin-loucas, Thosbk, ticosc, Tim-Albers, Tinapa -Itastri, TkPx, TM, tnntwister, TomR, Tomus, Tonio-Bilos, tony-carnide, Toover, toto-leroidelasaucisse, ToumToum, TP., trigrou, Tristan-Porteries, Tryph, Tursiops, tzilliox, U-&\_\`HbAAe4onnpN9!e+/#42\*5>k^E, Ulrich-Norbisrath, Un Sur Quatre, Valerio-Paladino, Valerio-Pilo, Valeryan\_24, Valou69, Vegattitude, Velome, Vergogne, Vero-Pajot, vianneyb, Victo-Sab, Victor -Hery, Victorien-Labalette, Vincent-Corrèze, Vincent-Fromentin, Vincent-Lamy, Vincent-Lasseur, VINCENT-PEYRET, vmorel, Walter-van Holst, Watsdesign, Wesley-Moore, williampolletdev, win100, wyk, Xaloc-Xaloc, Xavier ALT, Xavier-Chantry, Xavier-Godard, XoD, Yaaann, Yann-Delaunoy, Yann-Nave, yannick-grenzinger, yanselmetti, Ykatsot, Yohann-Bacha, yopox, Youen-Toupin, Yves-Caniou, Yves-Gerech, zar-rok, ZeBlackPearl, ZeGreg 213Quonfucius, IP Solution, \_Laure\_, @lex666, 0x010C, 3dsman, 3rw4n-G3D, aallrd, Abel-Berger, Adam-Odell, adechambost, adim, adngdb, Adrien Thurotte, Adrien-BARAN, Adrien-Hamraoui, Adrien-Horcholle, Adrien-Luxey, Adrien-Polar, Adrien-Touminet, Agathe Begault, Agence-Différente, Ahmed-Al-Ahmed, aiprole, akpoptro, Al-Nimr, Alain-Delgrange, Alain-Fyon, Alain-Girard, Alain-MICHEL, Aleksandar-Aleksandrov, Alex-Chancellé, Alex-Dufournet, Alex-Gleason, Alexander-Murray-Watters, Alexandre-Alapetite, Alexandre-Badez, Alexandre-Giuliani, Alexandre-Mercier, Alexandre-Roux-2, Alexandre-SIMON, Alexandre29, Alexia-Monsavoir, Alexis-Frn, Alexis-Gros, Alexis-Kauffmann, alfajet, Alias, alinemont, Aliocha-Lang, Alllightlong, aloisdg, Amanda Hinault, André-Rabe, Anne-PROTAS, antoine, Antoine Derouin, Antoine-Beauvillain, Antoine-Deléron, antomoro, Antón López, Antonin-DENIS, Antonin-Segault, aokami, Apichat-Apichat, Ar-To, ARIAS-Frédéric-2, ariasuni, Aris-Papathéodorou, Arnaud -Vigoureux , Arnaud-Mounier, Arnaud-Risler, Arnaud-Vigouroux, Arnulf, Arthur-Bellier, arthur-bello, Arthur-Charron, Arthur-De Kimpe, Arthur.Ball, Arthur.Frin, Arvi-LEFEVRE, athanael.fr, auber38, Auguste Psqr, Aurélien-Tamisier, Avel-Musicavel, axel-guegant, Axel-Plat, Aymeric-Dlv, Ayst, Azenilion, Bandino, baptiste-lemoine, Baptiste-Rochez, baruica, Bastien-Dangin, batlab, bcourtine, Bea-Schaack-2, beaufils, beaumme, Belmont1, Ben-Geeraerts, Ben-Meijering, Benjamin-Baratta, Benjamin-Roussel, Benoît Joffre, Benoîtdd, Bernard-Legrand, Bernard-Vauquelin, Bernhard-Hayden, bertrand.arlabosse, bigsicret, bjg, bnjbvr, bob\_isat, bobstechsite, Bolton-Allan, Boov', Boris-ARGAUD, Brice.Francois, broz42, Bruno Lefèvre, Bruno-Douville, Bruno-Fortabat, Bruno-Gadaleta, Bruno-VASTA, Bumblebee, Butchcassidy, Cadiou-Christophe, calendros, Candy-Ming, cappitaine, Carmen-Drocourt, carrigns, case, Cathy-Barbet, CBach, ccazin, Cecile-Obernesser, Cecilia-:), Cédric-Bleschet, Cédric.Bayle, Cestdoncvrai, cgay, champ contrechamp, chapa, charlerlin, charles-jacquin, Charlie-Duclut, charlotte-cgondre78, Chris-Doe, chris-louba, Christel-Berthelot, Christian-FERRARIS, christiannavelot, Christophe-Bastin, christophe-beziers la fosse, Christophe-Pieret, Christophe-Verhaege, christophec, Christopher-Bero, chtfn, chud, Claire-C, clairezed, Claude-POUGHEON, Clément-Hubert, Clément-Morelle, clydeb, Comamanel, Côme Chilliet, Confederac.io, Consulting-AZAPTEC, Corentin3892, CryoGen, cyp, Cypher-Goat, Cyril, Cyril\_M\_, Cyril-MONMOUTON, Cyril-Waechter, Damien-Gabard, Damien-Garaud, Dams3132, Daniel Kuebler, Daniel Waxweiler, Daniel-Bartsch, Daniel-PIPALA, Daniel-Struck, Daniel-Thul, Danny-Joerger, DansLeRuSH, DantSu, Dany-Marcoux, Daouzli-Adel, Darfeld, Darth\_Judge, Dashcom, David-BADOIL, David-Benoist, David-Dormoy, David-Gil-2, David-Velasco, David-Wagner, David-writ, davlgd, davyg2, dbudo72300, de Folleville -Matthieu , DeBugs, Denis-Lecourtiller, Denis-Vannier, Desmu, Didier-Bove, Diego-Crespo, Dimitri-Stouney, dino, Dinosaure, Doc Skellington, Dominique-Brun, dr4Ke, DreamClassier, DRogueRonin, dussydelf, Dylan-Moonfire, Ealhad, Edouard-SCHWEISGUTH, Elanndelh--, ElodieEtJimmy, Éloi-Rivard, Elric-Noel, Elwan-Héry, Emilie-Wietzke, Emilien-Ghomi, eparth, Eric-Bouhana, Eric-Hendricks, Eric.Vales, Erwan-Moreau, Erzender, ESS\_Clem, Etienne-Baqué, Etienne-Botek, Etienne-Lmn, Ex-Serv, fabeveynes, Fabien BERINI ( Rehvaro ) , Fabien Freling, Fabien-Roualdes, Fabien.Abraini, Fabien.Bonneval, fabrice-simon, farlistener, Felix-ROBICHON, FelixDouet, FHE, Fiamoa-McBenson, flamwenco, Flopômpôm, FloraGC, Florent-Deschamps, Florent-Fayolle, Florent-Mallet, Florent-Vasseur, Florent.Duveau, Florestan Fournier, Florian Kohrt, Florian-Bellafont, Florian-Douay, Florian-LE GOFF, Florian-Siegenthaler, Florian.Freyss, fobrice, FOKUZA, Fol-De Dol, FP45, Francis.Moraud, François-Dambrine, François-Deguerry, Francois-Goer, François-Lecomte, François-Lemaire, François-Malterre, François-MORLET, François-Schoubben, François-Xavier-Davanne, François-Zajéga, francois.peyratout, Frathom, Fred-Fred-2, Frédéric GUÉLEN, Frédéric-Blumstein, Frédéric-Meurou, Frederic-Reynaud, Frédéric-Sagot, Frek, FrenchHope, freyja, FugazziPL, Funky-Whale, Gabriel-Devillers, Gabriel-Mirété, Galedas, GardoToF, Gaspard-Kemlin, GauthierPLM, Gauvain "GovanifY" Roussel-Tarbouriech, Gavy, gdquest, Geek Faëries, Geneviève-Perello, Geoffroy-MANAUD, Geojulien, Georges-Dutreix, Georges-Sempéré, Gerald-Vannier, Gérard-Brasquet, Gérard-Sensevy, Gerrit-Großkopf, GGBNM, Ghislain-Fabre, Gil-Felot, Gilles-Brossier, Gilles-Moisan, Gilles-SACLIER, Gilles-Trossevin, Gilou, GinGa, ginkgopr, glazzara, Glen-Lomax, Gof, Gonçalves-Daniel, goofy-goofy, grandlap, GRAP-Groupement Régional Alimentaire de Proximité, greg-chapuis, Grégoire-Delbeke, Grégory-Becq, Grégory-Goulaouic, Gregouw, Grizix, GrosCaillou, Grummfy, grumph, guiaug, Guillaume-Allart, Guillaume-Chambert, Guillaume-Chaslot, Guillaume-David, Guillaume-Duc, Guillaume-Gay, Guillaume-Lecoquierre, Guillaume007, guillaumefavre, Guiraud-Dominique, Guy-Torreilles, GwendalL, gwlolos, Hanna-E, Hanno-Wagner, Harald-Eilertsen, Harpocrate, Hebus82, Hellmut, Henri-ROS, hervelc, hguilbert, Hisham-Muhammad, Hoang-Mai-Lesaffre, Homerc, homosapienssapiens, hoper, Hoshin, Hugo-Lagouge, Hugo-SIMANCAS, Hugo-Simon, Hylm, IchbinRob, Ivan-Ogai, Ivan.D'halluin, Ivar-Troost, J-C-2, Jacques-Roos, James-Moore, James-Valleroy, Jan-Aagaard, Jan-Keromnes, Jancry, Janko-Mihelić, jano31coa, Jboot, jcgross, Jean CHARPENTIER, jean claude-skowron, Jean Dos, jean luc-PERROT, Jean-Baptiste-Maneyrol, Jean-charles-Surbayrole, Jean-claude-Jouanne, jean-dreyfus, jean-FISCHER, JEAN-FRANCOIS-BOUDEAU, Jean-Francois-Ducrot, Jean-François-PETITBON, Jean-François-Tomasi, Jean-Galland, Jean-louis-Bergamo, Jean-Luc-PIPO, Jean-Marie-Graïc, Jean-Martin Laval, Jean-Noel-Bruletout, Jean-Paul-GIBERT, Jean-Paul-Lescat, jean-philippe-bénétrix, Jean-Philippe-Eisenbarth, Jean-Philippe-Renaudet, Jean-Philippe-Rennard, Jean-Sébastien-Renaud, Jean-Yves Kiger, Jean-Yves-DUPARC, Jeanne-Corvellec, jeansebastien, Jelv, Jérémie -Wach, Jeremie-Lestel, Jérémy-Korwin, Jérôme-Avond, Jerome-Bu, Jerome-Denis, Jérôme-ISNARD, jerome-simonato, JeromeD, Jery, Jezza, Jim-McDoniel, jl-M-2, jlanca, jlcpuzzle, jn-m, jnthnctt, joakim.faiss, Joe-Riche, Joévin-SOULENQ, Johann-FONTAINE, John-Devor, John-Doe, Jojo-Boulix, Jonas-Aparicio, Jonathan-Dollé, Jonathan-Kohler, Jonathan-LAURENT, Jos-van den Oever, Joseph-Lawson, Jozef-Knaperek, jroger, ju, jubarbu, Julianoe-G, Julie-Bultez, Julien Loudet, Julien Maulny (alcalyn), Julien-AILHAUD, Julien-Aubin, Julien-Biaudet, Julien-Bréchet, Julien-Cochennec, Julien-Duroure, Julien-Huon, Julien-Lemaire, Julien-Weber, jyb, K-\_, KalambakA, Kanor, kari-kimber, Karim-Jouini, karl-bienfait, Kdecherf, Keplerpondorskell, kevin-Beranger, Kevin-Nguyen, King-Of Peons, Kioob, kloh, kokoklems, Konstantin-Kovar, Kriĉjo, Kyâne-PICHOU, L'elfe-Sylvain, La Gonz, Lara-Dufour, lareinedeselfes, Laurence-Giroud, laurent-fuentes, Laurent-HEINTZ, Laurent-PICQUENOT, ldubost, lebidibule, LeChi, LeDivinBueno, Legrave, Les Assortis, Leyokki-Tk, LibreEnFete-en Tregor, LilO. Moino, Liloumuloup, Linuxine-T, lionel-lachaud, Lionel-Schinckus, Loïc-L'Anton, Loïc.Guérin, Louis-Gatin, Louis-Marie-BAER, Louis-Rémi.Babé, Louis-Roche, Louisclement, Lu, ludovic-lainard, Ludovic-Pénet, Lukas-Steiblys, lusoheart, Mad Sugar, maguy-giorgi, mahen, maiido, Malphas, ManetteBE, Manon-Amalric, Manuel-Vazquez, ManuInzesky, Manumerique, Marc-BESSIERES, Marc-DUFOURNET, Marc-GASSER, Marc-Honnorat, marc-wilzius, marc.ribault.1, Marco-Heisig, Marie-PACHECO, Marien-Fressinaud, Marius-Lemonnier, Mark-O'Donovan, marliebo, marmat8951, mart1n, martensite, Mathdatech, Mathias-Bocquet, Mathieu-Amirault, Mathieu-B., Mathieu-Cornic, Mathieu-VIRAMAN, Matías-Pérez, Matilin-Torre, matt.faure, Mattéo-Delabre, Matthias-Devlamynck, Matthieu-Bollot, Matthieu-De Beule, Matthieu-DEVILLERS, Matthieu-Dupont de Dinechin, Matthieu-Gaudé, Matthieu-Sauboua-Beneluz, matthieublanco, MatthieuSchneider, Max-PENY, Maxime-de WYROW, Maxime-Desjardin, Maxime-Forest, maxime-haag, Maxime-Mangel, Maximilian Praeger, Mayeul-Cantan, Mayeul-Guiraud, mcg1712, metalvinze, Mewen, mheiber, Michael-Koppmann, Michael-Loew, Michael-Q. Bid, Michal-Herda, Michal-Noga, Michel-DUPONT, Michel-Le Lagadec, Michel-POUSSIER, Michel-Roux, Mickaël-Gauvin, Mickael-Liegard, MicMP3Man, Miguel-de la Cruz, Mike-Kasprzak, Mimon-Lapompe, Mister-Ocelot, mjhvc, Moutmout, MouTom, MP, mphdp, Mr-Tea, msellebulle, Mushussu, mylainos, nanouckd, Nasser-Debruyere, Nat-Tuck, Nathan.B, nayya, nazgulz666, Neal-Wilson, neeev, neodarz-neodarz, NepsKi, Nestorvep, NHenry, Nialix, NicoD, Nicolas-Auvray, nicolas-k, Nicolas-Pinault, Nicolas-Ruffel, NicolasCARPi, nicolaslegland, niconil, Niles, nitot, Nono1965, Norbert, Norde, Numcap, obergix, Obrow, Okki, Olivier-Calzi, Olivier-Ganneval, Olivier-Marouzé, Olivier-Mondoloni, olivier-pierret, Oncela-Petit Chat, Óskar-Sturluson, p3n15634n7, Paindesegle, Pas De-Panique, Pascal-BLEUSE, Pascal-Larramendy, Patrice-Jabeneau, patrice-maertens, patrick-bappel, PATRICK-GRANDIN, Patrick-MERCIER, Patrickl , Paul-Härle, Paul-Tardy, pbramy, Pedro-CADETE, Perrine-de Coëtlogon, Peter\_Fillgod, Petter-Joelson, Philippe-BATTMANN, Philippe-Cabaud, Philippe-Debar, philippe-giffard, Philippe-Lallemant, Philippe-Le Van, philippe-lhardy, Philippe-Thébault, Philippe-VINCENT-2, PhilOGM, Pierre 'catwell' Chapuis, Pierre Gros, Pierre-Antoine-Champin, Pierre-Bresson-2, Pierre-d'Alençon, Pierre-Equoy, Pierre-Girardeau, Pierre-Houmeau, Pierre-Marijon, Pierre-petch, Pierrick-Couturier, Pilou-CaraGk, Piotr-Miszczak, Pla, Plastic Yogi, PME2050, pmiossec, Pofilo, Polioman, Polios63, Poutchiny, PRALLET-Claude, PtrckVllnv, Pulov Yuran, queertube, Quentin-Dugne, Quentin-PAGÈS, ra-mon, Radhwan-Ben Madhkour, Raphaël-Brocq, Raphaël-Grolimund, Raphaël-Piédallu, raphane, Raphip, Raven, Raymond-Lutz, Razael, Rebecca-Breu, Remi-Durand, Rémi-Herrmann, Rémi-Verschelde, Remigho, Remix-the commons, Remy-Grauby, Rémy-Pradier, Renaud-Vincent, rgggn, rigelk, rip, Rivinbeg, Robert-Riemann, Robin Biechy, Roger-FRATTE, roipoussiere, Rolindes-Arroyo, Romain Théry-Hermain, Romain-Bouyé, Romain-Ortiz, RomainVENNE, Romuald-EYRAUD, royhome, Rudy-aparicio, Rusty-Dwyer, rverchere, sajous.net, Salah-ZERGUI, Sam-R, Samh, Samuel Tardieu, Samuel-FAYET, Samuel-Verschelde, Sanpi, Sascha-Brendel, Schwartz, Se7h, Sebastiaan-Glazenborg, Sebastian-Hugentobler, Sébastien Adam, Septie, Ser Eole, Severin-Suveren, severine-roger, shlagevuk-shlagevuk, Siegfried-Ehret, Simon-Hemery, Simon-Larcher, Simon-Reiser, Simounet, Siri-Louie, sissssou, skarab, Skurious, skynebula, Sohga-Sohga, Solène-Rapenne, solinux, Sophie-Imbach , Sosthen, Spiderweak, Stanislas-ANDRE, Stanislas-Michalak, starmatt, Steef, Stefan-Petrovski, Stéphane-Girardon, Stéphanie-Baltus, Stev-3d, Stoori, SuckyStrike, Sufflope, Sulfurax, SundownDEV, Swann-Fournial, Syk, Syluban, Sylv1c, Sylvain Bellone, Sylvain P, Sylvain\_M, Sylvain-Cazaux, Sylvain-GLAIZE, sylvain.arrachart, Sylvestre Ledru, sylvie-boutet, Sylvie-TORRES, tael67, tang35, tangi\_b, Tarulien, Taunya-Debolt, Tazimut-Khaelyor, terry-maire, Thanaen, Thatoo, Théophile-Noiré, Thibault-Vlieghe, Thierry-Chancé, Thierry-Fenasse, Thomas-Aurel, Thomas-CALVEZ, thomas-constans, Thomas-Kuntz, thomassin-loucas, Thosbk, ticosc, Tim-Albers, Tinapa -Itastri, TkPx, TM, tnntwister, TomR, Tomus, Tonio-Bilos, tony-carnide, Toover, toto-leroidelasaucisse, ToumToum, TP., trigrou, Tristan-Porteries, Tryph, Tursiops, tzilliox, U-&\_\`HbAAe4onnpN9!e+/#42\*5>k^E, Ulrich-Norbisrath, Un Sur Quatre, Valerio-Paladino, Valerio-Pilo, Valeryan\_24, Valou69, Vegattitude, Velome, Vergogne, Vero-Pajot, vianneyb, Victo-Sab, Victor -Hery, Victorien-Labalette, Vincent-Corrèze, Vincent-Fromentin, Vincent-Lamy, Vincent-Lasseur, VINCENT-PEYRET, vmorel, Walter-van Holst, Watsdesign, Wesley-Moore, williampolletdev, win100, wyk, Xaloc-Xaloc, Xavier ALT, Xavier-Chantry, Xavier-Godard, XoD, Yaaann, Yann-Delaunoy, Yann-Nave, yannick-grenzinger, yanselmetti, Ykatsot, Yohann-Bacha, yopox, Youen-Toupin, Yves-Caniou, Yves-Gerech, zar-rok, ZeBlackPearl, ZeGreg
216 214
217 215
diff --git a/client/package.json b/client/package.json
index a1dd94b76..0c5734c55 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "peertube-client", 2 "name": "peertube-client",
3 "version": "1.0.0", 3 "version": "1.1.0-alpha.1",
4 "private": true, 4 "private": true,
5 "licence": "GPLv3", 5 "licence": "GPLv3",
6 "author": { 6 "author": {
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 036e794d2..c1377c1ea 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -10,8 +10,15 @@
10 <div class="actor-name">{{ account.nameWithHost }}</div> 10 <div class="actor-name">{{ account.nameWithHost }}</div>
11 11
12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> 12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
13 13 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
14 <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"> 14 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Muted by your instance</span>
15 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Instance muted</span>
16 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
17
18 <my-user-moderation-dropdown
19 buttonSize="small" [account]="account" [user]="user"
20 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
21 >
15 </my-user-moderation-dropdown> 22 </my-user-moderation-dropdown>
16 </div> 23 </div>
17 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> 24 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 8c6db98d9..c06ae1d60 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -15,6 +15,7 @@ import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklis
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -41,6 +42,8 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service
41 VideoBlacklistListComponent, 42 VideoBlacklistListComponent,
42 VideoAbuseListComponent, 43 VideoAbuseListComponent,
43 ModerationCommentModalComponent, 44 ModerationCommentModalComponent,
45 InstanceServerBlocklistComponent,
46 InstanceAccountBlocklistComponent,
44 47
45 JobsComponent, 48 JobsComponent,
46 JobsListComponent, 49 JobsListComponent,
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index e2cbd35ca..dfbbfbb29 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -112,7 +112,7 @@
112 112
113 <my-peertube-checkbox 113 <my-peertube-checkbox
114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" 114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
115 i18n-labelText labelText="Video import with HTTP enabled" 115 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
116 ></my-peertube-checkbox> 116 ></my-peertube-checkbox>
117 117
118 <my-peertube-checkbox 118 <my-peertube-checkbox
diff --git a/client/src/app/+admin/moderation/instance-blocklist/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts
new file mode 100644
index 000000000..3e7a344bb
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './instance-account-blocklist.component'
2export * from './instance-server-blocklist.component'
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
new file mode 100644
index 000000000..7797bc56e
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -0,0 +1,22 @@
1<p-table
2 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Account</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 </tr>
11 </ng-template>
12
13 <ng-template pTemplate="body" let-accountBlock>
14 <tr>
15 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
16 <td>{{ accountBlock.createdAt }}</td>
17 <td class="action-cell">
18 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
19 </td>
20 </tr>
21 </ng-template>
22</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
new file mode 100644
index 000000000..3f243aee4
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
@@ -0,0 +1,59 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService, AccountBlock } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-instance-account-blocklist',
10 styleUrls: [ './instance-account-blocklist.component.scss' ],
11 templateUrl: './instance-account-blocklist.component.html'
12})
13export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notificationsService: NotificationsService,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByInstance(blockedAccount)
36 .subscribe(
37 () => {
38 this.notificationsService.success(
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
41 )
42
43 this.loadData()
44 }
45 )
46 }
47
48 protected loadData () {
49 return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort)
50 .subscribe(
51 resultList => {
52 this.blockedAccounts = resultList.data
53 this.totalRecords = resultList.total
54 },
55
56 err => this.notificationsService.error(this.i18n('Error'), err.message)
57 )
58 }
59}
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
new file mode 100644
index 000000000..f634ba834
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -0,0 +1,23 @@
1<p-table
2 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Instance</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th></th>
11 </tr>
12 </ng-template>
13
14 <ng-template pTemplate="body" let-serverBlock>
15 <tr>
16 <td>{{ serverBlock.blockedServer.host }}</td>
17 <td>{{ serverBlock.createdAt }}</td>
18 <td class="action-cell">
19 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
20 </td>
21 </tr>
22 </ng-template>
23</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
new file mode 100644
index 000000000..130009dc7
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -0,0 +1,60 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared'
8
9@Component({
10 selector: 'my-instance-server-blocklist',
11 styleUrls: [ './instance-server-blocklist.component.scss' ],
12 templateUrl: './instance-server-blocklist.component.html'
13})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notificationsService: NotificationsService,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByInstance(host)
37 .subscribe(
38 () => {
39 this.notificationsService.success(
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted by your instance.', { host })
42 )
43
44 this.loadData()
45 }
46 )
47 }
48
49 protected loadData () {
50 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
51 .subscribe(
52 resultList => {
53 this.blockedServers = resultList.data
54 this.totalRecords = resultList.total
55 },
56
57 err => this.notificationsService.error(this.i18n('Error'), err.message)
58 )
59 }
60}
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html
index 91e87fcd4..01457936c 100644
--- a/client/src/app/+admin/moderation/moderation.component.html
+++ b/client/src/app/+admin/moderation/moderation.component.html
@@ -5,6 +5,10 @@
5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> 5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
6 6
7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> 7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
8
9 <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10
11 <a *ngIf="hasServersBlocklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a>
8 </div> 12 </div>
9</div> 13</div>
10 14
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index 0f4efb970..2b2618933 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -16,4 +16,12 @@ export class ModerationComponent {
16 hasVideoBlacklistRight () { 16 hasVideoBlacklistRight () {
17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) 17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
18 } 18 }
19
20 hasAccountsBlocklistRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
22 }
23
24 hasServersBlocklistRight () {
25 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)
26 }
19} 27}
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 6d81b9b36..bc6dd49d5 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 6import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [
46 title: 'Blacklisted videos' 47 title: 'Blacklisted videos'
47 } 48 }
48 } 49 }
50 },
51 {
52 path: 'blocklist/accounts',
53 component: InstanceAccountBlocklistComponent,
54 canActivate: [ UserRightGuard ],
55 data: {
56 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
57 meta: {
58 title: 'Muted accounts'
59 }
60 }
61 },
62 {
63 path: 'blocklist/servers',
64 component: InstanceServerBlocklistComponent,
65 canActivate: [ UserRightGuard ],
66 data: {
67 userRight: UserRight.MANAGE_SERVER_REDUNDANCY,
68 meta: {
69 title: 'Muted instances'
70 }
71 }
49 } 72 }
50 ] 73 ]
51 } 74 }
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index 287ab3e46..0374b70ef 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -9,7 +9,7 @@
9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th i18n>Video</th> 10 <th i18n>Video</th>
11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> 11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
12 <th style="width: 50px;"></th> 12 <th style="width: 120px;"></th>
13 </tr> 13 </tr>
14 </ng-template> 14 </ng-template>
15 15
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index 0585e0490..ff4543b97 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -8,7 +8,7 @@
8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> 8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
9 <th i18n>Sensitive</th> 9 <th i18n>Sensitive</th>
10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> 10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
11 <th style="width: 50px;"></th> 11 <th style="width: 120px;"></th>
12 </tr> 12 </tr>
13 </ng-template> 13 </ng-template>
14 14
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 07b087b5b..99ce5804b 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,7 +1,6 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6 5
7export abstract class UserEdit extends FormReactive { 6export abstract class UserEdit extends FormReactive {
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index afa9ccfe4..eb8d30e17 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -60,8 +60,10 @@
60 </td> 60 </td>
61 61
62 <td> 62 <td>
63 {{ user.username }} 63 <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
64 <span *ngIf="user.blocked" class="banned-info">(banned)</span> 64 {{ user.username }}
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a>
65 </td> 67 </td>
66 <td>{{ user.email }}</td> 68 <td>{{ user.email }}</td>
67 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 69 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 33384dc35..ab2250722 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -55,20 +55,6 @@ export class UserListComponent extends RestTable implements OnInit {
55 ] 55 ]
56 } 56 }
57 57
58 protected loadData () {
59 this.selectedUsers = []
60
61 this.userService.getUsers(this.pagination, this.sort, this.search)
62 .subscribe(
63 resultList => {
64 this.users = resultList.data
65 this.totalRecords = resultList.total
66 },
67
68 err => this.notificationsService.error(this.i18n('Error'), err.message)
69 )
70 }
71
72 openBanUserModal (users: User[]) { 58 openBanUserModal (users: User[]) {
73 for (const user of users) { 59 for (const user of users) {
74 if (user.username === 'root') { 60 if (user.username === 'root') {
@@ -131,4 +117,18 @@ export class UserListComponent extends RestTable implements OnInit {
131 isInSelectionMode () { 117 isInSelectionMode () {
132 return this.selectedUsers.length !== 0 118 return this.selectedUsers.length !== 0
133 } 119 }
120
121 protected loadData () {
122 this.selectedUsers = []
123
124 this.userService.getUsers(this.pagination, this.sort, this.search)
125 .subscribe(
126 resultList => {
127 this.users = resultList.data
128 this.totalRecords = resultList.total
129 },
130
131 err => this.notificationsService.error(this.i18n('Error'), err.message)
132 )
133 }
134} 134}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
new file mode 100644
index 000000000..a96a11f5e
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
@@ -0,0 +1,26 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted accounts</div>
3</div>
4
5<p-table
6 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Account</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 </tr>
15 </ng-template>
16
17 <ng-template pTemplate="body" let-accountBlock>
18 <tr>
19 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
20 <td>{{ accountBlock.createdAt }}</td>
21 <td class="action-cell">
22 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
23 </td>
24 </tr>
25 </ng-template>
26</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
new file mode 100644
index 000000000..fbad28410
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
@@ -0,0 +1,59 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService, AccountBlock } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-account-blocklist',
10 styleUrls: [ './my-account-blocklist.component.scss' ],
11 templateUrl: './my-account-blocklist.component.html'
12})
13export class MyAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notificationsService: NotificationsService,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByUser(blockedAccount)
36 .subscribe(
37 () => {
38 this.notificationsService.success(
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
41 )
42
43 this.loadData()
44 }
45 )
46 }
47
48 protected loadData () {
49 return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
50 .subscribe(
51 resultList => {
52 this.blockedAccounts = resultList.data
53 this.totalRecords = resultList.total
54 },
55
56 err => this.notificationsService.error(this.i18n('Error'), err.message)
57 )
58 }
59}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
new file mode 100644
index 000000000..329cfb08f
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
@@ -0,0 +1,27 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted instances</div>
3</div>
4
5<p-table
6 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Instance</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 <th></th>
15 </tr>
16 </ng-template>
17
18 <ng-template pTemplate="body" let-serverBlock>
19 <tr>
20 <td>{{ serverBlock.blockedServer.host }}</td>
21 <td>{{ serverBlock.createdAt }}</td>
22 <td class="action-cell">
23 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
24 </td>
25 </tr>
26 </ng-template>
27</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
new file mode 100644
index 000000000..b411d6926
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
@@ -0,0 +1,60 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { ServerBlock } from '../../../../../shared'
7import { BlocklistService } from '@app/shared/blocklist'
8
9@Component({
10 selector: 'my-account-server-blocklist',
11 styleUrls: [ './my-account-server-blocklist.component.scss' ],
12 templateUrl: './my-account-server-blocklist.component.html'
13})
14export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notificationsService: NotificationsService,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByUser(host)
37 .subscribe(
38 () => {
39 this.notificationsService.success(
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted.', { host })
42 )
43
44 this.loadData()
45 }
46 )
47 }
48
49 protected loadData () {
50 return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
51 .subscribe(
52 resultList => {
53 this.blockedServers = resultList.data
54 this.totalRecords = resultList.total
55 },
56
57 err => this.notificationsService.error(this.i18n('Error'), err.message)
58 )
59 }
60}
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
index 520278671..0b51ac13c 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
@@ -34,18 +34,6 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
34 this.initialize() 34 this.initialize()
35 } 35 }
36 36
37 protected loadData () {
38 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
39 .subscribe(
40 resultList => {
41 this.videoChangeOwnerships = resultList.data
42 this.totalRecords = resultList.total
43 },
44
45 err => this.notificationsService.error(this.i18n('Error'), err.message)
46 )
47 }
48
49 createByString (account: Account) { 37 createByString (account: Account) {
50 return Account.CREATE_BY_STRING(account.name, account.host) 38 return Account.CREATE_BY_STRING(account.name, account.host)
51 } 39 }
@@ -65,4 +53,16 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
65 err => this.notificationsService.error(this.i18n('Error'), err.message) 53 err => this.notificationsService.error(this.i18n('Error'), err.message)
66 ) 54 )
67 } 55 }
56
57 protected loadData () {
58 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
59 .subscribe(
60 resultList => {
61 this.videoChangeOwnerships = resultList.data
62 this.totalRecords = resultList.total
63 },
64
65 err => this.notificationsService.error(this.i18n('Error'), err.message)
66 )
67 }
68} 68}
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 4b2168e35..601e517b4 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -11,6 +11,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' 13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
14import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
14 16
15const myAccountRoutes: Routes = [ 17const myAccountRoutes: Routes = [
16 { 18 {
@@ -94,6 +96,24 @@ const myAccountRoutes: Routes = [
94 title: 'Ownership changes' 96 title: 'Ownership changes'
95 } 97 }
96 } 98 }
99 },
100 {
101 path: 'blocklist/accounts',
102 component: MyAccountBlocklistComponent,
103 data: {
104 meta: {
105 title: 'Muted accounts'
106 }
107 }
108 },
109 {
110 path: 'blocklist/servers',
111 component: MyAccountServerBlocklistComponent,
112 data: {
113 meta: {
114 title: 'Muted instances'
115 }
116 }
97 } 117 }
98 ] 118 ]
99 } 119 }
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index b602fd69f..41333c25a 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -19,7 +19,21 @@
19 </div> 19 </div>
20 </div> 20 </div>
21 21
22 <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a> 22 <div ngbDropdown class="misc">
23 <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
24 <ng-container i18n>Misc</ng-container>
25 <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
26 </span>
27
28 <div ngbDropdownMenu>
29 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
30
31 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
32
33 <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
34 </div>
35 </div>
36
23 </div> 37 </div>
24 38
25 <div class="margin-content"> 39 <div class="margin-content">
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss
index 20b2639b5..6243c6dcf 100644
--- a/client/src/app/+my-account/my-account.component.scss
+++ b/client/src/app/+my-account/my-account.component.scss
@@ -1,4 +1,4 @@
1.my-library { 1.my-library, .misc {
2 span[role=button] { 2 span[role=button] {
3 cursor: pointer; 3 cursor: pointer;
4 } 4 }
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index bad60a8fb..d728caf07 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -13,6 +13,7 @@ import { Subscription } from 'rxjs'
13export class MyAccountComponent implements OnInit, OnDestroy { 13export class MyAccountComponent implements OnInit, OnDestroy {
14 14
15 libraryLabel = '' 15 libraryLabel = ''
16 miscLabel = ''
16 17
17 private routeSub: Subscription 18 private routeSub: Subscription
18 19
@@ -23,11 +24,11 @@ export class MyAccountComponent implements OnInit, OnDestroy {
23 ) {} 24 ) {}
24 25
25 ngOnInit () { 26 ngOnInit () {
26 this.updateLibraryLabel(this.router.url) 27 this.updateLabels(this.router.url)
27 28
28 this.routeSub = this.router.events 29 this.routeSub = this.router.events
29 .pipe(filter(event => event instanceof NavigationStart)) 30 .pipe(filter(event => event instanceof NavigationStart))
30 .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) 31 .subscribe((event: NavigationStart) => this.updateLabels(event.url))
31 } 32 }
32 33
33 ngOnDestroy () { 34 ngOnDestroy () {
@@ -40,7 +41,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
40 return importConfig.http.enabled || importConfig.torrent.enabled 41 return importConfig.http.enabled || importConfig.torrent.enabled
41 } 42 }
42 43
43 private updateLibraryLabel (url: string) { 44 private updateLabels (url: string) {
44 const [ path ] = url.split('?') 45 const [ path ] = url.split('?')
45 46
46 if (path.startsWith('/my-account/video-channels')) { 47 if (path.startsWith('/my-account/video-channels')) {
@@ -54,5 +55,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
54 } else { 55 } else {
55 this.libraryLabel = '' 56 this.libraryLabel = ''
56 } 57 }
58
59 if (path.startsWith('/my-account/blocklist/accounts')) {
60 this.miscLabel = this.i18n('Muted accounts')
61 } else if (path.startsWith('/my-account/blocklist/servers')) {
62 this.miscLabel = this.i18n('Muted instances')
63 } else {
64 this.miscLabel = ''
65 }
57 } 66 }
58} 67}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index ad21162a8..017ebd57d 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -19,6 +19,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' 20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
22import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
23import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
22 24
23@NgModule({ 25@NgModule({
24 imports: [ 26 imports: [
@@ -45,7 +47,9 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
45 ActorAvatarInfoComponent, 47 ActorAvatarInfoComponent,
46 MyAccountVideoImportsComponent, 48 MyAccountVideoImportsComponent,
47 MyAccountDangerZoneComponent, 49 MyAccountDangerZoneComponent,
48 MyAccountSubscriptionsComponent 50 MyAccountSubscriptionsComponent,
51 MyAccountBlocklistComponent,
52 MyAccountServerBlocklistComponent
49 ], 53 ],
50 54
51 exports: [ 55 exports: [
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
index 911d56843..ecffcafc1 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, RedirectService } from '@app/core' 3import { AuthService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
5import { forkJoin, Subscription } from 'rxjs' 5import { forkJoin, Subscription } from 'rxjs'
6import { SearchService } from '@app/search/search.service' 6import { SearchService } from '@app/search/search.service'
@@ -40,7 +40,6 @@ export class SearchComponent implements OnInit, OnDestroy {
40 private route: ActivatedRoute, 40 private route: ActivatedRoute,
41 private router: Router, 41 private router: Router,
42 private metaService: MetaService, 42 private metaService: MetaService,
43 private redirectService: RedirectService,
44 private notificationsService: NotificationsService, 43 private notificationsService: NotificationsService,
45 private searchService: SearchService, 44 private searchService: SearchService,
46 private authService: AuthService 45 private authService: AuthService
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
index 42f2cfeaf..c5cd2051c 100644
--- a/client/src/app/shared/account/account.model.ts
+++ b/client/src/app/shared/account/account.model.ts
@@ -5,6 +5,10 @@ export class Account extends Actor implements ServerAccount {
5 displayName: string 5 displayName: string
6 description: string 6 description: string
7 nameWithHost: string 7 nameWithHost: string
8 mutedByUser: boolean
9 mutedByInstance: boolean
10 mutedServerByUser: boolean
11 mutedServerByInstance: boolean
8 12
9 userId?: number 13 userId?: number
10 14
@@ -15,5 +19,10 @@ export class Account extends Actor implements ServerAccount {
15 this.description = hash.description 19 this.description = hash.description
16 this.userId = hash.userId 20 this.userId = hash.userId
17 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 21 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
22
23 this.mutedByUser = false
24 this.mutedByInstance = false
25 this.mutedServerByUser = false
26 this.mutedServerByInstance = false
18 } 27 }
19} 28}
diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts
new file mode 100644
index 000000000..e7b433d88
--- /dev/null
+++ b/client/src/app/shared/blocklist/account-block.model.ts
@@ -0,0 +1,14 @@
1import { AccountBlock as AccountBlockServer } from '../../../../../shared'
2import { Account } from '../account/account.model'
3
4export class AccountBlock implements AccountBlockServer {
5 byAccount: Account
6 blockedAccount: Account
7 createdAt: Date | string
8
9 constructor (block: AccountBlockServer) {
10 this.byAccount = new Account(block.byAccount)
11 this.blockedAccount = new Account(block.blockedAccount)
12 this.createdAt = block.createdAt
13 }
14}
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts
new file mode 100644
index 000000000..c1f7312f0
--- /dev/null
+++ b/client/src/app/shared/blocklist/blocklist.service.ts
@@ -0,0 +1,135 @@
1import { Injectable } from '@angular/core'
2import { environment } from '../../../environments/environment'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { RestExtractor, RestPagination, RestService } from '../rest'
5import { SortMeta } from 'primeng/api'
6import { catchError, map } from 'rxjs/operators'
7import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared'
8import { Account } from '@app/shared/account/account.model'
9import { AccountBlock } from '@app/shared/blocklist/account-block.model'
10
11@Injectable()
12export class BlocklistService {
13 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
14 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor,
19 private restService: RestService
20 ) { }
21
22 /*********************** User -> Account blocklist ***********************/
23
24 getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
25 let params = new HttpParams()
26 params = this.restService.addRestGetParams(params, pagination, sort)
27
28 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
29 .pipe(
30 map(res => this.restExtractor.convertResultListDateToHuman(res)),
31 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
32 catchError(err => this.restExtractor.handleError(err))
33 )
34 }
35
36 blockAccountByUser (account: Account) {
37 const body = { accountName: account.nameWithHost }
38
39 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body)
40 .pipe(catchError(err => this.restExtractor.handleError(err)))
41 }
42
43 unblockAccountByUser (account: Account) {
44 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
45
46 return this.authHttp.delete(path)
47 .pipe(catchError(err => this.restExtractor.handleError(err)))
48 }
49
50 /*********************** User -> Server blocklist ***********************/
51
52 getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
53 let params = new HttpParams()
54 params = this.restService.addRestGetParams(params, pagination, sort)
55
56 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
57 .pipe(
58 map(res => this.restExtractor.convertResultListDateToHuman(res)),
59 catchError(err => this.restExtractor.handleError(err))
60 )
61 }
62
63 blockServerByUser (host: string) {
64 const body = { host }
65
66 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body)
67 .pipe(catchError(err => this.restExtractor.handleError(err)))
68 }
69
70 unblockServerByUser (host: string) {
71 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host
72
73 return this.authHttp.delete(path)
74 .pipe(catchError(err => this.restExtractor.handleError(err)))
75 }
76
77 /*********************** Instance -> Account blocklist ***********************/
78
79 getInstanceAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
80 let params = new HttpParams()
81 params = this.restService.addRestGetParams(params, pagination, sort)
82
83 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
84 .pipe(
85 map(res => this.restExtractor.convertResultListDateToHuman(res)),
86 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
87 catchError(err => this.restExtractor.handleError(err))
88 )
89 }
90
91 blockAccountByInstance (account: Account) {
92 const body = { accountName: account.nameWithHost }
93
94 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', body)
95 .pipe(catchError(err => this.restExtractor.handleError(err)))
96 }
97
98 unblockAccountByInstance (account: Account) {
99 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
100
101 return this.authHttp.delete(path)
102 .pipe(catchError(err => this.restExtractor.handleError(err)))
103 }
104
105 /*********************** Instance -> Server blocklist ***********************/
106
107 getInstanceServerBlocklist (pagination: RestPagination, sort: SortMeta) {
108 let params = new HttpParams()
109 params = this.restService.addRestGetParams(params, pagination, sort)
110
111 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
112 .pipe(
113 map(res => this.restExtractor.convertResultListDateToHuman(res)),
114 catchError(err => this.restExtractor.handleError(err))
115 )
116 }
117
118 blockServerByInstance (host: string) {
119 const body = { host }
120
121 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', body)
122 .pipe(catchError(err => this.restExtractor.handleError(err)))
123 }
124
125 unblockServerByInstance (host: string) {
126 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers/' + host
127
128 return this.authHttp.delete(path)
129 .pipe(catchError(err => this.restExtractor.handleError(err)))
130 }
131
132 private formatAccountBlock (accountBlock: AccountBlockServer) {
133 return new AccountBlock(accountBlock)
134 }
135}
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts
new file mode 100644
index 000000000..5886ca07e
--- /dev/null
+++ b/client/src/app/shared/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './blocklist.service'
2export * from './account-block.model'
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 111627424..48230d6d8 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -9,13 +9,13 @@
9 9
10 <div ngbDropdownMenu class="dropdown-menu"> 10 <div ngbDropdownMenu class="dropdown-menu">
11 <ng-container *ngFor="let action of actions"> 11 <ng-container *ngFor="let action of actions">
12 <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> 12 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
13 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> 13 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
14 14
15 <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button"> 15 <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button">
16 {{ action.label }} 16 {{ action.label }}
17 </span> 17 </span>
18 </div> 18 </ng-container>
19 </ng-container> 19 </ng-container>
20 </div> 20 </div>
21</div> \ No newline at end of file 21</div> \ No newline at end of file
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 0a9aa7b04..92c4d1d2c 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -46,5 +46,10 @@
46 .dropdown-item { 46 .dropdown-item {
47 cursor: pointer; 47 cursor: pointer;
48 color: #000 !important; 48 color: #000 !important;
49
50 a, span {
51 display: block;
52 width: 100%;
53 }
49 } 54 }
50} \ No newline at end of file 55} \ No newline at end of file
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
index 01db7cd4a..7367a7e59 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
@@ -1,5 +1,8 @@
1<ng-container *ngIf="user && userActions.length !== 0"> 1<ng-container *ngIf="userActions.length !== 0">
2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> 2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
3 3
4 <my-action-dropdown [actions]="userActions" [entry]="user" [buttonSize]="buttonSize" [placement]="placement"></my-action-dropdown> 4 <my-action-dropdown
5 [actions]="userActions" [entry]="{ user: user, account: account }"
6 [buttonSize]="buttonSize" [placement]="placement"
7 ></my-action-dropdown>
5</ng-container> \ No newline at end of file 8</ng-container> \ No newline at end of file
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index 105c99d8b..908f0b8e0 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -1,4 +1,4 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
@@ -6,33 +6,38 @@ import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.com
6import { UserService } from '@app/shared/users' 6import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService } from '@app/core' 7import { AuthService, ConfirmService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users' 8import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model'
10import { BlocklistService } from '@app/shared/blocklist'
9 11
10@Component({ 12@Component({
11 selector: 'my-user-moderation-dropdown', 13 selector: 'my-user-moderation-dropdown',
12 templateUrl: './user-moderation-dropdown.component.html', 14 templateUrl: './user-moderation-dropdown.component.html',
13 styleUrls: [ './user-moderation-dropdown.component.scss' ] 15 styleUrls: [ './user-moderation-dropdown.component.scss' ]
14}) 16})
15export class UserModerationDropdownComponent implements OnInit { 17export class UserModerationDropdownComponent implements OnChanges {
16 @ViewChild('userBanModal') userBanModal: UserBanModalComponent 18 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
17 19
18 @Input() user: User 20 @Input() user: User
21 @Input() account: Account
22
19 @Input() buttonSize: 'normal' | 'small' = 'normal' 23 @Input() buttonSize: 'normal' | 'small' = 'normal'
20 @Input() placement = 'left' 24 @Input() placement = 'left'
21 25
22 @Output() userChanged = new EventEmitter() 26 @Output() userChanged = new EventEmitter()
23 @Output() userDeleted = new EventEmitter() 27 @Output() userDeleted = new EventEmitter()
24 28
25 userActions: DropdownAction<User>[] = [] 29 userActions: DropdownAction<{ user: User, account: Account }>[] = []
26 30
27 constructor ( 31 constructor (
28 private authService: AuthService, 32 private authService: AuthService,
29 private notificationsService: NotificationsService, 33 private notificationsService: NotificationsService,
30 private confirmService: ConfirmService, 34 private confirmService: ConfirmService,
31 private userService: UserService, 35 private userService: UserService,
36 private blocklistService: BlocklistService,
32 private i18n: I18n 37 private i18n: I18n
33 ) { } 38 ) { }
34 39
35 ngOnInit () { 40 ngOnChanges () {
36 this.buildActions() 41 this.buildActions()
37 } 42 }
38 43
@@ -92,6 +97,142 @@ export class UserModerationDropdownComponent implements OnInit {
92 ) 97 )
93 } 98 }
94 99
100 blockAccountByUser (account: Account) {
101 this.blocklistService.blockAccountByUser(account)
102 .subscribe(
103 () => {
104 this.notificationsService.success(
105 this.i18n('Success'),
106 this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })
107 )
108
109 this.account.mutedByUser = true
110 this.userChanged.emit()
111 },
112
113 err => this.notificationsService.error(this.i18n('Error'), err.message)
114 )
115 }
116
117 unblockAccountByUser (account: Account) {
118 this.blocklistService.unblockAccountByUser(account)
119 .subscribe(
120 () => {
121 this.notificationsService.success(
122 this.i18n('Success'),
123 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })
124 )
125
126 this.account.mutedByUser = false
127 this.userChanged.emit()
128 },
129
130 err => this.notificationsService.error(this.i18n('Error'), err.message)
131 )
132 }
133
134 blockServerByUser (host: string) {
135 this.blocklistService.blockServerByUser(host)
136 .subscribe(
137 () => {
138 this.notificationsService.success(
139 this.i18n('Success'),
140 this.i18n('Instance {{host}} muted.', { host })
141 )
142
143 this.account.mutedServerByUser = true
144 this.userChanged.emit()
145 },
146
147 err => this.notificationsService.error(this.i18n('Error'), err.message)
148 )
149 }
150
151 unblockServerByUser (host: string) {
152 this.blocklistService.unblockServerByUser(host)
153 .subscribe(
154 () => {
155 this.notificationsService.success(
156 this.i18n('Success'),
157 this.i18n('Instance {{host}} unmuted.', { host })
158 )
159
160 this.account.mutedServerByUser = false
161 this.userChanged.emit()
162 },
163
164 err => this.notificationsService.error(this.i18n('Error'), err.message)
165 )
166 }
167
168 blockAccountByInstance (account: Account) {
169 this.blocklistService.blockAccountByInstance(account)
170 .subscribe(
171 () => {
172 this.notificationsService.success(
173 this.i18n('Success'),
174 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
175 )
176
177 this.account.mutedByInstance = true
178 this.userChanged.emit()
179 },
180
181 err => this.notificationsService.error(this.i18n('Error'), err.message)
182 )
183 }
184
185 unblockAccountByInstance (account: Account) {
186 this.blocklistService.unblockAccountByInstance(account)
187 .subscribe(
188 () => {
189 this.notificationsService.success(
190 this.i18n('Success'),
191 this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost })
192 )
193
194 this.account.mutedByInstance = false
195 this.userChanged.emit()
196 },
197
198 err => this.notificationsService.error(this.i18n('Error'), err.message)
199 )
200 }
201
202 blockServerByInstance (host: string) {
203 this.blocklistService.blockServerByInstance(host)
204 .subscribe(
205 () => {
206 this.notificationsService.success(
207 this.i18n('Success'),
208 this.i18n('Instance {{host}} muted by the instance.', { host })
209 )
210
211 this.account.mutedServerByInstance = true
212 this.userChanged.emit()
213 },
214
215 err => this.notificationsService.error(this.i18n('Error'), err.message)
216 )
217 }
218
219 unblockServerByInstance (host: string) {
220 this.blocklistService.unblockServerByInstance(host)
221 .subscribe(
222 () => {
223 this.notificationsService.success(
224 this.i18n('Success'),
225 this.i18n('Instance {{host}} unmuted by the instance.', { host })
226 )
227
228 this.account.mutedServerByInstance = false
229 this.userChanged.emit()
230 },
231
232 err => this.notificationsService.error(this.i18n('Error'), err.message)
233 )
234 }
235
95 getRouterUserEditLink (user: User) { 236 getRouterUserEditLink (user: User) {
96 return [ '/admin', 'users', 'update', user.id ] 237 return [ '/admin', 'users', 'update', user.id ]
97 } 238 }
@@ -102,28 +243,89 @@ export class UserModerationDropdownComponent implements OnInit {
102 if (this.authService.isLoggedIn()) { 243 if (this.authService.isLoggedIn()) {
103 const authUser = this.authService.getUser() 244 const authUser = this.authService.getUser()
104 245
105 if (authUser.hasRight(UserRight.MANAGE_USERS)) { 246 if (this.user && authUser.id === this.user.id) return
247
248 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) {
106 this.userActions = this.userActions.concat([ 249 this.userActions = this.userActions.concat([
107 { 250 {
108 label: this.i18n('Edit'), 251 label: this.i18n('Edit'),
109 linkBuilder: this.getRouterUserEditLink 252 linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
110 }, 253 },
111 { 254 {
112 label: this.i18n('Delete'), 255 label: this.i18n('Delete'),
113 handler: user => this.removeUser(user) 256 handler: ({ user }) => this.removeUser(user)
114 }, 257 },
115 { 258 {
116 label: this.i18n('Ban'), 259 label: this.i18n('Ban'),
117 handler: user => this.openBanUserModal(user), 260 handler: ({ user }: { user: User }) => this.openBanUserModal(user),
118 isDisplayed: user => !user.blocked 261 isDisplayed: ({ user }: { user: User }) => !user.blocked
119 }, 262 },
120 { 263 {
121 label: this.i18n('Unban'), 264 label: this.i18n('Unban'),
122 handler: user => this.unbanUser(user), 265 handler: ({ user }: { user: User }) => this.unbanUser(user),
123 isDisplayed: user => user.blocked 266 isDisplayed: ({ user }: { user: User }) => user.blocked
124 } 267 }
125 ]) 268 ])
126 } 269 }
270
271 // Actions on accounts/servers
272 if (this.account) {
273 // User actions
274 this.userActions = this.userActions.concat([
275 {
276 label: this.i18n('Mute this account'),
277 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false,
278 handler: ({ account }: { account: Account }) => this.blockAccountByUser(account)
279 },
280 {
281 label: this.i18n('Unmute this account'),
282 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true,
283 handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account)
284 },
285 {
286 label: this.i18n('Mute the instance'),
287 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false,
288 handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host)
289 },
290 {
291 label: this.i18n('Unmute the instance'),
292 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true,
293 handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host)
294 }
295 ])
296
297 // Instance actions
298 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
299 this.userActions = this.userActions.concat([
300 {
301 label: this.i18n('Mute this account by your instance'),
302 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === false,
303 handler: ({ account }: { account: Account }) => this.blockAccountByInstance(account)
304 },
305 {
306 label: this.i18n('Unmute this account by your instance'),
307 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === true,
308 handler: ({ account }: { account: Account }) => this.unblockAccountByInstance(account)
309 }
310 ])
311 }
312
313 // Instance actions
314 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
315 this.userActions = this.userActions.concat([
316 {
317 label: this.i18n('Mute the instance by your instance'),
318 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false,
319 handler: ({ account }: { account: Account }) => this.blockServerByInstance(account.host)
320 },
321 {
322 label: this.i18n('Unmute the instance by your instance'),
323 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true,
324 handler: ({ account }: { account: Account }) => this.unblockServerByInstance(account.host)
325 }
326 ])
327 }
328 }
127 } 329 }
128 } 330 }
129} 331}
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
index 26748f245..884588207 100644
--- a/client/src/app/shared/rest/rest-table.ts
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -16,8 +16,6 @@ export abstract class RestTable {
16 private searchStream: Subject<string> 16 private searchStream: Subject<string>
17 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name 17 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name
18 18
19 protected abstract loadData (): void
20
21 initialize () { 19 initialize () {
22 this.loadSort() 20 this.loadSort()
23 this.initSearch() 21 this.initSearch()
@@ -71,4 +69,6 @@ export abstract class RestTable {
71 onSearch (search: string) { 69 onSearch (search: string) {
72 this.searchStream.next(search) 70 this.searchStream.next(search)
73 } 71 }
72
73 protected abstract loadData (): void
74} 74}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 9647a7966..40e05fcc7 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -58,6 +58,7 @@ import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-fe
58import { OverviewService } from '@app/shared/overview' 58import { OverviewService } from '@app/shared/overview'
59import { UserBanModalComponent } from '@app/shared/moderation' 59import { UserBanModalComponent } from '@app/shared/moderation'
60import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' 60import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
61import { BlocklistService } from '@app/shared/blocklist'
61 62
62@NgModule({ 63@NgModule({
63 imports: [ 64 imports: [
@@ -172,6 +173,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
172 OverviewService, 173 OverviewService,
173 VideoChangeOwnershipValidatorsService, 174 VideoChangeOwnershipValidatorsService,
174 VideoAcceptOwnershipValidatorsService, 175 VideoAcceptOwnershipValidatorsService,
176 BlocklistService,
175 177
176 I18nPrimengCalendarService, 178 I18nPrimengCalendarService,
177 ScreenService, 179 ScreenService,
diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss
index ce2532af5..77a20cfe1 100644
--- a/client/src/sass/include/_bootstrap-variables.scss
+++ b/client/src/sass/include/_bootstrap-variables.scss
@@ -29,4 +29,6 @@ $input-btn-focus-color: inherit;
29$input-focus-border-color: #ced4da; 29$input-focus-border-color: #ced4da;
30 30
31$nav-pills-link-active-bg: #F0F0F0; 31$nav-pills-link-active-bg: #F0F0F0;
32$nav-pills-link-active-color: #000; \ No newline at end of file 32$nav-pills-link-active-color: #000;
33
34$zindex-dropdown: 10000; \ No newline at end of file
diff --git a/client/tsconfig.json b/client/tsconfig.json
index e041769dd..431ea7d91 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -8,6 +8,7 @@
8 "emitDecoratorMetadata": true, 8 "emitDecoratorMetadata": true,
9 "experimentalDecorators": true, 9 "experimentalDecorators": true,
10 "noImplicitAny": false, 10 "noImplicitAny": false,
11 "alwaysStrict": true,
11 "target": "es5", 12 "target": "es5",
12 "typeRoots": [ 13 "typeRoots": [
13 "node_modules/@types" 14 "node_modules/@types"
diff --git a/client/tslint.json b/client/tslint.json
index e997088fd..fcc866ee3 100644
--- a/client/tslint.json
+++ b/client/tslint.json
@@ -7,10 +7,18 @@
7 "max-line-length": [true, 140], 7 "max-line-length": [true, 140],
8 "no-floating-promises": false, 8 "no-floating-promises": false,
9 "no-unused-variable": false, // Memory issues 9 "no-unused-variable": false, // Memory issues
10 "member-ordering": [true, 10 "member-ordering": [true, {
11 "public-before-private", 11 "order": [
12 "static-before-instance", 12 "public-static-field",
13 "variables-before-functions" 13 "private-static-field",
14 "public-instance-field",
15 "private-instance-field",
16 "public-constructor",
17 "private-constructor",
18 "public-instance-method",
19 "protected-instance-method",
20 "private-instance-method"
21 ]}
14 ], 22 ],
15 23
16 "angular-whitespace": [true, "check-interpolation", "check-semicolon"], 24 "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
diff --git a/package.json b/package.json
index 1fd6d7d19..e5b2bb50e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
1{ 1{
2 "name": "peertube", 2 "name": "peertube",
3 "description": "Federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.", 3 "description": "Federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.",
4 "version": "1.0.0", 4 "version": "1.1.0-alpha.1",
5 "private": true, 5 "private": true,
6 "licence": "AGPLv3", 6 "licence": "AGPLv3",
7 "engines": { 7 "engines": {
diff --git a/scripts/release.sh b/scripts/release.sh
index 3a8643b5a..ccb93bc44 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -34,8 +34,8 @@ fi
34 34
35version="v$1" 35version="v$1"
36github_prerelease_option="" 36github_prerelease_option=""
37if [[ "$version" = *".pre."* ]]; then 37if [[ "$version" = *"-alpha."* ]] || [[ "$version" = *"-beta."* ]] || [[ "$version" = *"-rc."* ]]; then
38 echo "This is a pre-release." 38 echo -e "This is a pre-release.\n"
39 github_prerelease_option="--pre-release" 39 github_prerelease_option="--pre-release"
40fi 40fi
41 41
@@ -45,7 +45,7 @@ tar_name="peertube-$version.tar.xz"
45 45
46changelog=$(awk -v version="$version" '/## v/ { printit = $2 == version }; printit;' CHANGELOG.md | grep -v "$version" | sed '1{/^$/d}') 46changelog=$(awk -v version="$version" '/## v/ { printit = $2 == version }; printit;' CHANGELOG.md | grep -v "$version" | sed '1{/^$/d}')
47 47
48printf "Changelog will be:\\n%s\\n" "$changelog" 48printf "Changelog will be:\\n\\n%s\\n\\n" "$changelog"
49 49
50read -p "Are you sure to release? " -n 1 -r 50read -p "Are you sure to release? " -n 1 -r
51echo 51echo
diff --git a/scripts/travis.sh b/scripts/travis.sh
index 628039ab7..ae4a9f926 100755
--- a/scripts/travis.sh
+++ b/scripts/travis.sh
@@ -31,6 +31,9 @@ elif [ "$1" = "api-2" ]; then
31elif [ "$1" = "api-3" ]; then 31elif [ "$1" = "api-3" ]; then
32 npm run build:server 32 npm run build:server
33 mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-3.ts 33 mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-3.ts
34elif [ "$1" = "api-3" ]; then
35 npm run build:server
36 mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-4.ts
34elif [ "$1" = "lint" ]; then 37elif [ "$1" = "lint" ]; then
35 npm run tslint -- --project ./tsconfig.json -c ./tslint.json server.ts "server/**/*.ts" "shared/**/*.ts" 38 npm run tslint -- --project ./tsconfig.json -c ./tslint.json server.ts "server/**/*.ts" "shared/**/*.ts"
36 39
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8e3f60010..86ef2aed1 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,7 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, commonVideosFiltersValidator, 4 asyncMiddleware,
5 commonVideosFiltersValidator,
5 listVideoAccountChannelsValidator, 6 listVideoAccountChannelsValidator,
6 optionalAuthenticate, 7 optionalAuthenticate,
7 paginationValidator, 8 paginationValidator,
@@ -90,7 +91,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
90 nsfw: buildNSFWFilter(res, req.query.nsfw), 91 nsfw: buildNSFWFilter(res, req.query.nsfw),
91 withFiles: false, 92 withFiles: false,
92 accountId: account.id, 93 accountId: account.id,
93 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 94 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
94 }) 95 })
95 96
96 return res.json(getFormattedObjects(resultList.data, resultList.total)) 97 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index a8a6cfb08..534305ba6 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -119,7 +119,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw), 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 filter: query.filter, 121 filter: query.filter,
122 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 122 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
123 }) 123 })
124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
125 125
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts
index 43bca2c10..c08192a8c 100644
--- a/server/controllers/api/server/index.ts
+++ b/server/controllers/api/server/index.ts
@@ -2,12 +2,14 @@ import * as express from 'express'
2import { serverFollowsRouter } from './follows' 2import { serverFollowsRouter } from './follows'
3import { statsRouter } from './stats' 3import { statsRouter } from './stats'
4import { serverRedundancyRouter } from './redundancy' 4import { serverRedundancyRouter } from './redundancy'
5import { serverBlocklistRouter } from './server-blocklist'
5 6
6const serverRouter = express.Router() 7const serverRouter = express.Router()
7 8
8serverRouter.use('/', serverFollowsRouter) 9serverRouter.use('/', serverFollowsRouter)
9serverRouter.use('/', serverRedundancyRouter) 10serverRouter.use('/', serverRedundancyRouter)
10serverRouter.use('/', statsRouter) 11serverRouter.use('/', statsRouter)
12serverRouter.use('/', serverBlocklistRouter)
11 13
12// --------------------------------------------------------------------------- 14// ---------------------------------------------------------------------------
13 15
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts
new file mode 100644
index 000000000..3cb3a96e2
--- /dev/null
+++ b/server/controllers/api/server/server-blocklist.ts
@@ -0,0 +1,132 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 ensureUserHasRight,
9 paginationValidator,
10 setDefaultPagination,
11 setDefaultSort
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockAccountByServerValidator,
19 unblockServerByServerValidator
20} from '../../../middlewares/validators'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26import { UserRight } from '../../../../shared/models/users'
27
28const serverBlocklistRouter = express.Router()
29
30serverBlocklistRouter.get('/blocklist/accounts',
31 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
33 paginationValidator,
34 accountsBlocklistSortValidator,
35 setDefaultSort,
36 setDefaultPagination,
37 asyncMiddleware(listBlockedAccounts)
38)
39
40serverBlocklistRouter.post('/blocklist/accounts',
41 authenticate,
42 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
43 asyncMiddleware(blockAccountValidator),
44 asyncRetryTransactionMiddleware(blockAccount)
45)
46
47serverBlocklistRouter.delete('/blocklist/accounts/:accountName',
48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
50 asyncMiddleware(unblockAccountByServerValidator),
51 asyncRetryTransactionMiddleware(unblockAccount)
52)
53
54serverBlocklistRouter.get('/blocklist/servers',
55 authenticate,
56 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
57 paginationValidator,
58 serversBlocklistSortValidator,
59 setDefaultSort,
60 setDefaultPagination,
61 asyncMiddleware(listBlockedServers)
62)
63
64serverBlocklistRouter.post('/blocklist/servers',
65 authenticate,
66 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
67 asyncMiddleware(blockServerValidator),
68 asyncRetryTransactionMiddleware(blockServer)
69)
70
71serverBlocklistRouter.delete('/blocklist/servers/:host',
72 authenticate,
73 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
74 asyncMiddleware(unblockServerByServerValidator),
75 asyncRetryTransactionMiddleware(unblockServer)
76)
77
78export {
79 serverBlocklistRouter
80}
81
82// ---------------------------------------------------------------------------
83
84async function listBlockedAccounts (req: express.Request, res: express.Response) {
85 const serverActor = await getServerActor()
86
87 const resultList = await AccountBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort)
88
89 return res.json(getFormattedObjects(resultList.data, resultList.total))
90}
91
92async function blockAccount (req: express.Request, res: express.Response) {
93 const serverActor = await getServerActor()
94 const accountToBlock: AccountModel = res.locals.account
95
96 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
97
98 return res.status(204).end()
99}
100
101async function unblockAccount (req: express.Request, res: express.Response) {
102 const accountBlock: AccountBlocklistModel = res.locals.accountBlock
103
104 await removeAccountFromBlocklist(accountBlock)
105
106 return res.status(204).end()
107}
108
109async function listBlockedServers (req: express.Request, res: express.Response) {
110 const serverActor = await getServerActor()
111
112 const resultList = await ServerBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort)
113
114 return res.json(getFormattedObjects(resultList.data, resultList.total))
115}
116
117async function blockServer (req: express.Request, res: express.Response) {
118 const serverActor = await getServerActor()
119 const serverToBlock: ServerModel = res.locals.server
120
121 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
122
123 return res.status(204).end()
124}
125
126async function unblockServer (req: express.Request, res: express.Response) {
127 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
128
129 await removeServerFromBlocklist(serverBlock)
130
131 return res.status(204).end()
132}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 4f8137c03..9fcb8077f 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user'
37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
38import { meRouter } from './me' 38import { meRouter } from './me'
39import { deleteUserToken } from '../../../lib/oauth-model' 39import { deleteUserToken } from '../../../lib/oauth-model'
40import { myBlocklistRouter } from './my-blocklist'
40 41
41const auditLogger = auditLoggerFactory('users') 42const auditLogger = auditLoggerFactory('users')
42 43
@@ -53,6 +54,7 @@ const askSendEmailLimiter = new RateLimit({
53}) 54})
54 55
55const usersRouter = express.Router() 56const usersRouter = express.Router()
57usersRouter.use('/', myBlocklistRouter)
56usersRouter.use('/', meRouter) 58usersRouter.use('/', meRouter)
57 59
58usersRouter.get('/autocomplete', 60usersRouter.get('/autocomplete',
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 3c511dc70..82299747d 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -238,7 +238,8 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
238 nsfw: buildNSFWFilter(res, req.query.nsfw), 238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter, 239 filter: req.query.filter as VideoFilter,
240 withFiles: false, 240 withFiles: false,
241 actorId: user.Account.Actor.id 241 actorId: user.Account.Actor.id,
242 user
242 }) 243 })
243 244
244 return res.json(getFormattedObjects(resultList.data, resultList.total)) 245 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
new file mode 100644
index 000000000..9575eab46
--- /dev/null
+++ b/server/controllers/api/users/my-blocklist.ts
@@ -0,0 +1,125 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 paginationValidator,
9 setDefaultPagination,
10 setDefaultSort,
11 unblockAccountByAccountValidator
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockServerByAccountValidator
19} from '../../../middlewares/validators'
20import { UserModel } from '../../../models/account/user'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26
27const myBlocklistRouter = express.Router()
28
29myBlocklistRouter.get('/me/blocklist/accounts',
30 authenticate,
31 paginationValidator,
32 accountsBlocklistSortValidator,
33 setDefaultSort,
34 setDefaultPagination,
35 asyncMiddleware(listBlockedAccounts)
36)
37
38myBlocklistRouter.post('/me/blocklist/accounts',
39 authenticate,
40 asyncMiddleware(blockAccountValidator),
41 asyncRetryTransactionMiddleware(blockAccount)
42)
43
44myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
45 authenticate,
46 asyncMiddleware(unblockAccountByAccountValidator),
47 asyncRetryTransactionMiddleware(unblockAccount)
48)
49
50myBlocklistRouter.get('/me/blocklist/servers',
51 authenticate,
52 paginationValidator,
53 serversBlocklistSortValidator,
54 setDefaultSort,
55 setDefaultPagination,
56 asyncMiddleware(listBlockedServers)
57)
58
59myBlocklistRouter.post('/me/blocklist/servers',
60 authenticate,
61 asyncMiddleware(blockServerValidator),
62 asyncRetryTransactionMiddleware(blockServer)
63)
64
65myBlocklistRouter.delete('/me/blocklist/servers/:host',
66 authenticate,
67 asyncMiddleware(unblockServerByAccountValidator),
68 asyncRetryTransactionMiddleware(unblockServer)
69)
70
71export {
72 myBlocklistRouter
73}
74
75// ---------------------------------------------------------------------------
76
77async function listBlockedAccounts (req: express.Request, res: express.Response) {
78 const user: UserModel = res.locals.oauth.token.User
79
80 const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
81
82 return res.json(getFormattedObjects(resultList.data, resultList.total))
83}
84
85async function blockAccount (req: express.Request, res: express.Response) {
86 const user: UserModel = res.locals.oauth.token.User
87 const accountToBlock: AccountModel = res.locals.account
88
89 await addAccountInBlocklist(user.Account.id, accountToBlock.id)
90
91 return res.status(204).end()
92}
93
94async function unblockAccount (req: express.Request, res: express.Response) {
95 const accountBlock: AccountBlocklistModel = res.locals.accountBlock
96
97 await removeAccountFromBlocklist(accountBlock)
98
99 return res.status(204).end()
100}
101
102async function listBlockedServers (req: express.Request, res: express.Response) {
103 const user: UserModel = res.locals.oauth.token.User
104
105 const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
106
107 return res.json(getFormattedObjects(resultList.data, resultList.total))
108}
109
110async function blockServer (req: express.Request, res: express.Response) {
111 const user: UserModel = res.locals.oauth.token.User
112 const serverToBlock: ServerModel = res.locals.server
113
114 await addServerInBlocklist(user.Account.id, serverToBlock.id)
115
116 return res.status(204).end()
117}
118
119async function unblockServer (req: express.Request, res: express.Response) {
120 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
121
122 await removeServerFromBlocklist(serverBlock)
123
124 return res.status(204).end()
125}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index c84d1be58..9bf3c5fd8 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -219,7 +219,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
219 nsfw: buildNSFWFilter(res, req.query.nsfw), 219 nsfw: buildNSFWFilter(res, req.query.nsfw),
220 withFiles: false, 220 withFiles: false,
221 videoChannelId: videoChannelInstance.id, 221 videoChannelId: videoChannelInstance.id,
222 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 222 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
223 }) 223 })
224 224
225 return res.json(getFormattedObjects(resultList.data, resultList.total)) 225 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 4f2b4faee..3875c8f79 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -8,7 +8,7 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
8import { 8import {
9 asyncMiddleware, 9 asyncMiddleware,
10 asyncRetryTransactionMiddleware, 10 asyncRetryTransactionMiddleware,
11 authenticate, 11 authenticate, optionalAuthenticate,
12 paginationValidator, 12 paginationValidator,
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort 14 setDefaultSort
@@ -36,10 +36,12 @@ videoCommentRouter.get('/:videoId/comment-threads',
36 setDefaultSort, 36 setDefaultSort,
37 setDefaultPagination, 37 setDefaultPagination,
38 asyncMiddleware(listVideoCommentThreadsValidator), 38 asyncMiddleware(listVideoCommentThreadsValidator),
39 optionalAuthenticate,
39 asyncMiddleware(listVideoThreads) 40 asyncMiddleware(listVideoThreads)
40) 41)
41videoCommentRouter.get('/:videoId/comment-threads/:threadId', 42videoCommentRouter.get('/:videoId/comment-threads/:threadId',
42 asyncMiddleware(listVideoThreadCommentsValidator), 43 asyncMiddleware(listVideoThreadCommentsValidator),
44 optionalAuthenticate,
43 asyncMiddleware(listVideoThreadComments) 45 asyncMiddleware(listVideoThreadComments)
44) 46)
45 47
@@ -69,10 +71,12 @@ export {
69 71
70async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { 72async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
71 const video = res.locals.video as VideoModel 73 const video = res.locals.video as VideoModel
74 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
75
72 let resultList: ResultList<VideoCommentModel> 76 let resultList: ResultList<VideoCommentModel>
73 77
74 if (video.commentsEnabled === true) { 78 if (video.commentsEnabled === true) {
75 resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort) 79 resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user)
76 } else { 80 } else {
77 resultList = { 81 resultList = {
78 total: 0, 82 total: 0,
@@ -85,10 +89,12 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne
85 89
86async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { 90async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const video = res.locals.video as VideoModel 91 const video = res.locals.video as VideoModel
92 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
93
88 let resultList: ResultList<VideoCommentModel> 94 let resultList: ResultList<VideoCommentModel>
89 95
90 if (video.commentsEnabled === true) { 96 if (video.commentsEnabled === true) {
91 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id) 97 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user)
92 } else { 98 } else {
93 resultList = { 99 resultList = {
94 total: 0, 100 total: 0,
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 6a73e13d0..664154406 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -437,7 +437,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
437 nsfw: buildNSFWFilter(res, req.query.nsfw), 437 nsfw: buildNSFWFilter(res, req.query.nsfw),
438 filter: req.query.filter as VideoFilter, 438 filter: req.query.filter as VideoFilter,
439 withFiles: false, 439 withFiles: false,
440 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 440 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
441 }) 441 })
442 442
443 return res.json(getFormattedObjects(resultList.data, resultList.total)) 443 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index a964abdd4..17f35fe8d 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -116,28 +116,27 @@ type TranscodeOptions = {
116 116
117function transcode (options: TranscodeOptions) { 117function transcode (options: TranscodeOptions) {
118 return new Promise<void>(async (res, rej) => { 118 return new Promise<void>(async (res, rej) => {
119 let fps = await getVideoFileFPS(options.inputPath)
120 // On small/medium resolutions, limit FPS
121 if (options.resolution !== undefined &&
122 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
123 fps > VIDEO_TRANSCODING_FPS.AVERAGE) {
124 fps = VIDEO_TRANSCODING_FPS.AVERAGE
125 }
126
119 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 127 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
120 .output(options.outputPath) 128 .output(options.outputPath)
121 .preset(standard) 129 command = await presetH264(command, options.resolution, fps)
122 130
123 if (CONFIG.TRANSCODING.THREADS > 0) { 131 if (CONFIG.TRANSCODING.THREADS > 0) {
124 // if we don't set any threads ffmpeg will chose automatically 132 // if we don't set any threads ffmpeg will chose automatically
125 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 133 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
126 } 134 }
127 135
128 let fps = await getVideoFileFPS(options.inputPath)
129 if (options.resolution !== undefined) { 136 if (options.resolution !== undefined) {
130 // '?x720' or '720x?' for example 137 // '?x720' or '720x?' for example
131 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` 138 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
132 command = command.size(size) 139 command = command.size(size)
133
134 // On small/medium resolutions, limit FPS
135 if (
136 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
137 fps > VIDEO_TRANSCODING_FPS.AVERAGE
138 ) {
139 fps = VIDEO_TRANSCODING_FPS.AVERAGE
140 }
141 } 140 }
142 141
143 if (fps) { 142 if (fps) {
@@ -148,12 +147,6 @@ function transcode (options: TranscodeOptions) {
148 command = command.withFPS(fps) 147 command = command.withFPS(fps)
149 } 148 }
150 149
151 // Constrained Encoding (VBV)
152 // https://slhck.info/video/2017/03/01/rate-control.html
153 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
154 const targetBitrate = getTargetBitrate(options.resolution, fps, VIDEO_TRANSCODING_FPS)
155 command.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
156
157 command 150 command
158 .on('error', (err, stdout, stderr) => { 151 .on('error', (err, stdout, stderr) => {
159 logger.error('Error in transcoding job.', { stdout, stderr }) 152 logger.error('Error in transcoding job.', { stdout, stderr })
@@ -199,9 +192,9 @@ function getVideoFileStream (path: string) {
199 * and quality. Superfast and ultrafast will give you better 192 * and quality. Superfast and ultrafast will give you better
200 * performance, but then quality is noticeably worse. 193 * performance, but then quality is noticeably worse.
201 */ 194 */
202function veryfast (_ffmpeg) { 195async function presetH264VeryFast (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg {
203 _ffmpeg 196 const localFfmpeg = await presetH264(ffmpeg, resolution, fps)
204 .preset(standard) 197 localFfmpeg
205 .outputOption('-preset:v veryfast') 198 .outputOption('-preset:v veryfast')
206 .outputOption(['--aq-mode=2', '--aq-strength=1.3']) 199 .outputOption(['--aq-mode=2', '--aq-strength=1.3'])
207 /* 200 /*
@@ -220,9 +213,9 @@ function veryfast (_ffmpeg) {
220/** 213/**
221 * A preset optimised for a stillimage audio video 214 * A preset optimised for a stillimage audio video
222 */ 215 */
223function audio (_ffmpeg) { 216async function presetStillImageWithAudio (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg {
224 _ffmpeg 217 const localFfmpeg = await presetH264VeryFast(ffmpeg, resolution, fps)
225 .preset(veryfast) 218 localFfmpeg
226 .outputOption('-tune stillimage') 219 .outputOption('-tune stillimage')
227} 220}
228 221
@@ -290,8 +283,8 @@ namespace audio {
290 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 283 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
291 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 284 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
292 */ 285 */
293async function standard (_ffmpeg) { 286async function presetH264 (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg {
294 let localFfmpeg = _ffmpeg 287 let localFfmpeg = ffmpeg
295 .format('mp4') 288 .format('mp4')
296 .videoCodec('libx264') 289 .videoCodec('libx264')
297 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 290 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
@@ -324,5 +317,16 @@ async function standard (_ffmpeg) {
324 317
325 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) 318 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate)
326 319
320 // Constrained Encoding (VBV)
321 // https://slhck.info/video/2017/03/01/rate-control.html
322 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
323 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
324 localFfmpeg.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
325
326 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
327 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
328 // https://superuser.com/a/908325
329 localFfmpeg.outputOption(`-g ${ fps * 2 }`)
330
327 return localFfmpeg 331 return localFfmpeg
328} 332}
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 39afb4e7b..049c3f8bc 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () {
40 const application = await ApplicationModel.load() 40 const application = await ApplicationModel.load()
41 if (!application) throw Error('Could not load Application from database.') 41 if (!application) throw Error('Could not load Application from database.')
42 42
43 return application.Account.Actor 43 const actor = application.Account.Actor
44 actor.Account = application.Account
45
46 return actor
44}) 47})
45 48
46function generateVideoTmpPath (target: string | ParseTorrent) { 49function generateVideoTmpPath (target: string | ParseTorrent) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index c3e4fcede..03158e356 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -47,7 +47,10 @@ const SORTABLE_COLUMNS = {
47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], 47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ],
48 48
49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], 49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ],
50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] 50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
51
52 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
53 SERVERS_BLOCKLIST: [ 'createdAt' ]
51} 54}
52 55
53const OAUTH_LIFETIME = { 56const OAUTH_LIFETIME = {
@@ -421,7 +424,7 @@ const VIDEO_CATEGORIES = {
421 8: 'People', 424 8: 'People',
422 9: 'Comedy', 425 9: 'Comedy',
423 10: 'Entertainment', 426 10: 'Entertainment',
424 11: 'News', 427 11: 'News & Politics',
425 12: 'How To', 428 12: 'How To',
426 13: 'Education', 429 13: 'Education',
427 14: 'Activism', 430 14: 'Activism',
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 482c03b31..dd5b9bf67 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -29,6 +29,8 @@ import { VideoViewModel } from '../models/video/video-views'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' 29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history' 31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist'
32 34
33require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 35require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
34 36
@@ -91,7 +93,9 @@ async function initDatabaseModels (silent: boolean) {
91 VideoImportModel, 93 VideoImportModel,
92 VideoViewModel, 94 VideoViewModel,
93 VideoRedundancyModel, 95 VideoRedundancyModel,
94 UserVideoHistoryModel 96 UserVideoHistoryModel,
97 AccountBlocklistModel,
98 ServerBlocklistModel
95 ]) 99 ])
96 100
97 // Check extensions exist in the database 101 // Check extensions exist in the database
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
new file mode 100644
index 000000000..1633e500c
--- /dev/null
+++ b/server/lib/blocklist.ts
@@ -0,0 +1,40 @@
1import { sequelizeTypescript } from '../initializers'
2import { AccountBlocklistModel } from '../models/account/account-blocklist'
3import { ServerBlocklistModel } from '../models/server/server-blocklist'
4
5function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
6 return sequelizeTypescript.transaction(async t => {
7 return AccountBlocklistModel.upsert({
8 accountId: byAccountId,
9 targetAccountId: targetAccountId
10 }, { transaction: t })
11 })
12}
13
14function addServerInBlocklist (byAccountId: number, targetServerId: number) {
15 return sequelizeTypescript.transaction(async t => {
16 return ServerBlocklistModel.upsert({
17 accountId: byAccountId,
18 targetServerId
19 }, { transaction: t })
20 })
21}
22
23function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) {
24 return sequelizeTypescript.transaction(async t => {
25 return accountBlock.destroy({ transaction: t })
26 })
27}
28
29function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) {
30 return sequelizeTypescript.transaction(async t => {
31 return serverBlock.destroy({ transaction: t })
32 })
33}
34
35export {
36 addAccountInBlocklist,
37 addServerInBlocklist,
38 removeAccountFromBlocklist,
39 removeServerFromBlocklist
40}
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index 70ba7c303..59bce7520 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
64 } 64 }
65 65
66 const parentCommentThread = idx[childComment.inReplyToCommentId] 66 const parentCommentThread = idx[childComment.inReplyToCommentId]
67 if (!parentCommentThread) { 67 // Maybe the parent comment was blocked by the admin/user
68 const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}` 68 if (!parentCommentThread) continue
69 throw new Error(msg)
70 }
71 69
72 parentCommentThread.children.push(childCommentThread) 70 parentCommentThread.children.push(childCommentThread)
73 idx[childComment.id] = childCommentThread 71 idx[childComment.id] = childCommentThread
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
new file mode 100644
index 000000000..109276c63
--- /dev/null
+++ b/server/middlewares/validators/blocklist.ts
@@ -0,0 +1,172 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
6import { UserModel } from '../../models/account/user'
7import { AccountBlocklistModel } from '../../models/account/account-blocklist'
8import { isHostValid } from '../../helpers/custom-validators/servers'
9import { ServerBlocklistModel } from '../../models/server/server-blocklist'
10import { ServerModel } from '../../models/server/server'
11import { CONFIG } from '../../initializers'
12import { getServerActor } from '../../helpers/utils'
13
14const blockAccountValidator = [
15 body('accountName').exists().withMessage('Should have an account name with host'),
16
17 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body })
19
20 if (areValidationErrors(req, res)) return
21 if (!await isAccountNameWithHostExist(req.body.accountName, res)) return
22
23 const user = res.locals.oauth.token.User as UserModel
24 const accountToBlock = res.locals.account
25
26 if (user.Account.id === accountToBlock.id) {
27 res.status(409)
28 .send({ error: 'You cannot block yourself.' })
29 .end()
30
31 return
32 }
33
34 return next()
35 }
36]
37
38const unblockAccountByAccountValidator = [
39 param('accountName').exists().withMessage('Should have an account name with host'),
40
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params })
43
44 if (areValidationErrors(req, res)) return
45 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
46
47 const user = res.locals.oauth.token.User as UserModel
48 const targetAccount = res.locals.account
49 if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return
50
51 return next()
52 }
53]
54
55const unblockAccountByServerValidator = [
56 param('accountName').exists().withMessage('Should have an account name with host'),
57
58 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params })
60
61 if (areValidationErrors(req, res)) return
62 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
63
64 const serverActor = await getServerActor()
65 const targetAccount = res.locals.account
66 if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return
67
68 return next()
69 }
70]
71
72const blockServerValidator = [
73 body('host').custom(isHostValid).withMessage('Should have a valid host'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
77
78 if (areValidationErrors(req, res)) return
79
80 const host: string = req.body.host
81
82 if (host === CONFIG.WEBSERVER.HOST) {
83 return res.status(409)
84 .send({ error: 'You cannot block your own server.' })
85 .end()
86 }
87
88 const server = await ServerModel.loadByHost(host)
89 if (!server) {
90 return res.status(404)
91 .send({ error: 'Server host not found.' })
92 .end()
93 }
94
95 res.locals.server = server
96
97 return next()
98 }
99]
100
101const unblockServerByAccountValidator = [
102 param('host').custom(isHostValid).withMessage('Should have an account name with host'),
103
104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params })
106
107 if (areValidationErrors(req, res)) return
108
109 const user = res.locals.oauth.token.User as UserModel
110 if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return
111
112 return next()
113 }
114]
115
116const unblockServerByServerValidator = [
117 param('host').custom(isHostValid).withMessage('Should have an account name with host'),
118
119 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
120 logger.debug('Checking unblockServerByServerValidator parameters', { parameters: req.params })
121
122 if (areValidationErrors(req, res)) return
123
124 const serverActor = await getServerActor()
125 if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return
126
127 return next()
128 }
129]
130
131// ---------------------------------------------------------------------------
132
133export {
134 blockServerValidator,
135 blockAccountValidator,
136 unblockAccountByAccountValidator,
137 unblockServerByAccountValidator,
138 unblockAccountByServerValidator,
139 unblockServerByServerValidator
140}
141
142// ---------------------------------------------------------------------------
143
144async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) {
145 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
146 if (!accountBlock) {
147 res.status(404)
148 .send({ error: 'Account block entry not found.' })
149 .end()
150
151 return false
152 }
153
154 res.locals.accountBlock = accountBlock
155
156 return true
157}
158
159async function isUnblockServerExists (accountId: number, host: string, res: express.Response) {
160 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
161 if (!serverBlock) {
162 res.status(404)
163 .send({ error: 'Server block entry not found.' })
164 .end()
165
166 return false
167 }
168
169 res.locals.serverBlock = serverBlock
170
171 return true
172}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 17226614c..46c7f0f3a 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './blocklist'
2export * from './oembed' 3export * from './oembed'
3export * from './activitypub' 4export * from './activitypub'
4export * from './pagination' 5export * from './pagination'
@@ -10,3 +11,4 @@ export * from './user-subscriptions'
10export * from './videos' 11export * from './videos'
11export * from './webfinger' 12export * from './webfinger'
12export * from './search' 13export * from './search'
14export * from './server'
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
new file mode 100644
index 000000000..a491dfeb3
--- /dev/null
+++ b/server/middlewares/validators/server.ts
@@ -0,0 +1,33 @@
1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isHostValid } from '../../helpers/custom-validators/servers'
5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator/check'
7
8const serverGetValidator = [
9 body('host').custom(isHostValid).withMessage('Should have a valid host'),
10
11 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
13
14 if (areValidationErrors(req, res)) return
15
16 const server = await ServerModel.loadByHost(req.body.host)
17 if (!server) {
18 return res.status(404)
19 .send({ error: 'Server host not found.' })
20 .end()
21 }
22
23 res.locals.server = server
24
25 return next()
26 }
27]
28
29// ---------------------------------------------------------------------------
30
31export {
32 serverGetValidator
33}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 08dcc2680..4c0577d8f 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -16,6 +16,8 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V
16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) 17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) 18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
19 21
20const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 22const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
21const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 23const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -31,6 +33,8 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
31const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 33const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
32const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) 34const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
33const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) 35const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
36const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
37const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
34 38
35// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
36 40
@@ -48,5 +52,7 @@ export {
48 jobsSortValidator, 52 jobsSortValidator,
49 videoCommentThreadsSortValidator, 53 videoCommentThreadsSortValidator,
50 userSubscriptionsSortValidator, 54 userSubscriptionsSortValidator,
51 videoChannelsSearchSortValidator 55 videoChannelsSearchSortValidator,
56 accountsBlocklistSortValidator,
57 serversBlocklistSortValidator
52} 58}
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
new file mode 100644
index 000000000..fa2819235
--- /dev/null
+++ b/server/models/account/account-blocklist.ts
@@ -0,0 +1,111 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5
6enum ScopeNames {
7 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
8}
9
10@Scopes({
11 [ScopeNames.WITH_ACCOUNTS]: {
12 include: [
13 {
14 model: () => AccountModel,
15 required: true,
16 as: 'ByAccount'
17 },
18 {
19 model: () => AccountModel,
20 required: true,
21 as: 'BlockedAccount'
22 }
23 ]
24 }
25})
26
27@Table({
28 tableName: 'accountBlocklist',
29 indexes: [
30 {
31 fields: [ 'accountId', 'targetAccountId' ],
32 unique: true
33 },
34 {
35 fields: [ 'targetAccountId' ]
36 }
37 ]
38})
39export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
40
41 @CreatedAt
42 createdAt: Date
43
44 @UpdatedAt
45 updatedAt: Date
46
47 @ForeignKey(() => AccountModel)
48 @Column
49 accountId: number
50
51 @BelongsTo(() => AccountModel, {
52 foreignKey: {
53 name: 'accountId',
54 allowNull: false
55 },
56 as: 'ByAccount',
57 onDelete: 'CASCADE'
58 })
59 ByAccount: AccountModel
60
61 @ForeignKey(() => AccountModel)
62 @Column
63 targetAccountId: number
64
65 @BelongsTo(() => AccountModel, {
66 foreignKey: {
67 name: 'targetAccountId',
68 allowNull: false
69 },
70 as: 'BlockedAccount',
71 onDelete: 'CASCADE'
72 })
73 BlockedAccount: AccountModel
74
75 static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
76 const query = {
77 where: {
78 accountId,
79 targetAccountId
80 }
81 }
82
83 return AccountBlocklistModel.findOne(query)
84 }
85
86 static listForApi (accountId: number, start: number, count: number, sort: string) {
87 const query = {
88 offset: start,
89 limit: count,
90 order: getSort(sort),
91 where: {
92 accountId
93 }
94 }
95
96 return AccountBlocklistModel
97 .scope([ ScopeNames.WITH_ACCOUNTS ])
98 .findAndCountAll(query)
99 .then(({ rows, count }) => {
100 return { total: count, data: rows }
101 })
102 }
103
104 toFormattedJSON (): AccountBlock {
105 return {
106 byAccount: this.ByAccount.toFormattedJSON(),
107 blockedAccount: this.BlockedAccount.toFormattedJSON(),
108 createdAt: this.createdAt
109 }
110 }
111}
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
new file mode 100644
index 000000000..450f27152
--- /dev/null
+++ b/server/models/server/server-blocklist.ts
@@ -0,0 +1,121 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort } from '../utils'
6
7enum ScopeNames {
8 WITH_ACCOUNT = 'WITH_ACCOUNT',
9 WITH_SERVER = 'WITH_SERVER'
10}
11
12@Scopes({
13 [ScopeNames.WITH_ACCOUNT]: {
14 include: [
15 {
16 model: () => AccountModel,
17 required: true
18 }
19 ]
20 },
21 [ScopeNames.WITH_SERVER]: {
22 include: [
23 {
24 model: () => ServerModel,
25 required: true
26 }
27 ]
28 }
29})
30
31@Table({
32 tableName: 'serverBlocklist',
33 indexes: [
34 {
35 fields: [ 'accountId', 'targetServerId' ],
36 unique: true
37 },
38 {
39 fields: [ 'targetServerId' ]
40 }
41 ]
42})
43export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
44
45 @CreatedAt
46 createdAt: Date
47
48 @UpdatedAt
49 updatedAt: Date
50
51 @ForeignKey(() => AccountModel)
52 @Column
53 accountId: number
54
55 @BelongsTo(() => AccountModel, {
56 foreignKey: {
57 name: 'accountId',
58 allowNull: false
59 },
60 onDelete: 'CASCADE'
61 })
62 ByAccount: AccountModel
63
64 @ForeignKey(() => ServerModel)
65 @Column
66 targetServerId: number
67
68 @BelongsTo(() => ServerModel, {
69 foreignKey: {
70 name: 'targetServerId',
71 allowNull: false
72 },
73 onDelete: 'CASCADE'
74 })
75 BlockedServer: ServerModel
76
77 static loadByAccountAndHost (accountId: number, host: string) {
78 const query = {
79 where: {
80 accountId
81 },
82 include: [
83 {
84 model: ServerModel,
85 where: {
86 host
87 },
88 required: true
89 }
90 ]
91 }
92
93 return ServerBlocklistModel.findOne(query)
94 }
95
96 static listForApi (accountId: number, start: number, count: number, sort: string) {
97 const query = {
98 offset: start,
99 limit: count,
100 order: getSort(sort),
101 where: {
102 accountId
103 }
104 }
105
106 return ServerBlocklistModel
107 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
108 .findAndCountAll(query)
109 .then(({ rows, count }) => {
110 return { total: count, data: rows }
111 })
112 }
113
114 toFormattedJSON (): ServerBlock {
115 return {
116 byAccount: this.ByAccount.toFormattedJSON(),
117 blockedServer: this.BlockedServer.toFormattedJSON(),
118 createdAt: this.createdAt
119 }
120 }
121}
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index ca3b24d51..300d70938 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> {
49 49
50 return ServerModel.findOne(query) 50 return ServerModel.findOne(query)
51 } 51 }
52
53 toFormattedJSON () {
54 return {
55 host: this.host
56 }
57 }
52} 58}
diff --git a/server/models/utils.ts b/server/models/utils.ts
index e0bf091ad..60b0906e8 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -64,9 +64,25 @@ function createSimilarityAttribute (col: string, value: string) {
64 ) 64 )
65} 65}
66 66
67function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) {
68 const blockerIds = [ serverAccountId ]
69 if (userAccountId) blockerIds.push(userAccountId)
70
71 const blockerIdsString = blockerIds.join(', ')
72
73 const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
74 ' UNION ALL ' +
75 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
76 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
77 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
78
79 return query
80}
81
67// --------------------------------------------------------------------------- 82// ---------------------------------------------------------------------------
68 83
69export { 84export {
85 buildBlockedAccountSQL,
70 SortType, 86 SortType,
71 getSort, 87 getSort,
72 getVideoSort, 88 getVideoSort,
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index f84c1880c..dd6d08139 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,6 +1,17 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { 2import {
3 AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, 3 AllowNull,
4 BeforeDestroy,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 ForeignKey,
10 IFindOptions,
11 Is,
12 Model,
13 Scopes,
14 Table,
4 UpdatedAt 15 UpdatedAt
5} from 'sequelize-typescript' 16} from 'sequelize-typescript'
6import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' 17import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
@@ -13,9 +24,11 @@ import { AccountModel } from '../account/account'
13import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
14import { AvatarModel } from '../avatar/avatar' 25import { AvatarModel } from '../avatar/avatar'
15import { ServerModel } from '../server/server' 26import { ServerModel } from '../server/server'
16import { getSort, throwIfNotValid } from '../utils' 27import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
17import { VideoModel } from './video' 28import { VideoModel } from './video'
18import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user'
19 32
20enum ScopeNames { 33enum ScopeNames {
21 WITH_ACCOUNT = 'WITH_ACCOUNT', 34 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -25,18 +38,29 @@ enum ScopeNames {
25} 38}
26 39
27@Scopes({ 40@Scopes({
28 [ScopeNames.ATTRIBUTES_FOR_API]: { 41 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
29 attributes: { 42 return {
30 include: [ 43 attributes: {
31 [ 44 include: [
32 Sequelize.literal( 45 [
33 '(SELECT COUNT("replies"."id") ' + 46 Sequelize.literal(
34 'FROM "videoComment" AS "replies" ' + 47 '(' +
35 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' 48 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
36 ), 49 'SELECT COUNT("replies"."id") - (' +
37 'totalReplies' 50 'SELECT COUNT("replies"."id") ' +
51 'FROM "videoComment" AS "replies" ' +
52 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
53 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
54 ')' +
55 'FROM "videoComment" AS "replies" ' +
56 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
57 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
58 ')'
59 ),
60 'totalReplies'
61 ]
38 ] 62 ]
39 ] 63 }
40 } 64 }
41 }, 65 },
42 [ScopeNames.WITH_ACCOUNT]: { 66 [ScopeNames.WITH_ACCOUNT]: {
@@ -267,26 +291,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
267 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) 291 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
268 } 292 }
269 293
270 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { 294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
295 const serverActor = await getServerActor()
296 const serverAccountId = serverActor.Account.id
297 const userAccountId = user ? user.Account.id : undefined
298
271 const query = { 299 const query = {
272 offset: start, 300 offset: start,
273 limit: count, 301 limit: count,
274 order: getSort(sort), 302 order: getSort(sort),
275 where: { 303 where: {
276 videoId, 304 videoId,
277 inReplyToCommentId: null 305 inReplyToCommentId: null,
306 accountId: {
307 [Sequelize.Op.notIn]: Sequelize.literal(
308 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
309 )
310 }
278 } 311 }
279 } 312 }
280 313
314 // FIXME: typings
315 const scopes: any[] = [
316 ScopeNames.WITH_ACCOUNT,
317 {
318 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
319 }
320 ]
321
281 return VideoCommentModel 322 return VideoCommentModel
282 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 323 .scope(scopes)
283 .findAndCountAll(query) 324 .findAndCountAll(query)
284 .then(({ rows, count }) => { 325 .then(({ rows, count }) => {
285 return { total: count, data: rows } 326 return { total: count, data: rows }
286 }) 327 })
287 } 328 }
288 329
289 static listThreadCommentsForApi (videoId: number, threadId: number) { 330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
331 const serverActor = await getServerActor()
332 const serverAccountId = serverActor.Account.id
333 const userAccountId = user ? user.Account.id : undefined
334
290 const query = { 335 const query = {
291 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
292 where: { 337 where: {
@@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
294 [ Sequelize.Op.or ]: [ 339 [ Sequelize.Op.or ]: [
295 { id: threadId }, 340 { id: threadId },
296 { originCommentId: threadId } 341 { originCommentId: threadId }
297 ] 342 ],
343 accountId: {
344 [Sequelize.Op.notIn]: Sequelize.literal(
345 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
346 )
347 }
298 } 348 }
299 } 349 }
300 350
351 const scopes: any[] = [
352 ScopeNames.WITH_ACCOUNT,
353 {
354 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
355 }
356 ]
357
301 return VideoCommentModel 358 return VideoCommentModel
302 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 359 .scope(scopes)
303 .findAndCountAll(query) 360 .findAndCountAll(query)
304 .then(({ rows, count }) => { 361 .then(({ rows, count }) => {
305 return { total: count, data: rows } 362 return { total: count, data: rows }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 4f3f75613..6c183933b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -27,7 +27,7 @@ import {
27 Table, 27 Table,
28 UpdatedAt 28 UpdatedAt
29} from 'sequelize-typescript' 29} from 'sequelize-typescript'
30import { VideoPrivacy, VideoState } from '../../../shared' 30import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
33import { VideoFilter } from '../../../shared/models/videos/video-query.type' 33import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
70import { ActorModel } from '../activitypub/actor' 70import { ActorModel } from '../activitypub/actor'
71import { AvatarModel } from '../avatar/avatar' 71import { AvatarModel } from '../avatar/avatar'
72import { ServerModel } from '../server/server' 72import { ServerModel } from '../server/server'
73import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' 73import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
74import { TagModel } from './tag' 74import { TagModel } from './tag'
75import { VideoAbuseModel } from './video-abuse' 75import { VideoAbuseModel } from './video-abuse'
76import { VideoChannelModel } from './video-channel' 76import { VideoChannelModel } from './video-channel'
@@ -93,6 +93,7 @@ import {
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user'
96 97
97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
98const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -138,6 +139,7 @@ type ForAPIOptions = {
138} 139}
139 140
140type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number
141 actorId: number 143 actorId: number
142 includeLocalVideos: boolean 144 includeLocalVideos: boolean
143 filter?: VideoFilter 145 filter?: VideoFilter
@@ -151,6 +153,7 @@ type AvailableForListIDsOptions = {
151 accountId?: number 153 accountId?: number
152 videoChannelId?: number 154 videoChannelId?: number
153 trendingDays?: number 155 trendingDays?: number
156 user?: UserModel
154} 157}
155 158
156@Scopes({ 159@Scopes({
@@ -235,6 +238,15 @@ type AvailableForListIDsOptions = {
235 ) 238 )
236 } 239 }
237 ] 240 ]
241 },
242 channelId: {
243 [ Sequelize.Op.notIn ]: Sequelize.literal(
244 '(' +
245 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
246 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
247 ')' +
248 ')'
249 )
238 } 250 }
239 }, 251 },
240 include: [] 252 include: []
@@ -975,10 +987,10 @@ export class VideoModel extends Model<VideoModel> {
975 videoChannelId?: number, 987 videoChannelId?: number,
976 actorId?: number 988 actorId?: number
977 trendingDays?: number, 989 trendingDays?: number,
978 userId?: number 990 user?: UserModel
979 }, countVideos = true) { 991 }, countVideos = true) {
980 if (options.filter && options.filter === 'all-local' && !options.userId) { 992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
981 throw new Error('Try to filter all-local but no userId is provided') 993 throw new Error('Try to filter all-local but no user has not the see all videos right')
982 } 994 }
983 995
984 const query: IFindOptions<VideoModel> = { 996 const query: IFindOptions<VideoModel> = {
@@ -994,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
994 query.group = 'VideoModel.id' 1006 query.group = 'VideoModel.id'
995 } 1007 }
996 1008
1009 const serverActor = await getServerActor()
1010
997 // actorId === null has a meaning, so just check undefined 1011 // actorId === null has a meaning, so just check undefined
998 const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id 1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
999 1013
1000 const queryOptions = { 1014 const queryOptions = {
1001 actorId, 1015 actorId,
1016 serverAccountId: serverActor.Account.id,
1002 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
1003 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
1004 licenceOneOf: options.licenceOneOf, 1019 licenceOneOf: options.licenceOneOf,
@@ -1010,7 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
1010 accountId: options.accountId, 1025 accountId: options.accountId,
1011 videoChannelId: options.videoChannelId, 1026 videoChannelId: options.videoChannelId,
1012 includeLocalVideos: options.includeLocalVideos, 1027 includeLocalVideos: options.includeLocalVideos,
1013 userId: options.userId, 1028 user: options.user,
1014 trendingDays 1029 trendingDays
1015 } 1030 }
1016 1031
@@ -1033,7 +1048,7 @@ export class VideoModel extends Model<VideoModel> {
1033 tagsAllOf?: string[] 1048 tagsAllOf?: string[]
1034 durationMin?: number // seconds 1049 durationMin?: number // seconds
1035 durationMax?: number // seconds 1050 durationMax?: number // seconds
1036 userId?: number, 1051 user?: UserModel,
1037 filter?: VideoFilter 1052 filter?: VideoFilter
1038 }) { 1053 }) {
1039 const whereAnd = [] 1054 const whereAnd = []
@@ -1104,6 +1119,7 @@ export class VideoModel extends Model<VideoModel> {
1104 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1105 const queryOptions = { 1120 const queryOptions = {
1106 actorId: serverActor.id, 1121 actorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id,
1107 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1108 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
1109 categoryOneOf: options.categoryOneOf, 1125 categoryOneOf: options.categoryOneOf,
@@ -1111,7 +1127,7 @@ export class VideoModel extends Model<VideoModel> {
1111 languageOneOf: options.languageOneOf, 1127 languageOneOf: options.languageOneOf,
1112 tagsOneOf: options.tagsOneOf, 1128 tagsOneOf: options.tagsOneOf,
1113 tagsAllOf: options.tagsAllOf, 1129 tagsAllOf: options.tagsAllOf,
1114 userId: options.userId, 1130 user: options.user,
1115 filter: options.filter 1131 filter: options.filter
1116 } 1132 }
1117 1133
@@ -1239,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> {
1239 1255
1240 // threshold corresponds to how many video the field should have to be returned 1256 // threshold corresponds to how many video the field should have to be returned
1241 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1242 const actorId = (await getServerActor()).id 1258 const serverActor = await getServerActor()
1259 const actorId = serverActor.id
1243 1260
1244 const scopeOptions = { 1261 const scopeOptions: AvailableForListIDsOptions = {
1262 serverAccountId: serverActor.Account.id,
1245 actorId, 1263 actorId,
1246 includeLocalVideos: true 1264 includeLocalVideos: true
1247 } 1265 }
@@ -1287,7 +1305,7 @@ export class VideoModel extends Model<VideoModel> {
1287 1305
1288 private static async getAvailableForApi ( 1306 private static async getAvailableForApi (
1289 query: IFindOptions<VideoModel>, 1307 query: IFindOptions<VideoModel>,
1290 options: AvailableForListIDsOptions & { userId?: number}, 1308 options: AvailableForListIDsOptions,
1291 countVideos = true 1309 countVideos = true
1292 ) { 1310 ) {
1293 const idsScope = { 1311 const idsScope = {
@@ -1320,8 +1338,8 @@ export class VideoModel extends Model<VideoModel> {
1320 } 1338 }
1321 ] 1339 ]
1322 1340
1323 if (options.userId) { 1341 if (options.user) {
1324 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] }) 1342 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1325 } 1343 }
1326 1344
1327 const secondQuery = { 1345 const secondQuery = {
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
new file mode 100644
index 000000000..c745ac975
--- /dev/null
+++ b/server/tests/api/check-params/blocklist.ts
@@ -0,0 +1,494 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeDeleteRequest,
12 makeGetRequest,
13 makePostBodyRequest,
14 ServerInfo,
15 setAccessTokensToServers, userLogin
16} from '../../utils'
17import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
18
19describe('Test blocklist API validators', function () {
20 let servers: ServerInfo[]
21 let server: ServerInfo
22 let userAccessToken: string
23
24 before(async function () {
25 this.timeout(60000)
26
27 await flushTests()
28
29 servers = await flushAndRunMultipleServers(2)
30 await setAccessTokensToServers(servers)
31
32 server = servers[0]
33
34 const user = { username: 'user1', password: 'password' }
35 await createUser(server.url, server.accessToken, user.username, user.password)
36
37 userAccessToken = await userLogin(server, user)
38
39 await doubleFollow(servers[0], servers[1])
40 })
41
42 // ---------------------------------------------------------------
43
44 describe('When managing user blocklist', function () {
45
46 describe('When managing user accounts blocklist', function () {
47 const path = '/api/v1/users/me/blocklist/accounts'
48
49 describe('When listing blocked accounts', function () {
50 it('Should fail with an unauthenticated user', async function () {
51 await makeGetRequest({
52 url: server.url,
53 path,
54 statusCodeExpected: 401
55 })
56 })
57
58 it('Should fail with a bad start pagination', async function () {
59 await checkBadStartPagination(server.url, path, server.accessToken)
60 })
61
62 it('Should fail with a bad count pagination', async function () {
63 await checkBadCountPagination(server.url, path, server.accessToken)
64 })
65
66 it('Should fail with an incorrect sort', async function () {
67 await checkBadSortPagination(server.url, path, server.accessToken)
68 })
69 })
70
71 describe('When blocking an account', function () {
72 it('Should fail with an unauthenticated user', async function () {
73 await makePostBodyRequest({
74 url: server.url,
75 path,
76 fields: { accountName: 'user1' },
77 statusCodeExpected: 401
78 })
79 })
80
81 it('Should fail with an unknown account', async function () {
82 await makePostBodyRequest({
83 url: server.url,
84 token: server.accessToken,
85 path,
86 fields: { accountName: 'user2' },
87 statusCodeExpected: 404
88 })
89 })
90
91 it('Should fail to block ourselves', async function () {
92 await makePostBodyRequest({
93 url: server.url,
94 token: server.accessToken,
95 path,
96 fields: { accountName: 'root' },
97 statusCodeExpected: 409
98 })
99 })
100
101 it('Should succeed with the correct params', async function () {
102 await makePostBodyRequest({
103 url: server.url,
104 token: server.accessToken,
105 path,
106 fields: { accountName: 'user1' },
107 statusCodeExpected: 204
108 })
109 })
110 })
111
112 describe('When unblocking an account', function () {
113 it('Should fail with an unauthenticated user', async function () {
114 await makeDeleteRequest({
115 url: server.url,
116 path: path + '/user1',
117 statusCodeExpected: 401
118 })
119 })
120
121 it('Should fail with an unknown account block', async function () {
122 await makeDeleteRequest({
123 url: server.url,
124 path: path + '/user2',
125 token: server.accessToken,
126 statusCodeExpected: 404
127 })
128 })
129
130 it('Should succeed with the correct params', async function () {
131 await makeDeleteRequest({
132 url: server.url,
133 path: path + '/user1',
134 token: server.accessToken,
135 statusCodeExpected: 204
136 })
137 })
138 })
139 })
140
141 describe('When managing user servers blocklist', function () {
142 const path = '/api/v1/users/me/blocklist/servers'
143
144 describe('When listing blocked servers', function () {
145 it('Should fail with an unauthenticated user', async function () {
146 await makeGetRequest({
147 url: server.url,
148 path,
149 statusCodeExpected: 401
150 })
151 })
152
153 it('Should fail with a bad start pagination', async function () {
154 await checkBadStartPagination(server.url, path, server.accessToken)
155 })
156
157 it('Should fail with a bad count pagination', async function () {
158 await checkBadCountPagination(server.url, path, server.accessToken)
159 })
160
161 it('Should fail with an incorrect sort', async function () {
162 await checkBadSortPagination(server.url, path, server.accessToken)
163 })
164 })
165
166 describe('When blocking a server', function () {
167 it('Should fail with an unauthenticated user', async function () {
168 await makePostBodyRequest({
169 url: server.url,
170 path,
171 fields: { host: 'localhost:9002' },
172 statusCodeExpected: 401
173 })
174 })
175
176 it('Should fail with an unknown server', async function () {
177 await makePostBodyRequest({
178 url: server.url,
179 token: server.accessToken,
180 path,
181 fields: { host: 'localhost:9003' },
182 statusCodeExpected: 404
183 })
184 })
185
186 it('Should fail with our own server', async function () {
187 await makePostBodyRequest({
188 url: server.url,
189 token: server.accessToken,
190 path,
191 fields: { host: 'localhost:9001' },
192 statusCodeExpected: 409
193 })
194 })
195
196 it('Should succeed with the correct params', async function () {
197 await makePostBodyRequest({
198 url: server.url,
199 token: server.accessToken,
200 path,
201 fields: { host: 'localhost:9002' },
202 statusCodeExpected: 204
203 })
204 })
205 })
206
207 describe('When unblocking a server', function () {
208 it('Should fail with an unauthenticated user', async function () {
209 await makeDeleteRequest({
210 url: server.url,
211 path: path + '/localhost:9002',
212 statusCodeExpected: 401
213 })
214 })
215
216 it('Should fail with an unknown server block', async function () {
217 await makeDeleteRequest({
218 url: server.url,
219 path: path + '/localhost:9003',
220 token: server.accessToken,
221 statusCodeExpected: 404
222 })
223 })
224
225 it('Should succeed with the correct params', async function () {
226 await makeDeleteRequest({
227 url: server.url,
228 path: path + '/localhost:9002',
229 token: server.accessToken,
230 statusCodeExpected: 204
231 })
232 })
233 })
234 })
235 })
236
237 describe('When managing server blocklist', function () {
238
239 describe('When managing server accounts blocklist', function () {
240 const path = '/api/v1/server/blocklist/accounts'
241
242 describe('When listing blocked accounts', function () {
243 it('Should fail with an unauthenticated user', async function () {
244 await makeGetRequest({
245 url: server.url,
246 path,
247 statusCodeExpected: 401
248 })
249 })
250
251 it('Should fail with a user without the appropriate rights', async function () {
252 await makeGetRequest({
253 url: server.url,
254 token: userAccessToken,
255 path,
256 statusCodeExpected: 403
257 })
258 })
259
260 it('Should fail with a bad start pagination', async function () {
261 await checkBadStartPagination(server.url, path, server.accessToken)
262 })
263
264 it('Should fail with a bad count pagination', async function () {
265 await checkBadCountPagination(server.url, path, server.accessToken)
266 })
267
268 it('Should fail with an incorrect sort', async function () {
269 await checkBadSortPagination(server.url, path, server.accessToken)
270 })
271 })
272
273 describe('When blocking an account', function () {
274 it('Should fail with an unauthenticated user', async function () {
275 await makePostBodyRequest({
276 url: server.url,
277 path,
278 fields: { accountName: 'user1' },
279 statusCodeExpected: 401
280 })
281 })
282
283 it('Should fail with a user without the appropriate rights', async function () {
284 await makePostBodyRequest({
285 url: server.url,
286 token: userAccessToken,
287 path,
288 fields: { accountName: 'user1' },
289 statusCodeExpected: 403
290 })
291 })
292
293 it('Should fail with an unknown account', async function () {
294 await makePostBodyRequest({
295 url: server.url,
296 token: server.accessToken,
297 path,
298 fields: { accountName: 'user2' },
299 statusCodeExpected: 404
300 })
301 })
302
303 it('Should fail to block ourselves', async function () {
304 await makePostBodyRequest({
305 url: server.url,
306 token: server.accessToken,
307 path,
308 fields: { accountName: 'root' },
309 statusCodeExpected: 409
310 })
311 })
312
313 it('Should succeed with the correct params', async function () {
314 await makePostBodyRequest({
315 url: server.url,
316 token: server.accessToken,
317 path,
318 fields: { accountName: 'user1' },
319 statusCodeExpected: 204
320 })
321 })
322 })
323
324 describe('When unblocking an account', function () {
325 it('Should fail with an unauthenticated user', async function () {
326 await makeDeleteRequest({
327 url: server.url,
328 path: path + '/user1',
329 statusCodeExpected: 401
330 })
331 })
332
333 it('Should fail with a user without the appropriate rights', async function () {
334 await makeDeleteRequest({
335 url: server.url,
336 path: path + '/user1',
337 token: userAccessToken,
338 statusCodeExpected: 403
339 })
340 })
341
342 it('Should fail with an unknown account block', async function () {
343 await makeDeleteRequest({
344 url: server.url,
345 path: path + '/user2',
346 token: server.accessToken,
347 statusCodeExpected: 404
348 })
349 })
350
351 it('Should succeed with the correct params', async function () {
352 await makeDeleteRequest({
353 url: server.url,
354 path: path + '/user1',
355 token: server.accessToken,
356 statusCodeExpected: 204
357 })
358 })
359 })
360 })
361
362 describe('When managing server servers blocklist', function () {
363 const path = '/api/v1/server/blocklist/servers'
364
365 describe('When listing blocked servers', function () {
366 it('Should fail with an unauthenticated user', async function () {
367 await makeGetRequest({
368 url: server.url,
369 path,
370 statusCodeExpected: 401
371 })
372 })
373
374 it('Should fail with a user without the appropriate rights', async function () {
375 await makeGetRequest({
376 url: server.url,
377 token: userAccessToken,
378 path,
379 statusCodeExpected: 403
380 })
381 })
382
383 it('Should fail with a bad start pagination', async function () {
384 await checkBadStartPagination(server.url, path, server.accessToken)
385 })
386
387 it('Should fail with a bad count pagination', async function () {
388 await checkBadCountPagination(server.url, path, server.accessToken)
389 })
390
391 it('Should fail with an incorrect sort', async function () {
392 await checkBadSortPagination(server.url, path, server.accessToken)
393 })
394 })
395
396 describe('When blocking a server', function () {
397 it('Should fail with an unauthenticated user', async function () {
398 await makePostBodyRequest({
399 url: server.url,
400 path,
401 fields: { host: 'localhost:9002' },
402 statusCodeExpected: 401
403 })
404 })
405
406 it('Should fail with a user without the appropriate rights', async function () {
407 await makePostBodyRequest({
408 url: server.url,
409 token: userAccessToken,
410 path,
411 fields: { host: 'localhost:9002' },
412 statusCodeExpected: 403
413 })
414 })
415
416 it('Should fail with an unknown server', async function () {
417 await makePostBodyRequest({
418 url: server.url,
419 token: server.accessToken,
420 path,
421 fields: { host: 'localhost:9003' },
422 statusCodeExpected: 404
423 })
424 })
425
426 it('Should fail with our own server', async function () {
427 await makePostBodyRequest({
428 url: server.url,
429 token: server.accessToken,
430 path,
431 fields: { host: 'localhost:9001' },
432 statusCodeExpected: 409
433 })
434 })
435
436 it('Should succeed with the correct params', async function () {
437 await makePostBodyRequest({
438 url: server.url,
439 token: server.accessToken,
440 path,
441 fields: { host: 'localhost:9002' },
442 statusCodeExpected: 204
443 })
444 })
445 })
446
447 describe('When unblocking a server', function () {
448 it('Should fail with an unauthenticated user', async function () {
449 await makeDeleteRequest({
450 url: server.url,
451 path: path + '/localhost:9002',
452 statusCodeExpected: 401
453 })
454 })
455
456 it('Should fail with a user without the appropriate rights', async function () {
457 await makeDeleteRequest({
458 url: server.url,
459 path: path + '/localhost:9002',
460 token: userAccessToken,
461 statusCodeExpected: 403
462 })
463 })
464
465 it('Should fail with an unknown server block', async function () {
466 await makeDeleteRequest({
467 url: server.url,
468 path: path + '/localhost:9003',
469 token: server.accessToken,
470 statusCodeExpected: 404
471 })
472 })
473
474 it('Should succeed with the correct params', async function () {
475 await makeDeleteRequest({
476 url: server.url,
477 path: path + '/localhost:9002',
478 token: server.accessToken,
479 statusCodeExpected: 204
480 })
481 })
482 })
483 })
484 })
485
486 after(async function () {
487 killallServers(servers)
488
489 // Keep the logs if the test failed
490 if (this['ok']) {
491 await flushTests()
492 }
493 })
494})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index bfc550ae5..877ceb0a7 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,5 +1,6 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './accounts' 2import './accounts'
3import './blocklist'
3import './config' 4import './config'
4import './follows' 5import './follows'
5import './jobs' 6import './jobs'
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts
new file mode 100644
index 000000000..8e69b95a6
--- /dev/null
+++ b/server/tests/api/index-4.ts
@@ -0,0 +1 @@
import './redundancy'
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index 2d996dbf9..bc140f860 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -2,3 +2,4 @@
2import './index-1' 2import './index-1'
3import './index-2' 3import './index-2'
4import './index-3' 4import './index-3'
5import './index-4'
diff --git a/server/tests/api/redundancy/index.ts b/server/tests/api/redundancy/index.ts
new file mode 100644
index 000000000..8e69b95a6
--- /dev/null
+++ b/server/tests/api/redundancy/index.ts
@@ -0,0 +1 @@
import './redundancy'
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
new file mode 100644
index 000000000..1960854b6
--- /dev/null
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -0,0 +1,483 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos'
6import {
7 doubleFollow,
8 flushAndRunMultipleServers,
9 getFollowingListPaginationAndSort,
10 getVideo,
11 immutableAssign,
12 killallServers, makeGetRequest,
13 root,
14 ServerInfo,
15 setAccessTokensToServers, unfollow,
16 uploadVideo,
17 viewVideo,
18 wait,
19 waitUntilLog,
20 checkVideoFilesWereRemoved, removeVideo
21} from '../../utils'
22import { waitJobs } from '../../utils/server/jobs'
23import * as magnetUtil from 'magnet-uri'
24import { updateRedundancy } from '../../utils/server/redundancy'
25import { ActorFollow } from '../../../../shared/models/actors'
26import { readdir } from 'fs-extra'
27import { join } from 'path'
28import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
29import { getStats } from '../../utils/server/stats'
30import { ServerStats } from '../../../../shared/models/server/server-stats.model'
31
32const expect = chai.expect
33
34let servers: ServerInfo[] = []
35let video1Server2UUID: string
36
37function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
38 const parsed = magnetUtil.decode(file.magnetUri)
39
40 for (const ws of baseWebseeds) {
41 const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
42 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
43 }
44
45 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
46}
47
48async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) {
49 const config = {
50 redundancy: {
51 videos: {
52 check_interval: '5 seconds',
53 strategies: [
54 immutableAssign({
55 min_lifetime: '1 hour',
56 strategy: strategy,
57 size: '100KB'
58 }, additionalParams)
59 ]
60 }
61 }
62 }
63 servers = await flushAndRunMultipleServers(3, config)
64
65 // Get the access tokens
66 await setAccessTokensToServers(servers)
67
68 {
69 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' })
70 video1Server2UUID = res.body.video.uuid
71
72 await viewVideo(servers[ 1 ].url, video1Server2UUID)
73 }
74
75 await waitJobs(servers)
76
77 // Server 1 and server 2 follow each other
78 await doubleFollow(servers[ 0 ], servers[ 1 ])
79 // Server 1 and server 3 follow each other
80 await doubleFollow(servers[ 0 ], servers[ 2 ])
81 // Server 2 and server 3 follow each other
82 await doubleFollow(servers[ 1 ], servers[ 2 ])
83
84 await waitJobs(servers)
85}
86
87async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) {
88 if (!videoUUID) videoUUID = video1Server2UUID
89
90 const webseeds = [
91 'http://localhost:9002/static/webseed/' + videoUUID
92 ]
93
94 for (const server of servers) {
95 {
96 const res = await getVideo(server.url, videoUUID)
97
98 const video: VideoDetails = res.body
99 for (const f of video.files) {
100 checkMagnetWebseeds(f, webseeds, server)
101 }
102 }
103 }
104}
105
106async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
107 const res = await getStats(servers[0].url)
108 const data: ServerStats = res.body
109
110 expect(data.videosRedundancy).to.have.lengthOf(1)
111 const stat = data.videosRedundancy[0]
112
113 expect(stat.strategy).to.equal(strategy)
114 expect(stat.totalSize).to.equal(102400)
115 expect(stat.totalUsed).to.be.at.least(1).and.below(102401)
116 expect(stat.totalVideoFiles).to.equal(4)
117 expect(stat.totalVideos).to.equal(1)
118}
119
120async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
121 const res = await getStats(servers[0].url)
122 const data: ServerStats = res.body
123
124 expect(data.videosRedundancy).to.have.lengthOf(1)
125
126 const stat = data.videosRedundancy[0]
127 expect(stat.strategy).to.equal(strategy)
128 expect(stat.totalSize).to.equal(102400)
129 expect(stat.totalUsed).to.equal(0)
130 expect(stat.totalVideoFiles).to.equal(0)
131 expect(stat.totalVideos).to.equal(0)
132}
133
134async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) {
135 if (!videoUUID) videoUUID = video1Server2UUID
136
137 const webseeds = [
138 'http://localhost:9001/static/webseed/' + videoUUID,
139 'http://localhost:9002/static/webseed/' + videoUUID
140 ]
141
142 for (const server of servers) {
143 const res = await getVideo(server.url, videoUUID)
144
145 const video: VideoDetails = res.body
146
147 for (const file of video.files) {
148 checkMagnetWebseeds(file, webseeds, server)
149
150 // Only servers 1 and 2 have the video
151 if (server.serverNumber !== 3) {
152 await makeGetRequest({
153 url: server.url,
154 statusCodeExpected: 200,
155 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`,
156 contentType: null
157 })
158 }
159 }
160 }
161
162 for (const directory of [ 'test1', 'test2' ]) {
163 const files = await readdir(join(root(), directory, 'videos'))
164 expect(files).to.have.length.at.least(4)
165
166 for (const resolution of [ 240, 360, 480, 720 ]) {
167 expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
168 }
169 }
170}
171
172async function enableRedundancyOnServer1 () {
173 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true)
174
175 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
176 const follows: ActorFollow[] = res.body.data
177 const server2 = follows.find(f => f.following.host === 'localhost:9002')
178 const server3 = follows.find(f => f.following.host === 'localhost:9003')
179
180 expect(server3).to.not.be.undefined
181 expect(server3.following.hostRedundancyAllowed).to.be.false
182
183 expect(server2).to.not.be.undefined
184 expect(server2.following.hostRedundancyAllowed).to.be.true
185}
186
187async function disableRedundancyOnServer1 () {
188 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false)
189
190 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
191 const follows: ActorFollow[] = res.body.data
192 const server2 = follows.find(f => f.following.host === 'localhost:9002')
193 const server3 = follows.find(f => f.following.host === 'localhost:9003')
194
195 expect(server3).to.not.be.undefined
196 expect(server3.following.hostRedundancyAllowed).to.be.false
197
198 expect(server2).to.not.be.undefined
199 expect(server2.following.hostRedundancyAllowed).to.be.false
200}
201
202async function cleanServers () {
203 killallServers(servers)
204}
205
206describe('Test videos redundancy', function () {
207
208 describe('With most-views strategy', function () {
209 const strategy = 'most-views'
210
211 before(function () {
212 this.timeout(120000)
213
214 return runServers(strategy)
215 })
216
217 it('Should have 1 webseed on the first video', async function () {
218 await check1WebSeed(strategy)
219 await checkStatsWith1Webseed(strategy)
220 })
221
222 it('Should enable redundancy on server 1', function () {
223 return enableRedundancyOnServer1()
224 })
225
226 it('Should have 2 webseed on the first video', async function () {
227 this.timeout(40000)
228
229 await waitJobs(servers)
230 await waitUntilLog(servers[0], 'Duplicated ', 4)
231 await waitJobs(servers)
232
233 await check2Webseeds(strategy)
234 await checkStatsWith2Webseed(strategy)
235 })
236
237 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
238 this.timeout(40000)
239
240 await disableRedundancyOnServer1()
241
242 await waitJobs(servers)
243 await wait(5000)
244
245 await check1WebSeed(strategy)
246
247 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
248 })
249
250 after(function () {
251 return cleanServers()
252 })
253 })
254
255 describe('With trending strategy', function () {
256 const strategy = 'trending'
257
258 before(function () {
259 this.timeout(120000)
260
261 return runServers(strategy)
262 })
263
264 it('Should have 1 webseed on the first video', async function () {
265 await check1WebSeed(strategy)
266 await checkStatsWith1Webseed(strategy)
267 })
268
269 it('Should enable redundancy on server 1', function () {
270 return enableRedundancyOnServer1()
271 })
272
273 it('Should have 2 webseed on the first video', async function () {
274 this.timeout(40000)
275
276 await waitJobs(servers)
277 await waitUntilLog(servers[0], 'Duplicated ', 4)
278 await waitJobs(servers)
279
280 await check2Webseeds(strategy)
281 await checkStatsWith2Webseed(strategy)
282 })
283
284 it('Should unfollow on server 1 and remove duplicated videos', async function () {
285 this.timeout(40000)
286
287 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
288
289 await waitJobs(servers)
290 await wait(5000)
291
292 await check1WebSeed(strategy)
293
294 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
295 })
296
297 after(function () {
298 return cleanServers()
299 })
300 })
301
302 describe('With recently added strategy', function () {
303 const strategy = 'recently-added'
304
305 before(function () {
306 this.timeout(120000)
307
308 return runServers(strategy, { min_views: 3 })
309 })
310
311 it('Should have 1 webseed on the first video', async function () {
312 await check1WebSeed(strategy)
313 await checkStatsWith1Webseed(strategy)
314 })
315
316 it('Should enable redundancy on server 1', function () {
317 return enableRedundancyOnServer1()
318 })
319
320 it('Should still have 1 webseed on the first video', async function () {
321 this.timeout(40000)
322
323 await waitJobs(servers)
324 await wait(15000)
325 await waitJobs(servers)
326
327 await check1WebSeed(strategy)
328 await checkStatsWith1Webseed(strategy)
329 })
330
331 it('Should view 2 times the first video to have > min_views config', async function () {
332 this.timeout(40000)
333
334 await viewVideo(servers[ 0 ].url, video1Server2UUID)
335 await viewVideo(servers[ 2 ].url, video1Server2UUID)
336
337 await wait(10000)
338 await waitJobs(servers)
339 })
340
341 it('Should have 2 webseed on the first video', async function () {
342 this.timeout(40000)
343
344 await waitJobs(servers)
345 await waitUntilLog(servers[0], 'Duplicated ', 4)
346 await waitJobs(servers)
347
348 await check2Webseeds(strategy)
349 await checkStatsWith2Webseed(strategy)
350 })
351
352 it('Should remove the video and the redundancy files', async function () {
353 this.timeout(20000)
354
355 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
356
357 await waitJobs(servers)
358
359 for (const server of servers) {
360 await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber)
361 }
362 })
363
364 after(function () {
365 return cleanServers()
366 })
367 })
368
369 describe('Test expiration', function () {
370 const strategy = 'recently-added'
371
372 async function checkContains (servers: ServerInfo[], str: string) {
373 for (const server of servers) {
374 const res = await getVideo(server.url, video1Server2UUID)
375 const video: VideoDetails = res.body
376
377 for (const f of video.files) {
378 expect(f.magnetUri).to.contain(str)
379 }
380 }
381 }
382
383 async function checkNotContains (servers: ServerInfo[], str: string) {
384 for (const server of servers) {
385 const res = await getVideo(server.url, video1Server2UUID)
386 const video: VideoDetails = res.body
387
388 for (const f of video.files) {
389 expect(f.magnetUri).to.not.contain(str)
390 }
391 }
392 }
393
394 before(async function () {
395 this.timeout(120000)
396
397 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
398
399 await enableRedundancyOnServer1()
400 })
401
402 it('Should still have 2 webseeds after 10 seconds', async function () {
403 this.timeout(40000)
404
405 await wait(10000)
406
407 try {
408 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
409 } catch {
410 // Maybe a server deleted a redundancy in the scheduler
411 await wait(2000)
412
413 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
414 }
415 })
416
417 it('Should stop server 1 and expire video redundancy', async function () {
418 this.timeout(40000)
419
420 killallServers([ servers[0] ])
421
422 await wait(10000)
423
424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425 })
426
427 after(function () {
428 return killallServers([ servers[1], servers[2] ])
429 })
430 })
431
432 describe('Test file replacement', function () {
433 let video2Server2UUID: string
434 const strategy = 'recently-added'
435
436 before(async function () {
437 this.timeout(120000)
438
439 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
440
441 await enableRedundancyOnServer1()
442
443 await waitJobs(servers)
444 await waitUntilLog(servers[0], 'Duplicated ', 4)
445 await waitJobs(servers)
446
447 await check2Webseeds(strategy)
448 await checkStatsWith2Webseed(strategy)
449
450 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
451 video2Server2UUID = res.body.video.uuid
452 })
453
454 it('Should cache video 2 webseed on the first video', async function () {
455 this.timeout(50000)
456
457 await waitJobs(servers)
458
459 await wait(7000)
460
461 try {
462 await check1WebSeed(strategy, video1Server2UUID)
463 await check2Webseeds(strategy, video2Server2UUID)
464 } catch {
465 await wait(3000)
466
467 try {
468 await check1WebSeed(strategy, video1Server2UUID)
469 await check2Webseeds(strategy, video2Server2UUID)
470 } catch {
471 await wait(5000)
472
473 await check1WebSeed(strategy, video1Server2UUID)
474 await check2Webseeds(strategy, video2Server2UUID)
475 }
476 }
477 })
478
479 after(function () {
480 return cleanServers()
481 })
482 })
483})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index c74c68a33..eeb8b7a28 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -3,7 +3,6 @@ import './email'
3import './follows' 3import './follows'
4import './handle-down' 4import './handle-down'
5import './jobs' 5import './jobs'
6import './redundancy'
7import './reverse-proxy' 6import './reverse-proxy'
8import './stats' 7import './stats'
9import './tracker' 8import './tracker'
diff --git a/server/tests/api/server/redundancy.ts b/server/tests/api/server/redundancy.ts
index 1960854b6..f50d6e3cf 100644
--- a/server/tests/api/server/redundancy.ts
+++ b/server/tests/api/server/redundancy.ts
@@ -419,7 +419,7 @@ describe('Test videos redundancy', function () {
419 419
420 killallServers([ servers[0] ]) 420 killallServers([ servers[0] ])
421 421
422 await wait(10000) 422 await wait(15000)
423 423
424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') 424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425 }) 425 })
@@ -452,26 +452,22 @@ describe('Test videos redundancy', function () {
452 }) 452 })
453 453
454 it('Should cache video 2 webseed on the first video', async function () { 454 it('Should cache video 2 webseed on the first video', async function () {
455 this.timeout(50000) 455 this.timeout(120000)
456 456
457 await waitJobs(servers) 457 await waitJobs(servers)
458 458
459 await wait(7000) 459 let checked = false
460 460
461 try { 461 while (checked === false) {
462 await check1WebSeed(strategy, video1Server2UUID) 462 await wait(1000)
463 await check2Webseeds(strategy, video2Server2UUID)
464 } catch {
465 await wait(3000)
466 463
467 try { 464 try {
468 await check1WebSeed(strategy, video1Server2UUID) 465 await check1WebSeed(strategy, video1Server2UUID)
469 await check2Webseeds(strategy, video2Server2UUID) 466 await check2Webseeds(strategy, video2Server2UUID)
470 } catch {
471 await wait(5000)
472 467
473 await check1WebSeed(strategy, video1Server2UUID) 468 checked = true
474 await check2Webseeds(strategy, video2Server2UUID) 469 } catch {
470 checked = false
475 } 471 }
476 } 472 }
477 }) 473 })
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
new file mode 100644
index 000000000..eed4b9f3e
--- /dev/null
+++ b/server/tests/api/users/blocklist.ts
@@ -0,0 +1,511 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index'
6import {
7 createUser,
8 doubleFollow,
9 flushAndRunMultipleServers,
10 flushTests,
11 killallServers,
12 ServerInfo,
13 uploadVideo,
14 userLogin
15} from '../../utils/index'
16import { setAccessTokensToServers } from '../../utils/users/login'
17import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos'
18import {
19 addVideoCommentReply,
20 addVideoCommentThread,
21 getVideoCommentThreads,
22 getVideoThreadComments
23} from '../../utils/videos/video-comments'
24import { waitJobs } from '../../utils/server/jobs'
25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
26import {
27 addAccountToAccountBlocklist,
28 addAccountToServerBlocklist,
29 addServerToAccountBlocklist,
30 addServerToServerBlocklist,
31 getAccountBlocklistByAccount,
32 getAccountBlocklistByServer,
33 getServerBlocklistByAccount,
34 getServerBlocklistByServer,
35 removeAccountFromAccountBlocklist,
36 removeAccountFromServerBlocklist,
37 removeServerFromAccountBlocklist,
38 removeServerFromServerBlocklist
39} from '../../utils/users/blocklist'
40
41const expect = chai.expect
42
43async function checkAllVideos (url: string, token: string) {
44 {
45 const res = await getVideosListWithToken(url, token)
46
47 expect(res.body.data).to.have.lengthOf(4)
48 }
49
50 {
51 const res = await getVideosList(url)
52
53 expect(res.body.data).to.have.lengthOf(4)
54 }
55}
56
57async function checkAllComments (url: string, token: string, videoUUID: string) {
58 const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 5, '-createdAt', token)
59
60 const threads: VideoComment[] = resThreads.body.data
61 expect(threads).to.have.lengthOf(2)
62
63 for (const thread of threads) {
64 const res = await getVideoThreadComments(url, videoUUID, thread.id, token)
65
66 const tree: VideoCommentThreadTree = res.body
67 expect(tree.children).to.have.lengthOf(1)
68 }
69}
70
71describe('Test blocklist', function () {
72 let servers: ServerInfo[]
73 let videoUUID1: string
74 let videoUUID2: string
75 let userToken1: string
76 let userModeratorToken: string
77 let userToken2: string
78
79 before(async function () {
80 this.timeout(60000)
81
82 await flushTests()
83
84 servers = await flushAndRunMultipleServers(2)
85 await setAccessTokensToServers(servers)
86
87 {
88 const user = { username: 'user1', password: 'password' }
89 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
90
91 userToken1 = await userLogin(servers[0], user)
92 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
93 }
94
95 {
96 const user = { username: 'moderator', password: 'password' }
97 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
98
99 userModeratorToken = await userLogin(servers[0], user)
100 }
101
102 {
103 const user = { username: 'user2', password: 'password' }
104 await createUser(servers[1].url, servers[1].accessToken, user.username, user.password)
105
106 userToken2 = await userLogin(servers[1], user)
107 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
108 }
109
110 {
111 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
112 videoUUID1 = res.body.video.uuid
113 }
114
115 {
116 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
117 videoUUID2 = res.body.video.uuid
118 }
119
120 await doubleFollow(servers[0], servers[1])
121
122 {
123 const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1')
124 const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1')
125 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1')
126 }
127
128 {
129 const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1')
130 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1')
131 }
132
133 await waitJobs(servers)
134 })
135
136 describe('User blocklist', function () {
137
138 describe('When managing account blocklist', function () {
139 it('Should list all videos', function () {
140 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken)
141 })
142
143 it('Should list the comments', function () {
144 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1)
145 })
146
147 it('Should block a remote account', async function () {
148 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002')
149 })
150
151 it('Should hide its videos', async function () {
152 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken)
153
154 const videos: Video[] = res.body.data
155 expect(videos).to.have.lengthOf(3)
156
157 const v = videos.find(v => v.name === 'video user 2')
158 expect(v).to.be.undefined
159 })
160
161 it('Should block a local account', async function () {
162 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1')
163 })
164
165 it('Should hide its videos', async function () {
166 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken)
167
168 const videos: Video[] = res.body.data
169 expect(videos).to.have.lengthOf(2)
170
171 const v = videos.find(v => v.name === 'video user 1')
172 expect(v).to.be.undefined
173 })
174
175 it('Should hide its comments', async function () {
176 const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', servers[ 0 ].accessToken)
177
178 const threads: VideoComment[] = resThreads.body.data
179 expect(threads).to.have.lengthOf(1)
180 expect(threads[ 0 ].totalReplies).to.equal(0)
181
182 const t = threads.find(t => t.text === 'comment user 1')
183 expect(t).to.be.undefined
184
185 for (const thread of threads) {
186 const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, servers[ 0 ].accessToken)
187
188 const tree: VideoCommentThreadTree = res.body
189 expect(tree.children).to.have.lengthOf(0)
190 }
191 })
192
193 it('Should list all the videos with another user', async function () {
194 return checkAllVideos(servers[ 0 ].url, userToken1)
195 })
196
197 it('Should list all the comments with another user', async function () {
198 return checkAllComments(servers[ 0 ].url, userToken1, videoUUID1)
199 })
200
201 it('Should list blocked accounts', async function () {
202 {
203 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
204 const blocks: AccountBlock[] = res.body.data
205
206 expect(res.body.total).to.equal(2)
207
208 const block = blocks[ 0 ]
209 expect(block.byAccount.displayName).to.equal('root')
210 expect(block.byAccount.name).to.equal('root')
211 expect(block.blockedAccount.displayName).to.equal('user2')
212 expect(block.blockedAccount.name).to.equal('user2')
213 expect(block.blockedAccount.host).to.equal('localhost:9002')
214 }
215
216 {
217 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt')
218 const blocks: AccountBlock[] = res.body.data
219
220 expect(res.body.total).to.equal(2)
221
222 const block = blocks[ 0 ]
223 expect(block.byAccount.displayName).to.equal('root')
224 expect(block.byAccount.name).to.equal('root')
225 expect(block.blockedAccount.displayName).to.equal('user1')
226 expect(block.blockedAccount.name).to.equal('user1')
227 expect(block.blockedAccount.host).to.equal('localhost:9001')
228 }
229 })
230
231 it('Should unblock the remote account', async function () {
232 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002')
233 })
234
235 it('Should display its videos', async function () {
236 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken)
237
238 const videos: Video[] = res.body.data
239 expect(videos).to.have.lengthOf(3)
240
241 const v = videos.find(v => v.name === 'video user 2')
242 expect(v).not.to.be.undefined
243 })
244
245 it('Should unblock the local account', async function () {
246 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1')
247 })
248
249 it('Should display its comments', function () {
250 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1)
251 })
252 })
253
254 describe('When managing server blocklist', function () {
255 it('Should list all videos', function () {
256 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken)
257 })
258
259 it('Should list the comments', function () {
260 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1)
261 })
262
263 it('Should block a remote server', async function () {
264 await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002')
265 })
266
267 it('Should hide its videos', async function () {
268 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken)
269
270 const videos: Video[] = res.body.data
271 expect(videos).to.have.lengthOf(2)
272
273 const v1 = videos.find(v => v.name === 'video user 2')
274 const v2 = videos.find(v => v.name === 'video server 2')
275
276 expect(v1).to.be.undefined
277 expect(v2).to.be.undefined
278 })
279
280 it('Should list all the videos with another user', async function () {
281 return checkAllVideos(servers[ 0 ].url, userToken1)
282 })
283
284 it('Should hide its comments')
285
286 it('Should list blocked servers', async function () {
287 const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
288 const blocks: ServerBlock[] = res.body.data
289
290 expect(res.body.total).to.equal(1)
291
292 const block = blocks[ 0 ]
293 expect(block.byAccount.displayName).to.equal('root')
294 expect(block.byAccount.name).to.equal('root')
295 expect(block.blockedServer.host).to.equal('localhost:9002')
296 })
297
298 it('Should unblock the remote server', async function () {
299 await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002')
300 })
301
302 it('Should display its videos', function () {
303 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken)
304 })
305
306 it('Should display its comments', function () {
307 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1)
308 })
309 })
310 })
311
312 describe('Server blocklist', function () {
313
314 describe('When managing account blocklist', function () {
315 it('Should list all videos', async function () {
316 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
317 await checkAllVideos(servers[ 0 ].url, token)
318 }
319 })
320
321 it('Should list the comments', async function () {
322 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
323 await checkAllComments(servers[ 0 ].url, token, videoUUID1)
324 }
325 })
326
327 it('Should block a remote account', async function () {
328 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002')
329 })
330
331 it('Should hide its videos', async function () {
332 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
333 const res = await getVideosListWithToken(servers[ 0 ].url, token)
334
335 const videos: Video[] = res.body.data
336 expect(videos).to.have.lengthOf(3)
337
338 const v = videos.find(v => v.name === 'video user 2')
339 expect(v).to.be.undefined
340 }
341 })
342
343 it('Should block a local account', async function () {
344 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1')
345 })
346
347 it('Should hide its videos', async function () {
348 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
349 const res = await getVideosListWithToken(servers[ 0 ].url, token)
350
351 const videos: Video[] = res.body.data
352 expect(videos).to.have.lengthOf(2)
353
354 const v = videos.find(v => v.name === 'video user 1')
355 expect(v).to.be.undefined
356 }
357 })
358
359 it('Should hide its comments', async function () {
360 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
361 const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', token)
362
363 const threads: VideoComment[] = resThreads.body.data
364 expect(threads).to.have.lengthOf(1)
365 expect(threads[ 0 ].totalReplies).to.equal(0)
366
367 const t = threads.find(t => t.text === 'comment user 1')
368 expect(t).to.be.undefined
369
370 for (const thread of threads) {
371 const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, token)
372
373 const tree: VideoCommentThreadTree = res.body
374 expect(tree.children).to.have.lengthOf(0)
375 }
376 }
377 })
378
379 it('Should list blocked accounts', async function () {
380 {
381 const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
382 const blocks: AccountBlock[] = res.body.data
383
384 expect(res.body.total).to.equal(2)
385
386 const block = blocks[ 0 ]
387 expect(block.byAccount.displayName).to.equal('peertube')
388 expect(block.byAccount.name).to.equal('peertube')
389 expect(block.blockedAccount.displayName).to.equal('user2')
390 expect(block.blockedAccount.name).to.equal('user2')
391 expect(block.blockedAccount.host).to.equal('localhost:9002')
392 }
393
394 {
395 const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt')
396 const blocks: AccountBlock[] = res.body.data
397
398 expect(res.body.total).to.equal(2)
399
400 const block = blocks[ 0 ]
401 expect(block.byAccount.displayName).to.equal('peertube')
402 expect(block.byAccount.name).to.equal('peertube')
403 expect(block.blockedAccount.displayName).to.equal('user1')
404 expect(block.blockedAccount.name).to.equal('user1')
405 expect(block.blockedAccount.host).to.equal('localhost:9001')
406 }
407 })
408
409 it('Should unblock the remote account', async function () {
410 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002')
411 })
412
413 it('Should display its videos', async function () {
414 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
415 const res = await getVideosListWithToken(servers[ 0 ].url, token)
416
417 const videos: Video[] = res.body.data
418 expect(videos).to.have.lengthOf(3)
419
420 const v = videos.find(v => v.name === 'video user 2')
421 expect(v).not.to.be.undefined
422 }
423 })
424
425 it('Should unblock the local account', async function () {
426 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1')
427 })
428
429 it('Should display its comments', async function () {
430 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
431 await checkAllComments(servers[ 0 ].url, token, videoUUID1)
432 }
433 })
434 })
435
436 describe('When managing server blocklist', function () {
437 it('Should list all videos', async function () {
438 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
439 await checkAllVideos(servers[ 0 ].url, token)
440 }
441 })
442
443 it('Should list the comments', async function () {
444 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
445 await checkAllComments(servers[ 0 ].url, token, videoUUID1)
446 }
447 })
448
449 it('Should block a remote server', async function () {
450 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002')
451 })
452
453 it('Should hide its videos', async function () {
454 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
455 const res1 = await getVideosList(servers[ 0 ].url)
456 const res2 = await getVideosListWithToken(servers[ 0 ].url, token)
457
458 for (const res of [ res1, res2 ]) {
459 const videos: Video[] = res.body.data
460 expect(videos).to.have.lengthOf(2)
461
462 const v1 = videos.find(v => v.name === 'video user 2')
463 const v2 = videos.find(v => v.name === 'video server 2')
464
465 expect(v1).to.be.undefined
466 expect(v2).to.be.undefined
467 }
468 }
469 })
470
471 it('Should hide its comments')
472
473 it('Should list blocked servers', async function () {
474 const res = await getServerBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
475 const blocks: ServerBlock[] = res.body.data
476
477 expect(res.body.total).to.equal(1)
478
479 const block = blocks[ 0 ]
480 expect(block.byAccount.displayName).to.equal('peertube')
481 expect(block.byAccount.name).to.equal('peertube')
482 expect(block.blockedServer.host).to.equal('localhost:9002')
483 })
484
485 it('Should unblock the remote server', async function () {
486 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002')
487 })
488
489 it('Should list all videos', async function () {
490 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
491 await checkAllVideos(servers[ 0 ].url, token)
492 }
493 })
494
495 it('Should list the comments', async function () {
496 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
497 await checkAllComments(servers[ 0 ].url, token, videoUUID1)
498 }
499 })
500 })
501 })
502
503 after(async function () {
504 killallServers(servers)
505
506 // Keep the logs if the test failed
507 if (this[ 'ok' ]) {
508 await flushTests()
509 }
510 })
511})
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
index 21d75da3e..0a1b8b0b2 100644
--- a/server/tests/api/users/index.ts
+++ b/server/tests/api/users/index.ts
@@ -1,3 +1,4 @@
1import './blocklist'
1import './user-subscriptions' 2import './user-subscriptions'
2import './users' 3import './users'
3import './users-verification' 4import './users-verification'
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index e3d62b7a0..089c3df25 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -118,7 +118,7 @@ describe('Test a single server', function () {
118 const categories = res.body 118 const categories = res.body
119 expect(Object.keys(categories)).to.have.length.above(10) 119 expect(Object.keys(categories)).to.have.length.above(10)
120 120
121 expect(categories[11]).to.equal('News') 121 expect(categories[11]).to.equal('News & Politics')
122 }) 122 })
123 123
124 it('Should list video licences', async function () { 124 it('Should list video licences', async function () {
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index b7866d529..aaee79a4a 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -30,7 +30,7 @@ describe('Test video imports', function () {
30 const videoHttp: VideoDetails = resHttp.body 30 const videoHttp: VideoDetails = resHttp.body
31 31
32 expect(videoHttp.name).to.equal('small video - youtube') 32 expect(videoHttp.name).to.equal('small video - youtube')
33 expect(videoHttp.category.label).to.equal('News') 33 expect(videoHttp.category.label).to.equal('News & Politics')
34 expect(videoHttp.licence.label).to.equal('Attribution') 34 expect(videoHttp.licence.label).to.equal('Attribution')
35 expect(videoHttp.language.label).to.equal('Unknown') 35 expect(videoHttp.language.label).to.equal('Unknown')
36 expect(videoHttp.nsfw).to.be.false 36 expect(videoHttp.nsfw).to.be.false
diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts
index 27a529eda..5796540f7 100644
--- a/server/tests/utils/requests/requests.ts
+++ b/server/tests/utils/requests/requests.ts
@@ -37,9 +37,7 @@ function makeDeleteRequest (options: {
37 37
38 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 38 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
39 39
40 return req 40 return req.expect(options.statusCodeExpected)
41 .expect('Content-Type', /json/)
42 .expect(options.statusCodeExpected)
43} 41}
44 42
45function makeUploadRequest (options: { 43function makeUploadRequest (options: {
diff --git a/server/tests/utils/users/blocklist.ts b/server/tests/utils/users/blocklist.ts
new file mode 100644
index 000000000..35b537571
--- /dev/null
+++ b/server/tests/utils/users/blocklist.ts
@@ -0,0 +1,198 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeDeleteRequest, makePostBodyRequest } from '../index'
4import { makeGetRequest } from '../requests/requests'
5
6function getAccountBlocklistByAccount (
7 url: string,
8 token: string,
9 start: number,
10 count: number,
11 sort = '-createdAt',
12 statusCodeExpected = 200
13) {
14 const path = '/api/v1/users/me/blocklist/accounts'
15
16 return makeGetRequest({
17 url,
18 token,
19 query: { start, count, sort },
20 path,
21 statusCodeExpected
22 })
23}
24
25function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
26 const path = '/api/v1/users/me/blocklist/accounts'
27
28 return makePostBodyRequest({
29 url,
30 path,
31 token,
32 fields: {
33 accountName: accountToBlock
34 },
35 statusCodeExpected
36 })
37}
38
39function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
40 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
41
42 return makeDeleteRequest({
43 url,
44 path,
45 token,
46 statusCodeExpected
47 })
48}
49
50function getServerBlocklistByAccount (
51 url: string,
52 token: string,
53 start: number,
54 count: number,
55 sort = '-createdAt',
56 statusCodeExpected = 200
57) {
58 const path = '/api/v1/users/me/blocklist/servers'
59
60 return makeGetRequest({
61 url,
62 token,
63 query: { start, count, sort },
64 path,
65 statusCodeExpected
66 })
67}
68
69function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
70 const path = '/api/v1/users/me/blocklist/servers'
71
72 return makePostBodyRequest({
73 url,
74 path,
75 token,
76 fields: {
77 host: serverToBlock
78 },
79 statusCodeExpected
80 })
81}
82
83function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
84 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
85
86 return makeDeleteRequest({
87 url,
88 path,
89 token,
90 statusCodeExpected
91 })
92}
93
94function getAccountBlocklistByServer (
95 url: string,
96 token: string,
97 start: number,
98 count: number,
99 sort = '-createdAt',
100 statusCodeExpected = 200
101) {
102 const path = '/api/v1/server/blocklist/accounts'
103
104 return makeGetRequest({
105 url,
106 token,
107 query: { start, count, sort },
108 path,
109 statusCodeExpected
110 })
111}
112
113function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
114 const path = '/api/v1/server/blocklist/accounts'
115
116 return makePostBodyRequest({
117 url,
118 path,
119 token,
120 fields: {
121 accountName: accountToBlock
122 },
123 statusCodeExpected
124 })
125}
126
127function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
128 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
129
130 return makeDeleteRequest({
131 url,
132 path,
133 token,
134 statusCodeExpected
135 })
136}
137
138function getServerBlocklistByServer (
139 url: string,
140 token: string,
141 start: number,
142 count: number,
143 sort = '-createdAt',
144 statusCodeExpected = 200
145) {
146 const path = '/api/v1/server/blocklist/servers'
147
148 return makeGetRequest({
149 url,
150 token,
151 query: { start, count, sort },
152 path,
153 statusCodeExpected
154 })
155}
156
157function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
158 const path = '/api/v1/server/blocklist/servers'
159
160 return makePostBodyRequest({
161 url,
162 path,
163 token,
164 fields: {
165 host: serverToBlock
166 },
167 statusCodeExpected
168 })
169}
170
171function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
172 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
173
174 return makeDeleteRequest({
175 url,
176 path,
177 token,
178 statusCodeExpected
179 })
180}
181
182// ---------------------------------------------------------------------------
183
184export {
185 getAccountBlocklistByAccount,
186 addAccountToAccountBlocklist,
187 removeAccountFromAccountBlocklist,
188 getServerBlocklistByAccount,
189 addServerToAccountBlocklist,
190 removeServerFromAccountBlocklist,
191
192 getAccountBlocklistByServer,
193 addAccountToServerBlocklist,
194 removeAccountFromServerBlocklist,
195 getServerBlocklistByServer,
196 addServerToServerBlocklist,
197 removeServerFromServerBlocklist
198}
diff --git a/server/tests/utils/videos/video-comments.ts b/server/tests/utils/videos/video-comments.ts
index 1b9ee452e..7d4cae364 100644
--- a/server/tests/utils/videos/video-comments.ts
+++ b/server/tests/utils/videos/video-comments.ts
@@ -1,7 +1,7 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { makeDeleteRequest } from '../' 2import { makeDeleteRequest } from '../'
3 3
4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { 4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
5 const path = '/api/v1/videos/' + videoId + '/comment-threads' 5 const path = '/api/v1/videos/' + videoId + '/comment-threads'
6 6
7 const req = request(url) 7 const req = request(url)
@@ -10,20 +10,24 @@ function getVideoCommentThreads (url: string, videoId: number | string, start: n
10 .query({ count: count }) 10 .query({ count: count })
11 11
12 if (sort) req.query({ sort }) 12 if (sort) req.query({ sort })
13 if (token) req.set('Authorization', 'Bearer ' + token)
13 14
14 return req.set('Accept', 'application/json') 15 return req.set('Accept', 'application/json')
15 .expect(200) 16 .expect(200)
16 .expect('Content-Type', /json/) 17 .expect('Content-Type', /json/)
17} 18}
18 19
19function getVideoThreadComments (url: string, videoId: number | string, threadId: number) { 20function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
20 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId 21 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
21 22
22 return request(url) 23 const req = request(url)
23 .get(path) 24 .get(path)
24 .set('Accept', 'application/json') 25 .set('Accept', 'application/json')
25 .expect(200) 26
26 .expect('Content-Type', /json/) 27 if (token) req.set('Authorization', 'Bearer ' + token)
28
29 return req.expect(200)
30 .expect('Content-Type', /json/)
27} 31}
28 32
29function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { 33function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
diff --git a/server/tools/repl.ts b/server/tools/repl.ts
new file mode 100644
index 000000000..6800ff8ab
--- /dev/null
+++ b/server/tools/repl.ts
@@ -0,0 +1,79 @@
1import * as repl from 'repl'
2import * as path from 'path'
3import * as _ from 'lodash'
4import * as uuidv1 from 'uuid/v1'
5import * as uuidv3 from 'uuid/v3'
6import * as uuidv4 from 'uuid/v4'
7import * as uuidv5 from 'uuid/v5'
8import * as Sequelize from 'sequelize'
9import * as YoutubeDL from 'youtube-dl'
10
11import { initDatabaseModels, sequelizeTypescript } from '../initializers'
12import * as cli from '../tools/cli'
13import { logger } from '../helpers/logger'
14import * as constants from '../initializers/constants'
15import * as modelsUtils from '../models/utils'
16import * as coreUtils from '../helpers/core-utils'
17import * as ffmpegUtils from '../helpers/ffmpeg-utils'
18import * as peertubeCryptoUtils from '../helpers/peertube-crypto'
19import * as signupUtils from '../helpers/signup'
20import * as utils from '../helpers/utils'
21import * as YoutubeDLUtils from '../helpers/youtube-dl'
22
23let versionCommitHash
24
25const start = async () => {
26 await initDatabaseModels(true)
27
28 await utils.getVersion().then((data) => {
29 versionCommitHash = data
30 })
31
32 const initContext = (replServer) => {
33 return (context) => {
34 const properties = {
35 context, repl: replServer, env: process.env,
36 lodash: _, path,
37 uuidv1, uuidv3, uuidv4, uuidv5,
38 cli, logger, constants,
39 Sequelize, sequelizeTypescript, modelsUtils,
40 models: sequelizeTypescript.models, transaction: sequelizeTypescript.transaction,
41 query: sequelizeTypescript.query, queryInterface: sequelizeTypescript.getQueryInterface(),
42 YoutubeDL,
43 coreUtils, ffmpegUtils, peertubeCryptoUtils, signupUtils, utils, YoutubeDLUtils
44 }
45
46 for (let prop in properties) {
47 Object.defineProperty(context, prop, {
48 configurable: false,
49 enumerable: true,
50 value: properties[prop]
51 })
52 }
53 }
54 }
55
56 const replServer = repl.start({
57 prompt: `PeerTube [${cli.version}] (${versionCommitHash})> `
58 })
59
60 initContext(replServer)(replServer.context)
61 replServer.on('reset', initContext(replServer))
62
63 const resetCommand = {
64 help: 'Reset REPL',
65 action () {
66 this.write('.clear\n')
67 this.displayPrompt()
68 }
69 }
70 replServer.defineCommand('reset', resetCommand)
71 replServer.defineCommand('r', resetCommand)
72
73}
74
75start().then((data) => {
76 // do nothing
77}).catch((err) => {
78 console.error(err)
79})
diff --git a/shared/models/blocklist/account-block.model.ts b/shared/models/blocklist/account-block.model.ts
new file mode 100644
index 000000000..a942ed614
--- /dev/null
+++ b/shared/models/blocklist/account-block.model.ts
@@ -0,0 +1,7 @@
1import { Account } from '../actors'
2
3export interface AccountBlock {
4 byAccount: Account
5 blockedAccount: Account
6 createdAt: Date | string
7}
diff --git a/shared/models/blocklist/index.ts b/shared/models/blocklist/index.ts
new file mode 100644
index 000000000..fc7873270
--- /dev/null
+++ b/shared/models/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './account-block.model'
2export * from './server-block.model'
diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/blocklist/server-block.model.ts
new file mode 100644
index 000000000..a8b8af0b7
--- /dev/null
+++ b/shared/models/blocklist/server-block.model.ts
@@ -0,0 +1,9 @@
1import { Account } from '../actors'
2
3export interface ServerBlock {
4 byAccount: Account
5 blockedServer: {
6 host: string
7 }
8 createdAt: Date | string
9}
diff --git a/shared/models/index.ts b/shared/models/index.ts
index e61d6cbdc..062533834 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -1,6 +1,7 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './actors' 2export * from './actors'
3export * from './avatars' 3export * from './avatars'
4export * from './blocklist'
4export * from './redundancy' 5export * from './redundancy'
5export * from './users' 6export * from './users'
6export * from './videos' 7export * from './videos'
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index ed2c536ce..51c59d20a 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -8,6 +8,9 @@ export enum UserRight {
8 MANAGE_JOBS, 8 MANAGE_JOBS,
9 MANAGE_CONFIGURATION, 9 MANAGE_CONFIGURATION,
10 10
11 MANAGE_ACCOUNTS_BLOCKLIST,
12 MANAGE_SERVERS_BLOCKLIST,
13
11 MANAGE_VIDEO_BLACKLIST, 14 MANAGE_VIDEO_BLACKLIST,
12 15
13 REMOVE_ANY_VIDEO, 16 REMOVE_ANY_VIDEO,
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts
index d7020c0f2..adef8fd95 100644
--- a/shared/models/users/user-role.ts
+++ b/shared/models/users/user-role.ts
@@ -27,7 +27,9 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
27 UserRight.REMOVE_ANY_VIDEO_CHANNEL, 27 UserRight.REMOVE_ANY_VIDEO_CHANNEL,
28 UserRight.REMOVE_ANY_VIDEO_COMMENT, 28 UserRight.REMOVE_ANY_VIDEO_COMMENT,
29 UserRight.UPDATE_ANY_VIDEO, 29 UserRight.UPDATE_ANY_VIDEO,
30 UserRight.SEE_ALL_VIDEOS 30 UserRight.SEE_ALL_VIDEOS,
31 UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
32 UserRight.MANAGE_SERVERS_BLOCKLIST
31 ], 33 ],
32 34
33 [UserRole.USER]: [] 35 [UserRole.USER]: []
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts
index e40e5b58b..2eee03843 100644
--- a/shared/models/videos/video-resolution.enum.ts
+++ b/shared/models/videos/video-resolution.enum.ts
@@ -9,13 +9,13 @@ export enum VideoResolution {
9} 9}
10 10
11/** 11/**
12 * Bitrate targets for different resolutions and frame rates, in bytes per second. 12 * Bitrate targets for different resolutions, at VideoTranscodingFPS.AVERAGE.
13 *
13 * Sources for individual quality levels: 14 * Sources for individual quality levels:
14 * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en 15 * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en
15 * YouTube Video Info (tested with random music video): https://www.h3xed.com/blogmedia/youtube-info.php 16 * YouTube Video Info (tested with random music video): https://www.h3xed.com/blogmedia/youtube-info.php
16 */ 17 */
17export function getTargetBitrate (resolution: VideoResolution, fps: number, 18function getBaseBitrate (resolution: VideoResolution) {
18 fpsTranscodingConstants: VideoTranscodingFPS) {
19 switch (resolution) { 19 switch (resolution) {
20 case VideoResolution.H_240P: 20 case VideoResolution.H_240P:
21 // quality according to Google Live Encoder: 300 - 700 Kbps 21 // quality according to Google Live Encoder: 300 - 700 Kbps
@@ -30,23 +30,11 @@ export function getTargetBitrate (resolution: VideoResolution, fps: number,
30 // Quality according to YouTube Video Info: 879 Kbps 30 // Quality according to YouTube Video Info: 879 Kbps
31 return 900 * 1000 31 return 900 * 1000
32 case VideoResolution.H_720P: 32 case VideoResolution.H_720P:
33 if (fps === fpsTranscodingConstants.MAX) {
34 // quality according to Google Live Encoder: 2,250 - 6,000 Kbps
35 // Quality according to YouTube Video Info: 2634 Kbps
36 return 2600 * 1000
37 }
38
39 // quality according to Google Live Encoder: 1,500 - 4,000 Kbps 33 // quality according to Google Live Encoder: 1,500 - 4,000 Kbps
40 // Quality according to YouTube Video Info: 1752 Kbps 34 // Quality according to YouTube Video Info: 1752 Kbps
41 return 1750 * 1000 35 return 1750 * 1000
42 case VideoResolution.H_1080P: // fallthrough 36 case VideoResolution.H_1080P: // fallthrough
43 default: 37 default:
44 if (fps === fpsTranscodingConstants.MAX) {
45 // quality according to Google Live Encoder: 3000 - 6000 Kbps
46 // Quality according to YouTube Video Info: 4387 Kbps
47 return 4400 * 1000
48 }
49
50 // quality according to Google Live Encoder: 3000 - 6000 Kbps 38 // quality according to Google Live Encoder: 3000 - 6000 Kbps
51 // Quality according to YouTube Video Info: 3277 Kbps 39 // Quality according to YouTube Video Info: 3277 Kbps
52 return 3300 * 1000 40 return 3300 * 1000
@@ -54,6 +42,33 @@ export function getTargetBitrate (resolution: VideoResolution, fps: number,
54} 42}
55 43
56/** 44/**
45 * Calculate the target bitrate based on video resolution and FPS.
46 *
47 * The calculation is based on two values:
48 * Bitrate at VideoTranscodingFPS.AVERAGE is always the same as
49 * getBaseBitrate(). Bitrate at VideoTranscodingFPS.MAX is always
50 * getBaseBitrate() * 1.4. All other values are calculated linearly
51 * between these two points.
52 */
53export function getTargetBitrate (resolution: VideoResolution, fps: number,
54 fpsTranscodingConstants: VideoTranscodingFPS) {
55 const baseBitrate = getBaseBitrate(resolution)
56 // The maximum bitrate, used when fps === VideoTranscodingFPS.MAX
57 // Based on numbers from Youtube, 60 fps bitrate divided by 30 fps bitrate:
58 // 720p: 2600 / 1750 = 1.49
59 // 1080p: 4400 / 3300 = 1.33
60 const maxBitrate = baseBitrate * 1.4
61 const maxBitrateDifference = maxBitrate - baseBitrate
62 const maxFpsDifference = fpsTranscodingConstants.MAX - fpsTranscodingConstants.AVERAGE
63 // For 1080p video with default settings, this results in the following formula:
64 // 3300 + (x - 30) * (1320/30)
65 // Example outputs:
66 // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps
67 // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps
68 return baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)
69}
70
71/**
57 * The maximum bitrate we expect to see on a transcoded video in bytes per second. 72 * The maximum bitrate we expect to see on a transcoded video in bytes per second.
58 */ 73 */
59export function getMaxBitrate (resolution: VideoResolution, fps: number, 74export function getMaxBitrate (resolution: VideoResolution, fps: number,
diff --git a/support/doc/tools.md b/support/doc/tools.md
index 8efb0c13d..7f93c94f2 100644
--- a/support/doc/tools.md
+++ b/support/doc/tools.md
@@ -1,3 +1,30 @@
1<!-- START doctoc generated TOC please keep comment here to allow auto update -->
2<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
4
5- [CLI tools guide](#cli-tools-guide)
6 - [CLI wrapper](#cli-wrapper)
7 - [Remote Tools](#remote-tools)
8 - [Dependencies](#dependencies)
9 - [Installation](#installation)
10 - [peertube-import-videos.js](#peertube-import-videosjs)
11 - [peertube-upload.js](#peertube-uploadjs)
12 - [peertube-watch.js](#peertube-watchjs)
13 - [Server tools](#server-tools)
14 - [parse-log](#parse-log)
15 - [create-transcoding-job.js](#create-transcoding-jobjs)
16 - [create-import-video-file-job.js](#create-import-video-file-jobjs)
17 - [prune-storage.js](#prune-storagejs)
18 - [optimize-old-videos.js](#optimize-old-videosjs)
19 - [update-host.js](#update-hostjs)
20 - [REPL (Read Eval Print Loop)](#repl-read-eval-print-loop)
21 - [.help](#help)
22 - [Lodash example](#lodash-example)
23 - [YoutubeDL example](#youtubedl-example)
24 - [Models examples](#models-examples)
25
26<!-- END doctoc generated TOC please keep comment here to allow auto update -->
27
1# CLI tools guide 28# CLI tools guide
2 - [CLI wrapper](#cli-wrapper) 29 - [CLI wrapper](#cli-wrapper)
3 - [Remote tools](#remote-tools) 30 - [Remote tools](#remote-tools)
@@ -159,7 +186,7 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
159 186
160### create-transcoding-job.js 187### create-transcoding-job.js
161 188
162You can use this script to force transcoding of an existing video. 189You can use this script to force transcoding of an existing video. PeerTube needs to be running.
163 190
164``` 191```
165$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-transcoding-job -- -v [videoUUID] 192$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-transcoding-job -- -v [videoUUID]
@@ -172,7 +199,7 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
172 199
173### create-import-video-file-job.js 200### create-import-video-file-job.js
174 201
175You can use this script to import a video file to replace an already uploaded file or to add a new resolution to a video. 202You can use this script to import a video file to replace an already uploaded file or to add a new resolution to a video. PeerTube needs to be running.
176 203
177``` 204```
178$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-import-video-file-job -- -v [videoUUID] -i [videoFile] 205$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run create-import-video-file-job -- -v [videoUUID] -i [videoFile]
@@ -189,9 +216,10 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
189 216
190### optimize-old-videos.js 217### optimize-old-videos.js
191 218
192Before version v1.0.0-beta.16, Peertube did not specify a bitrate for the transcoding of uploaded videos. 219Before version v1.0.0-beta.16, Peertube did not specify a bitrate for the
193This means that videos might be encoded into very large files that are too large for streaming. This script 220transcoding of uploaded videos. This means that videos might be encoded into
194re-transcodes these videos so that they can be watched properly, even on slow connections. 221very large files that are too large for streaming. This script re-transcodes
222these videos so that they can be watched properly, even on slow connections.
195 223
196``` 224```
197$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run optimize-old-videos 225$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run optimize-old-videos
@@ -200,9 +228,141 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
200 228
201### update-host.js 229### update-host.js
202 230
203If you started PeerTube with a domain, and then changed it you will have invalid torrent files and invalid URLs in your database. 231If you started PeerTube with a domain, and then changed it you will have
204To fix this, you have to run: 232invalid torrent files and invalid URLs in your database. To fix this, you have
233to run:
205 234
206``` 235```
207$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run update-host 236$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run update-host
208``` 237```
238
239### REPL ([Read Eval Print Loop](https://nodejs.org/docs/latest-v8.x/api/repl.html))
240
241If you want to interact with the application libraries and objects even when PeerTube is not running, there is a REPL for that.
242
243usage: `node ./dist/server/tools/repl.js`
244
245"The default evaluator will, by default, assign the result of the most recently evaluated expression to the special variable `_` (underscore). Explicitly setting `_` to a value will disable this behavior."
246
247- type `.help` to list commands available in the repl, notice it starts with a dot
248- type `.exit` to exit, note that you still have to press CTRL-C to actually exit, or press CTRL-C (3 times) without typing `.exit` to exit
249- type `context` to list all available objects and libraries in the context, note: `Promise` is also available but it's not listed in the context, in case you need promises for something
250- type `env` to see the loaded environment variables
251- type `path` to access path library
252- type `lodash` to access lodash library
253- type `uuidv1` to access uuid/v1 library
254- type `uuidv3` to access uuid/v3 library
255- type `uuidv4` to access uuid/v4 library
256- type `uuidv5` to access uuid/v5 library
257- type `YoutubeDL` to access youtube-dl library
258- type `cli` to access the cli helpers object
259- type `logger` to access the logger; if you log to it, it will write to stdout and to the peertube.log file
260- type `constants` to access the constants loaded by the server
261- type `coreUtils` to access the core-utils helpers object
262- type `ffmpegUtils` to access the ffmpeg-utils helpers object
263- type `peertubeCryptoUtils` to access the peertube-crypto helpers object
264- type `signupUtils` to access the signup helpers object
265- type `utils` to access the utils helpers object
266- type `YoutubeDLUtils` to access the youtube-dl helpers object
267- type `sequelizeTypescript` to access sequelizeTypescript
268- type `modelsUtils` to access the models/utils
269- type `models` to access the shortcut to sequelizeTypescript.models
270- type `transaction` to access the shortcut to sequelizeTypescript.transaction
271- type `query` to access the shortcut to sequelizeTypescript.query
272- type `queryInterface` to access the shortcut to sequelizeTypescript.queryInterface
273
274#### .help
275
276```
277PeerTube [1.0.0] (b10eb595)> .help
278.break Sometimes you get stuck, this gets you out
279.clear Break, and also clear the local context
280.editor Enter editor mode
281.exit Exit the repl
282.help Print this help message
283.load Load JS from a file into the REPL session
284.r Reset REPL
285.reset Reset REPL
286.save Save all evaluated commands in this REPL session to a file
287PeerTube [1.0.0] (b10eb595)>
288```
289
290#### Lodash example
291
292```
293PeerTube [1.0.0] (b10eb595)> lodash.keys(context)
294[ 'global',
295 'console',
296 'DTRACE_NET_SERVER_CONNECTION',
297 'DTRACE_NET_STREAM_END',
298 'DTRACE_HTTP_SERVER_REQUEST',
299 'DTRACE_HTTP_SERVER_RESPONSE',
300 'DTRACE_HTTP_CLIENT_REQUEST',
301 'DTRACE_HTTP_CLIENT_RESPONSE',
302 'process',
303 'Buffer',
304 'clearImmediate',
305 'clearInterval',
306 'clearTimeout',
307 'setImmediate',
308 'setInterval',
309 'setTimeout',
310 'XMLHttpRequest',
311 'compact2string',
312 'module',
313 'require',
314 'path',
315 'repl',
316 'context',
317 'env',
318 'lodash',
319 'uuidv1',
320 'uuidv3',
321 'uuidv4',
322 'uuidv5',
323 'cli',
324 'logger',
325 'constants',
326 'Sequelize',
327 'sequelizeTypescript',
328 'modelsUtils',
329 'models',
330 'transaction',
331 'query',
332 'queryInterface',
333 'YoutubeDL',
334 'coreUtils',
335 'ffmpegUtils',
336 'peertubeCryptoUtils',
337 'signupUtils',
338 'utils',
339 'YoutubeDLUtils' ]
340PeerTube [1.0.0] (b10eb595)>
341```
342
343#### YoutubeDL example
344```
345YoutubeDL.getInfo('https://www.youtube.com/watch?v=I5ZN289jjDo', function(err, data) {console.log(err, data)})
346```
347
348#### Models examples
349```
350PeerTube [1.0.0] (b10eb595)> new models.ActorModel({id: 3}).getVideoChannel().then(function(data){console.log(data.dataValues.name)})
351Promise {
352 _bitField: 0,
353 _fulfillmentHandler0: undefined,
354 _rejectionHandler0: undefined,
355 _promise0: undefined,
356 _receiver0: undefined }
357PeerTube [1.0.0] (b10eb595)> Main root channel
358PeerTube [1.0.0] (b10eb595)> let out; new models.UserModel({id: 1}).getAccount().then(function (data) {out = data.dataValues.id})
359Promise {
360 _bitField: 0,
361 _fulfillmentHandler0: undefined,
362 _rejectionHandler0: undefined,
363 _promise0: undefined,
364 _receiver0: undefined }
365PeerTube [1.0.0] (b10eb595)> out
3662
367PeerTube [1.0.0] (b10eb595)>
368```
diff --git a/tslint.json b/tslint.json
index 92d0bd2b1..6828f4325 100644
--- a/tslint.json
+++ b/tslint.json
@@ -4,7 +4,7 @@
4 "await-promise": [true, "Bluebird"], 4 "await-promise": [true, "Bluebird"],
5 "no-inferrable-types": true, 5 "no-inferrable-types": true,
6 "eofline": true, 6 "eofline": true,
7 "indent": ["spaces"], 7 "indent": [true, "spaces"],
8 "ter-indent": [true, 2], 8 "ter-indent": [true, 2],
9 "max-line-length": [true, 140], 9 "max-line-length": [true, 140],
10 "no-unused-variable": false, // Memory issues 10 "no-unused-variable": false, // Memory issues