Merge pull request #780 from pixelfed/frontend-ui-refactor

v0.8.0rc1
This commit is contained in:
daniel 2019-02-11 00:15:57 -07:00 committed by GitHub
commit afc758764c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
198 changed files with 7395 additions and 8839 deletions

View file

@ -53,3 +53,14 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_APP_URL="${APP_URL}"
MIX_API_BASE="${API_BASE}"
MIX_API_SEARCH="${API_SEARCH}"
ACTIVITYPUB_INBOX=false
ACTIVITYPUB_SHAREDINBOX=false
# Set these both "true" to enable federation.
# You might need to also run:
# php artisan cache:clear
# php artisan optimize:clear
# php artisan optimize
ACTIVITY_PUB=false
REMOTE_FOLLOW=false

View file

@ -40,7 +40,7 @@ SESSION_SECURE_COOKIE=true
API_BASE="/api/1/"
API_SEARCH="/api/search"
OPEN_REGISTRATION=true
OPEN_REGISTRATION=false
RECAPTCHA_ENABLED=false
ENFORCE_EMAIL_VERIFICATION=true
@ -55,3 +55,4 @@ MIX_API_BASE="${API_BASE}"
MIX_API_SEARCH="${API_SEARCH}"
TELESCOPE_ENABLED=false
PF_MAX_USERS=1000

103
README.md
View file

