diff --git a/.eslintrc.js b/.eslintrc.js index 56e3d0530..177496d3a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,11 @@ module.exports = { 'node_modules', '\\.(css|scss|json)$', ], + 'import/resolver': { + node: { + paths: ['app/javascript'], + }, + }, }, rules: { diff --git a/.rubocop.yml b/.rubocop.yml index 59e8a757a..f1095e022 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -80,7 +80,7 @@ Rails/HttpStatus: Rails/Exit: Exclude: - 'lib/mastodon/*' - - 'lib/cli' + - 'lib/cli.rb' Style/ClassAndModuleChildren: Enabled: false diff --git a/.ruby-version b/.ruby-version index e70b4523a..6a6a3d8e3 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.0 +2.6.1 diff --git a/AUTHORS.md b/AUTHORS.md index 3171214e0..8d3aaf480 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -6,8 +6,8 @@ and provided thanks to the work of the following contributors: * [Gargron](https://github.com/Gargron) * [ykzts](https://github.com/ykzts) -* [akihikodaki](https://github.com/akihikodaki) * [ThibG](https://github.com/ThibG) +* [akihikodaki](https://github.com/akihikodaki) * [mjankowski](https://github.com/mjankowski) * [dependabot[bot]](https://github.com/apps/dependabot) * [unarist](https://github.com/unarist) @@ -27,14 +27,14 @@ and provided thanks to the work of the following contributors: * [blackle](https://github.com/blackle) * [Quent-in](https://github.com/Quent-in) * [JantsoP](https://github.com/JantsoP) -* [mabkenar](https://github.com/mabkenar) * [Kjwon15](https://github.com/Kjwon15) +* [mabkenar](https://github.com/mabkenar) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) * [shuheiktgw](https://github.com/shuheiktgw) * [ashfurrow](https://github.com/ashfurrow) -* [Quenty31](https://github.com/Quenty31) * [zunda](https://github.com/zunda) +* [Quenty31](https://github.com/Quenty31) * [eramdam](https://github.com/eramdam) * [takayamaki](https://github.com/takayamaki) * [masarakki](https://github.com/masarakki) @@ -45,8 +45,8 @@ and provided thanks to the work of the following contributors: * [stephenburgess8](https://github.com/stephenburgess8) * [Wonderfall](https://github.com/Wonderfall) * [matteoaquila](https://github.com/matteoaquila) -* [rkarabut](https://github.com/rkarabut) * [yukimochi](https://github.com/yukimochi) +* [rkarabut](https://github.com/rkarabut) * [Artoria2e5](https://github.com/Artoria2e5) * [nightpool](https://github.com/nightpool) * [marrus-sh](https://github.com/marrus-sh) @@ -64,11 +64,14 @@ and provided thanks to the work of the following contributors: * [MaciekBaron](https://github.com/MaciekBaron) * [MitarashiDango](mailto:mitarashidango@users.noreply.github.com) * [beatrix-bitrot](https://github.com/beatrix-bitrot) +* [Aditoo17](https://github.com/Aditoo17) * [adbelle](https://github.com/adbelle) * [evanminto](https://github.com/evanminto) * [MightyPork](https://github.com/MightyPork) * [yhirano55](https://github.com/yhirano55) +* [rinsuki](https://github.com/rinsuki) * [camponez](https://github.com/camponez) +* [hinaloe](https://github.com/hinaloe) * [SerCom-KC](https://github.com/SerCom-KC) * [aschmitz](https://github.com/aschmitz) * [devkral](https://github.com/devkral) @@ -81,10 +84,8 @@ and provided thanks to the work of the following contributors: * [lindwurm](https://github.com/lindwurm) * [victorhck](mailto:victorhck@geeko.site) * [voidsatisfaction](https://github.com/voidsatisfaction) -* [rinsuki](https://github.com/rinsuki) * [hikari-no-yume](https://github.com/hikari-no-yume) * [angristan](https://github.com/angristan) -* [hinaloe](https://github.com/hinaloe) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) * [spla](mailto:spla@mastodont.cat) @@ -102,9 +103,10 @@ and provided thanks to the work of the following contributors: * [victorhck](https://github.com/victorhck) * [kedamaDQ](https://github.com/kedamaDQ) * [puckipedia](https://github.com/puckipedia) +* [trwnh](https://github.com/trwnh) * [fvh-P](https://github.com/fvh-P) -* [contraexemplo](https://github.com/contraexemplo) -* [Aditoo17](https://github.com/Aditoo17) +* [Anna e só](mailto:contraexemplos@gmail.com) +* [BenLubar](https://github.com/BenLubar) * [kazu9su](https://github.com/kazu9su) * [Komic](https://github.com/Komic) * [lmorchard](https://github.com/lmorchard) @@ -117,7 +119,6 @@ and provided thanks to the work of the following contributors: * [goofy-bz](mailto:goofy@babelzilla.org) * [kadiix](https://github.com/kadiix) * [kodacs](https://github.com/kodacs) -* [trwnh](https://github.com/trwnh) * [JMendyk](https://github.com/JMendyk) * [KScl](https://github.com/KScl) * [sterdev](https://github.com/sterdev) @@ -133,6 +134,7 @@ and provided thanks to the work of the following contributors: * [Reverite](https://github.com/Reverite) * [JohnD28](https://github.com/JohnD28) * [znz](https://github.com/znz) +* [marek-lach](https://github.com/marek-lach) * [Naouak](https://github.com/Naouak) * [pawelngei](https://github.com/pawelngei) * [rtucker](https://github.com/rtucker) @@ -150,7 +152,6 @@ and provided thanks to the work of the following contributors: * [178inaba](https://github.com/178inaba) * [alyssais](https://github.com/alyssais) * [hiphref](https://github.com/hiphref) -* [BenLubar](https://github.com/BenLubar) * [stalker314314](https://github.com/stalker314314) * [huertanix](https://github.com/huertanix) * [genesixx](https://github.com/genesixx) @@ -161,16 +162,16 @@ and provided thanks to the work of the following contributors: * [kmichl](https://github.com/kmichl) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [saper](https://github.com/saper) -* [marek-lach](https://github.com/marek-lach) * [nevillepark](https://github.com/nevillepark) * [ornithocoder](https://github.com/ornithocoder) * [pierreozoux](https://github.com/pierreozoux) * [qguv](https://github.com/qguv) * [Ram Lmn](mailto:ramlmn@users.noreply.github.com) +* [sascha-sl](https://github.com/sascha-sl) * [harukasan](https://github.com/harukasan) * [stamak](https://github.com/stamak) * [Technowix](mailto:technowix@users.noreply.github.com) -* [Eychics](https://github.com/Eychics) +* [Zoeille](https://github.com/Zoeille) * [Thor Harald Johansen](mailto:thj@thj.no) * [0x70b1a5](https://github.com/0x70b1a5) * [gled-rs](https://github.com/gled-rs) @@ -244,9 +245,9 @@ and provided thanks to the work of the following contributors: * [raymestalez](https://github.com/raymestalez) * [remram44](https://github.com/remram44) * [sts10](https://github.com/sts10) -* [sascha-sl](https://github.com/sascha-sl) * [u1-liquid](https://github.com/u1-liquid) * [sim6](https://github.com/sim6) +* [Sir-Boops](https://github.com/Sir-Boops) * [stemid](https://github.com/stemid) * [sumdog](https://github.com/sumdog) * [ThomasLeister](https://github.com/ThomasLeister) @@ -316,8 +317,11 @@ and provided thanks to the work of the following contributors: * [Andreas Drop](mailto:andy@remline.de) * [andi1984](https://github.com/andi1984) * [schas002](https://github.com/schas002) +* [contraexemplo](https://github.com/contraexemplo) * [abackstrom](https://github.com/abackstrom) +* [armandfardeau](https://github.com/armandfardeau) * [jumbosushi](https://github.com/jumbosushi) +* [aurelien-reeves](https://github.com/aurelien-reeves) * [ayumin](https://github.com/ayumin) * [BaptisteGelez](https://github.com/BaptisteGelez) * [bzg](https://github.com/bzg) @@ -335,7 +339,7 @@ and provided thanks to the work of the following contributors: * [Motoma](https://github.com/Motoma) * [chriswk](https://github.com/chriswk) * [csu](https://github.com/csu) -* [clarcharr](https://github.com/clarcharr) +* [clarfon](https://github.com/clarfon) * [kklleemm](https://github.com/kklleemm) * [colindean](https://github.com/colindean) * [dachinat](https://github.com/dachinat) @@ -358,6 +362,7 @@ and provided thanks to the work of the following contributors: * [eai04191](https://github.com/eai04191) * [d3vgru](https://github.com/d3vgru) * [Elizafox](https://github.com/Elizafox) +* [enewhuis](https://github.com/enewhuis) * [ericblade](https://github.com/ericblade) * [mikoim](https://github.com/mikoim) * [espenronnevik](https://github.com/espenronnevik) @@ -446,6 +451,7 @@ and provided thanks to the work of the following contributors: * [mouse-reeve](https://github.com/mouse-reeve) * [Mozinet-fr](https://github.com/Mozinet-fr) * [lae](https://github.com/lae) +* [nosada](https://github.com/nosada) * [Nanamachi](https://github.com/Nanamachi) * [orinthe](https://github.com/orinthe) * [NecroTechno](https://github.com/NecroTechno) @@ -462,10 +468,11 @@ and provided thanks to the work of the following contributors: * [noppa](https://github.com/noppa) * [Otakan951](https://github.com/Otakan951) * [fahy](https://github.com/fahy) -* [PatrickRWells](https://github.com/PatrickRWells) -* [Pangoraw](https://github.com/Pangoraw) -* [peterkeen](https://github.com/peterkeen) -* [pgate](https://github.com/pgate) +* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com) +* [Paul](mailto:naydex.mc+github@gmail.com) +* [Pete Keen](mailto:pete@petekeen.net) +* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com) +* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru) * [Reto Kromer](mailto:retokromer@users.noreply.github.com) * [Rey Tucker](mailto:git@reytucker.us) * [Rob Watson](mailto:rfwatson@users.noreply.github.com) @@ -488,7 +495,6 @@ and provided thanks to the work of the following contributors: * [Sho Kusano](mailto:rosylilly@aduca.org) * [Shouko Yu](mailto:imshouko@gmail.com) * [Sina Mashek](mailto:sina@mashek.xyz) -* [Sir-Boops](mailto:admin@boops.me) * [Soshi Kato](mailto:mail@sossii.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Stanislas](mailto:angristan@pm.me) @@ -555,12 +561,14 @@ and provided thanks to the work of the following contributors: * [karlyeurl](mailto:karl.yeurl@gmail.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com) * [kodai](mailto:shirafuta.kodai@gmail.com) +* [koyu](mailto:me@koyu.space) * [kuro5hin](mailto:rusty@kuro5hin.org) * [luzpaz](mailto:luzpaz@users.noreply.github.com) * [maxypy](mailto:maxime@mpigou.fr) * [mhe](mailto:mail@marcus-herrmann.com) * [mike castleman](mailto:m@mlcastle.net) * [mimikun](mailto:dzdzble_effort_311@outlook.jp) +* [mohemohe](mailto:mohemohe@users.noreply.github.com) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [muan](mailto:muan@github.com) * [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com) @@ -599,243 +607,338 @@ This document is provided for informational purposes only. Since it is only upda Following people have contributed to translation of Mastodon: +- **Albanian** + - Besnik Bleta + - Aditoo - **Arabic** - ButterflyOfFire + - Aditoo + - Amrz0 - **Asturian** - ButterflyOfFire - Enol P. + - Aditoo - **Basque** + - Osoitz + - Aditoo - Aitzol - ButterflyOfFire - - Gorka Azkarate - - Osoitz - Peru Iparragirre + - Gorka Azkarate +- **Bengali** + - dxwc - **Bulgarian** - ButterflyOfFire + - Aditoo - **Catalan** + - spla + - Aditoo - ButterflyOfFire - Joan Montané - Jose Luis - - spla - **Chinese (Hong Kong)** - ButterflyOfFire - Luzi Leung + - Aditoo - **Chinese (Simplified)** - Allen Zhong - ButterflyOfFire - SerCom_KC + - martialarts + - Kaitian Xie + - Aditoo + - pan93412 - **Chinese (Traditional)** + - Aditoo - ButterflyOfFire - James58899 - - Jeff Huang + - pan93412 - S1ttidoe477 - SHA265 + - Jeff Huang - **Corsican** - Alix D. R. + - Aditoo - ButterflyOfFire - **Croatian** - ButterflyOfFire + - Aditoo - **Czech** - - ButterflyOfFire - - Lorem Ipsum + - Aditoo - Marek Ľach + - ButterflyOfFire - **Danish** - - ButterflyOfFire + - Einhjeriar - Rasmus Sæderup -- **Dutch** + - Aditoo + - ButterflyOfFire +- **Dutch** + - Albakham - ButterflyOfFire - - Jelv - jeroenpraat - rscmbbng + - Aditoo + - Jelv - **English** - ButterflyOfFire - Renato "Lond" Cerqueira +- **English (United Kingdom)** + - Albakham - **Esperanto** + - Aditoo - ButterflyOfFire + - Becci Cat - Jeong Arm - - Martin Bodin - Mélanie Chauvel - Vanege + - Martin Bodin - tuxayo/Victor Grousset - **Finnish** - ButterflyOfFire - - Jonne Arjoranta - - S Heija + - Mikko Poussu - Taru Luojola + - S Heija + - Aditoo + - Jonne Arjoranta - **French** - - Alda Marteau-Hardi + - Albakham - Alix D. R. - - Baptiste Jonglez - ButterflyOfFire - - Franck Paul - - Jean-Baptiste Holcroft + - codl + - Leia + - Alda Marteau-Hardi + - Mélanie Chauvel + - Paul Marques Mota + - azenet + - Olivier Humbert + - Aditoo - Jonathan Chan - Letiteuf55 - - Martin Bodin - - Mélanie Chauvel - - Olivier Humbert - - Paul Marques Mota - - Sylvhem + - Baptiste Jonglez + - goofy-mdn + - Jean-Baptiste Holcroft - Technowix - - Thibaut Girka + - Martin Bodin - Théodore - - azenet - - codl + - Thibaut Girka + - Franck Paul + - Sylvhem - **Galician** - ButterflyOfFire - Xose M. + - Aditoo - manequim - **Georgian** - ButterflyOfFire + - Aditoo - **German** - - Benedikt Geißler + - Aditoo - ButterflyOfFire - Daniel - - Eugen Rochko - - Koyu Berteon - - Patrick Figel - - Weblate Admin - averageunicorn - - ePirat - - koyu + - Koyu Berteon - larsreineke + - koyu + - Austin Jones - lilo + - Benedikt Geißler + - ePirat + - Eugen Rochko + - Weblate Admin + - Patrick Figel - **Greek** - - Antonis - - ButterflyOfFire - Dimitris Maroulidis + - Antonis + - Aditoo + - ButterflyOfFire - Konstantinos Grevenitis - **Hebrew** - ButterflyOfFire + - Aditoo - Ira - Yaron Shahrabani - **Hungarian** - - Adam Paszternak - ButterflyOfFire + - Adam Paszternak + - Aditoo - Tibike Miklós - **Ido** - ButterflyOfFire + - Aditoo - **Indonesian** - - Alfiana Sibuea + - afachri - ButterflyOfFire - Dito Kurnia Pratama - Eirworks - - afachri + - Aditoo + - Alfiana Sibuea - se7entime +- **Irish** + - Albakham + - Kevin Houlihan - **Italian** - Alessandro Levati + - Albakham - ButterflyOfFire + - Marcin Mikołajczak + - Aditoo - Giuseppe Pignataro - Stefano - **Japanese** - - ButterflyOfFire - - Kumasun Morino - - Yamagishi Kazutoshi + - Hinaloe + - 小鳥遊まりあ - mayaeh - osapon - - unarist - - 小鳥遊まりあ - 森の子リスのミーコの大冒険 -- **Korean** + - Kumasun Morino + - Yamagishi Kazutoshi + - Aditoo - ButterflyOfFire - Jeong Arm + - unarist +- **Kazakh** + - arshat + - Aditoo +- **Korean** + - Aditoo + - Jeong Arm + - ButterflyOfFire - Minori Hiraoka - Yamagishi Kazutoshi +- **Lithuanian** + - Sarunas Medeikis - **Malay** - - ButterflyOfFire - Muhammad Nur Hidayat (MNH48) + - Aditoo + - ButterflyOfFire - **Norwegian (old code)** - ButterflyOfFire - Espen Rønnevik + - Aditoo - Tale - **Occitan** + - Aditoo - ButterflyOfFire - - Maxenç - Quenti2 - Quentí + - Maxenç - **Persian** - - ButterflyOfFire - Masoud Abkenar -- **Polish** + - Aditoo + - ButterflyOfFire +- **Polish** + - Aditoo + - Albakham - ButterflyOfFire - - Jakub Mendyk - - Marcin Mikołajczak - - Marek Ľach - Stasiek Michalski + - Marcin Mikołajczak + - Jakub Mendyk + - Marek Ľach - krkk - **Portuguese** + - Albakham + - João Pinheiro + - manequim + - Aditoo - ButterflyOfFire - Hugo Gameiro - - manequim - **Portuguese (Brazil)** - - André Andrade + - Aditoo + - Albakham - Anna e só - - ButterflyOfFire - Renato "Lond" Cerqueira -- **Romanian** + - André Andrade - ButterflyOfFire +- **Romanian** - adrianbblk + - ButterflyOfFire + - Aditoo - **Russian** - - Andrew Zyabin + - Albakham - ButterflyOfFire - Evgeny Petrov + - Aditoo + - Павел Гастелло + - Andrew Zyabin - Yaron Shahrabani - **Serbian** - Branko Kokanovic - Burekz Finezt + - Aditoo - ButterflyOfFire - **Serbian (latin)** - ButterflyOfFire + - Aditoo - **Slovak** + - Aditoo - ButterflyOfFire - Ivan Pleva - - Lorem Ipsum - Marek Ľach - Peter - **Slovenian** - - ButterflyOfFire - Kristijan Tkalec + - Aditoo + - ButterflyOfFire - **Spanish** - - Angeles Broullón - - Antón López + - Albakham - ButterflyOfFire - Carlos Mondragon + - Antón López + - Max Winkler + - Pablo de la Concepción Sanz + - Sergio Soriano + - Angeles Broullón + - Lothar Wolf + - Aditoo - David Charte - Emmanuel - - Lothar Wolf - - Pablo de la Concepción Sanz - **Swedish** - ButterflyOfFire - - Elias Mårtenson - Isak Holmström - Shellkr + - Aditoo + - Elias Mårtenson - Stefan Midjich - Tim Stahel + - Jonas Hultén - **Telugu** + - avndp + - Ranjith Tellakula + - Aditoo - ButterflyOfFire - Joseph Nuthalapati - - Ranjith Tellakula - - avndp - **Thai** - ButterflyOfFire + - parnikkapore + - Thai Localization + - Aditoo - **Turkish** + - Ali Demirtas - ButterflyOfFire + - Aditoo - **Ukrainian** - - ButterflyOfFire - - Ivan Verchenko - alexcleac -- **Welsh** - ButterflyOfFire - - Jaz-Michael King - - Kevin Beynon - - Owain Rhys Lewis - - Renato "Lond" Cerqueira - - Rhoslyn Prys + - Aditoo + - Ivan Verchenko +- **Welsh** - carl morris + - Jaz-Michael King + - Owain Rhys Lewis + - Rhoslyn Prys + - Aditoo + - ButterflyOfFire + - Renato "Lond" Cerqueira + - Albakham + - Kevin Beynon - **Armenian** + - Aditoo - ButterflyOfFire - **Latvian** + - Aditoo - ButterflyOfFire + - Maigonis - **Tamil** + - Aditoo - ButterflyOfFire - Prasanna Venkadesh diff --git a/CHANGELOG.md b/CHANGELOG.md index f3356d542..17626e027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,120 @@ Changelog All notable changes to this project will be documented in this file. +## [2.8.0] - 2019-04-10 +### Added + +- Add polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10111), [ThibG](https://github.com/tootsuite/mastodon/pull/10155), [Gargron](https://github.com/tootsuite/mastodon/pull/10184), [ThibG](https://github.com/tootsuite/mastodon/pull/10196), [Gargron](https://github.com/tootsuite/mastodon/pull/10248), [ThibG](https://github.com/tootsuite/mastodon/pull/10255), [ThibG](https://github.com/tootsuite/mastodon/pull/10322), [Gargron](https://github.com/tootsuite/mastodon/pull/10138), [Gargron](https://github.com/tootsuite/mastodon/pull/10139), [Gargron](https://github.com/tootsuite/mastodon/pull/10144), [Gargron](https://github.com/tootsuite/mastodon/pull/10145),[Gargron](https://github.com/tootsuite/mastodon/pull/10146), [Gargron](https://github.com/tootsuite/mastodon/pull/10148), [Gargron](https://github.com/tootsuite/mastodon/pull/10151), [ThibG](https://github.com/tootsuite/mastodon/pull/10150), [Gargron](https://github.com/tootsuite/mastodon/pull/10168), [Gargron](https://github.com/tootsuite/mastodon/pull/10165), [Gargron](https://github.com/tootsuite/mastodon/pull/10172), [Gargron](https://github.com/tootsuite/mastodon/pull/10170), [Gargron](https://github.com/tootsuite/mastodon/pull/10171), [Gargron](https://github.com/tootsuite/mastodon/pull/10186), [Gargron](https://github.com/tootsuite/mastodon/pull/10189), [ThibG](https://github.com/tootsuite/mastodon/pull/10200), [rinsuki](https://github.com/tootsuite/mastodon/pull/10203), [Gargron](https://github.com/tootsuite/mastodon/pull/10213), [Gargron](https://github.com/tootsuite/mastodon/pull/10246), [Gargron](https://github.com/tootsuite/mastodon/pull/10265), [Gargron](https://github.com/tootsuite/mastodon/pull/10261), [ThibG](https://github.com/tootsuite/mastodon/pull/10333), [Gargron](https://github.com/tootsuite/mastodon/pull/10352), [ThibG](https://github.com/tootsuite/mastodon/pull/10140), [ThibG](https://github.com/tootsuite/mastodon/pull/10142), [ThibG](https://github.com/tootsuite/mastodon/pull/10141), [ThibG](https://github.com/tootsuite/mastodon/pull/10162), [ThibG](https://github.com/tootsuite/mastodon/pull/10161), [ThibG](https://github.com/tootsuite/mastodon/pull/10158), [ThibG](https://github.com/tootsuite/mastodon/pull/10156), [ThibG](https://github.com/tootsuite/mastodon/pull/10160), [Gargron](https://github.com/tootsuite/mastodon/pull/10185), [Gargron](https://github.com/tootsuite/mastodon/pull/10188), [ThibG](https://github.com/tootsuite/mastodon/pull/10195), [ThibG](https://github.com/tootsuite/mastodon/pull/10208), [Gargron](https://github.com/tootsuite/mastodon/pull/10187), [ThibG](https://github.com/tootsuite/mastodon/pull/10214), [ThibG](https://github.com/tootsuite/mastodon/pull/10209)) +- Add follows & followers managing UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10268), [Gargron](https://github.com/tootsuite/mastodon/pull/10308), [Gargron](https://github.com/tootsuite/mastodon/pull/10404), [Gargron](https://github.com/tootsuite/mastodon/pull/10293)) +- Add identity proof integration with Keybase ([Gargron](https://github.com/tootsuite/mastodon/pull/10297), [xgess](https://github.com/tootsuite/mastodon/pull/10375), [Gargron](https://github.com/tootsuite/mastodon/pull/10338), [Gargron](https://github.com/tootsuite/mastodon/pull/10350), [Gargron](https://github.com/tootsuite/mastodon/pull/10414)) +- Add option to overwrite imported data instead of merging ([Gargron](https://github.com/tootsuite/mastodon/pull/9962)) +- Add featured hashtags to profiles ([Gargron](https://github.com/tootsuite/mastodon/pull/9755), [Gargron](https://github.com/tootsuite/mastodon/pull/10167), [Gargron](https://github.com/tootsuite/mastodon/pull/10249), [ThibG](https://github.com/tootsuite/mastodon/pull/10034)) +- Add admission-based registrations mode ([Gargron](https://github.com/tootsuite/mastodon/pull/10250), [ThibG](https://github.com/tootsuite/mastodon/pull/10269), [Gargron](https://github.com/tootsuite/mastodon/pull/10264), [ThibG](https://github.com/tootsuite/mastodon/pull/10321), [Gargron](https://github.com/tootsuite/mastodon/pull/10349), [Gargron](https://github.com/tootsuite/mastodon/pull/10469)) +- Add support for WebP uploads ([acid-chicken](https://github.com/tootsuite/mastodon/pull/9879)) +- Add "copy link" item to status action bars in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9983)) +- Add list title editing in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9748)) +- Add a "Block & Report" button to the block confirmation dialog in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10360)) +- Add disappointed elephant when the page crashes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10275)) +- Add ability to upload multiple files at once in web UI ([tmm576](https://github.com/tootsuite/mastodon/pull/9856)) +- Add indication when you are not allowed to follow an account in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10420), [Gargron](https://github.com/tootsuite/mastodon/pull/10491)) +- Add validations to admin settings to catch common mistakes ([Gargron](https://github.com/tootsuite/mastodon/pull/10348), [ThibG](https://github.com/tootsuite/mastodon/pull/10354)) +- Add `type`, `limit`, `offset`, `min_id`, `max_id`, `account_id` to search API ([Gargron](https://github.com/tootsuite/mastodon/pull/10091)) +- Add a preferences API so apps can share basic behaviours ([Gargron](https://github.com/tootsuite/mastodon/pull/10109)) +- Add `visibility` param to reblog REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9851), [ThibG](https://github.com/tootsuite/mastodon/pull/10302)) +- Add `allowfullscreen` attribute to OEmbed iframe ([rinsuki](https://github.com/tootsuite/mastodon/pull/10370)) +- Add `blocked_by` relationship to the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10373)) +- Add `tootctl statuses remove` to sweep unreferenced statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/10063)) +- Add `tootctl search deploy` to avoid ugly rake task syntax ([Gargron](https://github.com/tootsuite/mastodon/pull/10403)) +- Add `tootctl self-destruct` to shut down server gracefully ([Gargron](https://github.com/tootsuite/mastodon/pull/10367)) +- Add option to hide application used to toot ([ThibG](https://github.com/tootsuite/mastodon/pull/9897), [rinsuki](https://github.com/tootsuite/mastodon/pull/9994), [hinaloe](https://github.com/tootsuite/mastodon/pull/10086)) +- Add `DB_SSLMODE` configuration variable ([sascha-sl](https://github.com/tootsuite/mastodon/pull/10210)) +- Add click-to-copy UI to invites page ([Gargron](https://github.com/tootsuite/mastodon/pull/10259)) +- Add self-replies fetching ([ThibG](https://github.com/tootsuite/mastodon/pull/10106), [ThibG](https://github.com/tootsuite/mastodon/pull/10128), [ThibG](https://github.com/tootsuite/mastodon/pull/10175), [ThibG](https://github.com/tootsuite/mastodon/pull/10201)) +- Add rate limit for media proxy requests ([Gargron](https://github.com/tootsuite/mastodon/pull/10490)) +- Add `tootctl emoji purge` ([Gargron](https://github.com/tootsuite/mastodon/pull/10481)) +- Add `tootctl accounts approve` ([Gargron](https://github.com/tootsuite/mastodon/pull/10480)) +- Add `tootctl accounts reset-relationships` ([noellabo](https://github.com/tootsuite/mastodon/pull/10483)) + +### Changed + +- Change design of landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10232), [Gargron](https://github.com/tootsuite/mastodon/pull/10260), [ThibG](https://github.com/tootsuite/mastodon/pull/10284), [ThibG](https://github.com/tootsuite/mastodon/pull/10291), [koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/10356), [Gargron](https://github.com/tootsuite/mastodon/pull/10245)) +- Change design of profile column in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10337), [Aditoo17](https://github.com/tootsuite/mastodon/pull/10387), [ThibG](https://github.com/tootsuite/mastodon/pull/10390), [mayaeh](https://github.com/tootsuite/mastodon/pull/10379), [ThibG](https://github.com/tootsuite/mastodon/pull/10411)) +- Change language detector threshold from 140 characters to 4 words ([Gargron](https://github.com/tootsuite/mastodon/pull/10376)) +- Change language detector to always kick in for non-latin alphabets ([Gargron](https://github.com/tootsuite/mastodon/pull/10276)) +- Change icons of features on admin dashboard ([Gargron](https://github.com/tootsuite/mastodon/pull/10366)) +- Change DNS timeouts from 1s to 5s ([ThibG](https://github.com/tootsuite/mastodon/pull/10238)) +- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/tootsuite/mastodon/pull/10100), [BenLubar](https://github.com/tootsuite/mastodon/pull/10212)) +- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/tootsuite/mastodon/pull/9059)) +- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10339)) +- Change web UI to not not empty timeline of blocked users on block ([ThibG](https://github.com/tootsuite/mastodon/pull/10359)) +- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/tootsuite/mastodon/pull/10378)) +- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/tootsuite/mastodon/pull/9924)) +- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/tootsuite/mastodon/pull/10289)) +- Change web UI to use new Web Share Target API ([gol-cha](https://github.com/tootsuite/mastodon/pull/9963)) +- Change ActivityPub reports to have persistent URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/10303)) +- Change `tootctl accounts cull --dry-run` to list accounts that would be deleted ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460)) +- Change format of CSV exports of follows and mutes to include extra settings ([ThibG](https://github.com/tootsuite/mastodon/pull/10495), [ThibG](https://github.com/tootsuite/mastodon/pull/10335)) +- Change ActivityPub collections to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10467)) +- Change REST API and public profiles to not return follows/followers for users that have blocked you ([Gargron](https://github.com/tootsuite/mastodon/pull/10491)) +- Change the groupings of menu items in settings navigation ([Gargron](https://github.com/tootsuite/mastodon/pull/10533)) + +### Removed + +- Remove zopfli compression to speed up Webpack from 6min to 1min ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10288)) +- Remove stats.json generation to speed up Webpack ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10290)) + +### Fixed + +- Fix public timelines being broken by new toots when they are not mounted in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10131)) +- Fix quick filter settings not being saved when selecting a different filter in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10296)) +- Fix remote interaction dialogs being indexed by search engines ([Gargron](https://github.com/tootsuite/mastodon/pull/10240)) +- Fix maxed-out invites not showing up as expired in UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10274)) +- Fix scrollbar styles on compose textarea ([Gargron](https://github.com/tootsuite/mastodon/pull/10292)) +- Fix timeline merge workers being queued for remote users ([Gargron](https://github.com/tootsuite/mastodon/pull/10355)) +- Fix alternative relay support regression ([Gargron](https://github.com/tootsuite/mastodon/pull/10398)) +- Fix trying to fetch keys of unknown accounts on a self-delete from them ([ThibG](https://github.com/tootsuite/mastodon/pull/10326)) +- Fix CAS `:service_validate_url` option ([enewhuis](https://github.com/tootsuite/mastodon/pull/10328)) +- Fix race conditions when creating backups ([ThibG](https://github.com/tootsuite/mastodon/pull/10234)) +- Fix whitespace not being stripped out of username before validation ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10239)) +- Fix n+1 query when deleting status ([Gargron](https://github.com/tootsuite/mastodon/pull/10247)) +- Fix exiting follows not being rejected when suspending a remote account ([ThibG](https://github.com/tootsuite/mastodon/pull/10230)) +- Fix the underlying button element in a disabled icon button not being disabled ([ThibG](https://github.com/tootsuite/mastodon/pull/10194)) +- Fix race condition when streaming out deleted statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10280)) +- Fix performance of admin federation UI by caching account counts ([Gargron](https://github.com/tootsuite/mastodon/pull/10374)) +- Fix JS error on pages that don't define a CSRF token ([hinaloe](https://github.com/tootsuite/mastodon/pull/10383)) +- Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460)) + +## [2.7.4] - 2019-03-05 +### Fixed + +- Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/tootsuite/mastodon/pull/10108)) +- Fix redundant HTTP requests when resolving private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10115)) +- Fix performance of account media query ([abcang](https://github.com/tootsuite/mastodon/pull/10121)) +- Fix mention processing for unknown accounts ([ThibG](https://github.com/tootsuite/mastodon/pull/10125)) +- Fix getting started column not scrolling on short screens ([trwnh](https://github.com/tootsuite/mastodon/pull/10075)) +- Fix direct messages pagination in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10126)) +- Fix serialization of Announce activities ([ThibG](https://github.com/tootsuite/mastodon/pull/10129)) +- Fix home timeline perpetually reloading when empty in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10130)) +- Fix lists export ([ThibG](https://github.com/tootsuite/mastodon/pull/10136)) +- Fix edit profile page crash for suspended-then-unsuspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/10178)) + +## [2.7.3] - 2019-02-23 +### Added + +- Add domain filter to the admin federation page ([ThibG](https://github.com/tootsuite/mastodon/pull/10071)) +- Add quick link from admin account view to block/unblock instance ([ThibG](https://github.com/tootsuite/mastodon/pull/10073)) + +### Fixed + +- Fix video player width not being updated to fit container width ([ThibG](https://github.com/tootsuite/mastodon/pull/10069)) +- Fix domain filter being shown in admin page when local filter is active ([ThibG](https://github.com/tootsuite/mastodon/pull/10074)) +- Fix crash when conversations have no valid participants ([ThibG](https://github.com/tootsuite/mastodon/pull/10078)) +- Fix error when performing admin actions on no statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10094)) + +### Changed + +- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/tootsuite/mastodon/pull/10090)) + ## [2.7.2] - 2019-02-17 ### Added diff --git a/Dockerfile b/Dockerfile index 190905337..6373172fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,90 +1,128 @@ -FROM node:8.15-alpine as node -FROM ruby:2.6-alpine3.8 +FROM ubuntu:18.04 as build-dep -LABEL maintainer="https://github.com/tootsuite/mastodon" \ - description="Your self-hosted, globally interconnected microblogging community" +# Use bash for the shell +SHELL ["bash", "-c"] +# Install Node +ENV NODE_VER="8.15.0" +RUN echo "Etc/UTC" > /etc/localtime && \ + apt update && \ + apt -y dist-upgrade && \ + apt -y install wget make gcc g++ python && \ + cd ~ && \ + wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ + tar xf node-v$NODE_VER.tar.gz && \ + cd node-v$NODE_VER && \ + ./configure --prefix=/opt/node && \ + make -j$(nproc) > /dev/null && \ + make install + +# Install jemalloc +ENV JE_VER="5.1.0" +RUN apt update && \ + apt -y install autoconf && \ + cd ~ && \ + wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \ + tar xf $JE_VER.tar.gz && \ + cd jemalloc-$JE_VER && \ + ./autogen.sh && \ + ./configure --prefix=/opt/jemalloc && \ + make -j$(nproc) > /dev/null && \ + make install_bin install_include install_lib + +# Install ruby +ENV RUBY_VER="2.6.1" +ENV CPPFLAGS="-I/opt/jemalloc/include" +ENV LDFLAGS="-L/opt/jemalloc/lib/" +RUN apt update && \ + apt -y install build-essential \ + bison libyaml-dev libgdbm-dev libreadline-dev \ + libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \ + cd ~ && \ + wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \ + tar xf ruby-$RUBY_VER.tar.gz && \ + cd ruby-$RUBY_VER && \ + ./configure --prefix=/opt/ruby \ + --with-jemalloc \ + --with-shared \ + --disable-install-doc && \ + ln -s /opt/jemalloc/lib/* /usr/lib/ && \ + make -j$(nproc) > /dev/null && \ + make install + +ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" + +RUN npm install -g yarn && \ + gem install bundler && \ + apt update && \ + apt -y install git libicu-dev libidn11-dev \ + libpq-dev libprotobuf-dev protobuf-compiler + +COPY Gemfile* package.json yarn.lock /opt/mastodon/ + +RUN cd /opt/mastodon && \ + bundle install -j$(nproc) --deployment --without development test && \ + yarn install --pure-lockfile + +FROM ubuntu:18.04 + +# Copy over all the langs needed for runtime +COPY --from=build-dep /opt/node /opt/node +COPY --from=build-dep /opt/ruby /opt/ruby +COPY --from=build-dep /opt/jemalloc /opt/jemalloc + +# Add more PATHs to the PATH +ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" + +# Create the mastodon user ARG UID=991 ARG GID=991 +RUN apt update && \ + echo "Etc/UTC" > /etc/localtime && \ + ln -s /opt/jemalloc/lib/* /usr/lib/ && \ + apt -y dist-upgrade && \ + apt install -y whois wget && \ + addgroup --gid $GID mastodon && \ + useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ + echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd -ENV PATH=/mastodon/bin:$PATH \ - RAILS_SERVE_STATIC_FILES=true \ - RAILS_ENV=production \ - NODE_ENV=production +# Install masto runtime deps +RUN apt -y --no-install-recommends install \ + libssl1.1 libpq5 imagemagick ffmpeg \ + libicu60 libprotobuf10 libidn11 libyaml-0-2 \ + file ca-certificates tzdata libreadline7 && \ + apt -y install gcc && \ + ln -s /opt/mastodon /mastodon && \ + gem install bundler && \ + rm -rf /var/cache && \ + rm -rf /var/lib/apt -ARG LIBICONV_VERSION=1.15 -ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 +# Add tini +ENV TINI_VERSION="0.18.0" +ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini +RUN echo "$TINI_SUM tini" | sha256sum -c - +RUN chmod +x /tini -EXPOSE 3000 4000 +# Copy over masto source, and dependencies from building, and set permissions +COPY --chown=mastodon:mastodon . /opt/mastodon +COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon -WORKDIR /mastodon +# Run masto services in prod mode +ENV RAILS_ENV="production" +ENV NODE_ENV="production" -COPY --from=node /usr/local/bin/node /usr/local/bin/node -COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=node /usr/local/bin/npm /usr/local/bin/npm -COPY --from=node /opt/yarn-* /opt/yarn - -RUN apk -U upgrade \ - && apk add -t build-dependencies \ - build-base \ - icu-dev \ - libidn-dev \ - libressl \ - libtool \ - libxml2-dev \ - libxslt-dev \ - postgresql-dev \ - protobuf-dev \ - python \ - && apk add \ - ca-certificates \ - ffmpeg \ - file \ - git \ - icu-libs \ - imagemagick \ - libidn \ - libpq \ - libxml2 \ - libxslt \ - protobuf \ - tini \ - tzdata \ - && update-ca-certificates \ - && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ - && ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \ - && mkdir -p /tmp/src /opt \ - && wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ - && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ - && tar -xzf libiconv.tar.gz -C /tmp/src \ - && rm libiconv.tar.gz \ - && cd /tmp/src/libiconv-$LIBICONV_VERSION \ - && ./configure --prefix=/usr/local \ - && make -j$(getconf _NPROCESSORS_ONLN)\ - && make install \ - && libtool --finish /usr/local/lib \ - && cd /mastodon \ - && rm -rf /tmp/* /var/cache/apk/* - -COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ - -RUN bundle config build.nokogiri --use-system-libraries --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \ - && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \ - && yarn install --pure-lockfile --ignore-engines \ - && yarn cache clean - -RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \ - && mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \ - && chown -R mastodon:mastodon /mastodon/public - -COPY . /mastodon - -RUN chown -R mastodon:mastodon /mastodon - -VOLUME /mastodon/public/system +# Tell rails to serve static files +ENV RAILS_SERVE_STATIC_FILES="true" +# Set the run user USER mastodon -RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile +# Precompile assets +RUN cd ~ && \ + OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \ + yarn cache clean -ENTRYPOINT ["/sbin/tini", "--"] +# Set the work dir and the container entry point +WORKDIR /opt/mastodon +ENTRYPOINT ["/tini", "--"] diff --git a/Gemfile b/Gemfile index 8266488f8..735920a66 100644 --- a/Gemfile +++ b/Gemfile @@ -6,16 +6,16 @@ ruby '>= 2.4.0', '< 2.7.0' gem 'pkg-config', '~> 1.3' gem 'puma', '~> 3.12' -gem 'rails', '~> 5.2.2' +gem 'rails', '~> 5.2.3' gem 'thor', '~> 0.20' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.1' gem 'makara', '~> 0.4' gem 'pghero', '~> 2.2' -gem 'dotenv-rails', '~> 2.6' +gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.30', require: false +gem 'aws-sdk-s3', '~> 1.36', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -24,13 +24,13 @@ gem 'streamio-ffmpeg', '~> 3.0' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.6' -gem 'bootsnap', '~> 1.3', require: false +gem 'bootsnap', '~> 1.4', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.6' gem 'iso-639' gem 'chewy', '~> 5.0' gem 'cld3', '~> 3.2.3' -gem 'devise', '~> 4.5' +gem 'devise', '~> 4.6' gem 'devise-two-factor', '~> 3.0' group :pam_authentication, optional: true do @@ -85,8 +85,8 @@ gem 'strong_migrations', '~> 0.3' gem 'tty-command', '~> 0.8', require: false gem 'tty-prompt', '~> 0.18', require: false gem 'twitter-text', '~> 1.14' -gem 'tzinfo-data', '~> 1.2018' -gem 'webpacker', '~> 3.5' +gem 'tzinfo-data', '~> 1.2019' +gem 'webpacker', '~> 4.0' gem 'webpush' gem 'json-ld', '~> 3.0' @@ -97,7 +97,7 @@ group :development, :test do gem 'fabrication', '~> 2.20' gem 'fuubar', '~> 2.3' gem 'i18n-tasks', '~> 0.9', require: false - gem 'pry-byebug', '~> 3.6' + gem 'pry-byebug', '~> 3.7' gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 3.8' end @@ -107,19 +107,19 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.12' + gem 'capybara', '~> 3.16' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' - gem 'microformats', '~> 4.0' + gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false gem 'webmock', '~> 3.5' - gem 'parallel_tests', '~> 2.27' + gem 'parallel_tests', '~> 2.28' end group :development do - gem 'active_record_query_trace', '~> 1.5' + gem 'active_record_query_trace', '~> 1.6' gem 'annotate', '~> 2.7' gem 'better_errors', '~> 2.5' gem 'binding_of_caller', '~> 0.7' @@ -127,8 +127,8 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' - gem 'rubocop', '~> 0.63', require: false - gem 'brakeman', '~> 4.4', require: false + gem 'rubocop', '~> 0.67', require: false + gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false gem 'scss_lint', '~> 0.57', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 7b8ffec9a..afe403dec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,49 +15,49 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.2) - actionpack (= 5.2.2) + actioncable (5.2.3) + actionpack (= 5.2.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.2) - actionpack (= 5.2.2) - actionview (= 5.2.2) - activejob (= 5.2.2) + actionmailer (5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.2) - actionview (= 5.2.2) - activesupport (= 5.2.2) + actionpack (5.2.3) + actionview (= 5.2.3) + activesupport (= 5.2.3) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.2) - activesupport (= 5.2.2) + actionview (5.2.3) + activesupport (= 5.2.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.10.8) + active_model_serializers (0.10.9) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_record_query_trace (1.5.4) - activejob (5.2.2) - activesupport (= 5.2.2) + active_record_query_trace (1.6.2) + activejob (5.2.3) + activesupport (= 5.2.3) globalid (>= 0.3.6) - activemodel (5.2.2) - activesupport (= 5.2.2) - activerecord (5.2.2) - activemodel (= 5.2.2) - activesupport (= 5.2.2) + activemodel (5.2.3) + activesupport (= 5.2.3) + activerecord (5.2.3) + activemodel (= 5.2.3) + activesupport (= 5.2.3) arel (>= 9.0) - activestorage (5.2.2) - actionpack (= 5.2.2) - activerecord (= 5.2.2) + activestorage (5.2.3) + actionpack (= 5.2.3) + activerecord (= 5.2.3) marcel (~> 0.3.1) - activesupport (5.2.2) + activesupport (5.2.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -75,32 +75,33 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-eventstream (1.0.1) - aws-partitions (1.131.0) - aws-sdk-core (3.45.0) - aws-eventstream (~> 1.0) + aws-eventstream (1.0.2) + aws-partitions (1.147.0) + aws-sdk-core (3.48.3) + aws-eventstream (~> 1.0, >= 1.0.2) aws-partitions (~> 1.0) - aws-sigv4 (~> 1.0) + aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.13.0) - aws-sdk-core (~> 3, >= 3.39.0) - aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.30.1) - aws-sdk-core (~> 3, >= 3.39.0) + aws-sdk-kms (1.16.0) + aws-sdk-core (~> 3, >= 3.48.2) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.36.0) + aws-sdk-core (~> 3, >= 3.48.2) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) - aws-sigv4 (1.0.3) + aws-sigv4 (1.1.0) + aws-eventstream (~> 1.0, >= 1.0.2) bcrypt (3.1.12) benchmark-ips (2.7.2) - better_errors (2.5.0) + better_errors (2.5.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.3.2) + bootsnap (1.4.3) msgpack (~> 1.0) - brakeman (4.4.0) + brakeman (4.5.0) browser (2.5.3) builder (3.2.3) bullet (5.9.0) @@ -109,7 +110,7 @@ GEM bundler-audit (0.6.1) bundler (>= 1.2.0, < 3) thor (~> 0.18) - byebug (10.0.2) + byebug (11.0.0) capistrano (3.11.0) airbrussh (>= 1.0.0) i18n @@ -126,7 +127,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.12.0) + capybara (3.16.1) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -148,7 +149,7 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.2) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.5) connection_pool (2.2.2) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -164,7 +165,7 @@ GEM rack (>= 1) rake (> 10, < 13) thor (~> 0.19) - devise (4.5.0) + devise (4.6.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 6.0) @@ -185,10 +186,10 @@ GEM unf (>= 0.0.5, < 1.0.0) doorkeeper (5.0.2) railties (>= 4.2) - dotenv (2.6.0) - dotenv-rails (2.6.0) - dotenv (= 2.6.0) - railties (>= 3.2, < 6.0) + dotenv (2.7.2) + dotenv-rails (2.7.2) + dotenv (= 2.7.2) + railties (>= 3.2, < 6.1) elasticsearch (6.0.2) elasticsearch-api (= 6.0.2) elasticsearch-transport (= 6.0.2) @@ -205,7 +206,7 @@ GEM tzinfo excon (0.62.0) fabrication (2.20.1) - faker (1.9.1) + faker (1.9.3) i18n (>= 0.7) faraday (0.15.0) multipart-post (>= 1.2, < 3) @@ -232,18 +233,18 @@ GEM rspec-core (~> 3.0) ruby-progressbar (~> 1.4) get_process_mem (0.2.3) - globalid (0.4.1) + globalid (0.4.2) activesupport (>= 4.2.0) goldfinger (2.1.0) addressable (~> 2.5) http (~> 3.0) nokogiri (~> 1.8) oj (~> 3.0) - hamlit (2.8.8) + hamlit (2.9.3) temple (>= 0.8.0) thor tilt - hamlit-rails (0.2.0) + hamlit-rails (0.2.3) actionpack (>= 4.0.1) activesupport (>= 4.0.1) hamlit (>= 1.2.0) @@ -253,7 +254,7 @@ GEM hashdiff (0.3.7) hashie (3.6.0) heapy (0.1.4) - highline (2.0.0) + highline (2.0.1) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -266,12 +267,12 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.2.0) + httplog (1.2.2) rack (>= 1.0) rainbow (>= 2.0.0) - i18n (1.5.2) + i18n (1.6.0) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.28) + i18n-tasks (0.9.29) activesupport (>= 4.0.2) ast (>= 2.1.0) erubi @@ -327,25 +328,25 @@ GEM nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - makara (0.4.0) + makara (0.4.1) activerecord (>= 3.0.0) marcel (0.3.3) mimemagic (~> 0.3.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) - memory_profiler (0.9.12) + memory_profiler (0.9.13) method_source (0.9.2) - microformats (4.0.7) - json - nokogiri + microformats (4.1.0) + json (~> 2.1) + nokogiri (~> 1.8, >= 1.8.3) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) - mimemagic (0.3.2) + mimemagic (0.3.3) mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.4) + msgpack (1.2.9) multi_json (1.13.1) multipart-post (2.0.0) necromancer (0.4.0) @@ -354,7 +355,7 @@ GEM net-ssh (>= 2.6.5) net-ssh (5.0.2) nio4r (2.3.1) - nokogiri (1.10.1) + nokogiri (1.10.2) mini_portile2 (~> 2.4.0) nokogumbo (2.0.0) nokogiri (~> 1.8, >= 1.8.4) @@ -363,7 +364,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.7.8) + oj (3.7.11) omniauth (1.9.0) hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) @@ -389,10 +390,10 @@ GEM paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) - parallel (1.13.0) - parallel_tests (2.27.1) + parallel (1.17.0) + parallel_tests (2.28.0) parallel - parser (2.6.0.0) + parser (2.6.2.0) ast (~> 2.4.0) pastel (0.7.2) equatable (~> 0.5.0) @@ -400,8 +401,7 @@ GEM pg (1.1.4) pghero (2.2.0) activerecord - pkg-config (1.3.2) - powerpack (0.1.2) + pkg-config (1.3.7) premailer (1.11.1) addressable css_parser (>= 1.6.0) @@ -413,38 +413,39 @@ GEM pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - pry-byebug (3.6.0) - byebug (~> 10.0) + pry-byebug (3.7.0) + byebug (~> 11.0) pry (~> 0.10) pry-rails (0.3.9) pry (>= 0.10.4) + psych (3.1.0) public_suffix (3.0.3) - puma (3.12.0) + puma (3.12.1) pundit (2.0.1) activesupport (>= 3.0.0) raabro (1.1.6) - rack (2.0.6) + rack (2.0.7) rack-attack (5.4.2) rack (>= 1.0, < 3) - rack-cors (1.0.2) + rack-cors (1.0.3) rack-protection (2.0.5) rack - rack-proxy (0.6.4) + rack-proxy (0.6.5) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.2) - actioncable (= 5.2.2) - actionmailer (= 5.2.2) - actionpack (= 5.2.2) - actionview (= 5.2.2) - activejob (= 5.2.2) - activemodel (= 5.2.2) - activerecord (= 5.2.2) - activestorage (= 5.2.2) - activesupport (= 5.2.2) + rails (5.2.3) + actioncable (= 5.2.3) + actionmailer (= 5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) + activemodel (= 5.2.3) + activerecord (= 5.2.3) + activestorage (= 5.2.3) + activesupport (= 5.2.3) bundler (>= 1.3.0) - railties (= 5.2.2) + railties (= 5.2.3) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.4) actionpack (>= 5.0.1.x) @@ -455,14 +456,14 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-i18n (5.1.2) + rails-i18n (5.1.3) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (5.2.2) - actionpack (= 5.2.2) - activesupport (= 5.2.2) + railties (5.2.3) + actionpack (= 5.2.3) + activesupport (= 5.2.3) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -498,9 +499,9 @@ GEM regexp_parser (1.3.0) request_store (1.4.1) rack (>= 1.4) - responders (2.4.0) - actionpack (>= 4.2.0, < 5.3) - railties (>= 4.2.0, < 5.3) + responders (2.4.1) + actionpack (>= 4.2.0, < 6.0) + railties (>= 4.2.0, < 6.0) rotp (2.1.2) rpam2 (4.0.2) rqrcode (0.10.1) @@ -525,14 +526,14 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.63.1) + rubocop (0.67.1) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) - powerpack (~> 0.1) + psych (>= 3.1.0) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.4.0) + unicode-display_width (>= 1.4.0, < 1.6) ruby-progressbar (1.10.0) ruby-saml (1.9.0) nokogiri (>= 1.5.10) @@ -563,9 +564,9 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 3) tilt (>= 1.4.0) - sidekiq-unique-jobs (6.0.8) + sidekiq-unique-jobs (6.0.12) concurrent-ruby (~> 1.0, >= 1.0.5) - sidekiq (>= 4.0, < 6.0) + sidekiq (>= 4.0, < 7.0) thor (~> 0) simple-navigation (4.0.5) activesupport (>= 2.3.2) @@ -594,14 +595,14 @@ GEM multi_json (~> 1.8) strong_migrations (0.3.1) activerecord (>= 3.2.0) - temple (0.8.0) + temple (0.8.1) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (0.20.3) thread_safe (0.3.6) - tilt (2.0.8) + tilt (2.0.9) timers (4.2.0) tty-color (0.4.3) tty-command (0.8.2) @@ -622,24 +623,24 @@ GEM unf (~> 0.1.0) tzinfo (1.2.5) thread_safe (~> 0.1) - tzinfo-data (1.2018.9) + tzinfo-data (1.2019.1) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.4.1) + unicode-display_width (1.5.0) uniform_notifier (1.12.1) - warden (1.2.7) - rack (>= 1.0) + warden (1.2.8) + rack (>= 2.0.6) webmock (3.5.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpacker (3.5.5) + webpacker (4.0.2) activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) - webpush (0.3.6) + webpush (0.3.7) hkdf (~> 0.2) jwt (~> 2.0) websocket-driver (0.7.0) @@ -654,14 +655,14 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10) - active_record_query_trace (~> 1.5) + active_record_query_trace (~> 1.6) addressable (~> 2.6) annotate (~> 2.7) - aws-sdk-s3 (~> 1.30) + aws-sdk-s3 (~> 1.36) better_errors (~> 2.5) binding_of_caller (~> 0.7) - bootsnap (~> 1.3) - brakeman (~> 4.4) + bootsnap (~> 1.4) + brakeman (~> 4.5) browser bullet (~> 5.9) bundler-audit (~> 0.6) @@ -669,18 +670,18 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.12) + capybara (~> 3.16) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.3) climate_control (~> 0.2) concurrent-ruby derailed_benchmarks - devise (~> 4.5) + devise (~> 4.6) devise-two-factor (~> 3.0) devise_pam_authenticatable2 (~> 9.2) doorkeeper (~> 5.0) - dotenv-rails (~> 2.6) + dotenv-rails (~> 2.7) fabrication (~> 2.20) faker (~> 1.9) fast_blank (~> 1.0) @@ -709,7 +710,7 @@ DEPENDENCIES makara (~> 0.4) mario-redis-lock (~> 1.2) memory_profiler - microformats (~> 4.0) + microformats (~> 4.1) mime-types (~> 3.2) net-ldap (~> 0.10) nokogiri (~> 1.10) @@ -722,20 +723,20 @@ DEPENDENCIES ox (~> 2.10) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel_tests (~> 2.27) + parallel_tests (~> 2.28) pg (~> 1.1) pghero (~> 2.2) pkg-config (~> 1.3) posix-spawn! premailer-rails private_address_check (~> 0.5) - pry-byebug (~> 3.6) + pry-byebug (~> 3.7) pry-rails (~> 0.3) puma (~> 3.12) pundit (~> 2.0) rack-attack (~> 5.4) rack-cors (~> 1.0) - rails (~> 5.2.2) + rails (~> 5.2.3) rails-controller-testing (~> 1.0) rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) @@ -746,7 +747,7 @@ DEPENDENCIES rqrcode (~> 0.10) rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) - rubocop (~> 0.63) + rubocop (~> 0.67) sanitize (~> 5.0) scss_lint (~> 0.57) sidekiq (~> 5.2) @@ -765,13 +766,13 @@ DEPENDENCIES tty-command (~> 0.8) tty-prompt (~> 0.18) twitter-text (~> 1.14) - tzinfo-data (~> 1.2018) + tzinfo-data (~> 1.2019) webmock (~> 3.5) - webpacker (~> 3.5) + webpacker (~> 4.0) webpush RUBY VERSION - ruby 2.6.0p0 + ruby 2.6.1p33 BUNDLED WITH 1.17.3 diff --git a/Vagrantfile b/Vagrantfile index f9839fffb..c4941f673 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -44,7 +44,18 @@ sudo apt-get install \ # Install rvm read RUBY_VERSION < .ruby-version -gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB + +gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" +$($gpg_command) +if [ $? -ne 0 ];then + echo "GPG command failed, This prevented RVM from installing." + echo "Retrying once..." && $($gpg_command) + if [ $? -ne 0 ];then + echo "GPG failed for the second time, please ensure network connectivity." + echo "Exiting..." && exit 1 + fi +fi + curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION source /home/vagrant/.rvm/scripts/rvm diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index eafc1818b..8ce413f8a 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -48,6 +48,7 @@ class StatusesIndex < Chewy::Index end root date_detection: false do + field :id, type: 'long' field :account_id, type: 'long' field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do @@ -55,7 +56,6 @@ class StatusesIndex < Chewy::Index end field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } - field :created_at, type: 'date' end end end diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 0dbf0283d..52a51fd62 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,26 +1,25 @@ # frozen_string_literal: true class AboutController < ApplicationController - before_action :set_body_classes + layout 'public' + before_action :set_instance_presenter, only: [:show, :more, :terms] def show - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json + @hide_navbar = true end - def more - render layout: 'public' - end + def more; end - def terms - render layout: 'public' - end + def terms; end private def new_user - User.new.tap(&:build_account) + User.new.tap do |user| + user.build_account + user.build_invite_request + end end helper_method :new_user @@ -28,15 +27,4 @@ class AboutController < ApplicationController def set_instance_presenter @instance_presenter = InstancePresenter.new end - - def set_body_classes - @body_classes = 'with-modals' - end - - def initial_state_params - { - settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, - token: current_session&.token, - } - end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 58f4f100d..3e31c00a2 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -10,6 +10,8 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do + mark_cacheable! unless user_signed_in? + @body_classes = 'with-modals' @pinned_statuses = [] @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @@ -30,17 +32,21 @@ class AccountsController < ApplicationController end format.atom do + mark_cacheable! + @entries = @account.stream_entries.where(hidden: false).with_includes.without_local_only.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) end format.rss do + mark_cacheable! + @statuses = cache_collection(default_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE), Status) render xml: RSS::AccountSerializer.render(@account, @statuses) end format.json do - skip_session! + mark_cacheable! render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) @@ -52,11 +58,12 @@ class AccountsController < ApplicationController private def show_pinned_statuses? - [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? + [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses default_statuses.tap do |statuses| + statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? statuses.merge!(no_replies_scope) unless replies_requested? end @@ -82,12 +89,21 @@ class AccountsController < ApplicationController Status.without_replies end - def set_account - @account = Account.find_local!(params[:username]) + def hashtag_scope + tag = Tag.find_normalized(params[:tag]) + + if tag + Status.tagged_with(tag.id) + else + Status.none + end + end + + def username_param + params[:username] end def older_url - ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") pagination_url(max_id: @statuses.last.id) end @@ -96,7 +112,9 @@ class AccountsController < ApplicationController end def pagination_url(max_id: nil, min_id: nil) - if media_requested? + if tag_requested? + short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) + elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) @@ -113,6 +131,10 @@ class AccountsController < ApplicationController request.path.ends_with?('/with_replies') end + def tag_requested? + request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + end + def filtered_status_page(params) if params[:min_id].present? filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 995da9c55..853f4f907 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -6,13 +6,19 @@ class ActivityPub::CollectionsController < Api::BaseController before_action :set_account before_action :set_size before_action :set_statuses + before_action :set_cache_headers def show - render json: collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json', - skip_activities: true + skip_session! + + render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do + ActiveModelSerializers::SerializableResource.new( + collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + skip_activities: true + ) + end end private diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 8f5e1887e..a0b7532c2 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -2,11 +2,14 @@ class ActivityPub::InboxesController < Api::BaseController include SignatureVerification + include JsonLdHelper before_action :set_account def create - if signed_request_account + if unknown_deleted_account? + head 202 + elsif signed_request_account upgrade_account process_payload head 202 @@ -17,12 +20,22 @@ class ActivityPub::InboxesController < Api::BaseController private + def unknown_deleted_account? + json = Oj.load(body, mode: :strict) + json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + rescue Oj::ParseError + false + end + def set_account @account = Account.find_local!(params[:account_username]) if params[:account_username] end def body - @body ||= request.body.read + return @body if defined?(@body) + @body = request.body.read.force_encoding('UTF-8') + request.body.rewind if request.body.respond_to?(:rewind) + @body end def upgrade_account @@ -36,6 +49,6 @@ class ActivityPub::InboxesController < Api::BaseController end def process_payload - ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id) + ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index be4289b21..438fa226e 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -7,8 +7,14 @@ class ActivityPub::OutboxesController < Api::BaseController before_action :set_account before_action :set_statuses + before_action :set_cache_headers def show + unless page_requested? + skip_session! + expires_in 1.minute, public: true + end + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 562fba996..e7795e95c 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,9 +2,9 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize] + before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] - before_action :require_local_account!, only: [:enable, :memorialize] + before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index authorize :account, :index? @@ -45,6 +45,18 @@ module Admin redirect_to admin_account_path(@account.id) end + def approve + authorize @account.user, :approve? + @account.user.approve! + redirect_to admin_accounts_path(pending: '1') + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + redirect_to admin_accounts_path(pending: '1') + end + def unsilence authorize @account, :unsilence? @account.unsilence! @@ -114,6 +126,7 @@ module Admin :remote, :by_domain, :active, + :pending, :silenced, :suspended, :username, diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index d61bafdf0..f77699166 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -5,6 +5,9 @@ module Admin before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_filter_params + include ObfuscateFilename + obfuscate_filename [:custom_emoji, :image] + def index authorize :custom_emoji, :index? @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index bb923c185..f23ed1508 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -10,7 +10,7 @@ module Admin @interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 @relay_enabled = Relay.enabled.exists? @single_user_mode = Rails.configuration.x.single_user_mode - @registrations_enabled = Setting.open_registrations + @registrations_enabled = Setting.registrations_mode != 'none' @deletions_enabled = Setting.open_deletion @invites_enabled = Setting.min_invite_role == 'user' @search_enabled = Chewy.enabled? @@ -29,6 +29,7 @@ module Admin @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @trending_hashtags = TrendingTags.get(7) @profile_directory = Setting.profile_directory + @timeline_preview = Setting.timeline_preview end private diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 431ce6f4d..6dd659a30 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -38,7 +38,7 @@ module Admin end def filter_params - params.permit(:limited) + params.permit(:limited, :by_domain) end end end diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb new file mode 100644 index 000000000..b62a9bc84 --- /dev/null +++ b/app/controllers/admin/pending_accounts_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Admin + class PendingAccountsController < BaseController + before_action :set_accounts, only: :index + + def index + @form = Form::AccountBatch.new + end + + def batch + @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.accounts.no_account_selected') + ensure + redirect_to admin_pending_accounts_path(current_params) + end + + def approve_all + Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save + redirect_to admin_pending_accounts_path(current_params) + end + + def reject_all + Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save + redirect_to admin_pending_accounts_path(current_params) + end + + private + + def set_accounts + @accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page]) + end + + def form_account_batch_params + params.require(:form_account_batch).permit(:action, account_ids: []) + end + + def action_from_button + if params[:approve] + 'approve' + elsif params[:reject] + 'reject' + end + end + + def current_params + params.slice(:page).permit(:page) + end + end +end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index d3c2f5e9e..3ba9f5df2 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -10,6 +10,10 @@ module Admin @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save + redirect_to admin_report_path(@report) + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.statuses.no_status_selected') + redirect_to admin_report_path(@report) end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 4a049fc23..dc1c79b7f 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -2,85 +2,29 @@ module Admin class SettingsController < BaseController - ADMIN_SETTINGS = %w( - site_contact_username - site_contact_email - site_title - site_short_description - site_description - site_extended_description - site_terms - open_registrations - closed_registrations_message - open_deletion - timeline_preview - show_staff_badge - bootstrap_timeline_accounts - theme - thumbnail - hero - mascot - min_invite_role - activity_api_enabled - peers_api_enabled - show_known_fediverse_at_about_page - preview_sensitive_media - custom_css - profile_directory - ).freeze - - BOOLEAN_SETTINGS = %w( - open_registrations - open_deletion - timeline_preview - show_staff_badge - activity_api_enabled - peers_api_enabled - show_known_fediverse_at_about_page - preview_sensitive_media - profile_directory - ).freeze - - UPLOAD_SETTINGS = %w( - thumbnail - hero - mascot - ).freeze - def edit authorize :settings, :show? + @admin_settings = Form::AdminSettings.new end def update authorize :settings, :update? - settings_params.each do |key, value| - if UPLOAD_SETTINGS.include?(key) - upload = SiteUpload.where(var: key).first_or_initialize(var: key) - upload.update(file: value) - else - setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: value_for_update(key, value)) - end - end + @admin_settings = Form::AdminSettings.new(settings_params) - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to edit_admin_settings_path + if @admin_settings.save + flash[:notice] = I18n.t('generic.changes_saved_msg') + redirect_to edit_admin_settings_path + else + render :edit + end end private def settings_params - params.require(:form_admin_settings).permit(ADMIN_SETTINGS) - end - - def value_for_update(key, value) - if BOOLEAN_SETTINGS.include?(key) - value == '1' - else - value - end + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index a1dd30918..3a92ee4e4 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController elsif current_user.disabled? render json: { error: 'Your login is currently disabled' }, status: 403 elsif !current_user.confirmed? - render json: { error: 'Email confirmation is not completed' }, status: 403 + render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 + elsif !current_user.approved? + render json: { error: 'Your login is currently pending approval' }, status: 403 else set_user_activity end diff --git a/app/controllers/api/proofs_controller.rb b/app/controllers/api/proofs_controller.rb new file mode 100644 index 000000000..a84ad2014 --- /dev/null +++ b/app/controllers/api/proofs_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::ProofsController < Api::BaseController + before_action :set_account + before_action :set_provider + before_action :check_account_approval + before_action :check_account_suspension + + def index + render json: @account, serializer: @provider.serializer_class + end + + private + + def set_provider + @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) + end + + def set_account + @account = Account.find_local!(params[:username]) + end + + def check_account_approval + not_found if @account.user_pending? + end + + def check_account_suspension + gone if @account.suspended? + end +end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index ec15debb0..2dabb8398 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def load_accounts - return [] if @account.user_hides_network? && current_account.id != @account.id + return [] if hide_results? default_accounts.merge(paginated_follows).to_a end + def hide_results? + (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account)) + end + def default_accounts Account.includes(:active_relationships, :account_stat).references(:active_relationships) end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index f3e112f2c..44e89804b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def load_accounts - return [] if @account.user_hides_network? && current_account.id != @account.id + return [] if hide_results? default_accounts.merge(paginated_follows).to_a end + def hide_results? + (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account)) + end + def default_accounts Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) end diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb new file mode 100644 index 000000000..bea51ae11 --- /dev/null +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::IdentityProofsController < Api::BaseController + before_action :require_user! + before_action :set_account + + respond_to :json + + def index + @proofs = @account.identity_proofs.active + render json: @proofs, each_serializer: REST::IdentityProofSerializer + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end +end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 91c9f1547..4217b527a 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -16,10 +16,11 @@ class Api::V1::Accounts::SearchController < Api::BaseController def account_search AccountSearchService.new.call( params[:q], - limit_param(DEFAULT_ACCOUNTS_LIMIT), current_account, + limit: limit_param(DEFAULT_ACCOUNTS_LIMIT), resolve: truthy_param?(:resolve), - following: truthy_param?(:following) + following: truthy_param?(:following), + offset: params[:offset] ) end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6c2a5c141..8cd8f8e79 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? statuses end @@ -50,9 +51,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController # Also, Avoid getting slow by not narrowing down by `statuses.account_id`. # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used # and the table will be joined by `Merge Semi Join`, so the query will be slow. - Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) - .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - .reorder(id: :desc).distinct(:id).pluck(:id) + @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) + .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) + .reorder(id: :desc).distinct(:id).pluck(:id) end def pinned_scope @@ -67,6 +68,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController Status.without_reblogs end + def hashtag_scope + tag = Tag.find_normalized(params[:tagged]) + + if tag + Status.tagged_with(tag.id) + else + Status.none + end + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 2ccbc3cbb..b0c62778e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -80,6 +80,10 @@ class Api::V1::AccountsController < Api::BaseController end def check_enabled_registrations - forbidden if single_user_mode? || !Setting.open_registrations + forbidden if single_user_mode? || !allowed_registrations? + end + + def allowed_registrations? + Setting.registrations_mode != 'none' end end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb new file mode 100644 index 000000000..3fa0b6a76 --- /dev/null +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Polls::VotesController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:statuses' } + before_action :require_user! + before_action :set_poll + + respond_to :json + + def create + VoteService.new.call(current_account, @poll, vote_params[:choices]) + render json: @poll, serializer: REST::PollSerializer + end + + private + + def set_poll + @poll = Poll.attached.find(params[:poll_id]) + authorize @poll.status, :show? + rescue Mastodon::NotPermittedError + raise ActiveRecord::RecordNotFound + end + + def vote_params + params.permit(choices: []) + end +end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb new file mode 100644 index 000000000..4f4a6858d --- /dev/null +++ b/app/controllers/api/v1/polls_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Api::V1::PollsController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show + + respond_to :json + + def show + @poll = Poll.attached.find(params[:id]) + ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale? + render json: @poll, serializer: REST::PollSerializer, include_results: true + end +end diff --git a/app/controllers/api/v1/preferences_controller.rb b/app/controllers/api/v1/preferences_controller.rb new file mode 100644 index 000000000..077d39f5d --- /dev/null +++ b/app/controllers/api/v1/preferences_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Api::V1::PreferencesController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action :require_user! + + respond_to :json + + def index + render json: current_account, serializer: REST::PreferencesSerializer + end +end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index dc1a37599..6131cbbb6 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -3,7 +3,7 @@ class Api::V1::SearchController < Api::BaseController include Authorization - RESULTS_LIMIT = 5 + RESULTS_LIMIT = 20 before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action :require_user! @@ -11,30 +11,22 @@ class Api::V1::SearchController < Api::BaseController respond_to :json def index - @search = Search.new(search) + @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer end private - def search - search_results.tap do |search| - search[:statuses].keep_if do |status| - begin - authorize status, :show? - rescue Mastodon::NotPermittedError - false - end - end - end - end - def search_results SearchService.new.call( params[:q], - RESULTS_LIMIT, - truthy_param?(:resolve), - current_account + current_account, + limit_param(RESULTS_LIMIT), + search_params.merge(resolve: truthy_param?(:resolve)) ) end + + def search_params + params.permit(:type, :offset, :min_id, :max_id, :account_id) + end end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 04847a6b7..ed4f55100 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController respond_to :json def create - @status = ReblogService.new.call(current_user.account, status_for_reblog) + @status = ReblogService.new.call(current_user.account, status_for_reblog, reblog_params) render json: @status, serializer: REST::StatusSerializer end @@ -32,4 +32,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController def status_for_destroy current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! end + + def reblog_params + params.permit(:visibility) + end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 70860f481..8d25603f3 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController visibility: status_params[:visibility], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, + poll: status_params[:poll], idempotency: request.headers['Idempotency-Key'], local_only: status_params[:local_only]) @@ -74,12 +75,26 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end def status_params - params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, :local_only, media_ids: []) + params.permit( + :status, + :in_reply_to_id, + :sensitive, + :spoiler_text, + :visibility, + :scheduled_at, + :local_only, + media_ids: [], + poll: [ + :multiple, + :hide_totals, + :expires_in, + options: [], + ] + ) end def pagination_params(core_params) diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 92c32c178..9adc4ad29 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Timelines::TagController < Api::BaseController private def load_tag - @tag = Tag.find_by(name: params[:id].downcase) + @tag = Tag.find_normalized(params[:id]) end def load_statuses diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 2e91d68ee..9aa6edc69 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -2,7 +2,7 @@ class Api::V2::SearchController < Api::V1::SearchController def index - @search = Search.new(search) + @search = Search.new(search_results) render json: @search, serializer: REST::V2::SearchSerializer end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b54e7b008..990aff857 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -151,6 +151,11 @@ class ApplicationController < ActionController::Base response.headers['Vary'] = 'Accept' end + def mark_cacheable! + skip_session! + expires_in 0, public: true + end + def skip_session! request.session_options[:skip] = true end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index ad7b1859f..5c1ff769a 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -10,6 +10,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update] + def new + super(&:build_invite_request) + end + def destroy not_found end @@ -24,17 +28,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController def build_resource(hash = nil) super(hash) - resource.locale = I18n.locale - resource.invite_code = params[:invite_code] if resource.invite_code.blank? - resource.agreement = true + resource.locale = I18n.locale + resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.agreement = true + resource.current_sign_in_ip = request.remote_ip - resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil? resource.build_account if resource.account.nil? end def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code) + u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code) end end @@ -65,7 +69,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def allowed_registrations? - Setting.open_registrations || @invite&.valid_for_use? + Setting.registrations_mode != 'none' || @invite&.valid_for_use? end def invite_code diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 6c27ef330..4f28941ae 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -7,16 +7,18 @@ module AccountControllerConcern included do layout 'public' + before_action :set_account + before_action :check_account_approval + before_action :check_account_suspension before_action :set_instance_presenter before_action :set_link_headers - before_action :check_account_suspension end private def set_account - @account = Account.find_local!(params[:account_username]) + @account = Account.find_local!(username_param) end def set_instance_presenter @@ -33,6 +35,10 @@ module AccountControllerConcern ) end + def username_param + params[:account_username] + end + def webfinger_account_link [ webfinger_account_url, @@ -58,7 +64,15 @@ module AccountControllerConcern webfinger_url(resource: @account.to_webfinger_s) end + def check_account_approval + not_found if @account.user_pending? + end + def check_account_suspension - gone if @account.suspended? + if @account.suspended? + skip_session! + expires_in(3.minutes, public: true) + gone + end end end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index ff7ff4a42..594907674 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -32,7 +32,7 @@ class DirectoriesController < ApplicationController end def set_accounts - @accounts = Account.discoverable.page(params[:page]).per(40).tap do |query| + @accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 99cb3676f..713365ea5 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -3,9 +3,13 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern + before_action :set_cache_headers + def index respond_to do |format| format.html do + mark_cacheable! unless user_signed_in? + next if @account.user_hides_network? follows @@ -15,6 +19,11 @@ class FollowerAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + if params[:page].blank? + skip_session! + expires_in 3.minutes, public: true + end + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 03c4b1046..1bfd901cf 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -3,9 +3,13 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern + before_action :set_cache_headers + def index respond_to do |format| format.html do + mark_cacheable! unless user_signed_in? + next if @account.user_hides_network? follows @@ -15,6 +19,11 @@ class FollowingAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + if params[:page].blank? + skip_session! + expires_in 3.minutes, public: true + end + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index b5d6460f9..d1bd0601e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -50,7 +50,7 @@ class HomeController < ApplicationController push_subscription: current_account.user.web_push_subscription(current_session), current_account: current_account, token: current_session.token, - admin: Account.find_local(Setting.site_contact_username), + admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), } end diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb new file mode 100644 index 000000000..53d4472d8 --- /dev/null +++ b/app/controllers/public_timelines_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class PublicTimelinesController < ApplicationController + layout 'public' + + before_action :check_enabled + before_action :set_body_classes + before_action :set_instance_presenter + + def show + respond_to do |format| + format.html do + @initial_state_json = ActiveModelSerializers::SerializableResource.new( + InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), + serializer: InitialStateSerializer + ).to_json + end + end + end + + private + + def check_enabled + raise ActiveRecord::RecordNotFound unless Setting.timeline_preview + end + + def set_body_classes + @body_classes = 'with-modals' + end + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end +end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb new file mode 100644 index 000000000..e6705c327 --- /dev/null +++ b/app/controllers/relationships_controller.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class RelationshipsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :set_accounts, only: :show + before_action :set_body_classes + + helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? + + def show + @form = Form::AccountBatch.new + end + + def update + @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + # Do nothing + ensure + redirect_to relationships_path(current_params) + end + + private + + def set_accounts + @accounts = relationships_scope.page(params[:page]).per(40) + end + + def relationships_scope + scope = begin + if following_relationship? + current_account.following.eager_load(:account_stat).reorder(nil) + else + current_account.followers.eager_load(:account_stat).reorder(nil) + end + end + + scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent' + scope.merge!(Account.by_recent_status) if params[:order] == 'active' + scope.merge!(mutual_relationship_scope) if mutual_relationship? + scope.merge!(moved_account_scope) if params[:status] == 'moved' + scope.merge!(primary_account_scope) if params[:status] == 'primary' + scope.merge!(by_domain_scope) if params[:by_domain].present? + scope.merge!(dormant_account_scope) if params[:activity] == 'dormant' + + scope + end + + def mutual_relationship_scope + Account.where(id: current_account.following) + end + + def moved_account_scope + Account.where.not(moved_to_account_id: nil) + end + + def primary_account_scope + Account.where(moved_to_account_id: nil) + end + + def dormant_account_scope + AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) + end + + def by_domain_scope + Account.where(domain: params[:by_domain]) + end + + def form_account_batch_params + params.require(:form_account_batch).permit(:action, account_ids: []) + end + + def following_relationship? + params[:relationship].blank? || params[:relationship] == 'following' + end + + def mutual_relationship? + params[:relationship] == 'mutual' + end + + def followed_by_relationship? + params[:relationship] == 'followed_by' + end + + def current_params + params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order) + end + + def action_from_button + if params[:unfollow] + 'unfollow' + elsif params[:remove_from_followers] + 'remove_from_followers' + elsif params[:block_domains] + 'block_domains' + end + end + + def set_body_classes + @body_classes = 'admin' + end +end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 0135f2189..3012fbf77 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -13,11 +13,25 @@ class Settings::ExportsController < Settings::BaseController end def create - authorize :backup, :create? + raise Mastodon::NotPermittedError unless user_signed_in? + + backup = nil + + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + authorize :backup, :create? + backup = current_user.backups.create! + else + raise Mastodon::RaceConditionError + end + end - backup = current_user.backups.create! BackupWorker.perform_async(backup.id) redirect_to settings_export_path end + + def lock_options + { redis: Redis.current, key: "backup:#{current_user.id}" } + end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb new file mode 100644 index 000000000..3a3241425 --- /dev/null +++ b/app/controllers/settings/featured_tags_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Settings::FeaturedTagsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + before_action :set_most_used_tags, only: :index + + def index + @featured_tag = FeaturedTag.new + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + + if @featured_tag.save + redirect_to settings_featured_tags_path + else + set_featured_tags + set_most_used_tags + + render :index + end + end + + def destroy + @featured_tag.destroy! + redirect_to settings_featured_tags_path + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) + end + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + end + + def featured_tag_params + params.require(:featured_tag).permit(:name) + end +end diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb deleted file mode 100644 index ce8ec985d..000000000 --- a/app/controllers/settings/follower_domains_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class Settings::FollowerDomainsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - - def show - @account = current_account - @domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) - end - - def update - domains = bulk_params[:select] || [] - - AfterAccountDomainBlockWorker.push_bulk(domains) do |domain| - [current_account.id, domain] - end - - redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size) - end - - private - - def bulk_params - params.permit(select: []) - end -end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb new file mode 100644 index 000000000..e22b4d9be --- /dev/null +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class Settings::IdentityProofsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :check_required_params, only: :new + + def index + @proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc) + @proofs.each(&:refresh!) + end + + def new + @proof = current_account.identity_proofs.new( + token: params[:token], + provider: params[:provider], + provider_username: params[:provider_username] + ) + + if current_account.username.casecmp(params[:username]).zero? + render layout: 'auth' + else + flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) + redirect_to settings_identity_proofs_path + end + end + + def create + @proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params) + @proof.token = resource_params[:token] + + if @proof.save + PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? + redirect_to @proof.on_success_path(params[:user_agent]) + else + flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) + redirect_to settings_identity_proofs_path + end + end + + private + + def check_required_params + redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? } + end + + def resource_params + params.require(:account_identity_proof).permit(:provider, :provider_username, :token) + end + + def publish_proof? + ActiveModel::Type::Boolean.new.cast(post_params[:post_status]) + end + + def post_params + params.require(:account_identity_proof).permit(:post_status, :status_text) + end + + def set_body_classes + @body_classes = '' + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 8daddab2d..8bb5201cf 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -49,7 +49,8 @@ class Settings::PreferencesController < Settings::BaseController :setting_theme, :setting_hide_network, :setting_aggregate_reblogs, - notification_emails: %i(follow follow_request reblog favourite mention digest report), + :setting_show_application, + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following) ) end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index db9081fdf..8b640cdca 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController end def set_account - @account = current_user.account + @account = current_account end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 11b150ffd..84ebb21f2 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Settings::SessionsController < Settings::BaseController + before_action :authenticate_user! before_action :set_session, only: :destroy def destroy diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 9ef1e0749..af605b98f 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -21,7 +21,7 @@ class SharesController < ApplicationController push_subscription: current_account.user.web_push_subscription(current_session), current_account: current_account, token: current_session.token, - admin: Account.find_local(Setting.site_contact_username), + admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), text: text, } end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 15d59fd89..fc44d5fb1 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -18,6 +18,7 @@ class StatusesController < ApplicationController before_action :redirect_to_original, only: [:show] before_action :set_referrer_policy_header, only: [:show] before_action :set_cache_headers + before_action :set_replies, only: [:replies] content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -26,6 +27,8 @@ class StatusesController < ApplicationController def show respond_to do |format| format.html do + mark_cacheable! unless user_signed_in? + @body_classes = 'with-modals' set_ancestors @@ -35,7 +38,7 @@ class StatusesController < ApplicationController end format.json do - skip_session! unless @stream_entry.hidden? + mark_cacheable! unless @stream_entry.hidden? render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) @@ -63,8 +66,37 @@ class StatusesController < ApplicationController render 'stream_entries/embed', layout: 'embedded' end + def replies + skip_session! + + render json: replies_collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json', + skip_activities: true + end + private + def replies_collection_presenter + page = ActivityPub::CollectionPresenter.new( + id: replies_account_status_url(@account, @status, page_params), + type: :unordered, + part_of: replies_account_status_url(@account, @status), + next: next_page, + items: @replies.map { |status| status.local ? status : status.id } + ) + if page_requested? + page + else + ActivityPub::CollectionPresenter.new( + id: replies_account_status_url(@account, @status), + type: :unordered, + first: page + ) + end + end + def create_descendant_thread(starting_depth, statuses) depth = starting_depth + statuses.size if depth < DESCENDANTS_DEPTH_LIMIT @@ -174,4 +206,27 @@ class StatusesController < ApplicationController return if @status.public_visibility? || @status.unlisted_visibility? response.headers['Referrer-Policy'] = 'origin' end + + def page_requested? + params[:page] == 'true' + end + + def set_replies + @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) + end + + def next_page + last_reply = @replies.last + return if last_reply.nil? + same_account = last_reply.account_id == @account.id + return unless same_account || @replies.size == DESCENDANTS_LIMIT + same_account = false unless @replies.size == DESCENDANTS_LIMIT + replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) + end + + def page_params + { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact + end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 4694c823a..66b184901 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -9,12 +9,14 @@ class TagsController < ApplicationController before_action :set_instance_presenter def show - @tag = Tag.find_by!(name: params[:id].downcase) + @tag = Tag.find_normalized!(params[:id]) respond_to do |format| format.html do - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json + @initial_state_json = ActiveModelSerializers::SerializableResource.new( + InitialStatePresenter.new(settings: {}, token: current_session&.token), + serializer: InitialStateSerializer + ).to_json end format.rss do @@ -25,8 +27,7 @@ class TagsController < ApplicationController end format.json do - @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]) - .paginate_by_max_id(PAGE_SIZE, params[:max_id]) + @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) render json: collection_presenter, @@ -55,11 +56,4 @@ class TagsController < ApplicationController items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } ) end - - def initial_state_params - { - settings: {}, - token: current_session&.token, - } - end end diff --git a/app/controllers/well_known/keybase_proof_config_controller.rb b/app/controllers/well_known/keybase_proof_config_controller.rb new file mode 100644 index 000000000..eb41e586f --- /dev/null +++ b/app/controllers/well_known/keybase_proof_config_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WellKnown + class KeybaseProofConfigController < ActionController::Base + def show + render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer + end + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 359d60b60..e5fbb1500 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -9,42 +9,6 @@ module Admin::ActionLogsHelper end end - def linkable_log_target(record) - case record.class.name - when 'Account' - link_to record.acct, admin_account_path(record.id) - when 'User' - link_to record.account.acct, admin_account_path(record.account_id) - when 'CustomEmoji' - record.shortcode - when 'Report' - link_to "##{record.id}", admin_report_path(record) - when 'DomainBlock', 'EmailDomainBlock' - link_to record.domain, "https://#{record.domain}" - when 'Status' - link_to record.account.acct, TagManager.instance.url_for(record) - when 'AccountWarning' - link_to record.target_account.acct, admin_account_path(record.target_account_id) - end - end - - def log_target_from_history(type, attributes) - case type - when 'CustomEmoji' - attributes['shortcode'] - when 'DomainBlock', 'EmailDomainBlock' - link_to attributes['domain'], "https://#{attributes['domain']}" - when 'Status' - tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) - - if tmp_status.account - link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) - else - I18n.t('admin.action_logs.deleted_status') - end - end - end - def relevant_log_changes(log) if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) log.recorded_changes.slice('domain') @@ -111,4 +75,40 @@ module Admin::ActionLogsHelper def opposite_verbs?(log) %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type) end + + def linkable_log_target(record) + case record.class.name + when 'Account' + link_to record.acct, admin_account_path(record.id) + when 'User' + link_to record.account.acct, admin_account_path(record.account_id) + when 'CustomEmoji' + record.shortcode + when 'Report' + link_to "##{record.id}", admin_report_path(record) + when 'DomainBlock', 'EmailDomainBlock' + link_to record.domain, "https://#{record.domain}" + when 'Status' + link_to record.account.acct, TagManager.instance.url_for(record) + when 'AccountWarning' + link_to record.target_account.acct, admin_account_path(record.target_account_id) + end + end + + def log_target_from_history(type, attributes) + case type + when 'CustomEmoji' + attributes['shortcode'] + when 'DomainBlock', 'EmailDomainBlock' + link_to attributes['domain'], "https://#{attributes['domain']}" + when 'Status' + tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) + + if tmp_status.account + link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) + else + I18n.t('admin.action_logs.deleted_status') + end + end + end end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb new file mode 100644 index 000000000..4ee2cdef4 --- /dev/null +++ b/app/helpers/admin/dashboard_helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Admin::DashboardHelper + def feature_hint(feature, enabled) + indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ') + class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint' + + safe_join([feature, content_tag(:span, indicator, class: class_names)]) + end +end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 97beb587f..0bda25974 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true module Admin::FilterHelper - ACCOUNT_FILTERS = %i(local remote by_domain active silenced suspended username display_name email ip staff).freeze + ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(hidden).freeze - INSTANCES_FILTERS = %i(limited).freeze + INSTANCES_FILTERS = %i(limited by_domain).freeze + FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5097a0953..9d113263d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -20,7 +20,23 @@ module ApplicationHelper end def open_registrations? - Setting.open_registrations + Setting.registrations_mode == 'open' + end + + def approved_registrations? + Setting.registrations_mode == 'approved' + end + + def closed_registrations? + Setting.registrations_mode == 'none' + end + + def available_sign_up_path + if closed_registrations? + 'https://joinmastodon.org/#getting-started' + else + new_user_registration_path + end end def open_deletion? @@ -101,4 +117,9 @@ module ApplicationHelper def storage_host? ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? end + + def quote_wrap(text, line_width: 80, break_sequence: "\n") + text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence) + text.split("\n").map { |line| '> ' + line }.join("\n") + end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 9b3f1380b..df60b7dd7 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -56,4 +56,22 @@ module HomeHelper 'emojify' end end + + def optional_link_to(condition, path, options = {}, &block) + if condition + link_to(path, options, &block) + else + content_tag(:div, &block) + end + end + + def sign_up_message + if closed_registrations? + t('auth.registration_closed', instance: site_hostname) + elsif open_registrations? + t('auth.register') + elsif approved_registrations? + t('auth.apply_for_account') + end + end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 532397272..5b4011275 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -47,6 +47,15 @@ module JsonLdHelper !uri.start_with?('http://', 'https://') end + def invalid_origin?(url) + return true if unsupported_uri_scheme?(url) + + needle = Addressable::URI.parse(url).host + haystack = Addressable::URI.parse(@account.uri).host + + !haystack.casecmp(needle).zero? + end + def canonicalize(json) graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) graph.dump(:normalize) @@ -63,12 +72,19 @@ module JsonLdHelper json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) build_request(uri, on_behalf_of).perform do |response| + unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response + end return body_to_json(response.body_with_limit) if response.code == 200 end # If request failed, retry without doing it on behalf of a user + return if on_behalf_of.nil? build_request(uri).perform do |response| + unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response + end response.code == 200 ? body_to_json(response.body_with_limit) : nil end end @@ -91,6 +107,14 @@ module JsonLdHelper private + def response_successful?(response) + (200...300).cover?(response.code) + end + + def response_error_unsalvageable?(response) + (400...500).cover?(response.code) && response.code != 429 + end + def build_request(uri, on_behalf_of = nil) request = Request.new(:get, uri) request.on_behalf_of(on_behalf_of) if on_behalf_of diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0e957e946..92bc222ea 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -4,8 +4,9 @@ module SettingsHelper HUMAN_LOCALES = { en: 'English', ar: 'العربية', - ast: 'l\'asturianu', + ast: 'Asturianu', bg: 'Български', + bn: 'বাংলা', ca: 'Català', co: 'Corsu', cs: 'Čeština', @@ -19,8 +20,10 @@ module SettingsHelper fa: 'فارسی', fi: 'Suomi', fr: 'Français', + ga: 'Gaeilge', gl: 'Galego', he: 'עברית', + hi: 'हिन्दी', hr: 'Hrvatski', hu: 'Magyar', hy: 'Հայերեն', @@ -29,24 +32,29 @@ module SettingsHelper it: 'Italiano', ja: '日本語', ka: 'ქართული', + kk: 'Қазақша', ko: '한국어', + lt: 'Lietuvių', + lv: 'Latviešu', ml: 'മലയാളം', + ms: 'Bahasa Melayu', nl: 'Nederlands', no: 'Norsk', oc: 'Occitan', - pl: 'Polszczyzna', + pl: 'Polski', pt: 'Português', 'pt-BR': 'Português do Brasil', - ro: 'Limba română', + ro: 'Română', ru: 'Русский', sk: 'Slovenčina', sl: 'Slovenščina', + sq: 'Shqip', sr: 'Српски', 'sr-Latn': 'Srpski (latinica)', sv: 'Svenska', ta: 'தமிழ்', te: 'తెలుగు', - th: 'ภาษาไทย', + th: 'ไทย', tr: 'Türkçe', uk: 'Українська', zh: '中文', diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 7a74c0b7d..d56efbfb9 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -23,7 +23,7 @@ module StreamEntriesHelper safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')]) end elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) end end @@ -104,9 +104,19 @@ module StreamEntriesHelper I18n.t('statuses.content_warning', warning: status.spoiler_text) end + def poll_summary(status) + return unless status.preloadable_poll + status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") + end + def status_description(status) components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] - components << status.text if status.spoiler_text.blank? + + if status.spoiler_text.blank? + components << status.text + components << poll_summary(status) + end + components.reject(&:blank?).join("\n\n") end diff --git a/app/javascript/images/logo_transparent_black.svg b/app/javascript/images/logo_transparent_black.svg new file mode 100644 index 000000000..e44bcf5e1 --- /dev/null +++ b/app/javascript/images/logo_transparent_black.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/proof_providers/keybase.png b/app/javascript/images/proof_providers/keybase.png new file mode 100644 index 000000000..7e3ac657f Binary files /dev/null and b/app/javascript/images/proof_providers/keybase.png differ diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index 3f5d7ef46..b2c7ab76a 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -22,7 +22,7 @@ export function clearAlert() { }; }; -export function showAlert(title, message) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { return { type: ALERT_SHOW, title, @@ -34,6 +34,11 @@ export function showAlertForError(error) { if (error.response) { const { data, status, statusText } = error.response; + if (status === 404 || status === 410) { + // Skip these errors as they are reflected in the UI + return {}; + } + let message = statusText; let title = `${status}`; @@ -44,6 +49,6 @@ export function showAlertForError(error) { return showAlert(title, message); } else { console.error(error); - return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); + return showAlert(); } } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 5dfb2b465..d23ebf278 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; let cancelFetchComposeSuggestionsAccounts; @@ -50,6 +52,18 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; +export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; +export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; +export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; +export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; +export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; + +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, + uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, +}); + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -126,6 +140,7 @@ export function submitCompose(routerHistory) { sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), visibility: getState().getIn(['compose', 'privacy']), + poll: getState().getIn(['compose', 'poll'], null), local_only: !getState().getIn(['compose', 'federation']), }, { headers: { @@ -145,7 +160,9 @@ export function submitCompose(routerHistory) { // into the columns const insertIfOnline = timelineId => { - if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) { + const timeline = getState().getIn(['timelines', timelineId]); + + if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) { dispatch(updateTimeline(timelineId, { ...response.data })); } }; @@ -186,20 +203,38 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const total = Array.from(files).reduce((a, v) => a + v.size, 0); + const progress = new Array(files.length).fill(0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); + return; + } + + if (getState().getIn(['compose', 'poll'])) { + dispatch(showAlert(undefined, messages.uploadErrorPoll)); return; } dispatch(uploadComposeRequest()); - resizeImage(files[0]).then(file => { - const data = new FormData(); - data.append('file', file); + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; - return api(getState).post('/api/v1/media', data, { - onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); - }).catch(error => dispatch(uploadComposeFail(error))); + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; }; }; @@ -475,4 +510,46 @@ export function changeComposing(value) { type: COMPOSE_COMPOSING_CHANGE, value, }; -} +}; + +export function addPoll() { + return { + type: COMPOSE_POLL_ADD, + }; +}; + +export function removePoll() { + return { + type: COMPOSE_POLL_REMOVE, + }; +}; + +export function addPollOption(title) { + return { + type: COMPOSE_POLL_OPTION_ADD, + title, + }; +}; + +export function changePollOption(index, title) { + return { + type: COMPOSE_POLL_OPTION_CHANGE, + index, + title, + }; +}; + +export function removePollOption(index) { + return { + type: COMPOSE_POLL_OPTION_REMOVE, + index, + }; +}; + +export function changePollSettings(expiresIn, isMultiple) { + return { + type: COMPOSE_POLL_SETTINGS_CHANGE, + expiresIn, + isMultiple, + }; +}; diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js index 3c2ea9680..c6e062ef7 100644 --- a/app/javascript/mastodon/actions/conversations.js +++ b/app/javascript/mastodon/actions/conversations.js @@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); } + const isLoadingRecent = !!params.since_id; + api(getState).get('/api/v1/conversations', { params }) .then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); - dispatch(expandConversationsSuccess(response.data, next ? next.uri : null)); + dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent)); }) .catch(err => dispatch(expandConversationsFail(err))); }; @@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({ type: CONVERSATIONS_FETCH_REQUEST, }); -export const expandConversationsSuccess = (conversations, next) => ({ +export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({ type: CONVERSATIONS_FETCH_SUCCESS, conversations, next, + isLoadingRecent, }); export const expandConversationsFail = error => ({ diff --git a/app/javascript/mastodon/actions/identity_proofs.js b/app/javascript/mastodon/actions/identity_proofs.js new file mode 100644 index 000000000..449debf61 --- /dev/null +++ b/app/javascript/mastodon/actions/identity_proofs.js @@ -0,0 +1,30 @@ +import api from '../api'; + +export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; + +export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { + dispatch(fetchAccountIdentityProofsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) + .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); +}; + +export const fetchAccountIdentityProofsRequest = id => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + id, +}); + +export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + accountId, + identity_proofs, +}); + +export const fetchAccountIdentityProofsFail = (accountId, err) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, + accountId, + err, +}); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 931711f4b..f4372fb31 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,11 +1,10 @@ -// import { autoPlayGif } from '../../initial_state'; -// import { putAccounts, putStatuses } from '../../storage/modifier'; -import { normalizeAccount, normalizeStatus } from './normalizer'; +import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; -export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; -export const STATUS_IMPORT = 'STATUS_IMPORT'; +export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -29,6 +28,10 @@ export function importStatuses(statuses) { return { type: STATUSES_IMPORT, statuses }; } +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) { } accounts.forEach(processAccount); - //putAccounts(normalAccounts, !autoPlayGif); return importAccounts(normalAccounts); } @@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) { return (dispatch, getState) => { const accounts = []; const normalStatuses = []; + const polls = []; function processStatus(status) { pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); @@ -66,12 +69,22 @@ export function importFetchedStatuses(statuses) { if (status.reblog && status.reblog.id) { processStatus(status.reblog); } + + if (status.poll && status.poll.id) { + pushUnique(polls, normalizePoll(status.poll)); + } } statuses.forEach(processStatus); - //putStatuses(normalStatuses); + dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); }; } + +export function importFetchedPoll(poll) { + return dispatch => { + dispatch(importPolls([normalizePoll(poll)])); + }; +} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 34a4150fa..5badb0c49 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.reblog = status.reblog.id; } + if (status.poll && status.poll.id) { + normalStatus.poll = status.poll.id; + } + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer if (normalOldStatus) { @@ -63,3 +67,16 @@ export function normalizeStatus(status, normalOldStatus) { return normalStatus; } + +export function normalizePoll(poll) { + const normalPoll = { ...poll }; + + const emojiMap = makeEmojiMap(normalPoll); + + normalPoll.options = poll.options.map(option => ({ + ...option, + title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + })); + + return normalPoll; +} diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 4c145febc..b0861fc6b 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -7,6 +7,7 @@ import { importFetchedStatus, importFetchedStatuses, } from './importer'; +import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; @@ -92,7 +93,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']); + const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); return allTypes.filterNot(item => item === filter).toJS(); }; @@ -187,5 +188,6 @@ export function setFilter (filterType) { value: filterType, }); dispatch(expandNotifications()); + dispatch(saveSettings()); }; }; diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js new file mode 100644 index 000000000..8e8b82df5 --- /dev/null +++ b/app/javascript/mastodon/actions/polls.js @@ -0,0 +1,60 @@ +import api from '../api'; +import { importFetchedPoll } from './importer'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch, getState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch, getState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index b670d25c3..7c06670eb 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -37,6 +37,7 @@ export function submitSearch() { params: { q: value, resolve: true, + limit: 5, }, }).then(response => { if (response.data.accounts) { diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index e7c89b4ba..1794538e2 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -140,7 +140,11 @@ export function redraft(status) { export function deleteStatus(id, router, withRedraft = false) { return (dispatch, getState) => { - const status = getState().getIn(['statuses', id]); + let status = getState().getIn(['statuses', id]); + + if (status.get('poll')) { + status = status.set('poll', getState().getIn(['polls', status.get('poll')])); + } dispatch(deleteStatusRequest(id)); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index cd319709d..c678e9393 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -3,6 +3,7 @@ import { updateTimeline, deleteFromTimelines, expandHomeTimeline, + connectTimeline, disconnectTimeline, } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; @@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, return connectStream (path, pollingRefresh, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); + return { + onConnect() { + dispatch(connectTimeline(timelineId)); + }, + onDisconnect() { dispatch(disconnectTimeline(timelineId)); }, diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 6e7bd027c..d92385e95 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export function updateTimeline(timeline, status, accept) { @@ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) { }; }; +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline, + }; +}; + export function disconnectTimeline(timeline) { return { type: TIMELINE_DISCONNECT, diff --git a/app/javascript/mastodon/api.js b/app/javascript/mastodon/api.js index 4be3eadb0..98d59de43 100644 --- a/app/javascript/mastodon/api.js +++ b/app/javascript/mastodon/api.js @@ -13,10 +13,14 @@ export const getLinks = response => { }; let csrfHeader = {}; + function setCSRFHeader() { - const csrfToken = document.querySelector('meta[name=csrf-token]').content; - csrfHeader['X-CSRF-Token'] = csrfToken; + const csrfToken = document.querySelector('meta[name=csrf-token]'); + if (csrfToken) { + csrfHeader['X-CSRF-Token'] = csrfToken.content; + } } + ready(setCSRFHeader); export default getState => axios.create({ diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js index 8e5bb0e0b..5dfa1464c 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/mastodon/components/attachment_list.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; @@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { return (
  • - {filename(displayUrl)} + {filename(displayUrl)}
  • ); })} @@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent { return (
    - +
      diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index 8a60c4192..f41045787 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -1,6 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; export default class ColumnBackButton extends React.PureComponent { @@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent { render () { return ( ); diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js index 964c100be..cc8bfb151 100644 --- a/app/javascript/mastodon/components/column_back_button_slim.js +++ b/app/javascript/mastodon/components/column_back_button_slim.js @@ -1,6 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import ColumnBackButton from './column_back_button'; +import Icon from 'mastodon/components/icon'; export default class ColumnBackButtonSlim extends ColumnBackButton { @@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton { return (
      - +
      diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index f68e4155e..f33c689e7 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent { } if (multiColumn && pinned) { - pinButton = ; + pinButton = ; moveButtons = (
      - - + +
      ); } else if (multiColumn) { - pinButton = ; + pinButton = ; } if (!pinned && (multiColumn || showBackButton)) { backButton = ( ); @@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent { } if (children || multiColumn) { - collapseButton = ; + collapseButton = ; } const hasTitle = icon && title; @@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {

      {hasTitle && ( )} diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index 32809778a..6b9dd6f81 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -22,7 +22,7 @@ export default class DisplayName extends React.PureComponent { suffix = `+${others.size - 2}`; } } else { - if (others) { + if (others && others.size > 0) { account = others.first(); } else { account = this.props.account; diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js new file mode 100644 index 000000000..d1ca5bf75 --- /dev/null +++ b/app/javascript/mastodon/components/error_boundary.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import illustration from '../../images/elephant_ui_disappointed.svg'; + +export default class ErrorBoundary extends React.PureComponent { + + static propTypes = { + children: PropTypes.node, + }; + + state = { + hasError: false, + stackTrace: undefined, + componentStack: undefined, + } + + componentDidCatch(error, info) { + this.setState({ + hasError: true, + stackTrace: error.stack, + componentStack: info && info.componentStack, + }); + } + + render() { + const { hasError } = this.state; + + if (!hasError) { + return this.props.children; + } + + return ( +
      + +
      + ); + } + +} diff --git a/app/javascript/mastodon/components/icon.js b/app/javascript/mastodon/components/icon.js new file mode 100644 index 000000000..d8a17722f --- /dev/null +++ b/app/javascript/mastodon/components/icon.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class Icon extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + className: PropTypes.string, + fixedWidth: PropTypes.bool, + }; + + render () { + const { id, className, fixedWidth, ...other } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index b96e48fd0..9d8a8d06b 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; export default class IconButton extends React.PureComponent { @@ -85,8 +86,9 @@ export default class IconButton extends React.PureComponent { onClick={this.handleClick} style={style} tabIndex={tabIndex} + disabled={disabled} > -