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

v0.8.0rc1
pull/834/head
daniel 7 years ago committed by GitHub
commit afc758764c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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

@ -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)
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.
**_Please note this is alpha software, not recommended for production use,
and federation is not supported yet._**
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.
## 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
## Installation
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
```
<p align="center"><img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/logos/pixelfed-full-color.svg" width="300px"></p>
<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>
## Introduction
A free and ethical photo sharing platform, powered by ActivityPub federation.
<p align="center">
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/Screen%20Shot%202019-02-05%20at%206.34.59%20PM.png">
</p>
## Official Documentation
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>

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

@ -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');
}
}

@ -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);
}
}

@ -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}");
}
}

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

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

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

@ -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);
}
}

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

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

@ -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);
}
}

@ -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;
}
}

@ -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([]);
}
}

@ -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'));
}
}

@ -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'));
}
}

@ -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'));
}
}

@ -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);
}
}

@ -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);
}

@ -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'));
}
}

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

@ -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');
}
}

@ -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'));
}

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

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

@ -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'));
}
}

@ -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, [

@ -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

@ -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);
}

@ -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];
}
}

@ -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();
}
}

@ -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]);
}
}

@ -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;

@ -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);
}
}

@ -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();

@ -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');
}
}

@ -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'));
}
}

@ -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'));
}
}

@ -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()) {

@ -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');
}
}

@ -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}");
}
}

@ -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.
*

@ -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.
*

@ -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);
}
}

@ -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.
*

@ -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.
*

@ -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.
*

@ -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.
*

@ -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.
*

@ -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.
*

@ -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();
}
}
}

@ -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.
*

@ -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.
*

@ -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.
*

@ -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();

@ -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.
*

@ -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.
*

@ -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.
*

@ -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.
*

@ -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;

@ -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];

@ -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);
}
}

@ -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));
}
}

@ -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);
}
}

@ -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));
}
}

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

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

@ -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);

@ -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);
}
}

@ -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();
}
}

@ -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);
}
}

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

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

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

@ -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
];
}
}

@ -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,

@ -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,

@ -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,

@ -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',
];

@ -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
];
}
}

@ -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());
}
}

@ -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,

@ -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(),
];
}
}

@ -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());
}
}

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

@ -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');
}
}

@ -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;
}
}

@ -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;
}
}

@ -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',

@ -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",

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

@ -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'))),
],
],
];

@ -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),

@ -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',
/*
|--------------------------------------------------------------------------

@ -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 \

@ -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');
});
}
}

@ -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');
}
}

@ -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

Loading…
Cancel
Save