@ -1,66 +1,27 @@
# PixelFed: Federated Image Sharing
[![Backers on Open Collective](https://opencollective.com/pixelfed-528/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/pixelfed-528/sponsors/badge.svg)](#sponsors)
<p align="center"><img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/logos/pixelfed-full-color.svg" width="300px"></p>
PixelFed is a federated social image sharing platform, similar to Instagram.
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
and interact with these platforms, as well as other instances of PixelFed.
<p align="center">
<a href="https://circleci.com/gh/pixelfed/pixelfed"><img src="https://circleci.com/gh/pixelfed/pixelfed.svg?style=svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/license.svg" alt="License"></a>
</p>
**_Please note this is alpha software, not recommended for production use,
and federation is not supported yet._**
## Introduction
PixelFed is very early into the development stage. If you would like to have a
permanent instance with minimal breakage, **do not use this software until
there is a stable release**. The following setup instructions are intended for
testing and development.
A free and ethical photo sharing platform, powered by ActivityPub federation.
## Requirements
- PHP >= 7.1.3 < 7.3 (7.2.x recommended for stable version)
- MySQL >= 5.7 (Postgres, MariaDB and sqlite are not supported)
- Redis
- Composer
- GD or ImageMagick
- OpenSSL PHP Extension
- PDO PHP Extension
- Mbstring PHP Extension
- Tokenizer PHP Extension
- XML PHP Extension
- Ctype PHP Extension
- JSON PHP Extension
- BCMath PHP Extension
- JpegOptim
- Optipng
- Pngquant 2
- SVGO
- Gifsicle
<p align="center">
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/Screen%20Shot%202019-02-05%20at%206.34.59%20PM.png">
</p>
## Installation
## Official Documentation
This guide assumes you have NGINX/Apache installed, along with the dependencies.
Those will not be covered in these early docs.
```bash
git clone https://github.com/pixelfed/pixelfed.git
cd pixelfed
composer install
cp .env.example .env
```
**Edit .env file with proper values**
```bash
php artisan key:generate
```
```bash
php artisan storage:link
php artisan migrate
php artisan horizon
```
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://pixelfed.github.io/docs/master/).
## License
Pixelfed is open-sourced software licensed under the AGPL license.
## Communication
@ -68,7 +29,7 @@ The ways you can communicate on the project are below. Before interacting, pleas
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
* IRC: #pixelfed on irc.freenode.net ([#freenode_#pixelfed:matrix.org through
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org))
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
@ -80,29 +41,27 @@ https://www.patreon.com/dansup
### Contributors
This project exists thanks to all the people who contribute.
<a href="https://github.com/pixelfed/pixelfed/graphs/contributors"><img src="https://opencollective.com/pixelfed-528/contributors.svg?width=890&button=false" /></a>
<a href="https://github.com/pixelfed/pixelfed/graphs/contributors"><img src="https://opencollective.com/pixelfed/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/pixelfed-528#backer)]
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/pixelfed#backer)]
<a href="https://opencollective.com/pixelfed-528#backers" target="_blank"><img src="https://opencollective.com/pixelfed-528/backers.svg?width=890"></a>
<a href="https://opencollective.com/pixelfed#backers" target="_blank"><img src="https://opencollective.com/pixelfed/backers.svg?width=890"></a>
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/pixelfed-528#sponsor)]
<a href="https://opencollective.com/pixelfed-528/sponsor/0/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/1/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/2/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/3/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/4/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/5/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/6/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/7/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/8/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed-528/sponsor/9/website" target="_blank"><img src="https://opencollective.com/pixelfed-528/sponsor/9/avatar.svg"></a>
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/pixelfed#sponsor)]
<a href="https://opencollective.com/pixelfed/sponsor/0/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/1/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/2/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/3/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/4/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/5/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/6/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/7/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/8/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/pixelfed/sponsor/9/website" target="_blank"><img src="https://opencollective.com/pixelfed/sponsor/9/avatar.svg"></a>

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class AccountLog extends Model
{
//
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -7,4 +7,14 @@ use Illuminate\Database\Eloquent\Model;
class Activity extends Model
{
protected $dates = ['processed_at'];
public function toProfile()
{
return $this->belongsTo(Profile::class, 'to_id');
}
public function fromProfile()
{
return $this->belongsTo(Profile::class, 'from_id');
}
}

View file

@ -6,5 +6,16 @@ use Illuminate\Database\Eloquent\Model;
class Bookmark extends Model
{
protected $fillable = ['profile_id', 'status_id'];
protected $fillable = ['profile_id', 'status_id'];
public function status()
{
return $this->belongsTo(Status::class);
}
public function profile()
{
return $this->belongsTo(Profile::class);
}
}

38
app/Circle.php Normal file
View file

@ -0,0 +1,38 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Circle extends Model
{
protected $fillable = [
'name',
'description',
'bcc',
'scope',
'active'
];
public function members()
{
return $this->hasManyThrough(
Profile::class,
CircleProfile::class,
'circle_id',
'id',
'id',
'profile_id'
);
}
public function owner()
{
return $this->belongsTo(Profile::class, 'profile_id');
}
public function url()
{
return url("/i/circle/show/{$this->id}");
}
}

13
app/CircleProfile.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class CircleProfile extends Model
{
protected $fillable = [
'circle_id',
'profile_id'
];
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class Collection extends Model
{
//
public function profile()
{
return $this->belongsTo(Profile::class);
}
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class CollectionItem extends Model
{
//
public function collection()
{
return $this->belongsTo(Collection::class);
}
}

64
app/DiscoverCategory.php Normal file
View file

@ -0,0 +1,64 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\{Status, StatusHashtag};
class DiscoverCategory extends Model
{
protected $fillable = ['slug'];
public function media()
{
return $this->belongsTo(Media::class);
}
public function url()
{
return url('/discover/c/'.$this->slug);
}
public function editUrl()
{
return url('/i/admin/discover/category/edit/' . $this->id);
}
public function thumb()
{
return $this->media->thumb();
}
public function mediaUrl()
{
return $this->media->url();
}
public function items()
{
return $this->hasMany(DiscoverCategoryHashtag::class, 'discover_category_id');
}
public function hashtags()
{
return $this->hasManyThrough(
Hashtag::class,
DiscoverCategoryHashtag::class,
'discover_category_id',
'id',
'id',
'hashtag_id'
);
}
public function posts()
{
return Status::select('*')
->join('status_hashtags', 'statuses.id', '=', 'status_hashtags.status_id')
->join('hashtags', 'status_hashtags.hashtag_id', '=', 'hashtags.id')
->join('discover_category_hashtags', 'hashtags.id', '=', 'discover_category_hashtags.hashtag_id')
->join('discover_categories', 'discover_category_hashtags.discover_category_id', '=', 'discover_categories.id')
->where('discover_categories.id', $this->id);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class DiscoverCategoryHashtag extends Model
{
protected $fillable = [
'discover_category_id',
'hashtag_id'
];
}

View file

@ -13,4 +13,9 @@ class EmailVerification extends Model
return "{$base}{$path}";
}
public function user()
{
return $this->belongsTo(User::class);
}
}

19
app/FailedJob.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class FailedJob extends Model
{
const CREATED_AT = 'failed_at';
const UPDATED_AT = 'failed_at';
public $timestamps = 'failed_at';
public function getFailedAtAttribute($val)
{
return Carbon::parse($val);
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{
DiscoverCategory,
DiscoverCategoryHashtag,
Hashtag,
Media,
Profile,
StatusHashtag
};
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
trait AdminDiscoverController
{
public function discoverHome()
{
$categories = DiscoverCategory::orderByDesc('id')->paginate(10);
return view('admin.discover.home', compact('categories'));
}
public function discoverCreateCategory()
{
return view('admin.discover.create-category');
}
public function discoverCreateCategoryStore(Request $request)
{
$this->validate($request, [
'name' => 'required|string|min:1',
'active' => 'required|boolean',
'media' => 'nullable|integer|min:1'
]);
$name = $request->input('name');
$slug = str_slug($name);
$active = $request->input('active');
$media = (int) $request->input('media');
$media = Media::findOrFail($media);
$category = DiscoverCategory::firstOrNew(['slug' => $slug]);
$category->name = $name;
$category->active = $active;
$category->media_id = $media->id;
$category->save();
return $category;
}
public function discoverCategoryEdit(Request $request, $id)
{
$category = DiscoverCategory::findOrFail($id);
return view('admin.discover.show', compact('category'));
}
public function discoverCategoryUpdate(Request $request, $id)
{
$this->validate($request, [
'name' => 'required|string|min:1',
'active' => 'required|boolean',
'media' => 'nullable|integer|min:1',
'hashtags' => 'nullable|string'
]);
$name = $request->input('name');
$slug = str_slug($name);
$active = $request->input('active');
$media = (int) $request->input('media');
$media = Media::findOrFail($media);
$category = DiscoverCategory::findOrFail($id);
$category->name = $name;
$category->active = $active;
$category->media_id = $media->id;
$category->save();
return $category;
}
public function discoveryCategoryTagStore(Request $request)
{
$this->validate($request, [
'category_id' => 'required|integer|min:1',
'hashtag' => 'required|string',
'action' => 'required|string|min:1|max:6'
]);
$category_id = $request->input('category_id');
$category = DiscoverCategory::findOrFail($category_id);
$hashtag = Hashtag::whereName($request->input('hashtag'))->firstOrFail();
$tag = DiscoverCategoryHashtag::firstOrCreate([
'hashtag_id' => $hashtag->id,
'discover_category_id' => $category->id
]);
if($request->input('action') == 'delete') {
$tag->delete();
return [];
}
return $tag;
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{Instance, Profile};
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
trait AdminInstanceController
{
public function instances(Request $request)
{
$this->validate($request, [
'filter' => [
'nullable',
'string',
'min:1',
'max:20',
Rule::in(['autocw', 'unlisted', 'banned'])
],
]);
if($request->has('filter') && $request->filled('filter')) {
switch ($request->filter) {
case 'autocw':
$instances = Instance::whereAutoCw(true)->orderByDesc('id')->paginate(5);
break;
case 'unlisted':
$instances = Instance::whereUnlisted(true)->orderByDesc('id')->paginate(5);
break;
case 'banned':
$instances = Instance::whereBanned(true)->orderByDesc('id')->paginate(5);
break;
}
} else {
$instances = Instance::orderByDesc('id')->paginate(5);
}
return view('admin.instances.home', compact('instances'));
}
public function instanceScan(Request $request)
{
DB::transaction(function() {
Profile::whereNotNull('domain')
->groupBy('domain')
->chunk(50, function($domains) {
foreach($domains as $domain) {
Instance::firstOrCreate([
'domain' => $domain->domain
]);
}
});
});
return redirect()->back();
}
public function instanceShow(Request $request, $id)
{
$instance = Instance::findOrFail($id);
return view('admin.instances.show', compact('instance'));
}
public function instanceEdit(Request $request, $id)
{
$this->validate($request, [
'action' => [
'required',
'string',
'min:1',
'max:20',
Rule::in(['autocw', 'unlist', 'ban'])
],
]);
$instance = Instance::findOrFail($id);
$unlisted = $instance->unlisted;
$autocw = $instance->auto_cw;
$banned = $instance->banned;
switch ($request->action) {
case 'autocw':
$instance->auto_cw = $autocw == true ? false : true;
$instance->save();
break;
case 'unlist':
$instance->unlisted = $unlisted == true ? false : true;
$instance->save();
break;
case 'ban':
$instance->banned = $banned == true ? false : true;
$instance->save();
break;
}
return response()->json([]);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{
Media,
Profile,
Status
};
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
trait AdminMediaController
{
public function media(Request $request)
{
$this->validate($request, [
'layout' => [
'nullable',
'string',
'min:1',
'max:4',
Rule::in(['grid','list'])
],
'search' => 'nullable|string|min:1|max:20'
]);
if($request->filled('search')) {
$profiles = Profile::where('username', 'like', '%'.$request->input('search').'%')->pluck('id')->toArray();
$media = Media::whereHas('status')
->with('status')
->orderby('id', 'desc')
->whereIn('profile_id', $profiles)
->orWhere('mime', $request->input('search'))
->paginate(12);
} else {
$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
}
return view('admin.media.home', compact('media'));
}
public function mediaShow(Request $request, $id)
{
$media = Media::findOrFail($id);
return view('admin.media.show', compact('media'));
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\Admin;
use Artisan, Cache, DB;
use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Http\Controllers\Controller;
use Jackiedo\DotenvEditor\Facades\DotenvEditor;
use App\Util\Lexer\PrettyNumber;
trait AdminSettingsController
{
public function settings(Request $request)
{
return view('admin.settings.home');
}
public function settingsBackups(Request $request)
{
$path = storage_path('app/PixelFed');
$files = new \DirectoryIterator($path);
return view('admin.settings.backups', compact('files'));
}
public function settingsConfig(Request $request, DotenvEditor $editor)
{
return view('admin.settings.config', compact('editor'));
}
public function settingsMaintenance(Request $request)
{
return view('admin.settings.maintenance');
}
public function settingsStorage(Request $request)
{
$databaseSum = Cache::remember('admin:settings:storage:db:storageUsed', 360, function() {
$q = 'SELECT sum(ROUND(((data_length + index_length)), 0)) AS size FROM information_schema.TABLES WHERE table_schema = ?';
$db = config('database.default');
$db = config("database.connections.{$db}.database");
return DB::select($q, [$db])[0]->size;
});
$mediaSum = Cache::remember('admin:settings:storage:media:storageUsed', 360, function() {
return Media::sum('size');
});
$backupSum = Cache::remember('admin:settings:storage:backups:storageUsed', 360, function() {
$dir = storage_path('app/'.config('app.name'));
$size = 0;
foreach (glob(rtrim($dir, '/').'/*', GLOB_NOSORT) as $each) {
$size += is_file($each) ? filesize($each) : folderSize($each);
}
return $size;
});
$storage = new \StdClass;
$storage->total = disk_total_space(base_path());
$storage->free = disk_free_space(base_path());
$storage->prettyTotal = PrettyNumber::size($storage->total, false, false);
$storage->prettyFree = PrettyNumber::size($storage->free, false, false);
$storage->percentFree = ceil($storage->free / $storage->total * 100);
$storage->percentUsed = ceil(100 - $storage->percentFree);
$storage->media = [
'used' => $mediaSum,
'prettyUsed' => PrettyNumber::size($mediaSum),
'percentUsed' => ceil($mediaSum / $storage->total * 100)
];
$storage->backups = [
'used' => $backupSum
];
$storage->database = [
'used' => $databaseSum
];
return view('admin.settings.storage', compact('storage'));
}
public function settingsFeatures(Request $request)
{
return view('admin.settings.features');
}
public function settingsHomeStore(Request $request)
{
$this->validate($request, [
'APP_NAME' => 'required|string',
]);
Artisan::call('config:clear');
DotenvEditor::setKey('APP_NAME', $request->input('APP_NAME'));
DotenvEditor::save();
return redirect()->back();
}
public function settingsPages(Request $request)
{
$pages = Page::orderByDesc('updated_at')->paginate(10);
return view('admin.pages.home', compact('pages'));
}
public function settingsPageEdit(Request $request)
{
return view('admin.pages.edit');
}
public function settingsSystem(Request $request)
{
$sys = [
'pixelfed' => config('pixelfed.version'),
'mysql' => DB::select( DB::raw("select version()") )[0]->{'version()'},
'php' => phpversion(),
'redis' => explode(' ',exec('redis-cli -v'))[1],
];
return view('admin.settings.system', compact('sys'));
}
}

View file

@ -2,21 +2,38 @@
namespace App\Http\Controllers;
use App\Media;
use App\Like;
use App\Profile;
use App\Report;
use App\Status;
use App\User;
use App\{
FailedJob,
Hashtag,
Instance,
Media,
Like,
OauthClient,
Profile,
Report,
Status,
User
};
use DB, Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Jackiedo\DotenvEditor\DotenvEditor;
use App\Http\Controllers\Admin\AdminReportController;
use App\Http\Controllers\Admin\{
AdminDiscoverController,
AdminInstanceController,
AdminReportController,
AdminMediaController,
AdminSettingsController
};
use App\Util\Lexer\PrettyNumber;
class AdminController extends Controller
{
use AdminReportController;
use AdminReportController,
AdminDiscoverController,
AdminMediaController,
AdminSettingsController,
AdminInstanceController;
public function __construct()
{
@ -26,7 +43,55 @@ class AdminController extends Controller
public function home()
{
return view('admin.home');
$data = Cache::remember('admin:dashboard:home:data', 15, function() {
return [
'failedjobs' => [
'count' => PrettyNumber::convert(FailedJob::where('failed_at', '>=', \Carbon\Carbon::now()->subDay())->count()),
'graph' => FailedJob::selectRaw('count(*) as count, day(failed_at) as d')->groupBy('d')->whereBetween('failed_at',[now()->subDays(24), now()])->orderBy('d')->pluck('count')
],
'reports' => [
'count' => PrettyNumber::convert(Report::whereNull('admin_seen')->count()),
'graph' => Report::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'statuses' => [
'count' => PrettyNumber::convert(Status::whereNull('in_reply_to_id')->whereNull('reblog_of_id')->count()),
'graph' => Status::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'replies' => [
'count' => PrettyNumber::convert(Status::whereNotNull('in_reply_to_id')->count()),
'graph' => Status::whereNotNull('in_reply_to_id')->selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'shares' => [
'count' => PrettyNumber::convert(Status::whereNotNull('reblog_of_id')->count()),
'graph' => Status::whereNotNull('reblog_of_id')->selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'likes' => [
'count' => PrettyNumber::convert(Like::count()),
'graph' => Like::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'profiles' => [
'count' => PrettyNumber::convert(Profile::count()),
'graph' => Profile::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'users' => [
'count' => PrettyNumber::convert(User::count()),
'graph' => User::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'instances' => [
'count' => PrettyNumber::convert(Instance::count()),
'graph' => Instance::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(28), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'media' => [
'count' => PrettyNumber::convert(Media::count()),
'graph' => Media::selectRaw('count(*) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
],
'storage' => [
'count' => Media::sum('size'),
'graph' => Media::selectRaw('sum(size) as count, day(created_at) as day')->whereBetween('created_at',[now()->subDays(14), now()])->groupBy('day')->orderBy('day')->pluck('count')
]
];
});
return view('admin.home', compact('data'));
}
public function users(Request $request)
@ -35,6 +100,7 @@ class AdminController extends Controller
$dir = $request->query('dir') ?? 'desc';
$stats = $this->collectUserStats($request);
$users = User::withCount('statuses')->orderBy($col, $dir)->paginate(10);
return view('admin.users.home', compact('users', 'stats'));
}
@ -59,16 +125,23 @@ class AdminController extends Controller
return view('admin.statuses.show', compact('status'));
}
public function media(Request $request)
{
$media = Status::whereHas('media')->orderby('id', 'desc')->paginate(12);
return view('admin.media.home', compact('media'));
}
public function reports(Request $request)
{
$reports = Report::orderBy('created_at','desc')->paginate(12);
$filter = $request->input('filter');
if(in_array($filter, ['open', 'closed'])) {
if($filter == 'open') {
$reports = Report::orderBy('created_at','desc')
->whereNotNull('admin_seen')
->paginate(10);
} else {
$reports = Report::orderBy('created_at','desc')
->whereNull('admin_seen')
->paginate(10);
}
} else {
$reports = Report::orderBy('created_at','desc')
->paginate(10);
}
return view('admin.reports.home', compact('reports'));
}
@ -78,7 +151,6 @@ class AdminController extends Controller
return view('admin.reports.show', compact('report'));
}
protected function collectUserStats($request)
{
$total_duration = $request->query('total_duration') ?? '30';
@ -106,4 +178,35 @@ class AdminController extends Controller
return $stats;
}
public function profiles(Request $request)
{
$profiles = Profile::orderBy('id','desc')->paginate(10);
return view('admin.profiles.home', compact('profiles'));
}
public function appsHome(Request $request)
{
$filter = $request->input('filter');
if(in_array($filter, ['revoked'])) {
$apps = OauthClient::with('user')
->whereNotNull('user_id')
->whereRevoked(true)
->orderByDesc('id')
->paginate(10);
} else {
$apps = OauthClient::with('user')
->whereNotNull('user_id')
->orderByDesc('id')
->paginate(10);
}
return view('admin.apps.home', compact('apps'));
}
public function hashtagsHome(Request $request)
{
$hashtags = Hashtag::orderByDesc('id')->paginate(10);
return view('admin.hashtags.home', compact('hashtags'));
}
}

View file

@ -13,7 +13,8 @@ use App\{
Avatar,
Notification,
Media,
Profile
Profile,
Status
};
use App\Transformer\Api\{
AccountTransformer,
@ -23,6 +24,7 @@ use App\Transformer\Api\{
};
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\VideoPipeline\{
@ -97,13 +99,46 @@ class BaseApiController extends Controller
public function accountStatuses(Request $request, $id)
{
$pid = Auth::user()->profile->id;
$profile = Profile::findOrFail($id);
$statuses = $profile->statuses();
if($pid === $profile->id) {
$statuses = $statuses->orderBy('id', 'desc')->paginate(20);
$this->validate($request, [
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:1',
'since_id' => 'nullable|integer|min:1',
'min_id' => 'nullable|integer|min:1',
'limit' => 'nullable|integer|min:1|max:24'
]);
$limit = $request->limit ?? 20;
$max_id = $request->max_id ?? false;
$min_id = $request->min_id ?? false;
$since_id = $request->since_id ?? false;
$only_media = $request->only_media ?? false;
$user = Auth::user();
$account = Profile::findOrFail($id);
$statuses = $account->statuses()->getQuery();
if($only_media == true) {
$statuses = $statuses
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id');
}
if($id == $account->id && !$max_id && !$min_id && !$since_id) {
$statuses = $statuses->orderBy('id', 'desc')
->paginate($limit);
} else if($since_id) {
$statuses = $statuses->where('id', '>', $since_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else if($min_id) {
$statuses = $statuses->where('id', '>', $min_id)
->orderBy('id', 'ASC')
->paginate($limit);
} else if($max_id) {
$statuses = $statuses->where('id', '<', $max_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else {
$statuses = $statuses->whereVisibility('public')->orderBy('id', 'desc')->paginate(20);
$statuses = $statuses->whereVisibility('public')->orderBy('id', 'desc')->paginate($limit);
}
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
@ -265,4 +300,5 @@ class BaseApiController extends Controller
return response()->json($res);
}
}

View file

@ -116,7 +116,13 @@ class RegisterController extends Controller
*/
public function showRegistrationForm()
{
$view = config('pixelfed.open_registration') == true ? 'auth.register' : 'site.closed-registration';
$count = User::count();
$limit = config('pixelfed.max_users');
if($limit && $limit <= $count) {
$view = 'site.closed-registration';
} else {
$view = config('pixelfed.open_registration') == true ? 'auth.register' : 'site.closed-registration';
}
return view($view);
}
@ -128,7 +134,9 @@ class RegisterController extends Controller
*/
public function register(Request $request)
{
if(false == config('pixelfed.open_registration')) {
$count = User::count();
$limit = config('pixelfed.max_users');
if(false == config('pixelfed.open_registration') || $limit && $limit <= $count) {
return abort(403);
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Auth;
use App\{
Circle,
CircleProfile,
Profile,
Status,
};
class CircleController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function home(Request $request)
{
$circles = Circle::whereProfileId(Auth::user()->profile->id)
->orderByDesc('created_at')
->paginate(10);
return view('account.circles.home', compact('circles'));
}
public function create(Request $request)
{
return view('account.circles.create');
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|string|min:1',
'description' => 'nullable|string|max:255',
'scope' => [
'required',
'string',
Rule::in([
'public',
'private',
'unlisted',
'exclusive'
])
],
]);
$circle = Circle::firstOrCreate([
'profile_id' => Auth::user()->profile->id,
'name' => $request->input('name')
], [
'description' => $request->input('description'),
'scope' => $request->input('scope'),
'active' => false
]);
return redirect(route('account.circles'));
}
public function show(Request $request, $id)
{
$circle = Circle::findOrFail($id);
return view('account.circles.show', compact('circle'));
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CircleProfileController extends Controller
{
//
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DeckController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function home()
{
return view('deck.index');
}
public function insights()
{
return view('deck.insights.index');
}
}

View file

@ -20,11 +20,12 @@ class DirectMessageController extends Controller
public function inbox(Request $request)
{
$profile = Auth::user()->profile;
$inbox = DirectMessage::whereToId($profile->id)
$inbox = DirectMessage::selectRaw('*, max(created_at) as createdAt')
->whereToId($profile->id)
->with(['author','status'])
->orderBy('created_at', 'desc')
->groupBy('from_id')
->paginate(10);
->orderBy('createdAt', 'desc')
->groupBy('from_id')
->paginate(12);
return view('account.messages', compact('inbox'));
}
@ -40,10 +41,12 @@ class DirectMessageController extends Controller
$msg = DirectMessage::whereToId($profile->id)
->findOrFail($mid);
$thread = DirectMessage::whereToId($profile->id)
->orWhere([['from_id', $profile->id],['to_id', $msg->from_id]])
$thread = DirectMessage::whereIn('to_id', [$profile->id, $msg->from_id])
->whereIn('from_id', [$profile->id,$msg->from_id])
->orderBy('created_at', 'desc')
->paginate(10);
->paginate(30);
$thread = $thread->reverse();
return view('account.message', compact('msg', 'profile', 'thread'));
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DiscoverCategoryController extends Controller
{
//
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DiscoverCategoryHashtagController extends Controller
{
//
}

View file

@ -3,10 +3,12 @@
namespace App\Http\Controllers;
use App\{
DiscoverCategory,
Follower,
Hashtag,
Profile,
Status,
StatusHashtag,
UserFilter
};
use Auth, DB, Cache;
@ -28,7 +30,7 @@ class DiscoverController extends Controller
{
$this->validate($request, [
'page' => 'nullable|integer|min:1|max:10',
]);
]);
$tag = Hashtag::with('posts')
->withCount('posts')
@ -51,4 +53,30 @@ class DiscoverController extends Controller
return view('discover.tags.show', compact('tag', 'posts'));
}
public function showCategory(Request $request, $slug)
{
$tag = DiscoverCategory::whereActive(true)
->whereSlug($slug)
->firstOrFail();
// todo refactor this mess
$tagids = $tag->hashtags->pluck('id')->toArray();
$sids = StatusHashtag::whereIn('hashtag_id', $tagids)->orderByDesc('status_id')->take(500)->pluck('status_id')->toArray();
$posts = Status::whereIn('id', $sids)->whereNull('uri')->whereType('photo')->whereNull('in_reply_to_id')->whereNull('reblog_of_id')->orderByDesc('created_at')->paginate(21);
$tag->posts_count = $tag->posts()->count();
return view('discover.tags.category', compact('tag', 'posts'));
}
public function showPersonal(Request $request)
{
$profile = Auth::user()->profile;
// todo refactor this mess
$tags = Hashtag::whereHas('posts')->orderByRaw('rand()')->take(5)->get();
$following = $profile->following->pluck('id');
$following = $following->push($profile->id)->toArray();
$posts = Status::withCount(['likes','comments'])->whereNotIn('profile_id', $following)->whereHas('media')->whereType('photo')->orderByDesc('created_at')->paginate(21);
$posts->post_count = Status::whereNotIn('profile_id', $following)->whereHas('media')->whereType('photo')->count();
return view('discover.personal', compact('posts', 'tags'));
}
}

View file

@ -82,37 +82,38 @@ class FederationController extends Controller
{
$res = Cache::remember('api:nodeinfo', 60, function () {
return [
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed',
],
],
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub',
],
'services' => [
'inbound' => [],
'outbound' => [],
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version'),
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'users' => [
'total' => \App\User::count(),
'activeHalfyear' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(6)->toDateTimeString())->groupBy('user_id')->get()->count(),
'activeMonth' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(1)->toDateTimeString())->groupBy('user_id')->get()->count(),
],
],
'version' => '2.0',
];
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed',
],
'captcha' => (bool) config('pixelfed.recaptcha'),
],
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub',
],
'services' => [
'inbound' => [],
'outbound' => [],
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version'),
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'users' => [
'total' => \App\User::count(),
'activeHalfyear' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(6)->toDateTimeString())->groupBy('user_id')->get()->count(),
'activeMonth' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(1)->toDateTimeString())->groupBy('user_id')->get()->count(),
],
],
'version' => '2.0',
];
});
return response()->json($res, 200, [

View file

@ -39,6 +39,7 @@ class FollowerController extends Controller
$user = Auth::user()->profile;
$target = Profile::where('id', '!=', $user->id)->whereNull('status')->findOrFail($item);
$private = (bool) $target->is_private;
$remote = (bool) $target->domain;
$blocked = UserFilter::whereUserId($target->id)
->whereFilterType('block')
->whereFilterableId($user->id)
@ -51,7 +52,7 @@ class FollowerController extends Controller
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
if($private == true && $isFollowing == 0) {
if($private == true && $isFollowing == 0 || $remote == true) {
$follow = FollowRequest::firstOrCreate([
'follower_id' => $user->id,
'following_id' => $target->id

View file

@ -124,7 +124,7 @@ trait Instagram
->firstOrFail();
$media = $request->file('media');
$file = file_get_contents($media);
$json = json_decode($file, true);
$json = json_decode($file, true, 5);
if(!$json || !isset($json['photos'])) {
return abort(500);
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{
DirectMessage,
DiscoverCategory,
Hashtag,
Follower,
Like,
@ -25,6 +26,7 @@ use App\Transformer\Api\{
use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Illuminate\Validation\Rule;
class InternalApiController extends Controller
{
@ -199,14 +201,21 @@ class InternalApiController extends Controller
{
$profile = Auth::user()->profile;
$pid = $profile->id;
$following = Cache::remember('feature:discover:following:'.$pid, 60, function() use ($pid) {
$following = Cache::remember('feature:discover:following:'.$pid, 15, function() use ($pid) {
return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
});
$filters = Cache::remember("user:filter:list:$pid", 60, function() use($pid) {
return UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
$filters = Cache::remember("user:filter:list:$pid", 15, function() use($pid) {
$private = Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id')
->toArray();
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')
->toArray();
return array_merge($private, $filters);
});
$following = array_merge($following, $filters);
@ -281,4 +290,94 @@ class InternalApiController extends Controller
return response()->json($res);
}
public function stories(Request $request)
{
}
public function discoverCategories(Request $request)
{
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
$res = $categories->map(function($item) {
return [
'name' => $item->name,
'url' => $item->url(),
'thumb' => $item->thumb()
];
});
return response()->json($res);
}
public function modAction(Request $request)
{
abort_unless(Auth::user()->is_admin, 403);
$this->validate($request, [
'action' => [
'required',
'string',
Rule::in([
'autocw',
'noautolink',
'unlisted',
'disable',
'suspend'
])
],
'item_id' => 'required|integer|min:1',
'item_type' => [
'required',
'string',
Rule::in(['status'])
]
]);
$action = $request->input('action');
$item_id = $request->input('item_id');
$item_type = $request->input('item_type');
switch($action) {
case 'autocw':
$profile = $item_type == 'status' ? Status::findOrFail($item_id)->profile : null;
$profile->cw = true;
$profile->save();
break;
case 'noautolink':
$profile = $item_type == 'status' ? Status::findOrFail($item_id)->profile : null;
$profile->no_autolink = true;
$profile->save();
break;
case 'unlisted':
$profile = $item_type == 'status' ? Status::findOrFail($item_id)->profile : null;
$profile->unlisted = true;
$profile->save();
break;
case 'disable':
$profile = $item_type == 'status' ? Status::findOrFail($item_id)->profile : null;
$user = $profile->user;
$profile->status = 'disabled';
$user->status = 'disabled';
$profile->save();
$user->save();
break;
case 'suspend':
$profile = $item_type == 'status' ? Status::findOrFail($item_id)->profile : null;
$user = $profile->user;
$profile->status = 'suspended';
$user->status = 'suspended';
$profile->save();
$user->save();
break;
default:
# code...
break;
}
return ['msg' => 200];
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{
Profile,
Status,
};
use Auth, DB, Purify;
use Illuminate\Validation\Rule;
class MicroController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function composeText(Request $request)
{
$this->validate($request, [
'type' => [
'required',
'string',
Rule::in(['text'])
],
'title' => 'nullable|string|max:140',
'content' => 'required|string|max:500',
'visibility' => [
'required',
'string',
Rule::in([
'public',
'unlisted',
'private',
'draft'
])
]
]);
$profile = Auth::user()->profile;
$title = $request->input('title');
$content = $request->input('content');
$visibility = $request->input('visibility');
$status = DB::transaction(function() use($profile, $content, $visibility, $title) {
$status = new Status;
$status->type = 'text';
$status->profile_id = $profile->id;
$status->caption = strip_tags($content);
$status->rendered = Purify::clean($content);
$status->is_nsfw = false;
// TODO: remove deprecated visibility in favor of scope
$status->visibility = $visibility;
$status->scope = $visibility;
$status->entities = json_encode(['title'=>$title]);
$status->save();
return $status;
});
$fractal = new \League\Fractal\Manager();
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
$s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer());
return $fractal->createData($s)->toArray();
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\Page;
class PageController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'admin']);
}
protected function authCheck($admin_only = false)
{
$auth = $admin_only ?
Auth::check() && Auth::user()->is_admin == true :
Auth::check();
if($auth == false) {
abort(403);
}
}
public function edit(Request $request)
{
$this->authCheck(true);
$this->validate($request, [
'page' => 'required|string'
]);
$slug = urldecode($request->page);
$page = Page::firstOrCreate(['slug' => $slug]);
return view('admin.pages.edit', compact('page'));
}
public function store(Request $request)
{
$this->validate($request, [
'slug' => 'required|string',
'content' => 'required|string',
'title' => 'nullable|string',
'active' => 'required|boolean'
]);
$slug = urldecode($request->input('slug'));
$page = Page::firstOrCreate(['slug' => $slug]);
$page->content = $request->input('content');
$page->title = $request->input('title');
$page->active = (bool) $request->input('active');
$page->save();
return response()->json(['msg' => 200]);
}
}

View file

@ -187,7 +187,7 @@ class ProfileController extends Controller
return view('profile.private', compact('user', 'is_following'));
}
}
$followers = $profile->followers()->whereNull('status')->orderBy('created_at', 'desc')->simplePaginate(12);
$followers = $profile->followers()->whereNull('status')->orderBy('followers.created_at', 'desc')->simplePaginate(12);
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
if ($user->remote_url) {
$settings = new \StdClass;
@ -217,7 +217,7 @@ class ProfileController extends Controller
return view('profile.private', compact('user', 'is_following'));
}
}
$following = $profile->following()->whereNull('status')->orderBy('created_at', 'desc')->simplePaginate(12);
$following = $profile->following()->whereNull('status')->orderBy('followers.created_at', 'desc')->simplePaginate(12);
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
if ($user->remote_url) {
$settings = new \StdClass;

View file

@ -19,6 +19,7 @@ use Carbon\Carbon;
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
RelationshipTransformer,
StatusTransformer,
};
use App\Jobs\StatusPipeline\NewStatusPipeline;
@ -32,7 +33,6 @@ class PublicApiController extends Controller
public function __construct()
{
$this->middleware('throttle:3000, 30');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
@ -222,7 +222,11 @@ class PublicApiController extends Controller
// $timeline = Timeline::build()->local();
$pid = Auth::user()->profile->id;
$private = Profile::whereIsPrivate(true)->orWhereNotNull('status')->where('id', '!=', $pid)->pluck('id');
$private = Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->where('id', '!=', $pid)
->pluck('id');
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
@ -330,4 +334,100 @@ class PublicApiController extends Controller
return response()->json($res);
}
public function relationships(Request $request)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'id' => 'required|array|min:1|max:20',
'id.*' => 'required|integer'
]);
$ids = collect($request->input('id'));
$filtered = $ids->filter(function($v) {
return $v != Auth::user()->profile->id;
});
$relations = Profile::findOrFail($filtered->all());
$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
$res = $this->fractal->createData($fractal)->toArray();
return response()->json($res);
}
public function account(Request $request, $id)
{
$profile = Profile::whereNull('status')->findOrFail($id);
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function accountFollowers(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$followers = $profile->followers;
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function accountFollowing(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$following = $profile->following;
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function accountStatuses(Request $request, $id)
{
$this->validate($request, [
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:1',
'since_id' => 'nullable|integer|min:1',
'min_id' => 'nullable|integer|min:1',
'limit' => 'nullable|integer|min:1|max:24'
]);
$limit = $request->limit ?? 20;
$max_id = $request->max_id ?? false;
$min_id = $request->min_id ?? false;
$since_id = $request->since_id ?? false;
$only_media = $request->only_media ?? false;
$user = Auth::user();
$account = Profile::findOrFail($id);
$statuses = $account->statuses()->getQuery();
if($only_media == true) {
$statuses = $statuses
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id');
}
if($id == $account->id && !$max_id && !$min_id && !$since_id) {
$statuses = $statuses->orderBy('id', 'desc')
->paginate($limit);
} else if($since_id) {
$statuses = $statuses->where('id', '>', $since_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else if($min_id) {
$statuses = $statuses->where('id', '>', $min_id)
->orderBy('id', 'ASC')
->paginate($limit);
} else if($max_id) {
$statuses = $statuses->where('id', '<', $max_id)
->orderBy('id', 'DESC')
->paginate($limit);
} else {
$statuses = $statuses->whereVisibility('public')->orderBy('id', 'desc')->paginate($limit);
}
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
}

View file

@ -7,6 +7,7 @@ use App\Hashtag;
use App\Profile;
use App\Status;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache;
class SearchController extends Controller
@ -24,6 +25,35 @@ class SearchController extends Controller
$hash = hash('sha256', $tag);
$tokens = Cache::remember('api:search:tag:'.$hash, 5, function () use ($tag) {
$tokens = collect([]);
if(Helpers::validateUrl($tag)) {
$remote = Helpers::fetchFromUrl($tag);
if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
$type = $remote['type'];
if($type == 'Person') {
$item = Helpers::profileFirstOrNew($tag);
$tokens->push([[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
]]);
} else if ($type == 'Create') {
$item = Helpers::statusFirstOrFetch($tag, false);
$tokens->push([[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]]);
}
}
}
$hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->limit(20)->get();
if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
@ -41,6 +71,7 @@ class SearchController extends Controller
$users = Profile::select('username', 'name', 'id')
->whereNull('status')
->where('username', 'like', '%'.$tag.'%')
->orWhere('remote_url', $tag)
->limit(20)
->get();
@ -66,6 +97,7 @@ class SearchController extends Controller
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile->id)
->where('caption', 'like', '%'.$tag.'%')
->orWhere('uri', $tag)
->orderBy('created_at', 'desc')
->get();

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Settings;
use App\AccountLog;
use App\EmailVerification;
use App\Instance;
use App\Media;
use App\Profile;
use App\User;
@ -121,7 +122,56 @@ trait PrivacySettings
public function blockedInstances()
{
$settings = Auth::user()->settings;
return view('settings.privacy.blocked-instances');
$pid = Auth::user()->profile->id;
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Instance')
->whereFilterType('block')
->orderByDesc('id')
->paginate(10);
return view('settings.privacy.blocked-instances', compact('filters'));
}
public function blockedInstanceStore(Request $request)
{
$this->validate($request, [
'domain' => [
'required',
'min:3',
'max:100',
function($attribute, $value, $fail) {
if(!filter_var($value, FILTER_VALIDATE_DOMAIN)) {
$fail($attribute. 'is invalid');
}
}
]
]);
$domain = $request->input('domain');
$instance = Instance::firstOrCreate(['domain' => $domain]);
$filter = new UserFilter;
$filter->user_id = Auth::user()->profile->id;
$filter->filterable_id = $instance->id;
$filter->filterable_type = 'App\Instance';
$filter->filter_type = 'block';
$filter->save();
return response()->json(['msg' => 200]);
}
public function blockedInstanceUnblock(Request $request)
{
$this->validate($request, [
'id' => 'required|integer|min:1'
]);
$pid = Auth::user()->profile->id;
$filter = UserFilter::whereFilterableType('App\Instance')
->whereUserId($pid)
->findOrFail($request->input('id'));
$filter->delete();
return redirect(route('settings.privacy.blocked-instances'));
}
public function blockedKeywords()
{
return view('settings.privacy.blocked-keywords');
}
}

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\AccountLog;
use App\Following;
use App\Report;
use App\UserFilter;
use Auth, DB, Cache, Purify;
use Carbon\Carbon;
@ -160,6 +161,7 @@ class SettingsController extends Controller
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
$user = Auth::user();
if($user->is_admin == true) {
return abort(400, 'You cannot delete an admin account.');
@ -175,5 +177,18 @@ class SettingsController extends Controller
Auth::logout();
return redirect('/');
}
public function requestFullExport(Request $request)
{
$user = Auth::user();
return view('settings.export.show');
}
public function reportsHome(Request $request)
{
$profile = Auth::user()->profile;
$reports = Report::whereProfileId($profile->id)->orderByDesc('created_at')->paginate(10);
return view('settings.reports', compact('reports'));
}
}

View file

@ -2,16 +2,10 @@
namespace App\Http\Controllers;
use App;
use App\Follower;
use App\Profile;
use App\Status;
use App\User;
use App\UserFilter;
use App\Util\Lexer\PrettyNumber;
use Auth;
use Cache;
use Illuminate\Http\Request;
use App, Auth, Cache, View;
use App\Util\Lexer\PrettyNumber;
use App\{Follower, Page, Profile, Status, User, UserFilter};
class SiteController extends Controller
{
@ -47,18 +41,42 @@ class SiteController extends Controller
public function about()
{
$stats = Cache::remember('site:about:stats', 1440, function() {
return [
'posts' => Status::whereLocal(true)->count(),
'users' => User::count(),
'admin' => User::whereIsAdmin(true)->first()
];
$res = Cache::remember('site:about', 120, function() {
$custom = Page::whereSlug('/site/about')->whereActive(true)->exists();
if($custom) {
$stats = Cache::remember('site:about:stats', 60, function() {
return [
'posts' => Status::whereLocal(true)->count(),
'users' => User::count(),
'admin' => User::whereIsAdmin(true)->first()
];
});
return View::make('site.about')->with('stats', $stats)->render();
} else {
$stats = Cache::remember('site:about:stats', 60, function() {
return [
'posts' => Status::whereLocal(true)->count(),
'users' => User::count(),
'admin' => User::whereIsAdmin(true)->first()
];
});
//return view('site.about', compact('stats'));
return View::make('site.about')->with('stats', $stats)->render();
}
});
return view('site.about', compact('stats'));
return $res;
}
public function language()
{
return view('site.language');
}
public function communityGuidelines(Request $request)
{
$slug = '/site/kb/community-guidelines';
$page = Page::whereSlug($slug)->whereActive(true)->first();
return view('site.help.community-guidelines', compact('page'));
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\Media;
use App\Profile;
use App\Status;
@ -234,8 +235,10 @@ class StatusController extends Controller
$share = new Status();
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;
$share->save();
$count++;
SharePipeline::dispatch($share);
}
if ($request->ajax()) {

View file

@ -2,6 +2,18 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class StoryController extends Controller
{
public function construct()
{
$this->middleware('auth');
}
public function home(Request $request)
{
return view('stories.home');
}
}

View file

@ -6,5 +6,63 @@ use Illuminate\Database\Eloquent\Model;
class Instance extends Model
{
protected $fillable = ['domain'];
protected $fillable = ['domain'];
public function profiles()
{
return $this->hasMany(Profile::class, 'domain', 'domain');
}
public function statuses()
{
return $this->hasManyThrough(
Status::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function reported()
{
return $this->hasManyThrough(
Report::class,
Profile::class,
'domain',
'reported_profile_id',
'domain',
'id'
);
}
public function reports()
{
return $this->hasManyThrough(
Report::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function media()
{
return $this->hasManyThrough(
Media::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function getUrl()
{
return url("/i/admin/instances/show/{$this->id}");
}
}

View file

@ -19,6 +19,13 @@ class AvatarOptimize implements ShouldQueue
protected $profile;
protected $current;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -20,6 +20,13 @@ class CreateAvatar implements ShouldQueue
protected $profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -0,0 +1,95 @@
<?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);
}
}

View file

@ -20,6 +20,13 @@ class CommentPipeline implements ShouldQueue
protected $status;
protected $comment;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -21,6 +21,13 @@ class FollowActivityPubDeliver implements ShouldQueue
protected $followRequest;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -18,6 +18,13 @@ class FollowPipeline implements ShouldQueue
protected $follower;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -15,6 +15,13 @@ class ImageOptimize implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -16,6 +16,13 @@ class ImageResize implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -17,6 +17,13 @@ class ImageThumbnail implements ShouldQueue
protected $media;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -2,6 +2,7 @@
namespace App\Jobs\ImageOptimizePipeline;
use Storage;
use App\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -9,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use ImageOptimizer;
use Illuminate\Http\File;
class ImageUpdate implements ShouldQueue
{
@ -17,11 +19,17 @@ class ImageUpdate implements ShouldQueue
protected $media;
protected $protectedMimes = [
'image/gif',
'image/bmp',
'video/mp4',
'image/jpeg',
'image/png',
];
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
@ -43,21 +51,31 @@ class ImageUpdate implements ShouldQueue
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
try {
if (!in_array($media->mime, $this->protectedMimes)) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
}
} catch (Exception $e) {
return;
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
}
if (!is_file($path) || !is_file($thumb)) {
return;
}
$photo_size = filesize($path);
$thumb_size = filesize($thumb);
$total = ($photo_size + $thumb_size);
$media->size = $total;
$media->save();
if(config('pixelfed.cloud_storage') == true) {
$p = explode('/', $media->media_path);
$monthHash = $p[2];
$userHash = $p[3];
$storagePath = "public/m/{$monthHash}/{$userHash}";
$file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
$url = Storage::disk(config('filesystems.cloud'))->url($file);
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->save();
}
}
}

View file

@ -25,7 +25,14 @@ class ImportInstagram implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $job;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -19,6 +19,13 @@ class LikePipeline implements ShouldQueue
protected $like;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -18,6 +18,13 @@ class MentionPipeline implements ShouldQueue
protected $status;
protected $mention;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -17,7 +17,14 @@ class SharePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $like;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
@ -37,32 +44,32 @@ class SharePipeline implements ShouldQueue
public function handle()
{
$status = $this->status;
$actor = $this->status->profile;
$target = $this->status->parent()->profile;
$actor = $status->profile;
$target = $status->parent()->profile;
if ($status->url !== null) {
if ($status->uri !== null) {
// Ignore notifications to remote statuses
return;
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
$exists = Notification::whereProfileId($target->id)
->whereActorId($status->profile_id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->count();
if ($actor->id === $status->profile_id || $exists !== 0) {
if ($target->id === $status->profile_id || $exists !== 0) {
return true;
}
try {
$notification = new Notification();
$notification->profile_id = $status->profile_id;
$notification = new Notification;
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'like';
$notification->message = $like->toText();
$notification->rendered = $like->toHtml();
$notification->action = 'share';
$notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml();
$notification->item_id = $status->id;
$notification->item_type = "App\Status";
$notification->save();

View file

@ -16,7 +16,14 @@ class NewStatusPipeline implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -18,7 +18,14 @@ class StatusActivityPubDeliver implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -19,7 +19,14 @@ class StatusDelete implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -24,7 +24,14 @@ class StatusEntityLexer implements ShouldQueue
protected $status;
protected $entities;
protected $autolink;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -49,7 +49,7 @@ class VideoThumbnail implements ShouldQueue
} elseif($video->getDurationInSeconds() < 5) {
$video->getFrameFromSeconds(4);
}
$video->export()
$video->export()
->save($save);
$media->thumbnail_path = $save;

View file

@ -17,13 +17,23 @@ class Media extends Model
*/
protected $dates = ['deleted_at'];
public function status()
{
return $this->belongsTo(Status::class);
}
public function profile()
{
return $this->belongsTo(Profile::class);
}
public function url()
{
if(!empty($this->remote_media) && $this->remote_url) {
$url = $this->remote_url;
} else {
$path = $this->media_path;
$url = Storage::url($path);
$url = $this->cdn_url ?? Storage::url($path);
}
return url($url);
@ -37,6 +47,11 @@ class Media extends Model
return url($url);
}
public function thumb()
{
return $this->thumbnailUrl();
}
public function mimeType()
{
return explode('/', $this->mime)[0];

18
app/OauthClient.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class OauthClient extends Model
{
protected $table = 'oauth_clients';
public function user()
{
return $this->belongsTo(User::class);
}
}

25
app/Page.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
const SLUG_ROOT = [
'site',
'page'
];
protected $fillable = ['slug'];
public function url()
{
return url($this->slug);
}
public function editUrl()
{
return url("/i/admin/settings/pages/edit?page=".urlencode($this->slug));
}
}

View file

@ -12,7 +12,7 @@ class Profile extends Model
protected $dates = ['deleted_at'];
protected $hidden = ['private_key'];
protected $visible = ['username', 'name'];
protected $visible = ['id', 'user_id', 'username', 'name'];
public function user()
{
@ -274,4 +274,9 @@ class Profile extends Model
->unique()
->toArray();
}
public function circles()
{
return $this->hasMany(Circle::class);
}
}

View file

@ -25,10 +25,8 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
// Passport::routes();
// Passport::tokensExpireIn(now()->addDays(15));
// Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
}
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class ReportComment extends Model
{
//
public function profile()
{
return $this->belongsTo(Profile::class);
}
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class ReportLog extends Model
{
//
public function profile()
{
return $this->belongsTo(Profile::class);
}
}

View file

@ -118,7 +118,11 @@ class Status extends Model
$media = $this->firstMedia();
$path = $media->media_path;
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
$url = Storage::url($path)."?v={$hash}";
if(config('pixelfed.cloud_storage') == true) {
$url = Storage::disk(config('filesystems.cloud'))->url($path)."?v={$hash}";
} else {
$url = Storage::url($path)."?v={$hash}";
}
return url($url);
}
@ -270,6 +274,22 @@ class Status extends Model
__('notification.commented');
}
public function shareToText()
{
$actorName = $this->profile->username;
return "{$actorName} ".__('notification.shared');
}
public function shareToHtml()
{
$actorName = $this->profile->username;
$actorUrl = $this->profile->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
__('notification.shared');
}
public function recentComments()
{
return $this->comments()->orderBy('created_at', 'desc')->take(3);

View file

@ -7,4 +7,14 @@ use Illuminate\Database\Eloquent\Model;
class StatusHashtag extends Model
{
public $fillable = ['status_id', 'hashtag_id'];
public function status()
{
return $this->belongsTo(Status::class);
}
public function hashtag()
{
return $this->belongsTo(Hashtag::class);
}
}

View file

@ -2,9 +2,36 @@
namespace App;
use Auth;
use Illuminate\Database\Eloquent\Model;
class Story extends Model
{
//
protected $visible = ['id'];
public function profile()
{
return $this->belongsTo(Profile::class);
}
public function items()
{
return $this->hasMany(StoryItem::class);
}
public function reactions()
{
return $this->hasMany(StoryReaction::class);
}
public function views()
{
return $this->hasMany(StoryView::class);
}
public function seen($pid = false)
{
$id = $pid ?? Auth::user()->profile->id;
return $this->views()->whereProfileId($id)->exists();
}
}

19
app/StoryItem.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage;
class StoryItem extends Model
{
public function story()
{
return $this->belongsTo(Story::class);
}
public function url()
{
return Storage::url($this->media_path);
}
}

View file

@ -6,5 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class StoryReaction extends Model
{
//
public function story()
{
return $this->belongsTo(Story::class);
}
}

13
app/StoryView.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class StoryView extends Model
{
public function story()
{
return $this->belongsTo(Story::class);
}
}

View file

@ -50,7 +50,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'type' => 'Document',
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => null,
'name' => $media->caption
];
}),
'tag' => [],

View file

@ -9,8 +9,9 @@ class AccountTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
$is_admin = $profile->domain ? false : $profile->user->is_admin;
return [
'id' => $profile->id,
'id' => (string) $profile->id,
'username' => $profile->username,
'acct' => $profile->username,
'display_name' => $profile->name,
@ -28,6 +29,9 @@ class AccountTransformer extends Fractal\TransformerAbstract
'moved' => null,
'fields' => null,
'bot' => null,
'website' => $profile->website,
'software' => 'pixelfed',
'is_admin' => (bool) $is_admin
];
}
}

View file

@ -9,7 +9,7 @@ class AttachmentTransformer extends Fractal\TransformerAbstract
public function transform(Media $media)
{
return [
'id' => $media->id,
'id' => (string) $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
'remote_url' => null,

View file

@ -10,7 +10,7 @@ class MediaTransformer extends Fractal\TransformerAbstract
public function transform(Media $media)
{
return [
'id' => $media->id,
'id' => (string) $media->id,
'type' => $media->activityVerb(),
'url' => $media->url(),
'remote_url' => null,

View file

@ -10,7 +10,7 @@ class MentionTransformer extends Fractal\TransformerAbstract
public function transform(Profile $profile)
{
return [
'id' => $profile->id,
'id' => (string) $profile->id,
'url' => $profile->url(),
'username' => $profile->username,
'acct' => $profile->username,

View file

@ -15,7 +15,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
public function transform(Notification $notification)
{
return [
'id' => $notification->id,
'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) $notification->created_at,
'account' => null,
@ -44,6 +44,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',
'share' => 'share',
'like' => 'favourite',
'comment' => 'comment',
];

View file

@ -2,6 +2,7 @@
namespace App\Transformer\Api;
use Auth;
use App\Profile;
use League\Fractal;
@ -9,17 +10,18 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
$user = Auth::user()->profile;
return [
'id' => $profile->id,
'following' => null,
'followed_by' => null,
'id' => (string) $profile->id,
'following' => $user->follows($profile),
'followed_by' => $user->followedBy($profile),
'blocking' => null,
'muting' => null,
'muting_notifications' => null,
'requested' => null,
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => null
'endorsed' => false
];
}
}

View file

@ -8,12 +8,12 @@ class ResultsTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'account',
'mentions',
'media_attachments',
'tags',
'accounts',
'statuses',
'hashtags',
];
public function transform()
public function transform($results)
{
return [
'accounts' => [],
@ -21,4 +21,22 @@ class ResultsTransformer extends Fractal\TransformerAbstract
'hashtags' => []
];
}
public function includeAccounts($results)
{
$accounts = $results->accounts;
return $this->collection($accounts, new AccountTransformer());
}
public function includeStatuses($results)
{
$statuses = $results->statuses;
return $this->collection($statuses, new StatusTransformer());
}
public function includeTags($results)
{
$hashtags = $status->hashtags;
return $this->collection($hashtags, new HashtagTransformer());
}
}

View file

@ -17,7 +17,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
public function transform(Status $status)
{
return [
'id' => $status->id,
'id' => (string) $status->id,
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id,

View file

@ -0,0 +1,27 @@
<?php
namespace App\Transformer\Api;
use App\StoryItem;
use League\Fractal;
use Illuminate\Support\Str;
class StoryItemTransformer extends Fractal\TransformerAbstract
{
public function transform(StoryItem $item)
{
return [
'id' => (string) Str::uuid(),
'type' => $item->type,
'length' => $item->duration,
'src' => $item->url(),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $item->updated_at->format('U'),
'seen' => $item->story->seen(),
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Transformer\Api;
use App\Story;
use League\Fractal;
class StoryTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'items',
];
public function transform(Story $story)
{
return [
'id' => (string) $story->id,
'photo' => $story->profile->avatarUrl(),
'name' => '',
'link' => '',
'lastUpdated' => $story->updated_at->format('U'),
'seen' => $story->seen(),
'items' => [],
];
}
public function includeItems(Story $story)
{
$items = $story->items;
return $this->collection($items, new StoryItemTransformer());
}
}

View file

@ -62,6 +62,11 @@ class User extends Authenticatable
);
}
public function filters()
{
return $this->hasMany(UserFilter::class);
}
public function receivesBroadcastNotificationsOn()
{
return 'App.User.'.$this->id;

View file

@ -21,7 +21,6 @@ class UserFilter extends Model
->pluck('filterable_id');
}
public function blockedUserIds($profile_id)
{
return $this->whereUserId($profile_id)
@ -29,4 +28,9 @@ class UserFilter extends Model
->whereFilterType('block')
->pluck('filterable_id');
}
public function instance()
{
return $this->belongsTo(Instance::class, 'filterable_id');
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Follow {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Follow'])
],
'actor' => 'required|url|active_url',
'object' => 'required|url|active_url'
])->passes();
return $valid;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class Like {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Like'])
],
'actor' => 'required|url|active_url',
'object' => 'required|url|active_url'
])->passes();
return $valid;
}
}

View file

@ -17,15 +17,6 @@ class RestrictedNames
'contact-us',
'contact_us',
'copyright',
'd',
'dashboard',
'dev',
'developer',
'developers',
'discover',
'discovers',
'doc',
'docs',
'download',
'domainadmin',
'domainadministrator',
@ -41,10 +32,7 @@ class RestrictedNames
'guests',
'hostmaster',
'hostmaster',
'image',
'images',
'imap',
'img',
'info',
'info',
'is',
@ -57,7 +45,6 @@ class RestrictedNames
'mailerdaemon',
'marketing',
'me',
'media',
'mis',
'mx',
'new',
@ -82,7 +69,6 @@ class RestrictedNames
'pop3',
'postmaster',
'pricing',
'privacy',
'root',
'sales',
'security',
@ -96,7 +82,6 @@ class RestrictedNames
'sys',
'sysadmin',
'system',
'terms',
'tutorial',
'tutorials',
'usenet',
@ -121,34 +106,68 @@ class RestrictedNames
'account',
'api',
'auth',
'bartender',
'broadcast',
'broadcaster',
'booth',
'bouncer',
'c',
'css',
'checkpoint',
'collection',
'collections',
'c',
'costar',
'costars',
'cdn',
'd',
'dashboard',
'deck',
'dev',
'developer',
'developers',
'discover',
'discovers',
'dj',
'doc',
'docs',
'docs',
'drive',
'driver',
'error',
'explore',
'font',
'fonts',
'gdpr',
'home',
'help',
'helpcenter',
'help-center',
'help_center',
'help_center_',
'help-center-',
'help-center_',
'help_center-',
'i',
'img',
'imgs',
'image',
'images',
'js',
'legal',
'live',
'login',
'logout',
'media',
'menu',
'oauth',
'official',
'p',
'page',
'pages',
'photo',
'photos',
'password',
'privacy',
'reset',
'report',
'reports',
@ -161,10 +180,14 @@ class RestrictedNames
'statuses',
'site',
'sites',
'stage',
'static',
'story',
'stories',
'support',
'svg',
'svgs',
'terms',
'telescope',
'timeline',
'timelines',
@ -174,9 +197,11 @@ class RestrictedNames
'username',
'usernames',
'vendor',
'waiter',
'ws',
'wss',
'www',
'valet',
'400',
'401',
'403',

View file

@ -6,6 +6,12 @@
"type": "project",
"require": {
"php": "^7.1.3",
"ext-bcmath": "*",
"ext-ctype": "*",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"beyondcode/laravel-self-diagnosis": "^1.0.2",
"bitverse/identicon": "^1.1",
"doctrine/dbal": "^2.7",

View file

@ -42,7 +42,7 @@ return [
],
'api' => [
'driver' => 'token',
'driver' => 'passport',
'provider' => 'users',
],
],

View file

@ -65,6 +65,21 @@ return [
'endpoint' => env('AWS_ENDPOINT'),
],
'spaces' => [
'driver' => 's3',
'key' => env('DO_SPACES_KEY'),
'secret' => env('DO_SPACES_SECRET'),
'endpoint' => env('DO_SPACES_ENDPOINT'),
'region' => env('DO_SPACES_REGION'),
'bucket' => env('DO_SPACES_BUCKET'),
'visibility' => 'public',
'options' => [
'CacheControl' => 'max-age=31536000'
],
'root' => env('DO_SPACES_ROOT','/'),
'url' => str_replace(env('DO_SPACES_REGION'),env('DO_SPACES_BUCKET').'.'.env('DO_SPACES_REGION'),str_replace("digitaloceanspaces","cdn.digitaloceanspaces",env('DO_SPACES_ENDPOINT'))),
],
],
];

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.7.10',
'version' => '0.8.0rc1',
/*
|--------------------------------------------------------------------------
@ -198,6 +198,46 @@ return [
*/
'account_delete_after' => env('ACCOUNT_DELETE_AFTER', false),
/*
|--------------------------------------------------------------------------
| Enable Cloud Storage
|--------------------------------------------------------------------------
|
| Store media on object storage like S3, Digital Ocean Spaces, Rackspace
|
*/
'cloud_storage' => env('PF_ENABLE_CLOUD', false),
/*
|--------------------------------------------------------------------------
| Max User Limit
|--------------------------------------------------------------------------
|
| Allow a maximum number of user accounts. Default: off
|
*/
'max_users' => env('PF_MAX_USERS', false),
/*
|--------------------------------------------------------------------------
| Optimize Images
|--------------------------------------------------------------------------
|
| Resize and optimize image uploads. Default: on
|
*/
'optimize_image' => env('PF_OPTIMIZE_IMAGES', true),
/*
|--------------------------------------------------------------------------
| Optimize Videos
|--------------------------------------------------------------------------
|
| Resize and optimize video uploads. Default: on
|
*/
'optimize_video' => env('PF_OPTIMIZE_VIDEOS', true),
'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true),

View file

@ -54,7 +54,7 @@ return [
|
*/
'HTML.Doctype' => 'XHTML 1.0 Strict',
'HTML.Doctype' => 'XHTML 1.0 Transitional',
/*
|--------------------------------------------------------------------------
@ -67,7 +67,7 @@ return [
|
*/
'HTML.Allowed' => 'a[href|title|rel],p',
'HTML.Allowed' => 'a[href|title|rel],p,strong,em,i,u,h1,h2,h3,h4,h5,ul,ol,li',
/*
|--------------------------------------------------------------------------

View file

@ -17,7 +17,15 @@ RUN apt-get update \
&& docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
&& pecl install imagick \
&& docker-php-ext-enable imagick pcntl imagick gd exif \
&& a2enmod rewrite \
&& a2enmod rewrite remoteip \
&& {\
echo RemoteIPHeader X-Real-IP ;\
echo RemoteIPTrustedProxy 10.0.0.0/8 ;\
echo RemoteIPTrustedProxy 172.16.0.0/12 ;\
echo RemoteIPTrustedProxy 192.168.0.0/16 ;\
echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\
} > /etc/apache2/conf-available/remoteip.conf \
&& a2enconf remoteip \
&& 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 \

View file

@ -0,0 +1,77 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Stories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('story_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('story_id')->unsigned()->index();
$table->string('media_path')->nullable();
$table->string('media_url')->nullable();
$table->tinyInteger('duration')->unsigned();
$table->string('filter')->nullable();
$table->string('link_url')->nullable()->index();
$table->string('link_text')->nullable();
$table->tinyInteger('order')->unsigned()->nullable();
$table->string('type')->default('photo');
$table->json('layers')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
Schema::create('story_views', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('story_id')->unsigned()->index();
$table->bigInteger('profile_id')->unsigned()->index();
$table->unique(['story_id', 'profile_id']);
$table->timestamps();
});
Schema::table('stories', function (Blueprint $table) {
$table->string('title')->nullable()->after('profile_id');
$table->boolean('preview_photo')->default(false)->after('title');
$table->boolean('local_only')->default(false)->after('preview_photo');
$table->boolean('is_live')->default(false)->after('local_only');
$table->string('broadcast_url')->nullable()->after('is_live');
$table->string('broadcast_key')->nullable()->after('broadcast_url');
});
Schema::table('story_reactions', function (Blueprint $table) {
$table->bigInteger('story_id')->unsigned()->index()->after('profile_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('story_items');
Schema::dropIfExists('story_views');
Schema::table('stories', function (Blueprint $table) {
$table->dropColumn('title');
$table->dropColumn('preview_photo');
$table->dropColumn('local_only');
$table->dropColumn('is_live');
$table->dropColumn('broadcast_url');
$table->dropColumn('broadcast_key');
});
Schema::table('story_reactions', function (Blueprint $table) {
$table->dropColumn('story_id');
});
}
}

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('pages', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('root')->nullable()->index();
$table->string('slug')->nullable()->unique()->index();
$table->string('title')->nullable();
$table->unsignedInteger('category_id')->nullable()->index();
$table->longText('content')->nullable();
$table->string('template')->default('layouts.app')->index();
$table->boolean('active')->default(false)->index();
$table->boolean('cached')->default(true)->index();
$table->timestamp('active_until')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('pages');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRemoteToAvatarsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('avatars', function (Blueprint $table) {
$table->string('remote_url')->nullable()->index()->after('thumb_path');
$table->timestamp('last_fetched_at')->nullable()->after('change_count');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('avatars', function (Blueprint $table) {
$table->dropColumn('remote_url');
$table->dropColumn('last_fetched_at');
});
}
}

Some files were not shown because too many files have changed in this diff Show more