mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-29 17:53:16 +00:00
commit
754801e4fe
229 changed files with 782673 additions and 5031 deletions
|
@ -25,6 +25,7 @@ SESSION_DRIVER=redis
|
|||
SESSION_LIFETIME=120
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
REDIS_SCHEME=tcp
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
|
|
@ -25,6 +25,7 @@ SESSION_DRIVER=redis
|
|||
SESSION_LIFETIME=120
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
REDIS_SCHEME=tcp
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
|
127
CHANGELOG.md
Normal file
127
CHANGELOG.md
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.9.4...dev)
|
||||
|
||||
|
||||
## [v0.9.X (TBD)](https://github.com/pixelfed/pixelfed/compare/v0.9.4...dev)
|
||||
|
||||
### Added
|
||||
- Add StatusService [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [425ec91](https://github.com/pixelfed/pixelfed/commit/425ec91)
|
||||
- Add PublicTimelineService [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [734e892](https://github.com/pixelfed/pixelfed/commit/734e892)
|
||||
- Add RelationshipSettings trait [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [bf8340f](https://github.com/pixelfed/pixelfed/commit/bf8340f)
|
||||
- Add Remote Follows [#1388](https://github.com/pixelfed/pixelfed/pull/1388)
|
||||
- Add Relationship Settings [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [b10e03d](https://github.com/pixelfed/pixelfed/commit/b10e03d)
|
||||
- Add Configuration Editor to Admin Dashboard [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [323dca1](https://github.com/pixelfed/pixelfed/commit/323dca1)
|
||||
- Add Migration, adding profile_id to users table [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [bdfe633](https://github.com/pixelfed/pixelfed/commit/bdfe633)
|
||||
- Add Media configuration [#1414](https://github.com/pixelfed/pixelfed/pull/1414)
|
||||
- Add Content Warnings to comments [#1430](https://github.com/pixelfed/pixelfed/pull/1430), [42d81fc](https://github.com/pixelfed/pixelfed/commit/42d81fc) [8d4b3bd](https://github.com/pixelfed/pixelfed/commit/8d4b3bd) [73e162e4](https://github.com/pixelfed/pixelfed/commit/3e162e4)
|
||||
- Add new rate limits [#1436](https://github.com/pixelfed/pixelfed/pull/1436) [1f1df2d](https://github.com/pixelfed/pixelfed/commit/1f1df2d)
|
||||
- Add RegenerateThumbnails command to force thumbnail regeneration [#1437](https://github.com/pixelfed/pixelfed/pull/1437) [a3be4cd](https://github.com/pixelfed/pixelfed/commit/a3be4cd)
|
||||
- Add Pages Editor to Admin Dashboard [#1438](https://github.com/pixelfed/pixelfed/pull/1438) [ef3e30d](https://github.com/pixelfed/pixelfed/commit/ef3e30d) [718375a](https://github.com/pixelfed/pixelfed/commit/718375a) [79524a0](https://github.com/pixelfed/pixelfed/commit/79524a0) [13ceef0](https://github.com/pixelfed/pixelfed/commit/13ceef0) [2fbcd6d](https://github.com/pixelfed/pixelfed/commit/2fbcd6d) [bb207a4](https://github.com/pixelfed/pixelfed/commit/bb207a4) [ef07e31](https://github.com/pixelfed/pixelfed/commit/ef07e31) [aca5114](https://github.com/pixelfed/pixelfed/commit/aca5114) [59fcfc2](https://github.com/pixelfed/pixelfed/commit/59fcfc2) [e3cfd81](https://github.com/pixelfed/pixelfed/commit/e3cfd81) [7ade78b](https://github.com/pixelfed/pixelfed/commit/7ade78b) [4539afa](https://github.com/pixelfed/pixelfed/commit/4539afa) [1dbfcae](https://github.com/pixelfed/pixelfed/commit/1dbfcae)
|
||||
|
||||
### Changed
|
||||
- Update SearchController, fix AP verb typo [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [dc8acf9](https://github.com/pixelfed/pixelfed/commit/dc8acf9)
|
||||
- Update StatusTransformer, increase media cache ttl to 14 days [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [f35718b](https://github.com/pixelfed/pixelfed/commit/f35718b)
|
||||
- Update webpack config, extract vendor librarys [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [b42db89](https://github.com/pixelfed/pixelfed/commit/b42db89)
|
||||
- Update admin statuses view, make table header light [#1387](https://github.com/pixelfed/pixelfed/pull/1387), [44afcc7](https://github.com/pixelfed/pixelfed/commit/44afcc7)
|
||||
- Update settings, move disable/delete to Security Settings [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [ca0d638](https://github.com/pixelfed/pixelfed/commit/ca0d638)
|
||||
- Update Installer command [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [506dd8b](https://github.com/pixelfed/pixelfed/commit/506dd8b)
|
||||
- Update UserObserver [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [4ee3d10](https://github.com/pixelfed/pixelfed/commit/4ee3d10)
|
||||
- Update AuthLogin listener [#1388](https://github.com/pixelfed/pixelfed/pull/1388), [c27c751](https://github.com/pixelfed/pixelfed/commit/c27c751) [1e8b092](https://github.com/pixelfed/pixelfed/commit/1e8b092)
|
||||
- Update Image Optimization to not store EXIF by default [#1414](https://github.com/pixelfed/pixelfed/pull/1414)
|
||||
- Update Settings, hide OAuth/Developer pages when not enabled [#1413](https://github.com/pixelfed/pixelfed/pull/1413)
|
||||
- Update Presenter Components, move alt tag and filters to ```<img>``` element [#1415](https://github.com/pixelfed/pixelfed/pull/1415)
|
||||
- Update Api Controllers, add missing caption limit to ```composePost()``` and missing ```is_nsfw``` attribute to comment queries [#1429](https://github.com/pixelfed/pixelfed/pull/1429), [1cff278](https://github.com/pixelfed/pixelfed/commit/1cff278)
|
||||
- Update instances admin view, add scan button to find new instances [#1436](https://github.com/pixelfed/pixelfed/pull/1436) [a94a3ee](https://github.com/pixelfed/pixelfed/commit/a94a3ee)
|
||||
- Update registration page, add links to terms and privacy pages [#1488](https://github.com/pixelfed/pixelfed/pull/1488)
|
||||
|
||||
### Removed
|
||||
- Remove Classic Compose UI [#1434](https://github.com/pixelfed/pixelfed/pull/1434), [72bffd1](https://github.com/pixelfed/pixelfed/commit/72bffd1) [a2640af](https://github.com/pixelfed/pixelfed/commit/a2640af)
|
||||
-
|
||||
|
||||
|
||||
## [v0.9.4 (2019-06-03)](https://github.com/pixelfed/pixelfed/compare/v0.9.0...v0.9.4)
|
||||
|
||||
PSA: Due to the removal of Google Recaptcha, a one-time manual intervention is required. Please try the following after installing with composer:
|
||||
|
||||
```
|
||||
rm -rf bootstrap/cache/*
|
||||
composer dump-autoload
|
||||
php artisan config:cache
|
||||
```
|
||||
|
||||
### Added
|
||||
- Notification service
|
||||
- Notification card on timeline
|
||||
- Double-tap to like posts (no animation yet)
|
||||
- Moderator Mode for timelines
|
||||
- Emoji reaction bar
|
||||
- Like and reply to comments
|
||||
- Hello Loops! Short videos will now loop and be discoverable from the Discover page.
|
||||
- Labs: Optional profile recommendations
|
||||
- Labs: Show full caption instead of "read more" button
|
||||
- Labs: Simple "distraction-free" timeline -- no buttons, just images and captions
|
||||
|
||||
### Changed
|
||||
- Refactored notification view into a Vue component
|
||||
- Preparations for Circles, DMs, and other upcoming functionality
|
||||
- Default limit of 7500 follows
|
||||
- Default limit of 20 follows per hour
|
||||
- Default limit of 5 mentions per comment/caption
|
||||
- Default limit of 30 hashtags per comment/caption
|
||||
- Default limit of 2 links per comment/caption
|
||||
- Thumbnail info overlays on profiles should now scale down to small screens (#1234)
|
||||
- Moment UI containers are now properly sized (#1236)
|
||||
- Album posts now have contrast for next/prev arrows (#1238)
|
||||
- Filter previews now fit the image instead of stretching it (#1239)
|
||||
|
||||
### Removed
|
||||
- Google Recaptcha is no longer supported (#1231)
|
||||
- Lightbox has been deprecated in favor of double-tap-to-like; it will return as a dedicated button in the future (#1277)
|
||||
|
||||
|
||||
## [v0.9.0 (2019-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.8.6...v0.9.0)
|
||||
|
||||
### Added
|
||||
- Allow users to delete existing profile photos.
|
||||
- Preliminary support for managing developer tokens, as well as authorizing apps
|
||||
- Unmute and unblock users more easily. Profiles now reflect muting/blocking status.
|
||||
- Lazy-loading images with `loading="lazy"`, as supported in Blink
|
||||
- Added Network Timeline which includes non-local posts
|
||||
- Add broadcast events for real-time updates
|
||||
- Compose view now shows upload progress bar
|
||||
- You can now audit logged-in devices
|
||||
- Added WIP installer
|
||||
- Moment UI! This alternative profile view is less square and more full-width pictures.
|
||||
|
||||
### Changed
|
||||
- Allow admins to view reported private posts
|
||||
- Show sensitivity and privacy/audience in status views
|
||||
- Cleanup of legacy code
|
||||
- `commentsDisabled` has been replaced with preliminary support for Litepub Capability Enforcement (LiCE)
|
||||
- `rel="me"` now added to profile websites
|
||||
- Posts from locked accounts now default to followers-only
|
||||
|
||||
### Removed
|
||||
- Removed identicons due to SVG compatibility issues with federation. New users will instead be assigned a default avatar.
|
||||
|
||||
|
||||
## [v0.8.6 (2019-04-06)](https://github.com/pixelfed/pixelfed/compare/v0.8.5...v0.8.6)
|
||||
|
||||
### Added
|
||||
- Add COSTAR - Confirm Object Sentiment Transform and Reduce
|
||||
|
||||
COSTAR is a filtering system that allows admins to define environment variables that will dynamically apply certain policies to posts of a defined scope, similar to Pleroma's MRF system.
|
||||
|
||||
Scopes:
|
||||
- Domain: apply to posts from a specific website
|
||||
- Actor: apply to posts from a specific profile/user
|
||||
- Keyword: apply to posts containing a specific string
|
||||
|
||||
Policies:
|
||||
- Block: Default blocks the defined scope
|
||||
- CW: Automatically rewrites the scope to apply a warning
|
||||
- Unlist: Removes the scope from public timelines
|
||||
|
||||
|
||||
|
17
CONTRIBUTING.md
Normal file
17
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Contributing
|
||||
|
||||
## Bug Reports
|
||||
To encourage active collaboration, Pixelfed strongly encourages pull requests, not just bug reports. "Bug reports" may also be sent in the form of a pull request containing a failing test.
|
||||
|
||||
However, if you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix.
|
||||
|
||||
Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help yourself and others start on the path of fixing the problem.
|
||||
|
||||
## Core Development Discussion
|
||||
Informal discussion regarding bugs, new features, and implementation of existing features takes place in the ```#pixelfed-dev``` channel on the Freenode IRC network.
|
||||
|
||||
## Compiled Assets
|
||||
If you are submitting a change that will affect a compiled file, such as most of the files in ```resources/assets/sass``` or ```resources/assets/js``` of the pixelfed/pixelfed repository, do not commit the compiled files. Due to their large size, they cannot realistically be reviewed by a maintainer. This could be exploited as a way to inject malicious code into Pixelfed. In order to defensively prevent this, all compiled files will be generated and committed by Pixelfed maintainers.
|
||||
|
||||
## Security Vulnerabilities
|
||||
If you discover a security vulnerability within Pixelfed, please send an email to Daniel Supernault at hello@pixelfed.org. All security vulnerabilities will be promptly addressed.
|
|
@ -17,7 +17,7 @@ A free and ethical photo sharing platform, powered by ActivityPub federation.
|
|||
|
||||
## Official Documentation
|
||||
|
||||
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://pixelfed.github.io/docs/master/).
|
||||
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://docs.pixelfed.org/).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
|
||||
|
@ -16,8 +17,34 @@ class Collection extends Model
|
|||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
public $fillable = ['profile_id', 'published_at'];
|
||||
|
||||
public $dates = ['published_at'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(CollectionItem::class);
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Status::class,
|
||||
CollectionItem::class,
|
||||
'collection_id',
|
||||
'id',
|
||||
'id',
|
||||
'object_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return url("/c/{$this->id}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,13 @@ class CollectionItem extends Model
|
|||
{
|
||||
use HasSnowflakePrimary;
|
||||
|
||||
public $fillable = [
|
||||
'collection_id',
|
||||
'object_type',
|
||||
'object_id',
|
||||
'order'
|
||||
];
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
|
|
50
app/Console/Commands/BannedEmailCheck.php
Normal file
50
app/Console/Commands/BannedEmailCheck.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Services\EmailService;
|
||||
|
||||
class BannedEmailCheck extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'email:bancheck';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Checks user emails for banned domains';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$users = User::whereNull('status')->get()->filter(function($u) {
|
||||
return EmailService::isBanned($u->email) == true;
|
||||
});
|
||||
|
||||
foreach($users as $user) {
|
||||
$this->info('Found banned domain: ' . $user->email . PHP_EOL);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use DB;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Media;
|
||||
use Illuminate\Console\Command;
|
||||
|
@ -39,9 +40,20 @@ class CatchUnoptimizedMedia extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$medias = Media::whereNotNull('status_id')->whereNull('processed_at')->take(250)->get();
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
DB::transaction(function() {
|
||||
Media::whereNull('processed_at')
|
||||
->whereNull('remote_url')
|
||||
->whereNotNull('status_id')
|
||||
->whereNotNull('media_path')
|
||||
->whereIn('mime', [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
])
|
||||
->chunk(50, function($medias) {
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
109
app/Console/Commands/FixHashtags.php
Normal file
109
app/Console/Commands/FixHashtags.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use DB;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Status,
|
||||
StatusHashtag
|
||||
};
|
||||
|
||||
class FixHashtags extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fix:hashtags';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fix Hashtags';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' ');
|
||||
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||
$this->info(' ');
|
||||
$this->info('Running Fix Hashtags command');
|
||||
$this->info(' ');
|
||||
|
||||
$missingCount = StatusHashtag::doesntHave('profile')->doesntHave('status')->count();
|
||||
if($missingCount > 0) {
|
||||
$this->info("Found {$missingCount} orphaned StatusHashtag records to delete ...");
|
||||
$this->info(' ');
|
||||
$bar = $this->output->createProgressBar($missingCount);
|
||||
$bar->start();
|
||||
foreach(StatusHashtag::doesntHave('profile')->doesntHave('status')->get() as $tag) {
|
||||
$tag->delete();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->info(' ');
|
||||
} else {
|
||||
$this->info(' ');
|
||||
$this->info('Found no orphaned hashtags to delete!');
|
||||
}
|
||||
|
||||
|
||||
$this->info(' ');
|
||||
|
||||
$count = StatusHashtag::whereNull('status_visibility')->count();
|
||||
if($count > 0) {
|
||||
$this->info("Found {$count} hashtags to fix ...");
|
||||
$this->info(' ');
|
||||
} else {
|
||||
$this->info('Found no hashtags to fix!');
|
||||
$this->info(' ');
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
StatusHashtag::with('status')
|
||||
->whereNull('status_visibility')
|
||||
->chunk(50, function($tags) use($bar) {
|
||||
foreach($tags as $tag) {
|
||||
if(!$tag->status || !$tag->status->scope) {
|
||||
continue;
|
||||
}
|
||||
$tag->status_visibility = $tag->status->scope;
|
||||
$tag->save();
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->info(' ');
|
||||
$this->info(' ');
|
||||
}
|
||||
}
|
158
app/Console/Commands/ImportCities.php
Normal file
158
app/Console/Commands/ImportCities.php
Normal file
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Place;
|
||||
use DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImportCities extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'import:cities {chunk=1000}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import Cities to database';
|
||||
|
||||
/**
|
||||
* Checksum of city dataset.
|
||||
*
|
||||
*/
|
||||
const CHECKSUM = 'e203c0247538788b2a91166c7cf4b95f58291d998f514e9306d315aa72b09e48bfd3ddf310bf737afc4eefadca9083b8ff796c67796c6bd8e882a3d268bd16af';
|
||||
|
||||
/**
|
||||
* List of shortened countries.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $countries = [
|
||||
'AE' => 'UAE',
|
||||
'BA' => 'Bosnia-Herzegovina',
|
||||
'BO' => 'Bolivia',
|
||||
'CD' => 'Democratic Republic of Congo',
|
||||
'CG' => 'Republic of Congo',
|
||||
'FM' => 'Micronesia',
|
||||
'GB' => 'United Kingdom',
|
||||
'IR' => 'Iran',
|
||||
'KP' => 'DRPK',
|
||||
'KR' => 'South Korea',
|
||||
'LA' => 'Laos',
|
||||
'MD' => 'Moldova',
|
||||
'PS' => 'Palestine',
|
||||
'RU' => 'Russia',
|
||||
'SH' => 'Saint Helena',
|
||||
'SY' => 'Syria',
|
||||
'TW' => 'Taiwan',
|
||||
'TZ' => 'Tanzania',
|
||||
'US' => 'USA',
|
||||
'VE' => 'Venezuela',
|
||||
'XK' => 'Kosovo'
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
ini_set('memory_limit', '256M');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = storage_path('app/cities.json');
|
||||
|
||||
if(hash_file('sha512', $path) !== self::CHECKSUM) {
|
||||
$this->error('Invalid or corrupt storage/app/cities.json data.');
|
||||
$this->line('');
|
||||
$this->info('Run the following command to fix:');
|
||||
$this->info('git checkout storage/app/cities.json');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_file($path)) {
|
||||
$this->error('Missing storage/app/cities.json file!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Place::count() > 0) {
|
||||
DB::table('places')->truncate();
|
||||
}
|
||||
|
||||
$this->info('Importing city data into database ...');
|
||||
|
||||
$cities = json_decode(file_get_contents($path));
|
||||
$cityCount = count($cities);
|
||||
|
||||
$this->line('');
|
||||
$this->info("Found {$cityCount} cities to insert ...");
|
||||
$this->line('');
|
||||
|
||||
$bar = $this->output->createProgressBar($cityCount);
|
||||
$bar->start();
|
||||
|
||||
$buffer = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($cities as $city) {
|
||||
$buffer[] = [
|
||||
"name" => $city->name,
|
||||
"slug" => Str::slug($city->name),
|
||||
"country" => $this->codeToCountry($city->country),
|
||||
"lat" => $city->lat,
|
||||
"long" => $city->lng
|
||||
];
|
||||
|
||||
$count++;
|
||||
|
||||
if ($count % $this->argument('chunk') == 0) {
|
||||
$this->insertBuffer($buffer);
|
||||
$bar->advance(count($buffer));
|
||||
$buffer = [];
|
||||
}
|
||||
}
|
||||
$this->insertBuffer($buffer);
|
||||
|
||||
$bar->advance(count($buffer));
|
||||
|
||||
$bar->finish();
|
||||
|
||||
$this->line('');
|
||||
$this->line('');
|
||||
$this->info('Successfully imported ' . $cityCount . ' entries!');
|
||||
$this->line('');
|
||||
return;
|
||||
}
|
||||
|
||||
private function insertBuffer($buffer)
|
||||
{
|
||||
DB::table('places')->insert($buffer);
|
||||
}
|
||||
|
||||
private function codeToCountry($code)
|
||||
{
|
||||
$countries = $this->countries;
|
||||
if(isset($countries[$code])) {
|
||||
return $countries[$code];
|
||||
}
|
||||
|
||||
$country = (new \League\ISO3166\ISO3166)->alpha2($code);
|
||||
$this->countries[$code] = $country['name'];
|
||||
return $this->countries[$code];
|
||||
}
|
||||
}
|
|
@ -39,12 +39,16 @@ class MediaGarbageCollector extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$gc = Media::whereNull('status_id')
|
||||
->where('created_at', '<', Carbon::now()->subHours(6)->toDateTimeString())
|
||||
$limit = 20000;
|
||||
|
||||
$gc = Media::doesntHave('status')
|
||||
->where('created_at', '<', Carbon::now()->subHours(1)->toDateTimeString())
|
||||
->orderBy('created_at','asc')
|
||||
->take(500)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$bar = $this->output->createProgressBar($gc->count());
|
||||
$bar->start();
|
||||
foreach($gc as $media) {
|
||||
$path = storage_path("app/$media->media_path");
|
||||
$thumb = storage_path("app/$media->thumbnail_path");
|
||||
|
@ -55,6 +59,8 @@ class MediaGarbageCollector extends Command
|
|||
unlink($thumb);
|
||||
}
|
||||
$media->forceDelete();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
}
|
||||
}
|
||||
|
|
63
app/Console/Commands/StatusDedupe.php
Normal file
63
app/Console/Commands/StatusDedupe.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Status;
|
||||
use DB;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
|
||||
class StatusDedupe extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'status:dedup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Removes duplicate statuses from before unique uri migration';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
DB::table('statuses')
|
||||
->selectRaw('id, uri, count(uri) as occurences')
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('uri')
|
||||
->groupBy('uri')
|
||||
->orderBy('created_at')
|
||||
->having('occurences', '>', 1)
|
||||
->chunk(50, function($statuses) {
|
||||
foreach($statuses as $status) {
|
||||
$this->info("Found duplicate: $status->uri");
|
||||
Status::whereUri($status->uri)
|
||||
->where('id', '!=', $status->id)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
$this->info("Deleting Duplicate ID: $status->id");
|
||||
StatusDelete::dispatch($status);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,8 +20,8 @@ class Hashtag extends Model
|
|||
);
|
||||
}
|
||||
|
||||
public function url()
|
||||
public function url($suffix = '')
|
||||
{
|
||||
return config('routes.hashtag.base').$this->slug;
|
||||
return config('routes.hashtag.base').$this->slug.$suffix;
|
||||
}
|
||||
}
|
||||
|
|
19
app/HashtagFollow.php
Normal file
19
app/HashtagFollow.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HashtagFollow extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'profile_id',
|
||||
'hashtag_id'
|
||||
];
|
||||
|
||||
public function hashtag()
|
||||
{
|
||||
return $this->belongsTo(Hashtag::class);
|
||||
}
|
||||
}
|
|
@ -2,476 +2,444 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\EmailVerification;
|
||||
use App\Follower;
|
||||
use App\FollowRequest;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
use App\Mail\ConfirmEmail;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Mail;
|
||||
use Redis;
|
||||
use Carbon\Carbon;
|
||||
use App\Mail\ConfirmEmail;
|
||||
use Illuminate\Http\Request;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
use App\{
|
||||
EmailVerification,
|
||||
Follower,
|
||||
FollowRequest,
|
||||
Notification,
|
||||
Profile,
|
||||
User,
|
||||
UserFilter
|
||||
};
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
protected $filters = [
|
||||
'user.mute',
|
||||
'user.block',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function notifications(Request $request)
|
||||
{
|
||||
return view('account.activity');
|
||||
}
|
||||
|
||||
public function followingActivity(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
'a' => 'nullable|alpha_dash',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$action = $request->input('a');
|
||||
$allowed = ['like', 'follow'];
|
||||
$timeago = Carbon::now()->subMonths(3);
|
||||
$following = $profile->following->pluck('id');
|
||||
$notifications = Notification::whereIn('actor_id', $following)
|
||||
->whereIn('action', $allowed)
|
||||
->where('actor_id', '<>', $profile->id)
|
||||
->where('profile_id', '<>', $profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('notifications.created_at', 'desc')
|
||||
->simplePaginate(30);
|
||||
|
||||
return view('account.following', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
return view('account.verify_email');
|
||||
}
|
||||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||
->whereDate('created_at', '>', now()->subHours(12))->count();
|
||||
|
||||
if ($recentAttempt > 0) {
|
||||
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||
}
|
||||
|
||||
EmailVerification::whereUserId(Auth::id())->delete();
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = str_random(40);
|
||||
$rtoken = str_random(128);
|
||||
|
||||
$verify = new EmailVerification();
|
||||
$verify->user_id = $user->id;
|
||||
$verify->email = $user->email;
|
||||
$verify->user_token = $utoken;
|
||||
$verify->random_token = $rtoken;
|
||||
$verify->save();
|
||||
|
||||
Mail::to($user->email)->send(new ConfirmEmail($verify));
|
||||
|
||||
return redirect()->back()->with('status', 'Verification email sent!');
|
||||
}
|
||||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where('user_token', $userToken)
|
||||
->where('random_token', $randomToken)
|
||||
->firstOrFail();
|
||||
|
||||
if (Auth::id() === $verify->user_id &&
|
||||
$verify->user_token === $userToken &&
|
||||
$verify->random_token === $randomToken) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
return redirect('/');
|
||||
} else {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchNotifications(int $id)
|
||||
{
|
||||
$key = config('cache.prefix').":user.{$id}.notifications";
|
||||
$redis = Redis::connection();
|
||||
$notifications = $redis->lrange($key, 0, 30);
|
||||
if (empty($notifications)) {
|
||||
$notifications = Notification::whereProfileId($id)
|
||||
->orderBy('id', 'desc')->take(30)->get();
|
||||
} else {
|
||||
$notifications = $this->hydrateNotifications($notifications);
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
public function hydrateNotifications($keys)
|
||||
{
|
||||
$prefix = 'notification.';
|
||||
$notifications = collect([]);
|
||||
foreach ($keys as $key) {
|
||||
$notifications->push(Cache::get("{$prefix}{$key}"));
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return view('account.messages');
|
||||
}
|
||||
|
||||
public function direct()
|
||||
{
|
||||
return view('account.direct');
|
||||
}
|
||||
|
||||
public function showMessage(Request $request, $id)
|
||||
{
|
||||
return view('account.message');
|
||||
}
|
||||
|
||||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'mute',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('mute')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([200]);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function block(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type.'.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
|
||||
Follower::whereProfileId($profile->id)->whereFollowingId($user->id)->delete();
|
||||
Notification::whereProfileId($user->id)->whereActorId($profile->id)->delete();
|
||||
break;
|
||||
|
||||
default:
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'block',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
||||
public function unblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('block')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function followRequests(Request $request)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10);
|
||||
return view('account.follow-requests', compact('followers'));
|
||||
}
|
||||
|
||||
public function followRequestHandle(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string|max:10',
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = Auth::user()->profile->id;
|
||||
$action = $request->input('action') === 'accept' ? 'accept' : 'reject';
|
||||
$id = $request->input('id');
|
||||
$followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id);
|
||||
$follower = $followRequest->follower;
|
||||
|
||||
switch ($action) {
|
||||
case 'accept':
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
break;
|
||||
|
||||
case 'reject':
|
||||
$followRequest->is_rejected = true;
|
||||
$followRequest->save();
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json(['msg' => 'success'], 200);
|
||||
}
|
||||
|
||||
public function sudoMode(Request $request)
|
||||
{
|
||||
return view('auth.sudo');
|
||||
}
|
||||
|
||||
public function sudoModeVerify(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'password' => 'required|string|max:500'
|
||||
]);
|
||||
$user = Auth::user();
|
||||
$password = $request->input('password');
|
||||
$next = $request->session()->get('redirectNext', '/');
|
||||
if(password_verify($password, $user->password) === true) {
|
||||
$request->session()->put('sudoMode', time());
|
||||
return redirect($next);
|
||||
} else {
|
||||
return redirect()
|
||||
->back()
|
||||
->withErrors(['password' => __('auth.failed')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function twoFactorCheckpoint(Request $request)
|
||||
{
|
||||
return view('auth.checkpoint');
|
||||
}
|
||||
|
||||
public function twoFactorVerify(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'code' => 'required|string|max:32'
|
||||
]);
|
||||
$user = Auth::user();
|
||||
$code = $request->input('code');
|
||||
$google2fa = new Google2FA();
|
||||
$verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
|
||||
if($verify) {
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return redirect('/');
|
||||
} else {
|
||||
|
||||
if($this->twoFactorBackupCheck($request, $code, $user)) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
if($request->session()->has('2fa.attempts')) {
|
||||
$count = (int) $request->session()->has('2fa.attempts');
|
||||
$request->session()->push('2fa.attempts', $count + 1);
|
||||
} else {
|
||||
$request->session()->push('2fa.attempts', 1);
|
||||
}
|
||||
return redirect()->back()->withErrors([
|
||||
'code' => 'Invalid code'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function twoFactorBackupCheck($request, $code, User $user)
|
||||
{
|
||||
$backupCodes = $user->{'2fa_backup_codes'};
|
||||
if($backupCodes) {
|
||||
$codes = json_decode($backupCodes, true);
|
||||
foreach ($codes as $c) {
|
||||
if(hash_equals($c, $code)) {
|
||||
// remove code
|
||||
$codes = array_flatten(array_diff($codes, [$code]));
|
||||
$user->{'2fa_backup_codes'} = json_encode($codes);
|
||||
$user->save();
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function accountRestored(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
protected $filters = [
|
||||
'user.mute',
|
||||
'user.block',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function notifications(Request $request)
|
||||
{
|
||||
return view('account.activity');
|
||||
}
|
||||
|
||||
public function followingActivity(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
'a' => 'nullable|alpha_dash',
|
||||
]);
|
||||
|
||||
$action = $request->input('a');
|
||||
$allowed = ['like', 'follow'];
|
||||
$timeago = Carbon::now()->subMonths(3);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$following = $profile->following->pluck('id');
|
||||
|
||||
$notifications = Notification::whereIn('actor_id', $following)
|
||||
->whereIn('action', $allowed)
|
||||
->where('actor_id', '<>', $profile->id)
|
||||
->where('profile_id', '<>', $profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('notifications.created_at', 'desc')
|
||||
->simplePaginate(30);
|
||||
|
||||
return view('account.following', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
return view('account.verify_email');
|
||||
}
|
||||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||
->whereDate('created_at', '>', now()->subHours(12))->count();
|
||||
|
||||
if ($recentAttempt > 0) {
|
||||
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||
}
|
||||
|
||||
EmailVerification::whereUserId(Auth::id())->delete();
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = str_random(64);
|
||||
$rtoken = str_random(128);
|
||||
|
||||
$verify = new EmailVerification();
|
||||
$verify->user_id = $user->id;
|
||||
$verify->email = $user->email;
|
||||
$verify->user_token = $utoken;
|
||||
$verify->random_token = $rtoken;
|
||||
$verify->save();
|
||||
|
||||
Mail::to($user->email)->send(new ConfirmEmail($verify));
|
||||
|
||||
return redirect()->back()->with('status', 'Verification email sent!');
|
||||
}
|
||||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where('user_token', $userToken)
|
||||
->where('created_at', '>', now()->subWeeks(2))
|
||||
->where('random_token', $randomToken)
|
||||
->firstOrFail();
|
||||
|
||||
if (Auth::id() === $verify->user_id && $verify->user_token === $userToken && $verify->random_token === $randomToken) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
return redirect('/');
|
||||
} else {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return view('account.messages');
|
||||
}
|
||||
|
||||
public function direct()
|
||||
{
|
||||
return view('account.direct');
|
||||
}
|
||||
|
||||
public function showMessage(Request $request, $id)
|
||||
{
|
||||
return view('account.message');
|
||||
}
|
||||
|
||||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'mute',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('mute')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([200]);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function block(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type.'.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
|
||||
Follower::whereProfileId($profile->id)->whereFollowingId($user->id)->delete();
|
||||
Notification::whereProfileId($user->id)->whereActorId($profile->id)->delete();
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'block',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
||||
public function unblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('block')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function followRequests(Request $request)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10);
|
||||
return view('account.follow-requests', compact('followers'));
|
||||
}
|
||||
|
||||
public function followRequestHandle(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string|max:10',
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = Auth::user()->profile->id;
|
||||
$action = $request->input('action') === 'accept' ? 'accept' : 'reject';
|
||||
$id = $request->input('id');
|
||||
$followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id);
|
||||
$follower = $followRequest->follower;
|
||||
|
||||
switch ($action) {
|
||||
case 'accept':
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
break;
|
||||
|
||||
case 'reject':
|
||||
$followRequest->is_rejected = true;
|
||||
$followRequest->save();
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json(['msg' => 'success'], 200);
|
||||
}
|
||||
|
||||
public function sudoMode(Request $request)
|
||||
{
|
||||
return view('auth.sudo');
|
||||
}
|
||||
|
||||
public function sudoModeVerify(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'password' => 'required|string|max:500'
|
||||
]);
|
||||
$user = Auth::user();
|
||||
$password = $request->input('password');
|
||||
$next = $request->session()->get('redirectNext', '/');
|
||||
if(password_verify($password, $user->password) === true) {
|
||||
$request->session()->put('sudoMode', time());
|
||||
return redirect($next);
|
||||
} else {
|
||||
return redirect()
|
||||
->back()
|
||||
->withErrors(['password' => __('auth.failed')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function twoFactorCheckpoint(Request $request)
|
||||
{
|
||||
return view('auth.checkpoint');
|
||||
}
|
||||
|
||||
public function twoFactorVerify(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'code' => 'required|string|max:32'
|
||||
]);
|
||||
$user = Auth::user();
|
||||
$code = $request->input('code');
|
||||
$google2fa = new Google2FA();
|
||||
$verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
|
||||
if($verify) {
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return redirect('/');
|
||||
} else {
|
||||
|
||||
if($this->twoFactorBackupCheck($request, $code, $user)) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
if($request->session()->has('2fa.attempts')) {
|
||||
$count = (int) $request->session()->has('2fa.attempts');
|
||||
$request->session()->push('2fa.attempts', $count + 1);
|
||||
} else {
|
||||
$request->session()->push('2fa.attempts', 1);
|
||||
}
|
||||
return redirect()->back()->withErrors([
|
||||
'code' => 'Invalid code'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function twoFactorBackupCheck($request, $code, User $user)
|
||||
{
|
||||
$backupCodes = $user->{'2fa_backup_codes'};
|
||||
if($backupCodes) {
|
||||
$codes = json_decode($backupCodes, true);
|
||||
foreach ($codes as $c) {
|
||||
if(hash_equals($c, $code)) {
|
||||
$codes = array_flatten(array_diff($codes, [$code]));
|
||||
$user->{'2fa_backup_codes'} = json_encode($codes);
|
||||
$user->save();
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function accountRestored(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,10 +105,9 @@ class AdminController extends Controller
|
|||
{
|
||||
$col = $request->query('col') ?? 'id';
|
||||
$dir = $request->query('dir') ?? 'desc';
|
||||
$stats = $this->collectUserStats($request);
|
||||
$users = User::withCount('statuses')->orderBy($col, $dir)->simplePaginate(10);
|
||||
$users = User::select('id', 'username', 'status')->withCount('statuses')->orderBy($col, $dir)->simplePaginate(10);
|
||||
|
||||
return view('admin.users.home', compact('users', 'stats'));
|
||||
return view('admin.users.home', compact('users'));
|
||||
}
|
||||
|
||||
public function editUser(Request $request, $id)
|
||||
|
@ -158,34 +157,6 @@ class AdminController extends Controller
|
|||
return view('admin.reports.show', compact('report'));
|
||||
}
|
||||
|
||||
protected function collectUserStats($request)
|
||||
{
|
||||
$total_duration = $request->query('total_duration') ?? '30';
|
||||
$new_duration = $request->query('new_duration') ?? '7';
|
||||
$stats = [];
|
||||
$stats['total'] = [
|
||||
'count' => User::where('created_at', '>', Carbon::now()->subDays($total_duration))->count(),
|
||||
'points' => 0//User::selectRaw(''.$day.'created_at) day, count(*) as count')->where('created_at','>', Carbon::now()->subDays($total_duration))->groupBy('day')->pluck('count')
|
||||
];
|
||||
$stats['new'] = [
|
||||
'count' => User::where('created_at', '>', Carbon::now()->subDays($new_duration))->count(),
|
||||
'points' => 0//User::selectRaw(''.$day.'created_at) day, count(*) as count')->where('created_at','>', Carbon::now()->subDays($new_duration))->groupBy('day')->pluck('count')
|
||||
];
|
||||
$stats['active'] = [
|
||||
'count' => Status::groupBy('profile_id')->count()
|
||||
];
|
||||
$stats['profile'] = [
|
||||
'local' => Profile::whereNull('remote_url')->count(),
|
||||
'remote' => Profile::whereNotNull('remote_url')->count()
|
||||
];
|
||||
$stats['avg'] = [
|
||||
'likes' => floor(Like::average('profile_id')),
|
||||
'posts' => floor(Status::avg('profile_id'))
|
||||
];
|
||||
return $stats;
|
||||
|
||||
}
|
||||
|
||||
public function profiles(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
|
118
app/Http/Controllers/Api/AdminApiController.php
Normal file
118
app/Http/Controllers/Api/AdminApiController.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use Auth, Cache;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
Like,
|
||||
Media,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
|
||||
use App\Services\NotificationService;
|
||||
|
||||
class AdminApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth', 'admin']);
|
||||
}
|
||||
|
||||
public function activity(Request $request)
|
||||
{
|
||||
$activity = [];
|
||||
|
||||
$limit = request()->input('limit', 20);
|
||||
|
||||
$activity['captions'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
$activity['comments'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNotNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
return response()->json($activity, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function moderateStatus(Request $request)
|
||||
{
|
||||
abort(400, 'Unpublished API');
|
||||
return;
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:status,profile',
|
||||
'id' => 'required|integer|min:1',
|
||||
'action' => 'required|string|in:cw,unlink,unlist,suspend,delete'
|
||||
]);
|
||||
|
||||
$type = $request->input('type');
|
||||
$id = $request->input('id');
|
||||
$action = $request->input('action');
|
||||
|
||||
if ($type == 'status') {
|
||||
$status = Status::findOrFail($id);
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlink':
|
||||
$status->rendered = $status->caption;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlist':
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if ($type == 'profile') {
|
||||
$profile = Profile::findOrFail($id);
|
||||
switch ($action) {
|
||||
|
||||
case 'delete':
|
||||
StatusDelete::dispatch($status);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -59,14 +59,11 @@ class BaseApiController extends Controller
|
|||
$res = $this->fractal->createData($resource)->toArray();
|
||||
} else {
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|integer|min:1',
|
||||
'page' => 'nullable|integer|min:1|max:10',
|
||||
'limit' => 'nullable|integer|min:1|max:10'
|
||||
]);
|
||||
$limit = $request->input('limit') ?? 10;
|
||||
$page = $request->input('page') ?? 1;
|
||||
if($page > 3) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$end = (int) $page * $limit;
|
||||
$start = (int) $end - $limit;
|
||||
$res = NotificationService::get($pid, $start, $end);
|
||||
|
@ -121,7 +118,7 @@ class BaseApiController extends Controller
|
|||
$since_id = $request->since_id ?? false;
|
||||
$only_media = $request->only_media ?? false;
|
||||
$user = Auth::user();
|
||||
$account = Profile::findOrFail($id);
|
||||
$account = Profile::whereNull('status')->findOrFail($id);
|
||||
$statuses = $account->statuses()->getQuery();
|
||||
if($only_media == true) {
|
||||
$statuses = $statuses
|
||||
|
@ -153,15 +150,6 @@ class BaseApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function followSuggestions(Request $request)
|
||||
{
|
||||
$followers = Auth::user()->profile->recommendFollowers();
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function avatarUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
@ -200,14 +188,9 @@ class BaseApiController extends Controller
|
|||
|
||||
public function showTempMedia(Request $request, int $profileId, $mediaId)
|
||||
{
|
||||
if (!$request->hasValidSignature()) {
|
||||
abort(401);
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
if($profile->id !== $profileId) {
|
||||
abort(403);
|
||||
}
|
||||
$media = Media::whereProfileId($profile->id)->findOrFail($mediaId);
|
||||
abort_if(!$request->hasValidSignature(), 404);
|
||||
abort_if(Auth::user()->profile_id !== $profileId, 404);
|
||||
$media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
return response()->file($path);
|
||||
}
|
||||
|
@ -244,7 +227,7 @@ class BaseApiController extends Controller
|
|||
}
|
||||
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $user->id.(string) $user->created_at);
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
|
|
|
@ -6,10 +6,12 @@ use App\Http\Controllers\Api\BaseApiController;
|
|||
use App\{
|
||||
Follower,
|
||||
Like,
|
||||
Place,
|
||||
Profile,
|
||||
UserFilter
|
||||
};
|
||||
use Auth, Cache, Redis;
|
||||
use App\Util\Site\Config;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\SuggestionService;
|
||||
|
||||
|
@ -23,34 +25,7 @@ class ApiController extends BaseApiController
|
|||
|
||||
public function siteConfiguration(Request $request)
|
||||
{
|
||||
$res = Cache::remember('api:site:configuration', now()->addMinutes(30), function() {
|
||||
return [
|
||||
'uploader' => [
|
||||
'max_photo_size' => config('pixelfed.max_photo_size'),
|
||||
'max_caption_length' => config('pixelfed.max_caption_length'),
|
||||
'album_limit' => config('pixelfed.max_album_length'),
|
||||
'image_quality' => config('pixelfed.image_quality'),
|
||||
|
||||
'optimize_image' => config('pixelfed.optimize_image'),
|
||||
'optimize_video' => config('pixelfed.optimize_video'),
|
||||
|
||||
'media_types' => config('pixelfed.media_types'),
|
||||
'enforce_account_limit' => config('pixelfed.enforce_account_limit')
|
||||
],
|
||||
|
||||
'activitypub' => [
|
||||
'enabled' => config('federation.activitypub.enabled'),
|
||||
'remote_follow' => config('federation.activitypub.remoteFollow')
|
||||
],
|
||||
|
||||
'ab' => [
|
||||
'lc' => config('exp.lc'),
|
||||
'rec' => config('exp.rec'),
|
||||
'loops' => config('exp.loops')
|
||||
],
|
||||
];
|
||||
});
|
||||
return response()->json($res);
|
||||
return response()->json(Config::get());
|
||||
}
|
||||
|
||||
public function userRecommendations(Request $request)
|
||||
|
@ -104,4 +79,24 @@ class ApiController extends BaseApiController
|
|||
return response()->json($res->all());
|
||||
}
|
||||
|
||||
public function composeLocationSearch(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string'
|
||||
]);
|
||||
|
||||
$places = Place::where('name', 'like', '%' . $request->input('q') . '%')
|
||||
->take(25)
|
||||
->get()
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => $r->id,
|
||||
'name' => $r->name,
|
||||
'country' => $r->country,
|
||||
'url' => $r->url()
|
||||
];
|
||||
});
|
||||
return $places;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Hash;
|
|||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\EmailService;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
|
@ -53,6 +54,8 @@ class RegisterController extends Controller
|
|||
protected function validator(array $data)
|
||||
{
|
||||
$this->validateUsername($data['username']);
|
||||
$this->validateEmail($data['email']);
|
||||
|
||||
$usernameRules = [
|
||||
'required',
|
||||
'min:2',
|
||||
|
@ -73,7 +76,7 @@ class RegisterController extends Controller
|
|||
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
|
||||
'username' => $usernameRules,
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6|confirmed',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
];
|
||||
|
||||
return Validator::make($data, $rules);
|
||||
|
@ -105,6 +108,14 @@ class RegisterController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function validateEmail($email)
|
||||
{
|
||||
$banned = EmailService::isBanned($email);
|
||||
if($banned) {
|
||||
return abort(403, 'Invalid email.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application registration form.
|
||||
*
|
||||
|
@ -112,14 +123,17 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
$count = User::count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
if($limit && $limit <= $count) {
|
||||
$view = 'site.closed-registration';
|
||||
if(config('pixelfed.open_registration')) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
if($limit) {
|
||||
abort_if($limit <= User::count(), 404);
|
||||
return view('auth.register');
|
||||
} else {
|
||||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
$view = config('pixelfed.open_registration') == true ? 'auth.register' : 'site.closed-registration';
|
||||
abort(404);
|
||||
}
|
||||
return view($view);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -73,7 +73,7 @@ class AvatarController extends Controller
|
|||
|
||||
public function buildPath($id)
|
||||
{
|
||||
$padded = str_pad($id, 12, 0, STR_PAD_LEFT);
|
||||
$padded = str_pad($id, 19, 0, STR_PAD_LEFT);
|
||||
$parts = str_split($padded, 3);
|
||||
foreach ($parts as $k => $part) {
|
||||
if ($k == 0) {
|
||||
|
@ -93,6 +93,21 @@ class AvatarController extends Controller
|
|||
$prefix = storage_path('app/'.$avatarpath);
|
||||
$this->checkDir($prefix);
|
||||
}
|
||||
if ($k == 4) {
|
||||
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3].'/'.$parts[4];
|
||||
$prefix = storage_path('app/'.$avatarpath);
|
||||
$this->checkDir($prefix);
|
||||
}
|
||||
if ($k == 5) {
|
||||
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3].'/'.$parts[4].'/'.$parts[5];
|
||||
$prefix = storage_path('app/'.$avatarpath);
|
||||
$this->checkDir($prefix);
|
||||
}
|
||||
if ($k == 6) {
|
||||
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3].'/'.$parts[4].'/'.$parts[5].'/'.$parts[6];
|
||||
$prefix = storage_path('app/'.$avatarpath);
|
||||
$this->checkDir($prefix);
|
||||
}
|
||||
}
|
||||
|
||||
return $avatarpath;
|
||||
|
|
|
@ -3,8 +3,206 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\{
|
||||
Collection,
|
||||
CollectionItem,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
class CollectionController extends Controller
|
||||
{
|
||||
//
|
||||
public function create(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$collection = Collection::firstOrCreate([
|
||||
'profile_id' => $profile->id,
|
||||
'published_at' => null
|
||||
]);
|
||||
return view('collection.create', compact('collection'));
|
||||
}
|
||||
|
||||
public function show(Request $request, int $collection)
|
||||
{
|
||||
$collection = Collection::with('profile')->whereNotNull('published_at')->findOrFail($collection);
|
||||
if($collection->profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
if($collection->visibility !== 'public') {
|
||||
abort_if(!Auth::check() || Auth::user()->profile_id != $collection->profile_id, 404);
|
||||
}
|
||||
return view('collection.show', compact('collection'));
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function store(Request $request, int $id)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
$this->validate($request, [
|
||||
'title' => 'nullable',
|
||||
'description' => 'nullable',
|
||||
'visibility' => 'required|alpha|in:public,private'
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
|
||||
$collection->title = e($request->input('title'));
|
||||
$collection->description = e($request->input('description'));
|
||||
$collection->visibility = e($request->input('visibility'));
|
||||
$collection->save();
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function publish(Request $request, int $id)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
$this->validate($request, [
|
||||
'title' => 'nullable',
|
||||
'description' => 'nullable',
|
||||
'visibility' => 'required|alpha|in:public,private'
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
|
||||
if($collection->items()->count() == 0) {
|
||||
abort(404);
|
||||
}
|
||||
$collection->title = e($request->input('title'));
|
||||
$collection->description = e($request->input('description'));
|
||||
$collection->visibility = e($request->input('visibility'));
|
||||
$collection->published_at = now();
|
||||
$collection->save();
|
||||
|
||||
return $collection->url();
|
||||
}
|
||||
|
||||
public function delete(Request $request, int $id)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
$user = Auth::user();
|
||||
|
||||
$collection = Collection::whereProfileId($user->profile_id)->findOrFail($id);
|
||||
$collection->items()->delete();
|
||||
$collection->delete();
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
public function storeId(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'collection_id' => 'required|int|min:1|exists:collections,id',
|
||||
'post_id' => 'required|int|min:1|exists:statuses,id'
|
||||
]);
|
||||
|
||||
$profileId = Auth::user()->profile_id;
|
||||
$collectionId = $request->input('collection_id');
|
||||
$postId = $request->input('post_id');
|
||||
|
||||
$collection = Collection::whereProfileId($profileId)->findOrFail($collectionId);
|
||||
$count = $collection->items()->count();
|
||||
|
||||
if($count >= 18) {
|
||||
abort(400, 'You can only add 18 posts per collection');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
||||
$item = CollectionItem::firstOrCreate([
|
||||
'collection_id' => $collection->id,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id
|
||||
],[
|
||||
'order' => $count,
|
||||
]);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function get(Request $request, int $id)
|
||||
{
|
||||
$profile = Auth::check() ? Auth::user()->profile : [];
|
||||
|
||||
$collection = Collection::whereVisibility('public')->findOrFail($id);
|
||||
if($collection->published_at == null) {
|
||||
if(!Auth::check() || $profile->id !== $collection->profile_id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $collection->id,
|
||||
'title' => $collection->title,
|
||||
'description' => $collection->description,
|
||||
'visibility' => $collection->visibility
|
||||
];
|
||||
}
|
||||
|
||||
public function getItems(Request $request, int $id)
|
||||
{
|
||||
$collection = Collection::findOrFail($id);
|
||||
if($collection->visibility !== 'public') {
|
||||
abort_if(!Auth::check() || Auth::user()->profile_id != $collection->profile_id, 404);
|
||||
}
|
||||
$posts = $collection->posts()->orderBy('order', 'asc')->paginate(18);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($posts, new StatusTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function getUserCollections(Request $request, int $id)
|
||||
{
|
||||
$profile = Profile::whereNull('status')
|
||||
->whereNull('domain')
|
||||
->findOrFail($id);
|
||||
|
||||
if($profile->is_private) {
|
||||
abort_if(!Auth::check(), 404);
|
||||
abort_if(!$profile->followedBy(Auth::user()->profile) && $profile->id != Auth::user()->profile_id, 404);
|
||||
}
|
||||
|
||||
return $profile
|
||||
->collections()
|
||||
->has('posts')
|
||||
->with('posts')
|
||||
->whereVisibility('public')
|
||||
->whereNotNull('published_at')
|
||||
->orderByDesc('published_at')
|
||||
->paginate(9)
|
||||
->map(function($collection) {
|
||||
return [
|
||||
'id' => $collection->id,
|
||||
'title' => $collection->title,
|
||||
'description' => $collection->description,
|
||||
'thumb' => $collection->posts()->first()->thumb(),
|
||||
'url' => $collection->url(),
|
||||
'published_at' => $collection->published_at
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use App\Jobs\StatusPipeline\NewStatusPipeline;
|
|||
use App\Util\Lexer\Autolink;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
@ -57,6 +58,16 @@ class CommentController extends Controller
|
|||
return;
|
||||
}
|
||||
|
||||
$filtered = UserFilter::whereUserId($status->profile_id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['block'])
|
||||
->whereFilterableId($profile->id)
|
||||
->exists();
|
||||
|
||||
if($filtered == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reply = DB::transaction(function() use($comment, $status, $profile) {
|
||||
$autolink = Autolink::create()->autolink($comment);
|
||||
$reply = new Status();
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\{
|
|||
DiscoverCategory,
|
||||
Follower,
|
||||
Hashtag,
|
||||
HashtagFollow,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
|
@ -17,6 +18,7 @@ use App\Transformer\Api\StatusStatelessTransformer;
|
|||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\StatusHashtagService;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
|
@ -36,57 +38,11 @@ class DiscoverController extends Controller
|
|||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
|
||||
|
||||
$tag = Hashtag::whereSlug($hashtag)
|
||||
->firstOrFail();
|
||||
|
||||
$page = 1;
|
||||
$key = 'discover:tag-'.$tag->id.':page-'.$page;
|
||||
$keyMinutes = 15;
|
||||
|
||||
$posts = Cache::remember($key, now()->addMinutes($keyMinutes), function() use ($tag, $request) {
|
||||
$tags = StatusHashtag::select('status_id')
|
||||
->whereHashtagId($tag->id)
|
||||
->orderByDesc('id')
|
||||
->take(48)
|
||||
->pluck('status_id');
|
||||
|
||||
return Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'local',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->with('media')
|
||||
->whereLocal(true)
|
||||
->whereNull('uri')
|
||||
->whereIn('id', $tags)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereNull('url')
|
||||
->whereNull('uri')
|
||||
->withCount(['likes', 'comments'])
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
});
|
||||
|
||||
if($posts->count() == 0) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('discover.tags.show', compact('tag', 'posts'));
|
||||
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
|
||||
$tagCount = StatusHashtagService::count($tag->id);
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
|
||||
public function showCategory(Request $request, $slug)
|
||||
|
@ -156,7 +112,6 @@ class DiscoverController extends Controller
|
|||
return $res;
|
||||
}
|
||||
|
||||
|
||||
public function loopWatch(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
|
@ -171,4 +126,26 @@ class DiscoverController extends Controller
|
|||
|
||||
return response()->json(200);
|
||||
}
|
||||
|
||||
public function getHashtags(Request $request)
|
||||
{
|
||||
$auth = Auth::check();
|
||||
abort_if(!config('instance.discover.tags.is_public') && !$auth, 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|alphanum|min:2|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:' . ($auth ? 19 : 3)
|
||||
]);
|
||||
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
if($page == 1) {
|
||||
$res['follows'] = HashtagFollow::whereUserId(Auth::id())->whereHashtagId($hashtag->id)->exists();
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ use App\{
|
|||
AccountLog,
|
||||
Like,
|
||||
Profile,
|
||||
Status
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Transformer\ActivityPub\ProfileOutbox;
|
||||
use App\Util\Lexer\Nickname;
|
||||
|
@ -34,47 +35,22 @@ class FederationController extends Controller
|
|||
abort_if(!Auth::check(), 403);
|
||||
}
|
||||
|
||||
// deprecated, remove in 0.10
|
||||
public function authorizeFollow(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'acct' => 'required|string|min:3|max:255',
|
||||
]);
|
||||
$acct = $request->input('acct');
|
||||
$nickname = Nickname::normalizeProfileUrl($acct);
|
||||
|
||||
return view('federation.authorizefollow', compact('acct', 'nickname'));
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// deprecated, remove in 0.10
|
||||
public function remoteFollow()
|
||||
{
|
||||
$this->authCheck();
|
||||
|
||||
return view('federation.remotefollow');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// deprecated, remove in 0.10
|
||||
public function remoteFollowStore(Request $request)
|
||||
{
|
||||
return;
|
||||
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'url' => 'required|string',
|
||||
]);
|
||||
|
||||
abort_if(!config('federation.activitypub.remoteFollow'), 403);
|
||||
|
||||
$follower = Auth::user()->profile;
|
||||
$url = $request->input('url');
|
||||
$url = Helpers::validateUrl($url);
|
||||
|
||||
if(!$url) {
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteFollowPipeline::dispatch($follower, $url);
|
||||
|
||||
return response(['success' => true, 'follower' => $follower]);
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function nodeinfoWellKnown()
|
||||
|
@ -100,8 +76,8 @@ class FederationController extends Controller
|
|||
$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
|
||||
$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
|
||||
$count = collect([]);
|
||||
// $likes = Like::select('profile_id')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
|
||||
// $count = $count->merge($likes);
|
||||
$likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
|
||||
$count = $count->merge($likes);
|
||||
$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
|
||||
$count = $count->merge($statuses);
|
||||
$profiles = Profile::select('id')->whereNull('domain')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('id')->pluck('id')->toArray();
|
||||
|
@ -110,8 +86,8 @@ class FederationController extends Controller
|
|||
});
|
||||
$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(12), function() {
|
||||
$count = collect([]);
|
||||
// $likes = Like::select('profile_id')->where('created_at', '>', now()->subMonths(1)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
|
||||
// $count = $count->merge($likes);
|
||||
$likes = Like::select('profile_id')->where('created_at', '>', now()->subMonths(1)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
|
||||
$count = $count->merge($likes);
|
||||
$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(1)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
|
||||
$count = $count->merge($statuses);
|
||||
$profiles = Profile::select('id')->whereNull('domain')->where('created_at', '>', now()->subMonths(1)->toDateTimeString())->groupBy('id')->pluck('id')->toArray();
|
||||
|
@ -120,11 +96,12 @@ class FederationController extends Controller
|
|||
});
|
||||
return [
|
||||
'metadata' => [
|
||||
'nodeName' => config('app.name'),
|
||||
'nodeName' => config('pixelfed.domain.app'),
|
||||
'software' => [
|
||||
'homepage' => 'https://pixelfed.org',
|
||||
'repo' => 'https://github.com/pixelfed/pixelfed',
|
||||
],
|
||||
'config' => \App\Util\Site\Config::get()
|
||||
],
|
||||
'protocols' => [
|
||||
'activitypub',
|
||||
|
@ -138,12 +115,12 @@ class FederationController extends Controller
|
|||
'version' => config('pixelfed.version'),
|
||||
],
|
||||
'usage' => [
|
||||
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
|
||||
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
|
||||
'localPosts' => Status::whereLocal(true)->count(),
|
||||
'localComments' => 0,
|
||||
'users' => [
|
||||
'total' => \App\Profile::whereNull('status')->whereNull('domain')->count(),
|
||||
'activeHalfyear' => $activeHalfYear,
|
||||
'activeMonth' => $activeMonth,
|
||||
'total' => User::count(),
|
||||
'activeHalfyear' => (int) $activeHalfYear,
|
||||
'activeMonth' => (int) $activeMonth,
|
||||
],
|
||||
],
|
||||
'version' => '2.0',
|
||||
|
@ -162,10 +139,12 @@ class FederationController extends Controller
|
|||
$this->validate($request, ['resource'=>'required|string|min:3|max:255']);
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$hash = hash('sha256', $resource);
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if($parsed['domain'] !== config('pixelfed.domain.app')) {
|
||||
abort(404);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
if($profile->status != null) {
|
||||
return ProfileController::accountCheck($profile);
|
||||
}
|
||||
|
@ -179,7 +158,7 @@ class FederationController extends Controller
|
|||
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="{$path}?resource={uri}"/></XRD>';
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
|
@ -315,19 +294,16 @@ class FederationController extends Controller
|
|||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
return [];
|
||||
|
||||
if($profile->status != null) {
|
||||
return [];
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => $profile->following()->count(),
|
||||
'orderedItems' => $profile->following->map(function($f) {
|
||||
return $f->permalink();
|
||||
})
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
|
@ -341,20 +317,18 @@ class FederationController extends Controller
|
|||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
return [];
|
||||
|
||||
if($profile->status != null) {
|
||||
return [];
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => $profile->followers()->count(),
|
||||
'orderedItems' => $profile->followers->map(function($f) {
|
||||
return $f->permalink();
|
||||
})
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
|
||||
return response()->json($obj);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,16 +23,11 @@ class FollowerController extends Controller
|
|||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer',
|
||||
'item' => 'required|string',
|
||||
]);
|
||||
$item = $request->input('item');
|
||||
$item = (int) $request->input('item');
|
||||
$this->handleFollowRequest($item);
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([
|
||||
200
|
||||
], 200);
|
||||
}
|
||||
return redirect()->back();
|
||||
return response()->json(200);
|
||||
}
|
||||
|
||||
protected function handleFollowRequest($item)
|
||||
|
@ -53,7 +48,7 @@ class FollowerController extends Controller
|
|||
abort(400, 'You cannot follow this user.');
|
||||
}
|
||||
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
|
||||
if($private == true && $isFollowing == 0 || $remote == true) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
|
@ -83,13 +78,23 @@ class FollowerController extends Controller
|
|||
$follower->profile_id = $user->id;
|
||||
$follower->following_id = $target->id;
|
||||
$follower->save();
|
||||
|
||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
FollowPipeline::dispatch($follower);
|
||||
} else {
|
||||
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->firstOrFail();
|
||||
if($remote == true) {
|
||||
$request = FollowRequest::whereFollowerId($user->id)->whereFollowingId($target->id)->exists();
|
||||
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
if($remote == true && $request && !$follower) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
if($remote == true && $follower) {
|
||||
$this->sendUndoFollow($user, $target);
|
||||
}
|
||||
$follower->delete();
|
||||
Follower::whereProfileId($user->id)
|
||||
->whereFollowingId($target->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
Cache::forget('profile:following:'.$target->id);
|
||||
|
@ -109,6 +114,7 @@ class FollowerController extends Controller
|
|||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $user->permalink('#follow/'.$target->id.''),
|
||||
'type' => 'Follow',
|
||||
'actor' => $user->permalink(),
|
||||
'object' => $target->permalink()
|
||||
|
|
61
app/Http/Controllers/HashtagFollowController.php
Normal file
61
app/Http/Controllers/HashtagFollowController.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\{
|
||||
Hashtag,
|
||||
HashtagFollow,
|
||||
Status
|
||||
};
|
||||
|
||||
class HashtagFollowController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|alpha_num|min:1|max:124|exists:hashtags,name'
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$tag = $request->input('name');
|
||||
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
|
||||
$hashtagFollow = HashtagFollow::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'profile_id' => $user->profile_id ?? $user->profile->id,
|
||||
'hashtag_id' => $hashtag->id
|
||||
]);
|
||||
|
||||
if($hashtagFollow->wasRecentlyCreated) {
|
||||
$state = 'created';
|
||||
// todo: send to HashtagFollowService
|
||||
} else {
|
||||
$state = 'deleted';
|
||||
$hashtagFollow->delete();
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state
|
||||
];
|
||||
}
|
||||
|
||||
public function getTags(Request $request)
|
||||
{
|
||||
return HashtagFollow::with('hashtag')->whereUserId(Auth::id())
|
||||
->inRandomOrder()
|
||||
->take(3)
|
||||
->get()
|
||||
->map(function($follow, $k) {
|
||||
return $follow->hashtag->name;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -50,38 +50,7 @@ class InternalApiController extends Controller
|
|||
// deprecated
|
||||
public function discover(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$pid = $profile->id;
|
||||
$following = Cache::remember('feature:discover:following:'.$pid, now()->addMinutes(60), function() use ($pid) {
|
||||
return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
|
||||
});
|
||||
$filters = Cache::remember("user:filter:list:$pid", now()->addMinutes(60), function() use($pid) {
|
||||
return UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
});
|
||||
$following = array_merge($following, $filters);
|
||||
|
||||
$posts = Status::select('id', 'caption', 'profile_id')
|
||||
->whereHas('media')
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->whereNotIn('profile_id', $following)
|
||||
->with('media')
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(21)
|
||||
->get();
|
||||
|
||||
$res = [
|
||||
'posts' => $posts->map(function($post) {
|
||||
return [
|
||||
'url' => $post->url(),
|
||||
'thumb' => $post->thumb(),
|
||||
];
|
||||
})
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
return;
|
||||
}
|
||||
|
||||
public function discoverPosts(Request $request)
|
||||
|
@ -155,22 +124,9 @@ class InternalApiController extends Controller
|
|||
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function notificationMarkAllRead(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$notifications = Notification::whereProfileId($profile->id)->get();
|
||||
foreach($notifications as $n) {
|
||||
$n->read_at = Carbon::now();
|
||||
$n->save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public function statusReplies(Request $request, int $id)
|
||||
{
|
||||
$parent = Status::findOrFail($id);
|
||||
$parent = Status::whereScope('public')->findOrFail($id);
|
||||
|
||||
$children = Status::whereInReplyToId($parent->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
|
@ -283,7 +239,8 @@ class InternalApiController extends Controller
|
|||
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
|
||||
'media.*.license' => 'nullable|string|max:80',
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10'
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable'
|
||||
]);
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
|
@ -327,6 +284,9 @@ class InternalApiController extends Controller
|
|||
array_push($mimes, $m->mime);
|
||||
}
|
||||
|
||||
if($request->filled('place')) {
|
||||
$status->place_id = $request->input('place')['id'];
|
||||
}
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->scope = 'draft';
|
||||
$status->profile_id = $profile->id;
|
||||
|
@ -350,4 +310,18 @@ class InternalApiController extends Controller
|
|||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
return $status->url();
|
||||
}
|
||||
|
||||
public function bookmarks(Request $request)
|
||||
{
|
||||
$statuses = Auth::user()->profile
|
||||
->bookmarks()
|
||||
->withCount(['likes','comments'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->simplePaginate(10);
|
||||
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
||||
|
|
23
app/Http/Controllers/PlaceController.php
Normal file
23
app/Http/Controllers/PlaceController.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
Place,
|
||||
Status
|
||||
};
|
||||
|
||||
class PlaceController extends Controller
|
||||
{
|
||||
public function show(Request $request, $id, $slug)
|
||||
{
|
||||
// TODO: Replace with vue component + apis
|
||||
$place = Place::whereSlug($slug)->findOrFail($id);
|
||||
$posts = Status::wherePlaceId($place->id)
|
||||
->whereScope('public')
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10);
|
||||
return view('discover.places.show', compact('place', 'posts'));
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
|||
use Auth;
|
||||
use Cache;
|
||||
use App\Follower;
|
||||
use App\FollowRequest;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
|
@ -67,8 +68,12 @@ class ProfileController extends Controller
|
|||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
|
||||
if ($isPrivate == true || $isBlocked == true) {
|
||||
return view('profile.private', compact('user', 'is_following'));
|
||||
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
|
||||
->whereFollowingId($user->id)
|
||||
->exists() : false;
|
||||
return view('profile.private', compact('user', 'is_following', 'requested'));
|
||||
}
|
||||
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
|
@ -241,22 +246,9 @@ class ProfileController extends Controller
|
|||
return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following', 'is_admin', 'settings'));
|
||||
}
|
||||
|
||||
public function savedBookmarks(Request $request, $username)
|
||||
public function meRedirect()
|
||||
{
|
||||
if (Auth::check() === false || $username !== Auth::user()->username) {
|
||||
abort(403);
|
||||
}
|
||||
$user = $profile = Auth::user()->profile;
|
||||
if($profile->status != null) {
|
||||
return $this->accountCheck($profile);
|
||||
}
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
$owner = true;
|
||||
$following = false;
|
||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at', 'desc')->simplePaginate(10);
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.bookmarks', compact('user', 'profile', 'settings', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
abort_if(!Auth::check(), 404);
|
||||
return redirect(Auth::user()->url());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
17
app/Http/Controllers/ProfileSponsorController.php
Normal file
17
app/Http/Controllers/ProfileSponsorController.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\ProfileSponsor;
|
||||
use Auth;
|
||||
|
||||
class ProfileSponsorController extends Controller
|
||||
{
|
||||
public function get(Request $request, $id)
|
||||
{
|
||||
$profile = ProfileSponsor::whereProfileId($id)->first();
|
||||
$res = $profile ? $profile->sponsors : [];
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
|
@ -113,10 +113,22 @@ class PublicApiController extends Controller
|
|||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId);
|
||||
$this->scopeCheck($profile, $status);
|
||||
|
||||
if(Auth::check()) {
|
||||
$pid = Auth::user()->profile->id;
|
||||
$filtered = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
} else {
|
||||
$filtered = [];
|
||||
}
|
||||
|
||||
if($request->filled('min_id') || $request->filled('max_id')) {
|
||||
if($request->filled('min_id')) {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '>=', $request->min_id)
|
||||
->orderBy('id', 'desc')
|
||||
|
@ -125,6 +137,7 @@ class PublicApiController extends Controller
|
|||
if($request->filled('max_id')) {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '<=', $request->max_id)
|
||||
->orderBy('id', 'desc')
|
||||
|
@ -133,6 +146,7 @@ class PublicApiController extends Controller
|
|||
} else {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
|
@ -211,6 +225,10 @@ class PublicApiController extends Controller
|
|||
'limit' => 'nullable|integer|max:20'
|
||||
]);
|
||||
|
||||
if(config('instance.timeline.local.is_public') == false && !Auth::check()) {
|
||||
abort(403, 'Authentication required.');
|
||||
}
|
||||
|
||||
$page = $request->input('page');
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
|
@ -251,9 +269,11 @@ class PublicApiController extends Controller
|
|||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'place_id',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->where('id', $dir, $id)
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->whereLocal(true)
|
||||
->whereNull('uri')
|
||||
|
@ -280,8 +300,10 @@ class PublicApiController extends Controller
|
|||
'reply_count',
|
||||
'comments_disabled',
|
||||
'created_at',
|
||||
'place_id',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->whereLocal(true)
|
||||
->whereNull('uri')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
|
@ -325,13 +347,15 @@ class PublicApiController extends Controller
|
|||
return $following->push($pid)->toArray();
|
||||
});
|
||||
|
||||
$private = Cache::remember('profiles:private', 1440, function() {
|
||||
$private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
|
||||
return Profile::whereIsPrivate(true)
|
||||
->orWhere('unlisted', true)
|
||||
->orWhere('status', '!=', null)
|
||||
->pluck('id');
|
||||
});
|
||||
|
||||
$private = $private->diff($following)->flatten();
|
||||
|
||||
$filters = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
|
@ -355,9 +379,11 @@ class PublicApiController extends Controller
|
|||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'place_id',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
|
@ -382,9 +408,11 @@ class PublicApiController extends Controller
|
|||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'place_id',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereNull('in_reply_to_id')
|
||||
|
@ -499,7 +527,9 @@ class PublicApiController extends Controller
|
|||
|
||||
public function relationships(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
if(!Auth::check()) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|array|min:1|max:20',
|
||||
|
@ -509,7 +539,7 @@ class PublicApiController extends Controller
|
|||
$filtered = $ids->filter(function($v) {
|
||||
return $v != Auth::user()->profile->id;
|
||||
});
|
||||
$relations = Profile::findOrFail($filtered->all());
|
||||
$relations = Profile::whereNull('status')->findOrFail($filtered->all());
|
||||
$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
|
||||
$res = $this->fractal->createData($fractal)->toArray();
|
||||
return response()->json($res);
|
||||
|
|
|
@ -37,6 +37,7 @@ class SearchController extends Controller
|
|||
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
|
||||
$tokens = [];
|
||||
if(Helpers::validateUrl($tag) != false && config('federation.activitypub.enabled') == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
abort_if(Helpers::validateLocalUrl($tag), 404);
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if(isset($remote['type']) && in_array($remote['type'], ['Note', 'Person']) == true) {
|
||||
$type = $remote['type'];
|
||||
|
@ -50,8 +51,9 @@ class SearchController extends Controller
|
|||
'tokens' => [$item->username],
|
||||
'name' => $item->name,
|
||||
'entity' => [
|
||||
'id' => $item->id,
|
||||
'id' => (string) $item->id,
|
||||
'following' => $item->followedBy(Auth::user()->profile),
|
||||
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
|
||||
'thumb' => $item->avatarUrl()
|
||||
]
|
||||
]];
|
||||
|
@ -143,6 +145,7 @@ class SearchController extends Controller
|
|||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
'filter' => $item->firstMedia()->filter_class
|
||||
];
|
||||
});
|
||||
$tokens['posts'] = $posts;
|
||||
|
|
|
@ -103,7 +103,7 @@ trait ExportSettings
|
|||
$this->validate($request, [
|
||||
'type' => 'required|string|in:ap,api'
|
||||
]);
|
||||
$limit = 300;
|
||||
$limit = 500;
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$type = 'ap';
|
||||
|
@ -116,7 +116,7 @@ trait ExportSettings
|
|||
|
||||
$filename = 'outbox.json';
|
||||
if($type == 'ap') {
|
||||
$data = Cache::remember('account:export:profile:statuses:ap:'.Auth::user()->profile->id, now()->addDays(7), function() {
|
||||
$data = Cache::remember('account:export:profile:statuses:ap:'.Auth::user()->profile->id, now()->addHours(1), function() {
|
||||
$profile = Auth::user()->profile->statuses;
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
|
@ -125,7 +125,7 @@ trait ExportSettings
|
|||
});
|
||||
} else {
|
||||
$filename = 'api-statuses.json';
|
||||
$data = Cache::remember('account:export:profile:statuses:api:'.Auth::user()->profile->id, now()->addDays(7), function() {
|
||||
$data = Cache::remember('account:export:profile:statuses:api:'.Auth::user()->profile->id, now()->addHours(1), function() {
|
||||
$profile = Auth::user()->profile->statuses;
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
|
|
|
@ -18,15 +18,29 @@ trait RelationshipSettings
|
|||
|
||||
public function relationshipsHome(Request $request)
|
||||
{
|
||||
$mode = $request->input('mode') == 'following' ? 'following' : 'followers';
|
||||
$this->validate($request, [
|
||||
'mode' => 'nullable|string|in:following,followers,hashtags'
|
||||
]);
|
||||
|
||||
$mode = $request->input('mode');
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$following = $followers = [];
|
||||
switch ($mode) {
|
||||
case 'following':
|
||||
$data = $profile->following()->simplePaginate(10);
|
||||
break;
|
||||
|
||||
if($mode == 'following') {
|
||||
$data = $profile->following()->simplePaginate(10);
|
||||
} else {
|
||||
$data = $profile->followers()->simplePaginate(10);
|
||||
case 'followers':
|
||||
$data = $profile->followers()->simplePaginate(10);
|
||||
break;
|
||||
|
||||
case 'hashtags':
|
||||
$data = $profile->hashtagFollowing()->with('hashtag')->simplePaginate(10);
|
||||
break;
|
||||
|
||||
default:
|
||||
$data = [];
|
||||
break;
|
||||
}
|
||||
|
||||
return view('settings.relationships.home', compact('profile', 'mode', 'data'));
|
||||
|
|
|
@ -4,11 +4,13 @@ namespace App\Http\Controllers;
|
|||
|
||||
use App\AccountLog;
|
||||
use App\Following;
|
||||
use App\ProfileSponsor;
|
||||
use App\Report;
|
||||
use App\UserFilter;
|
||||
use Auth, Cookie, DB, Cache, Purify;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Http\Controllers\Settings\{
|
||||
ExportSettings,
|
||||
LabsSettings,
|
||||
|
@ -90,12 +92,18 @@ class SettingsController extends Controller
|
|||
|
||||
public function removeAccountTemporary(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
abort_if(!config('pixelfed.account_deletion'), 403);
|
||||
abort_if($user->is_admin, 403);
|
||||
|
||||
return view('settings.remove.temporary');
|
||||
}
|
||||
|
||||
public function removeAccountTemporarySubmit(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
abort_if(!config('pixelfed.account_deletion'), 403);
|
||||
abort_if($user->is_admin, 403);
|
||||
$profile = $user->profile;
|
||||
$user->status = 'disabled';
|
||||
$profile->status = 'disabled';
|
||||
|
@ -108,9 +116,8 @@ class SettingsController extends Controller
|
|||
|
||||
public function removeAccountPermanent(Request $request)
|
||||
{
|
||||
if(config('pixelfed.account_deletion') == false) {
|
||||
abort(404);
|
||||
}
|
||||
$user = Auth::user();
|
||||
abort_if($user->is_admin, 403);
|
||||
return view('settings.remove.permanent');
|
||||
}
|
||||
|
||||
|
@ -120,9 +127,8 @@ class SettingsController extends Controller
|
|||
abort(404);
|
||||
}
|
||||
$user = Auth::user();
|
||||
if($user->is_admin == true) {
|
||||
return abort(400, 'You cannot delete an admin account.');
|
||||
}
|
||||
abort_if(!config('pixelfed.account_deletion'), 403);
|
||||
abort_if($user->is_admin, 403);
|
||||
$profile = $user->profile;
|
||||
$ts = Carbon::now()->addMonth();
|
||||
$user->status = 'delete';
|
||||
|
@ -166,5 +172,61 @@ class SettingsController extends Controller
|
|||
|
||||
return response()->json([200])->cookie($cookie);
|
||||
}
|
||||
|
||||
public function sponsor()
|
||||
{
|
||||
$default = [
|
||||
'patreon' => null,
|
||||
'liberapay' => null,
|
||||
'opencollective' => null
|
||||
];
|
||||
$sponsors = ProfileSponsor::whereProfileId(Auth::user()->profile->id)->first();
|
||||
$sponsors = $sponsors ? json_decode($sponsors->sponsors, true) : $default;
|
||||
return view('settings.sponsor', compact('sponsors'));
|
||||
}
|
||||
|
||||
public function sponsorStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'patreon' => 'nullable|string',
|
||||
'liberapay' => 'nullable|string',
|
||||
'opencollective' => 'nullable|string'
|
||||
]);
|
||||
|
||||
$patreon = Str::startsWith($request->input('patreon'), 'https://') ?
|
||||
substr($request->input('patreon'), 8) :
|
||||
$request->input('patreon');
|
||||
|
||||
$liberapay = Str::startsWith($request->input('liberapay'), 'https://') ?
|
||||
substr($request->input('liberapay'), 8) :
|
||||
$request->input('liberapay');
|
||||
|
||||
$opencollective = Str::startsWith($request->input('opencollective'), 'https://') ?
|
||||
substr($request->input('opencollective'), 8) :
|
||||
$request->input('opencollective');
|
||||
|
||||
$patreon = Str::startsWith($patreon, 'patreon.com/') ? e($patreon) : null;
|
||||
$liberapay = Str::startsWith($liberapay, 'liberapay.com/') ? e($liberapay) : null;
|
||||
$opencollective = Str::startsWith($opencollective, 'opencollective.com/') ? e($opencollective) : null;
|
||||
|
||||
if(empty($patreon) && empty($liberapay) && empty($opencollective)) {
|
||||
return redirect(route('settings'))->with('error', 'An error occured. Please try again later.');;
|
||||
}
|
||||
|
||||
$res = [
|
||||
'patreon' => $patreon,
|
||||
'liberapay' => $liberapay,
|
||||
'opencollective' => $opencollective
|
||||
];
|
||||
|
||||
$sponsors = ProfileSponsor::firstOrCreate([
|
||||
'profile_id' => Auth::user()->profile_id ?? Auth::user()->profile->id
|
||||
]);
|
||||
$sponsors->sponsors = json_encode($res);
|
||||
$sponsors->save();
|
||||
$sponsors = $res;
|
||||
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class SiteController extends Controller
|
|||
public function about()
|
||||
{
|
||||
return Cache::remember('site:about', now()->addHours(12), function() {
|
||||
app()->setLocale('en');
|
||||
$page = Page::whereSlug('/site/about')->whereActive(true)->first();
|
||||
$stats = [
|
||||
'posts' => Status::whereLocal(true)->count(),
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Avatar;
|
||||
use App\Profile;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class ImportAvatar implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $url;
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($url, Profile $profile)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$url = $this->url;
|
||||
$profile = $this->profile;
|
||||
|
||||
$basePath = $this->buildPath();
|
||||
}
|
||||
|
||||
public function buildPath()
|
||||
{
|
||||
$baseDir = storage_path('app/public/avatars');
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
}
|
||||
|
||||
$prefix = $this->profile->id;
|
||||
$padded = str_pad($prefix, 12, 0, STR_PAD_LEFT);
|
||||
$parts = str_split($padded, 3);
|
||||
foreach ($parts as $k => $part) {
|
||||
if ($k == 0) {
|
||||
$prefix = storage_path('app/public/avatars/'.$parts[0]);
|
||||
if (!is_dir($prefix)) {
|
||||
mkdir($prefix);
|
||||
}
|
||||
}
|
||||
if ($k == 1) {
|
||||
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1]);
|
||||
if (!is_dir($prefix)) {
|
||||
mkdir($prefix);
|
||||
}
|
||||
}
|
||||
if ($k == 2) {
|
||||
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2]);
|
||||
if (!is_dir($prefix)) {
|
||||
mkdir($prefix);
|
||||
}
|
||||
}
|
||||
if ($k == 3) {
|
||||
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3];
|
||||
$prefix = storage_path('app/'.$avatarpath);
|
||||
if (!is_dir($prefix)) {
|
||||
mkdir($prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
$dir = storage_path('app/'.$avatarpath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir);
|
||||
}
|
||||
$path = $avatarpath.'/avatar.svg';
|
||||
return storage_path('app/'.$path);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,8 @@ namespace App\Jobs\CommentPipeline;
|
|||
|
||||
use App\{
|
||||
Notification,
|
||||
Status
|
||||
Status,
|
||||
UserFilter
|
||||
};
|
||||
use App\Services\NotificationService;
|
||||
use DB, Cache, Log, Redis;
|
||||
|
@ -56,6 +57,15 @@ class CommentPipeline implements ShouldQueue
|
|||
if ($actor->id === $target->id || $status->comments_disabled == true) {
|
||||
return true;
|
||||
}
|
||||
$filtered = UserFilter::whereUserId($target->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->whereFilterableId($actor->id)
|
||||
->exists();
|
||||
|
||||
if($filtered == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function() use($target, $actor, $comment) {
|
||||
$notification = new Notification();
|
||||
|
|
|
@ -80,8 +80,10 @@ class DeleteAccountPipeline implements ShouldQueue
|
|||
Bookmark::whereProfileId($user->profile->id)->forceDelete();
|
||||
|
||||
EmailVerification::whereUserId($user->id)->forceDelete();
|
||||
|
||||
$id = $user->profile->id;
|
||||
|
||||
StatusHashtag::whereProfileId($id)->delete();
|
||||
|
||||
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete();
|
||||
|
||||
Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete();
|
||||
|
|
|
@ -57,7 +57,7 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
|
||||
$audience = $status->profile->getAudienceInbox();
|
||||
|
||||
if(empty($audience) || $status->scope != 'public') {
|
||||
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ class StatusDelete implements ShouldQueue
|
|||
Report::whereObjectType('App\Status')
|
||||
->whereObjectId($status->id)
|
||||
->delete();
|
||||
$status->delete();
|
||||
$status->forceDelete();
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
|
@ -89,6 +89,9 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if(mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$slug = str_slug($tag, '-', false);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
|
@ -98,7 +101,8 @@ class StatusEntityLexer implements ShouldQueue
|
|||
[
|
||||
'status_id' => $status->id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_visibility' => $status->visibility,
|
||||
]
|
||||
);
|
||||
});
|
||||
|
|
|
@ -30,13 +30,14 @@ class Media extends Model
|
|||
public function url()
|
||||
{
|
||||
if(!empty($this->remote_media) && $this->remote_url) {
|
||||
//$url = \App\Services\MediaProxyService::get($this->remote_url, $this->mime);
|
||||
$url = $this->remote_url;
|
||||
} else {
|
||||
$path = $this->media_path;
|
||||
$url = $this->cdn_url ?? Storage::url($path);
|
||||
$url = $this->cdn_url ?? config('app.url') . Storage::url($path);
|
||||
}
|
||||
|
||||
return url($url);
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
|
|
64
app/Observers/StatusHashtagObserver.php
Normal file
64
app/Observers/StatusHashtagObserver.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\StatusHashtag;
|
||||
use App\Services\StatusHashtagService;
|
||||
|
||||
class StatusHashtagObserver
|
||||
{
|
||||
/**
|
||||
* Handle the notification "created" event.
|
||||
*
|
||||
* @param \App\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function created(StatusHashtag $hashtag)
|
||||
{
|
||||
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the notification "updated" event.
|
||||
*
|
||||
* @param \App\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function updated(StatusHashtag $hashtag)
|
||||
{
|
||||
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the notification "deleted" event.
|
||||
*
|
||||
* @param \App\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(StatusHashtag $hashtag)
|
||||
{
|
||||
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the notification "restored" event.
|
||||
*
|
||||
* @param \App\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function restored(StatusHashtag $hashtag)
|
||||
{
|
||||
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the notification "force deleted" event.
|
||||
*
|
||||
* @param \App\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleted(StatusHashtag $hashtag)
|
||||
{
|
||||
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
||||
}
|
||||
}
|
31
app/Place.php
Normal file
31
app/Place.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
|
||||
class Place extends Model
|
||||
{
|
||||
protected $visible = ['id', 'name', 'country', 'slug'];
|
||||
|
||||
public function url()
|
||||
{
|
||||
return url('/discover/places/' . $this->id . '/' . $this->slug);
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Status::class);
|
||||
}
|
||||
|
||||
public function postCount()
|
||||
{
|
||||
return $this->posts()->count();
|
||||
}
|
||||
|
||||
public function statuses()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'id', 'place_id');
|
||||
}
|
||||
}
|
|
@ -4,11 +4,19 @@ namespace App;
|
|||
|
||||
use Auth, Cache, Storage;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasSnowflakePrimary, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $hidden = ['private_key'];
|
||||
|
@ -132,7 +140,7 @@ class Profile extends Model
|
|||
$version = hash('sha256', $avatar->change_count);
|
||||
$path = "{$path}?v={$version}";
|
||||
|
||||
return url(Storage::url($path));
|
||||
return config('app.url') . Storage::url($path);
|
||||
});
|
||||
|
||||
return $url;
|
||||
|
@ -278,4 +286,21 @@ class Profile extends Model
|
|||
'hashtag_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function hashtagFollowing()
|
||||
{
|
||||
return $this->hasMany(HashtagFollow::class);
|
||||
}
|
||||
|
||||
public function collections()
|
||||
{
|
||||
return $this->hasMany(Collection::class);
|
||||
}
|
||||
|
||||
public function hasFollowRequestById(int $id)
|
||||
{
|
||||
return FollowRequest::whereFollowerId($id)
|
||||
->whereFollowingId($this->id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
|
15
app/ProfileSponsor.php
Normal file
15
app/ProfileSponsor.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProfileSponsor extends Model
|
||||
{
|
||||
public $fillable = ['profile_id'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
}
|
|
@ -5,11 +5,13 @@ namespace App\Providers;
|
|||
use App\Observers\{
|
||||
AvatarObserver,
|
||||
NotificationObserver,
|
||||
StatusHashtagObserver,
|
||||
UserObserver
|
||||
};
|
||||
use App\{
|
||||
Avatar,
|
||||
Notification,
|
||||
StatusHashtag,
|
||||
User
|
||||
};
|
||||
use Auth, Horizon, URL;
|
||||
|
@ -31,6 +33,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
|
||||
Avatar::observe(AvatarObserver::class);
|
||||
Notification::observe(NotificationObserver::class);
|
||||
StatusHashtag::observe(StatusHashtagObserver::class);
|
||||
User::observe(UserObserver::class);
|
||||
|
||||
Horizon::auth(function ($request) {
|
||||
|
|
23
app/Services/EmailService.php
Normal file
23
app/Services/EmailService.php
Normal file
File diff suppressed because one or more lines are too long
80
app/Services/StatusHashtagService.php
Normal file
80
app/Services/StatusHashtagService.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache, Redis;
|
||||
use App\{Status, StatusHashtag};
|
||||
use App\Transformer\Api\StatusHashtagTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
class StatusHashtagService {
|
||||
|
||||
const CACHE_KEY = 'pf:services:status-hashtag:collection:';
|
||||
|
||||
public static function get($id, $page = 1, $stop = 9)
|
||||
{
|
||||
return StatusHashtag::whereHashtagId($id)
|
||||
->whereStatusVisibility('public')
|
||||
->whereHas('media')
|
||||
->skip($stop)
|
||||
->latest()
|
||||
->take(9)
|
||||
->pluck('status_id')
|
||||
->map(function ($i, $k) use ($id) {
|
||||
return self::getStatus($i, $id);
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function coldGet($id, $start = 0, $stop = 2000)
|
||||
{
|
||||
$stop = $stop > 2000 ? 2000 : $stop;
|
||||
$ids = StatusHashtag::whereHashtagId($id)
|
||||
->whereStatusVisibility('public')
|
||||
->whereHas('media')
|
||||
->latest()
|
||||
->skip($start)
|
||||
->take($stop)
|
||||
->pluck('status_id');
|
||||
foreach($ids as $key) {
|
||||
self::set($id, $key);
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public static function set($key, $val)
|
||||
{
|
||||
return Redis::zadd(self::CACHE_KEY . $key, $val, $val);
|
||||
}
|
||||
|
||||
public static function del($key)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY . $key, $key);
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
$count = Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf');
|
||||
if(empty($count)) {
|
||||
$count = StatusHashtag::whereHashtagId($id)->count();
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
public static function getStatus($statusId, $hashtagId)
|
||||
{
|
||||
return Cache::remember('pf:services:status-hashtag:post:'.$statusId.':hashtag:'.$hashtagId, now()->addMonths(3), function() use($statusId, $hashtagId) {
|
||||
$statusHashtag = StatusHashtag::with('profile', 'status', 'hashtag')
|
||||
->whereStatusVisibility('public')
|
||||
->whereStatusId($statusId)
|
||||
->whereHashtagId($hashtagId)
|
||||
->first();
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($statusHashtag, new StatusHashtagTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -379,15 +379,33 @@ class Status extends Model
|
|||
break;
|
||||
|
||||
case 'unlisted':
|
||||
$res['to'] = [
|
||||
$this->profile->permalink('/followers')
|
||||
];
|
||||
$res['cc'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
$res['to'] = [
|
||||
$this->profile->permalink('/followers')
|
||||
];
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
|
||||
// TODO: Update scope when DMs are supported
|
||||
case 'direct':
|
||||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
}
|
||||
return $res[$audience];
|
||||
}
|
||||
|
||||
public function place()
|
||||
{
|
||||
return $this->belongsTo(Place::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ class StatusHashtag extends Model
|
|||
public $fillable = [
|
||||
'status_id',
|
||||
'hashtag_id',
|
||||
'profile_id'
|
||||
'profile_id',
|
||||
'status_visibility'
|
||||
];
|
||||
|
||||
public function status()
|
||||
|
@ -26,4 +27,16 @@ class StatusHashtag extends Model
|
|||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Media::class,
|
||||
Status::class,
|
||||
'id',
|
||||
'status_id',
|
||||
'status_id',
|
||||
'id'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ class ProfileTransformer extends Fractal\TransformerAbstract
|
|||
'https://w3id.org/security/v1',
|
||||
[
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'schema' => 'http://schema.org#',
|
||||
'value' => 'schema:value'
|
||||
],
|
||||
],
|
||||
'id' => $profile->permalink(),
|
||||
|
|
|
@ -7,20 +7,20 @@ use League\Fractal;
|
|||
|
||||
class Announce extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Announce',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
'cc' => [
|
||||
$status->profile->permalink(),
|
||||
$status->profile->follower_url ?? $status->profile->permalink('/followers')
|
||||
],
|
||||
'published' => $status->created_at->format(DATE_ISO8601),
|
||||
'object' => $status->parent()->url(),
|
||||
];
|
||||
}
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Announce',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
'cc' => [
|
||||
$status->profile->permalink(),
|
||||
$status->profile->follower_url ?? $status->profile->permalink('/followers')
|
||||
],
|
||||
'published' => $status->created_at->format(DATE_ISO8601),
|
||||
'object' => $status->parent()->url(),
|
||||
];
|
||||
}
|
||||
}
|
23
app/Transformer/Api/DirectMessageTransformer.php
Normal file
23
app/Transformer/Api/DirectMessageTransformer.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use League\Fractal;
|
||||
use App\DirectMessage;
|
||||
|
||||
class DirectMessageTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(DirectMessage $dm)
|
||||
{
|
||||
return [
|
||||
'id' => $dm->id,
|
||||
'to_id' => $dm->to_id,
|
||||
'from_id' => $dm->from_id,
|
||||
'from_profile_ids' => $dm->from_profile_ids,
|
||||
'group_message' => $dm->group_message,
|
||||
'status_id' => $dm->status_id,
|
||||
'read_at' => $dm->read_at,
|
||||
'created_at' => $dm->created_at
|
||||
];
|
||||
}
|
||||
}
|
|
@ -3,7 +3,10 @@
|
|||
namespace App\Transformer\Api;
|
||||
|
||||
use Auth;
|
||||
use App\Profile;
|
||||
use App\{
|
||||
FollowRequest,
|
||||
Profile
|
||||
};
|
||||
use League\Fractal;
|
||||
|
||||
class RelationshipTransformer extends Fractal\TransformerAbstract
|
||||
|
@ -12,6 +15,12 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
|
|||
{
|
||||
$auth = Auth::check();
|
||||
$user = $auth ? Auth::user()->profile : false;
|
||||
$requested = false;
|
||||
if($user) {
|
||||
$requested = FollowRequest::whereFollowerId($user->id)
|
||||
->whereFollowingId($profile->id)
|
||||
->exists();
|
||||
}
|
||||
return [
|
||||
'id' => (string) $profile->id,
|
||||
'following' => $auth ? $user->follows($profile) : false,
|
||||
|
@ -19,7 +28,7 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
|
|||
'blocking' => $auth ? $user->blockedIds()->contains($profile->id) : false,
|
||||
'muting' => $auth ? $user->mutedIds()->contains($profile->id) : false,
|
||||
'muting_notifications' => null,
|
||||
'requested' => null,
|
||||
'requested' => $requested,
|
||||
'domain_blocking' => null,
|
||||
'showing_reblogs' => null,
|
||||
'endorsed' => false
|
||||
|
|
38
app/Transformer/Api/StatusHashtagTransformer.php
Normal file
38
app/Transformer/Api/StatusHashtagTransformer.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\{Hashtag, Status, StatusHashtag};
|
||||
use League\Fractal;
|
||||
|
||||
class StatusHashtagTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(StatusHashtag $statusHashtag)
|
||||
{
|
||||
$hashtag = $statusHashtag->hashtag;
|
||||
$status = $statusHashtag->status;
|
||||
$profile = $statusHashtag->profile;
|
||||
|
||||
return [
|
||||
'status' => [
|
||||
'id' => (int) $status->id,
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'thumb' => $status->thumb(),
|
||||
'filter' => $status->firstMedia()->filter_class,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'like_count' => $status->likes_count,
|
||||
'share_count' => $status->reblogs_count,
|
||||
'user' => [
|
||||
'username' => $profile->username,
|
||||
'url' => $profile->url(),
|
||||
],
|
||||
'visibility' => $status->visibility ?? $status->scope
|
||||
],
|
||||
'hashtag' => [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -10,9 +10,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'mentions',
|
||||
'media_attachments',
|
||||
'tags',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
|
@ -41,13 +39,15 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
],
|
||||
'language' => null,
|
||||
'pinned' => null,
|
||||
|
||||
'mentions' => [],
|
||||
'tags' => [],
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false,
|
||||
'thread' => false,
|
||||
'replies' => [],
|
||||
'parent' => [],
|
||||
'place' => $status->place
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -58,13 +58,6 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
|
||||
return $this->collection($mentions, new MentionTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addDays(14), function() use($status) {
|
||||
|
@ -74,11 +67,4 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
|
||||
return $this->collection($tags, new HashtagTransformer());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ class Helpers {
|
|||
{
|
||||
$audience = self::normalizeAudience($data);
|
||||
$url = $profile->permalink();
|
||||
return in_array($url, $audience);
|
||||
return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
|
||||
}
|
||||
|
||||
public static function validateUrl($url)
|
||||
|
@ -181,7 +181,7 @@ class Helpers {
|
|||
public static function zttpUserAgent()
|
||||
{
|
||||
return [
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'Accept' => 'application/activity+json',
|
||||
'User-Agent' => 'PixelfedBot - https://pixelfed.org',
|
||||
];
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ class Helpers {
|
|||
$id = (int) last(explode('/', $url));
|
||||
return Status::findOrFail($id);
|
||||
} else {
|
||||
$cached = Status::whereUrl($url)->first();
|
||||
$cached = Status::whereUri($url)->orWhere('url', $url)->first();
|
||||
if($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ class Helpers {
|
|||
|
||||
$scope = 'private';
|
||||
|
||||
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
|
||||
$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
|
||||
|
||||
if(isset($res['to']) == true) {
|
||||
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
|
||||
|
@ -280,11 +280,9 @@ class Helpers {
|
|||
$unlisted = false;
|
||||
}
|
||||
|
||||
$cw = config('costar.domain.cw');
|
||||
if(in_array(parse_url($url, PHP_URL_HOST), $cw) == true) {
|
||||
$cwDomains = config('costar.domain.cw');
|
||||
if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
|
||||
$cw = true;
|
||||
} else {
|
||||
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,7 +326,9 @@ class Helpers {
|
|||
$status->scope = $scope;
|
||||
$status->visibility = $scope;
|
||||
$status->save();
|
||||
self::importNoteAttachment($res, $status);
|
||||
if($reply_to == null) {
|
||||
self::importNoteAttachment($res, $status);
|
||||
}
|
||||
return $status;
|
||||
});
|
||||
|
||||
|
@ -361,29 +361,16 @@ class Helpers {
|
|||
if(in_array($type, $allowed) == false || $valid == false) {
|
||||
continue;
|
||||
}
|
||||
$info = pathinfo($url);
|
||||
|
||||
// pleroma attachment fix
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
$img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
|
||||
$file = '/tmp/pxmi-'.str_random(32);
|
||||
file_put_contents($file, $img);
|
||||
$fdata = new File($file);
|
||||
$path = Storage::putFile($storagePath, $fdata, 'public');
|
||||
$media = new Media();
|
||||
$media->remote_media = true;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $status->profile_id;
|
||||
$media->user_id = null;
|
||||
$media->media_path = $path;
|
||||
$media->size = $fdata->getSize();
|
||||
$media->mime = $fdata->getMimeType();
|
||||
$media->media_path = $url;
|
||||
$media->remote_url = $url;
|
||||
$media->mime = $type;
|
||||
$media->save();
|
||||
|
||||
ImageThumbnail::dispatch($media);
|
||||
ImageOptimize::dispatch($media);
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
$status->viewType();
|
||||
|
@ -401,7 +388,10 @@ class Helpers {
|
|||
|
||||
if($local == true) {
|
||||
$id = last(explode('/', $url));
|
||||
return Profile::whereUsername($id)->firstOrFail();
|
||||
return Profile::whereNull('status')
|
||||
->whereNull('domain')
|
||||
->whereUsername($id)
|
||||
->firstOrFail();
|
||||
}
|
||||
$res = self::fetchProfileFromUrl($url);
|
||||
if(isset($res['id']) == false) {
|
||||
|
@ -423,8 +413,8 @@ class Helpers {
|
|||
$profile = new Profile();
|
||||
$profile->domain = $domain;
|
||||
$profile->username = (string) Purify::clean($remoteUsername);
|
||||
$profile->name = Purify::clean($res['name']) ?? 'user';
|
||||
$profile->bio = Purify::clean($res['summary']);
|
||||
$profile->name = isset($res['name']) ? Purify::clean($res['name']) : 'user';
|
||||
$profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null;
|
||||
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
|
||||
$profile->inbox_url = $res['inbox'];
|
||||
$profile->outbox_url = $res['outbox'];
|
||||
|
|
|
@ -15,8 +15,10 @@ use App\{
|
|||
use Carbon\Carbon;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
|
||||
use App\Util\ActivityPub\Validator\{
|
||||
Accept,
|
||||
Follow
|
||||
};
|
||||
|
||||
|
@ -101,7 +103,7 @@ class Inbox
|
|||
|
||||
public function actorFirstOrCreate($actorUrl)
|
||||
{
|
||||
return Helpers::profileFirstOrNew($actorUrl);
|
||||
return Helpers::profileFetch($actorUrl);
|
||||
}
|
||||
|
||||
public function handleCreateActivity()
|
||||
|
@ -135,15 +137,13 @@ class Inbox
|
|||
|
||||
public function handleNoteCreate()
|
||||
{
|
||||
return;
|
||||
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Helpers::userInAudience($this->profile, $this->payload) == false) {
|
||||
if($actor->followers()->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -176,7 +176,7 @@ class Inbox
|
|||
'following_id' => $target->id,
|
||||
'local_profile' => empty($actor->domain)
|
||||
]);
|
||||
if($follower->wasRecentlyCreated == true) {
|
||||
if($follower->wasRecentlyCreated == true && $target->domain == null) {
|
||||
// send notification
|
||||
Notification::firstOrCreate([
|
||||
'profile_id' => $target->id,
|
||||
|
@ -188,14 +188,19 @@ class Inbox
|
|||
'item_type' => 'App\Profile'
|
||||
]);
|
||||
}
|
||||
$payload = $this->payload;
|
||||
|
||||
// send Accept to remote profile
|
||||
$accept = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $target->permalink().'#accepts/follows/' . $follower->id,
|
||||
'type' => 'Accept',
|
||||
'actor' => $target->permalink(),
|
||||
'object' => $payload
|
||||
'object' => [
|
||||
'id' => $actor->permalink('#follows/' . $follower->id),
|
||||
'actor' => $actor->permalink(),
|
||||
'type' => 'Follow',
|
||||
'object' => $target->permalink()
|
||||
]
|
||||
];
|
||||
Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
|
||||
}
|
||||
|
@ -242,28 +247,40 @@ class Inbox
|
|||
|
||||
public function handleAcceptActivity()
|
||||
{
|
||||
$actor = $this->payload['actor'];
|
||||
$obj = $this->payload['object'];
|
||||
switch ($obj['type']) {
|
||||
case 'Follow':
|
||||
$accept = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $target->permalink().'#accepts/follows/' . $follower->id,
|
||||
'type' => 'Accept',
|
||||
'actor' => $target->permalink(),
|
||||
'object' => [
|
||||
'id' => $actor->permalink('#follows/'.$target->id),
|
||||
'type' => 'Follow',
|
||||
'actor' => $actor->permalink(),
|
||||
'object' => $target->permalink()
|
||||
]
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
$actor = $this->payload['object']['actor'];
|
||||
$obj = $this->payload['object']['object'];
|
||||
$type = $this->payload['object']['type'];
|
||||
|
||||
if($type !== 'Follow') {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = Helpers::validateLocalUrl($actor);
|
||||
$target = Helpers::validateUrl($obj);
|
||||
|
||||
if(!$actor || !$target) {
|
||||
return;
|
||||
}
|
||||
$actor = Helpers::profileFetch($actor);
|
||||
$target = Helpers::profileFetch($target);
|
||||
|
||||
$request = FollowRequest::whereFollowerId($actor->id)
|
||||
->whereFollowingId($target->id)
|
||||
->whereIsRejected(false)
|
||||
->first();
|
||||
|
||||
if(!$request) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower = Follower::firstOrCreate([
|
||||
'profile_id' => $actor->id,
|
||||
'following_id' => $target->id,
|
||||
]);
|
||||
FollowPipeline::dispatch($follower);
|
||||
|
||||
$request->delete();
|
||||
}
|
||||
|
||||
public function handleDeleteActivity()
|
||||
|
|
|
@ -16,15 +16,15 @@ class Accept {
|
|||
'required',
|
||||
Rule::in(['Accept'])
|
||||
],
|
||||
'actor' => 'required|url|active_url',
|
||||
'actor' => 'required|url',
|
||||
'object' => 'required',
|
||||
'object.id' => 'required|url|active_url',
|
||||
'object.id' => 'required|url',
|
||||
'object.type' => [
|
||||
'required',
|
||||
Rule::in(['Follow'])
|
||||
],
|
||||
'object.actor' => 'required|url|active_url',
|
||||
'object.object' => 'required|url|active_url|same:actor',
|
||||
'object.actor' => 'required|url',
|
||||
'object.object' => 'required|url|same:actor',
|
||||
])->passes();
|
||||
|
||||
return $valid;
|
||||
|
|
|
@ -264,7 +264,9 @@ class Extractor extends Regex
|
|||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(mb_strlen($hashtag[0]) > 124) {
|
||||
continue;
|
||||
}
|
||||
$tags[] = [
|
||||
'hashtag' => $hashtag[0],
|
||||
'indices' => [$start_position, $end_position],
|
||||
|
|
|
@ -5,10 +5,6 @@ namespace App\Util\Lexer;
|
|||
class RestrictedNames
|
||||
{
|
||||
public static $blacklist = [
|
||||
'about',
|
||||
'abuse',
|
||||
'administrator',
|
||||
'app',
|
||||
'autoconfig',
|
||||
'blog',
|
||||
'broadcasthost',
|
||||
|
@ -97,7 +93,11 @@ class RestrictedNames
|
|||
|
||||
// Reserved routes
|
||||
'a',
|
||||
'app',
|
||||
'about',
|
||||
'abuse',
|
||||
'account',
|
||||
'admins',
|
||||
'api',
|
||||
'audio',
|
||||
'auth',
|
||||
|
@ -124,6 +124,7 @@ class RestrictedNames
|
|||
'css',
|
||||
'd',
|
||||
'dashboard',
|
||||
'dmca',
|
||||
'db',
|
||||
'deck',
|
||||
'dev',
|
||||
|
@ -136,15 +137,27 @@ class RestrictedNames
|
|||
'docs',
|
||||
'docs',
|
||||
'drive',
|
||||
'drives',
|
||||
'driver',
|
||||
'e',
|
||||
'error',
|
||||
'explore',
|
||||
'export',
|
||||
'exports',
|
||||
'f',
|
||||
'feed',
|
||||
'font',
|
||||
'fonts',
|
||||
'follow',
|
||||
'follows',
|
||||
'followme',
|
||||
'follow-me',
|
||||
'follow_me',
|
||||
'g',
|
||||
'gdpr',
|
||||
'graph',
|
||||
'ghost',
|
||||
'ghosts',
|
||||
'group',
|
||||
'groups',
|
||||
'h',
|
||||
|
@ -164,7 +177,12 @@ class RestrictedNames
|
|||
'images',
|
||||
'invite',
|
||||
'invites',
|
||||
'import',
|
||||
'imports',
|
||||
'j',
|
||||
'js',
|
||||
'k',
|
||||
'key',
|
||||
'l',
|
||||
'lab',
|
||||
'labs',
|
||||
|
@ -186,6 +204,7 @@ class RestrictedNames
|
|||
'news',
|
||||
'news',
|
||||
'newsfeed',
|
||||
'o',
|
||||
'oauth',
|
||||
'official',
|
||||
'p',
|
||||
|
@ -197,13 +216,23 @@ class RestrictedNames
|
|||
'photos',
|
||||
'password',
|
||||
'privacy',
|
||||
'private',
|
||||
'q',
|
||||
'quote',
|
||||
'query',
|
||||
'r',
|
||||
'register',
|
||||
'registers',
|
||||
'review',
|
||||
'reset',
|
||||
'report',
|
||||
'results',
|
||||
'reports',
|
||||
'robot',
|
||||
'robots',
|
||||
's',
|
||||
'search',
|
||||
'sell',
|
||||
'send',
|
||||
'settings',
|
||||
'status',
|
||||
|
@ -217,20 +246,24 @@ class RestrictedNames
|
|||
'support',
|
||||
'svg',
|
||||
'svgs',
|
||||
't',
|
||||
'terms',
|
||||
'telescope',
|
||||
'timeline',
|
||||
'timelines',
|
||||
'tour',
|
||||
'tv',
|
||||
'u',
|
||||
'user',
|
||||
'users',
|
||||
'username',
|
||||
'usernames',
|
||||
'v',
|
||||
'valet',
|
||||
'video',
|
||||
'videos',
|
||||
'vendor',
|
||||
'w',
|
||||
'waiter',
|
||||
'wall',
|
||||
'whats-new',
|
||||
|
@ -240,7 +273,9 @@ class RestrictedNames
|
|||
'ws',
|
||||
'wss',
|
||||
'www',
|
||||
'valet',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
'400',
|
||||
'401',
|
||||
'403',
|
||||
|
|
|
@ -110,14 +110,15 @@ class Image
|
|||
$orientation = $ratio['orientation'];
|
||||
|
||||
try {
|
||||
$img = Intervention::make($file)->orientate();
|
||||
$img = Intervention::make($file);
|
||||
$metadata = $img->exif();
|
||||
$img->orientate();
|
||||
if($thumbnail) {
|
||||
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
} else {
|
||||
if(config('media.exif.database', false) == true) {
|
||||
$metadata = $img->exif();
|
||||
$media->metadata = json_encode($metadata);
|
||||
}
|
||||
|
||||
|
@ -130,7 +131,7 @@ class Image
|
|||
|
||||
$quality = config('pixelfed.image_quality');
|
||||
$img->save($newPath, $quality);
|
||||
|
||||
$img->destroy();
|
||||
if (!$thumbnail) {
|
||||
$media->orientation = $orientation;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ trait User {
|
|||
|
||||
public function getMaxPostsPerHourAttribute()
|
||||
{
|
||||
return 20;
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function getMaxPostsPerDayAttribute()
|
||||
|
@ -49,8 +49,38 @@ trait User {
|
|||
return 500;
|
||||
}
|
||||
|
||||
public function getMaxUserBansPerDayAttribute()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getMaxInstanceBansPerDayAttribute()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getMaxHashtagFollowsPerHourAttribute()
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
public function getMaxHashtagFollowsPerDayAttribute()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getMaxCollectionsPerHourAttribute()
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
public function getMaxCollectionsPerDayAttribute()
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
public function getMaxCollectionsPerMonthAttribute()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
47
app/Util/Site/Config.php
Normal file
47
app/Util/Site/Config.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\Site;
|
||||
|
||||
use Cache;
|
||||
|
||||
class Config {
|
||||
|
||||
public static function get() {
|
||||
return Cache::remember('api:site:configuration', now()->addMinutes(30), function() {
|
||||
return [
|
||||
'uploader' => [
|
||||
'max_photo_size' => config('pixelfed.max_photo_size'),
|
||||
'max_caption_length' => config('pixelfed.max_caption_length'),
|
||||
'album_limit' => config('pixelfed.max_album_length'),
|
||||
'image_quality' => config('pixelfed.image_quality'),
|
||||
|
||||
'optimize_image' => config('pixelfed.optimize_image'),
|
||||
'optimize_video' => config('pixelfed.optimize_video'),
|
||||
|
||||
'media_types' => config('pixelfed.media_types'),
|
||||
'enforce_account_limit' => config('pixelfed.enforce_account_limit')
|
||||
],
|
||||
|
||||
'activitypub' => [
|
||||
'enabled' => config('federation.activitypub.enabled'),
|
||||
'remote_follow' => config('federation.activitypub.remoteFollow')
|
||||
],
|
||||
|
||||
'ab' => [
|
||||
'lc' => config('exp.lc'),
|
||||
'rec' => config('exp.rec'),
|
||||
'loops' => config('exp.loops')
|
||||
],
|
||||
|
||||
'site' => [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'url' => config('app.url')
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public static function json() {
|
||||
return json_encode(self::get(), JSON_FORCE_OBJECT);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WebSub extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -23,6 +23,7 @@
|
|||
"laravel/tinker": "^1.0",
|
||||
"league/flysystem-aws-s3-v3": "~1.0",
|
||||
"league/flysystem-cached-adapter": "~1.0",
|
||||
"league/iso3166": "^2.1",
|
||||
"moontoast/math": "^1.1",
|
||||
"pbmedia/laravel-ffmpeg": "4.0.0",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
|
@ -38,10 +39,12 @@
|
|||
"stevebauman/purify": "2.0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
"fzaninotto/faker": "^1.4",
|
||||
"mockery/mockery": "^1.0",
|
||||
"nunomaduro/collision": "^2.0",
|
||||
"nunomaduro/phpinsights": "^1.7",
|
||||
"phpunit/phpunit": "^7.5"
|
||||
},
|
||||
"autoload": {
|
||||
|
|
3595
composer.lock
generated
3595
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -150,7 +150,6 @@ return [
|
|||
/*
|
||||
* Package Service Providers...
|
||||
*/
|
||||
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
|
@ -211,7 +210,6 @@ return [
|
|||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
|
||||
'PrettyNumber' => App\Util\Lexer\PrettyNumber::class,
|
||||
'Purify' => Stevebauman\Purify\Facades\Purify::class,
|
||||
'FFMpeg' => Pbmedia\LaravelFFMpeg\FFMpegFacade::class,
|
||||
|
|
|
@ -73,6 +73,8 @@ return [
|
|||
'client' => 'predis',
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
'path' => env('REDIS_PATH'),
|
||||
'host' => env('REDIS_HOST', 'localhost'),
|
||||
'password' => env('REDIS_PASSWORD', null),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
|
|
|
@ -109,6 +109,8 @@ return [
|
|||
'client' => 'predis',
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
'path' => env('REDIS_PATH'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'password' => env('REDIS_PASSWORD', null),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
|
|
|
@ -17,7 +17,7 @@ return [
|
|||
'inbox' => env('AP_INBOX', true),
|
||||
'sharedInbox' => env('AP_SHAREDINBOX', false),
|
||||
|
||||
'remoteFollow' => false,
|
||||
'remoteFollow' => env('AP_REMOTE_FOLLOW', false),
|
||||
|
||||
'delivery' => [
|
||||
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
|
||||
|
|
|
@ -1,15 +1,43 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'email' => env('INSTANCE_CONTACT_EMAIL'),
|
||||
|
||||
'announcement' => [
|
||||
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', false),
|
||||
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
|
||||
],
|
||||
|
||||
'contact' => [
|
||||
'enabled' => env('INSTANCE_CONTACT_FORM', false),
|
||||
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1),
|
||||
],
|
||||
|
||||
'announcement' => [
|
||||
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true),
|
||||
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
|
||||
]
|
||||
'discover' => [
|
||||
'loops' => [
|
||||
'enabled' => false
|
||||
],
|
||||
'tags' => [
|
||||
'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false)
|
||||
],
|
||||
],
|
||||
|
||||
'email' => env('INSTANCE_CONTACT_EMAIL'),
|
||||
|
||||
'timeline' => [
|
||||
'local' => [
|
||||
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', true)
|
||||
]
|
||||
],
|
||||
|
||||
'page' => [
|
||||
'404' => [
|
||||
'header' => env('PAGE_404_HEADER', 'Sorry, this page isn\'t available.'),
|
||||
'body' => env('PAGE_404_BODY', 'The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a>')
|
||||
],
|
||||
'503' => [
|
||||
'header' => env('PAGE_503_HEADER', 'Service Unavailable'),
|
||||
'body' => env('PAGE_503_BODY', 'Our service is in maintenance mode, please try again later.')
|
||||
]
|
||||
],
|
||||
|
||||
];
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your Pixelfed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.9.4',
|
||||
'version' => '0.10.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM php:7.3-apache
|
||||
FROM php:7.3-apache-buster
|
||||
|
||||
ARG COMPOSER_VERSION="1.8.5"
|
||||
ARG COMPOSER_CHECKSUM="4e4c1cd74b54a26618699f3190e6f5fc63bb308b13fa660f71f2a2df047c0e17"
|
||||
|
@ -7,13 +7,13 @@ RUN apt-get update \
|
|||
&& apt-get install -y --no-install-recommends apt-utils \
|
||||
&& apt-get install -y --no-install-recommends git gosu \
|
||||
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
|
||||
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-3 \
|
||||
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
|
||||
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
|
||||
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
|
||||
&& locale-gen && update-locale \
|
||||
&& docker-php-source extract \
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--enable-freetype \
|
||||
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--with-webp-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
FROM php:7.2-fpm
|
||||
FROM php:7.3-fpm-buster
|
||||
|
||||
ARG COMPOSER_VERSION="1.8.5"
|
||||
ARG COMPOSER_CHECKSUM="4e4c1cd74b54a26618699f3190e6f5fc63bb308b13fa660f71f2a2df047c0e17"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends apt-utils \
|
||||
&& apt-get install -y --no-install-recommends git gosu \
|
||||
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev \
|
||||
libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
|
||||
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
|
||||
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
|
||||
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
|
||||
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
|
||||
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
|
||||
&& locale-gen && update-locale \
|
||||
&& docker-php-source extract \
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--enable-freetype \
|
||||
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
--with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip \
|
||||
--with-webp-dir=/usr/lib/x86_64-linux-gnu/ \
|
||||
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip curl \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick pcntl imagick gd exif zip \
|
||||
&& docker-php-ext-enable imagick pcntl imagick gd exif zip curl \
|
||||
&& curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
|
||||
&& echo "${COMPOSER_CHECKSUM} /usr/bin/composer" | sha256sum -c - \
|
||||
&& chmod 755 /usr/bin/composer \
|
||||
|
@ -32,7 +33,7 @@ ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
|
|||
COPY . /var/www/
|
||||
|
||||
WORKDIR /var/www/
|
||||
RUN cp -r storage storage.skel \
|
||||
RUN mkdir public.ext && cp -r storage storage.skel \
|
||||
&& cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
|
||||
&& composer install --prefer-dist --no-interaction \
|
||||
&& rm -rf html && ln -s public html
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
file_uploads = On
|
||||
memory_limit = 64M
|
||||
memory_limit = 128M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 600
|
||||
|
|
|
@ -1,22 +1,49 @@
|
|||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name localhost;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name pixelfed.example; # change this to your fqdn
|
||||
root /home/pixelfed/public; # path to repo/public
|
||||
|
||||
index index.php index.html;
|
||||
root /var/www/html/public;
|
||||
ssl_certificate /etc/nginx/ssl/server.crt; # generate your own
|
||||
ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /$is_args$args;
|
||||
}
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.html index.htm index.php;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/run/php-fpm/php-fpm.sock; # make sure this is correct
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # or $request_filename
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server { # Redirect http to https
|
||||
server_name pixelfed.example; # change this to your fqdn
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateHashtagFollowsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('hashtag_follows', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->bigInteger('hashtag_id')->unsigned()->index();
|
||||
$table->unique(['user_id', 'profile_id', 'hashtag_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('hashtag_follows');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddStatusVisibilityToStatusHashtagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('status_hashtags', function (Blueprint $table) {
|
||||
$table->string('status_visibility')->nullable()->index()->after('profile_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('status_hashtags', function (Blueprint $table) {
|
||||
$table->dropColumn('status_visibility');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateProfileSponsorsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('profile_sponsors', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('profile_id')->unsigned()->unique()->index();
|
||||
$table->json('sponsors')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('profile_sponsors');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class RemoveWebSubsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('web_subs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_subs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreatePlacesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('places', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('slug')->index();
|
||||
$table->string('name')->index();
|
||||
$table->string('country')->index();
|
||||
$table->json('aliases')->nullable();
|
||||
$table->decimal('lat', 9, 6)->nullable();
|
||||
$table->decimal('long', 9, 6)->nullable();
|
||||
$table->unique(['slug', 'country', 'lat', 'long']);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::table('statuses', function (Blueprint $table) {
|
||||
$table->bigInteger('place_id')->unsigned()->nullable()->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('places');
|
||||
Schema::table('statuses', function (Blueprint $table) {
|
||||
$table->dropColumn('place_id');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddUniqueToStatusesTable extends Migration
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('statuses', function (Blueprint $table) {
|
||||
$table->string('uri')->nullable()->unique()->index()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('statuses', function (Blueprint $table) {
|
||||
$table->string('uri')->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
1019
package-lock.json
generated
1019
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "pixelfed",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
|
@ -15,26 +16,28 @@
|
|||
"bootstrap": ">=4.3.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"jquery": "^3.4.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": ">=4.17.13",
|
||||
"popper.js": "^1.15.0",
|
||||
"resolve-url-loader": "^2.3.2",
|
||||
"sass": "^1.21.0",
|
||||
"sass": "^1.22.3",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-masonry-css": "^1.0.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap-vue": "^2.0.0-rc.23",
|
||||
"@trevoreyre/autocomplete-vue": "^2.0.2",
|
||||
"bootstrap-vue": "^2.0.0-rc.26",
|
||||
"emoji-mart-vue": "^2.6.6",
|
||||
"filesize": "^3.6.1",
|
||||
"howler": "^2.1.2",
|
||||
"infinite-scroll": "^3.0.6",
|
||||
"laravel-echo": "^1.5.4",
|
||||
"laravel-mix": "^4.0.16",
|
||||
"laravel-mix": "^4.1.2",
|
||||
"node-sass": "^4.12.0",
|
||||
"opencollective": "^1.0.3",
|
||||
"opencollective-postinstall": "^2.0.2",
|
||||
"plyr": "^3.5.4",
|
||||
"plyr": "^3.5.6",
|
||||
"promise-polyfill": "8.1.0",
|
||||
"pusher-js": "^4.4.0",
|
||||
"quill": "^1.3.6",
|
||||
|
@ -43,6 +46,7 @@
|
|||
"sweetalert": "^2.1.2",
|
||||
"twitter-text": "^2.0.5",
|
||||
"vue-content-loader": "^0.2.2",
|
||||
"vue-cropperjs": "^4.0.0",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
"vue-loading-overlay": "^3.2.0",
|
||||
"vue-timeago": "^5.1.2"
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/css/appdark.css
vendored
BIN
public/css/appdark.css
vendored
Binary file not shown.
BIN
public/css/landing.css
vendored
BIN
public/css/landing.css
vendored
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 4 KiB |
BIN
public/js/ace.js
vendored
BIN
public/js/ace.js
vendored
Binary file not shown.
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/collectioncompose.js
vendored
Normal file
BIN
public/js/collectioncompose.js
vendored
Normal file
Binary file not shown.
BIN
public/js/collections.js
vendored
Normal file
BIN
public/js/collections.js
vendored
Normal file
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue