mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-01-11 14:40:46 +00:00
Merge branch 'frontend-ui-refactor' into gdpr-privacy-policy
This commit is contained in:
commit
921ff1ae61
115 changed files with 4988 additions and 1033 deletions
34
.env.example
34
.env.example
|
@ -1,46 +1,52 @@
|
|||
APP_NAME=Laravel
|
||||
APP_NAME="PixelFed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
ADMIN_DOMAIN="localhost"
|
||||
APP_DOMAIN="localhost"
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_LIFETIME=120
|
||||
QUEUE_DRIVER=sync
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="pixelfed@example.com
|
||||
MAIL_FROM_NAME="Pixelfed"
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
SESSION_DOMAIN=".pixelfed.dev"
|
||||
SESSION_DOMAIN="${APP_DOMAIN}"
|
||||
SESSION_SECURE_COOKIE=true
|
||||
API_BASE="/api/1/"
|
||||
API_SEARCH="/api/search"
|
||||
|
||||
OPEN_REGISTRATION=true
|
||||
RECAPTCHA_ENABLED=false
|
||||
ENFORCE_EMAIL_VERIFICATION=true
|
||||
|
||||
MAX_PHOTO_SIZE=15000
|
||||
MAX_CAPTION_LENGTH=150
|
||||
MAX_ALBUM_LENGTH=4
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
|
78
README.md
78
README.md
|
@ -1,4 +1,76 @@
|
|||
# PixelFed
|
||||
Federated Image Sharing
|
||||
# PixelFed: Federated Image Sharing
|
||||
|
||||
> This project is still in active development and not yet ready for use.
|
||||
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.2+ recommended for stable version)
|
||||
- MySQL, Postgres (MariaDB and sqlite are not supported yet)
|
||||
- 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
|
||||
- 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/dansup/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
|
||||
php artisan serve --host=localhost --port=80
|
||||
```
|
||||
|
||||
Check your browser at http://localhost
|
||||
|
||||
## Communication
|
||||
|
||||
The ways you can communicate on the project are below. Before interacting, please
|
||||
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)
|
||||
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||
|
||||
## Support
|
||||
|
||||
The lead maintainer is on Patreon! You can become a Patron at
|
||||
https://www.patreon.com/dansup
|
10
app/AccountLog.php
Normal file
10
app/AccountLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AccountLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,8 +3,16 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Avatar extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
}
|
||||
|
|
15
app/EmailVerification.php
Normal file
15
app/EmailVerification.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailVerification extends Model
|
||||
{
|
||||
public function url()
|
||||
{
|
||||
$base = config('app.url');
|
||||
$path = '/i/confirm-email/' . $this->user_token . '/' . $this->random_token;
|
||||
return "{$base}{$path}";
|
||||
}
|
||||
}
|
36
app/Events/AuthLoginEvent.php
Normal file
36
app/Events/AuthLoginEvent.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use App\{User, UserSetting};
|
||||
|
||||
class AuthLoginEvent
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function handle(User $user)
|
||||
{
|
||||
if(empty($user->settings)) {
|
||||
$settings = new UserSetting;
|
||||
$settings->user_id = $user->id;
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Hashtag extends Model
|
||||
{
|
||||
protected $fillable = ['name','slug'];
|
||||
public $fillable = ['name','slug'];
|
||||
|
||||
public function posts()
|
||||
{
|
||||
|
|
|
@ -4,8 +4,9 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use Auth, Cache, Redis;
|
||||
use App\{Notification, Profile, User};
|
||||
use App\Mail\ConfirmEmail;
|
||||
use Auth, DB, Cache, Mail, Redis;
|
||||
use App\{EmailVerification, Notification, Profile, User};
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
@ -17,19 +18,78 @@ class AccountController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3'
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
'a' => 'nullable|alpha_dash',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$action = $request->input('a');
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->take(30)
|
||||
->simplePaginate();
|
||||
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereAction($action)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
} else {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
}
|
||||
|
||||
return view('account.activity', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
return view('account.verify_email');
|
||||
}
|
||||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
|
||||
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||
->where('created_at', '>', $timeLimit)->count();
|
||||
$exists = EmailVerification::whereUserId(Auth::id())->count();
|
||||
|
||||
if($recentAttempt == 1 && $exists == 1) {
|
||||
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||
} elseif ($recentAttempt == 0 && $exists !== 0) {
|
||||
// Delete old verification and send new one.
|
||||
EmailVerification::whereUserId(Auth::id())->delete();
|
||||
}
|
||||
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = hash('sha512', $user->id);
|
||||
$rtoken = str_random(40);
|
||||
|
||||
$verify = new EmailVerification;
|
||||
$verify->user_id = $user->id;
|
||||
$verify->email = $user->email;
|
||||
$verify->user_token = $utoken;
|
||||
$verify->random_token = $rtoken;
|
||||
$verify->save();
|
||||
|
||||
Mail::to($user->email)->send(new ConfirmEmail($verify));
|
||||
|
||||
return redirect()->back()->with('status', 'Email verification email sent!');
|
||||
}
|
||||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where('user_token', $userToken)
|
||||
->where('random_token', $randomToken)
|
||||
->firstOrFail();
|
||||
|
||||
if(Auth::id() === $verify->user_id) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchNotifications($id)
|
||||
{
|
||||
$key = config('cache.prefix') . ":user.{$id}.notifications";
|
||||
|
@ -54,4 +114,5 @@ class AccountController extends Controller
|
|||
}
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
71
app/Http/Controllers/Api/BaseApiController.php
Normal file
71
app/Http/Controllers/Api/BaseApiController.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Auth;
|
||||
use App\{Like, Profile, Status};
|
||||
use League\Fractal;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer
|
||||
};
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
protected $fractal;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
|
||||
public function accounts(Request $request, $id)
|
||||
{
|
||||
$profile = Profile::findOrFail($id);
|
||||
$resource = new Fractal\Resource\Item($profile, new AccountTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
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, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
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, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
$profile = Profile::findOrFail($id);
|
||||
$statuses = $profile->statuses()->orderBy('id', 'desc')->paginate(20);
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
|
||||
public function followSuggestions(Request $request)
|
||||
{
|
||||
$followers = Auth::user()->profile->recommendFollowers();
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\{AccountLog, User};
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
|
||||
|
@ -25,7 +26,7 @@ class LoginController extends Controller
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/home';
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
@ -56,4 +57,25 @@ class LoginController extends Controller
|
|||
|
||||
$this->validate($request, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $user
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticated($request, $user)
|
||||
{
|
||||
$log = new AccountLog;
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'auth.login';
|
||||
$log->message = 'Account Login';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,23 +16,27 @@ class BookmarkController extends Controller
|
|||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1'
|
||||
'item' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
|
||||
if(!$bookmark->wasRecentlyCreated) {
|
||||
$bookmark->delete();
|
||||
}
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ class CommentController extends Controller
|
|||
|
||||
$reply = new Status();
|
||||
$reply->profile_id = $profile->id;
|
||||
$reply->caption = $comment;
|
||||
$reply->rendered = e($comment);
|
||||
$reply->caption = e($comment);
|
||||
$reply->rendered = $comment;
|
||||
$reply->in_reply_to_id = $status->id;
|
||||
$reply->in_reply_to_profile_id = $status->profile_id;
|
||||
$reply->save();
|
||||
|
@ -44,7 +44,7 @@ class CommentController extends Controller
|
|||
CommentPipeline::dispatch($status, $reply);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url()];
|
||||
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url(), 'comment' => $reply->caption];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Auth, Cache;
|
||||
use App\Profile;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
|
@ -13,15 +14,26 @@ use App\Transformer\ActivityPub\{
|
|||
ProfileTransformer
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
use App\Jobs\InboxPipeline\InboxWorker;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function authCheck()
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
return abort(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function authorizeFollow(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'acct' => 'required|string|min:3|max:255'
|
||||
]);
|
||||
$acct = $request->input('acct');
|
||||
$nickname = Nickname::normalizeProfileUrl($acct);
|
||||
return view('federation.authorizefollow', compact('acct', 'nickname'));
|
||||
}
|
||||
|
||||
public function remoteFollow()
|
||||
|
@ -64,61 +76,58 @@ class FederationController extends Controller
|
|||
|
||||
public function nodeinfo()
|
||||
{
|
||||
$res = [
|
||||
'metadata' => [
|
||||
'nodeName' => config('app.name'),
|
||||
'software' => [
|
||||
'homepage' => 'https://pixelfed.org',
|
||||
'github' => 'https://github.com/pixelfed',
|
||||
'follow' => 'https://mastodon.social/@pixelfed'
|
||||
],
|
||||
/*
|
||||
TODO: Custom Features for Trending
|
||||
'customFeatures' => [
|
||||
'trending' => [
|
||||
'description' => 'Trending API for federated discovery',
|
||||
'api' => [
|
||||
'url' => null,
|
||||
'docs' => null
|
||||
],
|
||||
$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)->count(),
|
||||
'users' => [
|
||||
'total' => \App\User::count()
|
||||
]
|
||||
],
|
||||
'version' => '2.0'
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
'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\User::where('updated_at', '>', Carbon::now()->subMonths(6)->toDateTimeString())->count(),
|
||||
'activeMonth' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(1)->toDateTimeString())->count(),
|
||||
]
|
||||
],
|
||||
'version' => '2.0'
|
||||
];
|
||||
});
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
$this->validate($request, ['resource'=>'required']);
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
$username = $parsed['username'];
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$webfinger = (new Webfinger($user))->generate();
|
||||
return response()->json($webfinger);
|
||||
$this->validate($request, ['resource'=>'required|string|min:3|max:255']);
|
||||
|
||||
$hash = hash('sha512', $request->input('resource'));
|
||||
|
||||
$webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function() use($request) {
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
$username = $parsed['username'];
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
return (new Webfinger($user))->generate();
|
||||
});
|
||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
|
@ -135,4 +144,20 @@ class FederationController extends Controller
|
|||
return response()->json($res['data']);
|
||||
}
|
||||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
if(config('pixelfed.activitypub_enabled') == false) {
|
||||
abort(403);
|
||||
}
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
if(!in_array($request->header('Content-Type'), $mimes)) {
|
||||
abort(500, 'Invalid request');
|
||||
}
|
||||
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||
InboxWorker::dispatch($request, $profile, $request->all());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImportDataController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Hashids;
|
||||
use Auth, Cache, Hashids;
|
||||
use App\{Like, Profile, Status, User};
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
|
||||
|
@ -27,7 +27,7 @@ class LikeController extends Controller
|
|||
|
||||
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
$like->delete();
|
||||
$like->forceDelete();
|
||||
$count--;
|
||||
} else {
|
||||
$like = new Like;
|
||||
|
@ -35,9 +35,15 @@ class LikeController extends Controller
|
|||
$like->status_id = $status->id;
|
||||
$like->save();
|
||||
$count++;
|
||||
LikePipeline::dispatch($like);
|
||||
}
|
||||
|
||||
LikePipeline::dispatch($like);
|
||||
$likes = Like::whereProfileId($profile->id)
|
||||
->orderBy('id', 'desc')
|
||||
->take(1000)
|
||||
->pluck('status_id');
|
||||
|
||||
Cache::put('api:like-ids:user:'.$profile->id, $likes, 1440);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
|
|
@ -18,17 +18,22 @@ class ProfileController extends Controller
|
|||
public function show(Request $request, $username)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
if($user->is_private == true) {
|
||||
$can_access = $this->privateProfileCheck($user);
|
||||
if($can_access !== true) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
// TODO: refactor this mess
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
|
@ -36,11 +41,26 @@ class ProfileController extends Controller
|
|||
$timeline = $user->statuses()
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->orderBy('created_at','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(21);
|
||||
|
||||
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
return view('profile.show', compact('user', 'settings', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
}
|
||||
|
||||
protected function privateProfileCheck(Profile $profile)
|
||||
{
|
||||
if(Auth::check() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$follower_ids = (array) $profile->followers()->pluck('followers.profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
if(!in_array($pid, $follower_ids) && $pid !== $profile->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
|
@ -89,11 +109,12 @@ class ProfileController extends Controller
|
|||
abort(403);
|
||||
}
|
||||
$user = Auth::user()->profile;
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
$owner = true;
|
||||
$following = false;
|
||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
return view('profile.show', compact('user', 'settings', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Profile, User};
|
||||
use Auth;
|
||||
use App\{AccountLog, Profile, User};
|
||||
use Auth, DB;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
|
@ -89,6 +89,34 @@ class SettingsController extends Controller
|
|||
return view('settings.avatar');
|
||||
}
|
||||
|
||||
public function accessibility()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
return view('settings.accessibility', compact('settings'));
|
||||
}
|
||||
|
||||
public function accessibilityStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$fields = [
|
||||
'compose_media_descriptions',
|
||||
'reduce_motion',
|
||||
'optimize_screen_reader',
|
||||
'high_contrast_mode',
|
||||
'video_autoplay'
|
||||
];
|
||||
foreach($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
{
|
||||
return view('settings.notifications');
|
||||
|
@ -96,12 +124,61 @@ class SettingsController extends Controller
|
|||
|
||||
public function privacy()
|
||||
{
|
||||
return view('settings.privacy');
|
||||
$settings = Auth::user()->settings;
|
||||
$is_private = Auth::user()->profile->is_private;
|
||||
$settings['is_private'] = (bool) $is_private;
|
||||
return view('settings.privacy', compact('settings'));
|
||||
}
|
||||
|
||||
public function privacyStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$profile = Auth::user()->profile;
|
||||
$fields = [
|
||||
'is_private',
|
||||
'crawlable',
|
||||
];
|
||||
foreach($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if($field == 'is_private') {
|
||||
if($form == 'on') {
|
||||
$profile->{$field} = true;
|
||||
$settings->show_guests = false;
|
||||
$settings->show_discover = false;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$field} = false;
|
||||
$profile->save();
|
||||
}
|
||||
} elseif($field == 'crawlable') {
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = false;
|
||||
} else {
|
||||
$settings->{$field} = true;
|
||||
}
|
||||
} else {
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function security()
|
||||
{
|
||||
return view('settings.security');
|
||||
$sessions = DB::table('sessions')
|
||||
->whereUserId(Auth::id())
|
||||
->limit(20)
|
||||
->get();
|
||||
$activity = AccountLog::whereUserId(Auth::id())
|
||||
->orderBy('created_at','desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
return view('settings.security', compact('sessions', 'activity'));
|
||||
}
|
||||
|
||||
public function applications()
|
||||
|
@ -121,7 +198,7 @@ class SettingsController extends Controller
|
|||
|
||||
public function dataImportInstagram()
|
||||
{
|
||||
return view('settings.import.ig');
|
||||
return view('settings.import.instagram.home');
|
||||
}
|
||||
|
||||
public function developers()
|
||||
|
|
|
@ -2,11 +2,39 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use App, Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Follower, Status, User};
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
|
||||
public function home()
|
||||
{
|
||||
if(Auth::check()) {
|
||||
return $this->homeTimeline();
|
||||
} else {
|
||||
return $this->homeGuest();
|
||||
}
|
||||
}
|
||||
|
||||
public function homeGuest()
|
||||
{
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function homeTimeline()
|
||||
{
|
||||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$following->push(Auth::user()->profile->id);
|
||||
$timeline = Status::whereIn('profile_id', $following)
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes', 'shares'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.template', compact('timeline'));
|
||||
}
|
||||
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
if(!App::isLocale($locale)) {
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Auth, Cache;
|
||||
use App\Jobs\StatusPipeline\{NewStatusPipeline, StatusDelete};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Media, Profile, Status, User};
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
|
@ -14,7 +15,7 @@ class StatusController extends Controller
|
|||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->withCount(['likes', 'comments'])
|
||||
->withCount(['likes', 'comments', 'media'])
|
||||
->findOrFail($id);
|
||||
if(!$status->media_path && $status->in_reply_to_id) {
|
||||
return redirect($status->url());
|
||||
|
@ -32,36 +33,51 @@ class StatusController extends Controller
|
|||
$user = Auth::user();
|
||||
|
||||
$this->validate($request, [
|
||||
'photo' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
||||
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
||||
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
|
||||
'cw' => 'nullable|string'
|
||||
'cw' => 'nullable|string',
|
||||
'filter_class' => 'nullable|string',
|
||||
'filter_name' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
||||
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
||||
}
|
||||
|
||||
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $request->photo->store($storagePath);
|
||||
$profile = $user->profile;
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = $request->caption;
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->is_nsfw = $cw;
|
||||
|
||||
$status->save();
|
||||
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $request->file('photo')->getClientSize();
|
||||
$media->mime = $request->file('photo')->getClientMimeType();
|
||||
$media->save();
|
||||
NewStatusPipeline::dispatch($status, $media);
|
||||
$photos = $request->file('photo');
|
||||
$order = 1;
|
||||
foreach ($photos as $k => $v) {
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $v->store($storagePath);
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $v->getClientSize();
|
||||
$media->mime = $v->getClientMimeType();
|
||||
$media->filter_class = $request->input('filter_class');
|
||||
$media->filter_name = $request->input('filter_name');
|
||||
$media->order = $order;
|
||||
$media->save();
|
||||
ImageOptimize::dispatch($media);
|
||||
$order++;
|
||||
}
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
// TODO: Parse Caption
|
||||
// TODO: Send to subscribers
|
||||
|
||||
return redirect($status->url());
|
||||
|
|
|
@ -60,5 +60,6 @@ class Kernel extends HttpKernel
|
|||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
|
||||
];
|
||||
}
|
||||
|
|
28
app/Http/Middleware/EmailVerificationCheck.php
Normal file
28
app/Http/Middleware/EmailVerificationCheck.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Auth, Closure;
|
||||
|
||||
class EmailVerificationCheck
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if($request->user() &&
|
||||
config('pixelfed.enforce_email_verification') &&
|
||||
is_null($request->user()->email_verified_at) &&
|
||||
!$request->is('i/verify-email') && !$request->is('log*') &&
|
||||
!$request->is('i/confirm-email/*')
|
||||
) {
|
||||
return redirect('/i/verify-email');
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ namespace App;
|
|||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
class ImportJob extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -38,7 +38,10 @@ class ImageUpdate implements ShouldQueue
|
|||
$thumb = storage_path('app/'. $media->thumbnail_path);
|
||||
try {
|
||||
ImageOptimizer::optimize($thumb);
|
||||
ImageOptimizer::optimize($path);
|
||||
if($media->mime !== 'image/gif')
|
||||
{
|
||||
ImageOptimizer::optimize($path);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
|
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal file
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class InboxWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, Profile $profile, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, $this->profile, $this->payload))->handle();
|
||||
}
|
||||
|
||||
}
|
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal file
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class SharedInboxWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, null, $this->payload))->handleSharedInbox();
|
||||
}
|
||||
}
|
|
@ -37,6 +37,11 @@ class LikePipeline implements ShouldQueue
|
|||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if($status->url !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
|
|
|
@ -16,17 +16,15 @@ class NewStatusPipeline implements ShouldQueue
|
|||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status, $media = false)
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,13 +35,10 @@ class NewStatusPipeline implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$media = $this->media;
|
||||
|
||||
StatusEntityLexer::dispatch($status);
|
||||
StatusActivityPubDeliver::dispatch($status);
|
||||
if($media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
//StatusActivityPubDeliver::dispatch($status);
|
||||
|
||||
Cache::forever('post.' . $status->id, $status);
|
||||
|
||||
$redis = Redis::connection();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use App\{Media, StatusHashtag, Status};
|
||||
use App\{Media, Notification, StatusHashtag, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
@ -58,8 +58,19 @@ class StatusDelete implements ShouldQueue
|
|||
|
||||
}
|
||||
}
|
||||
$comments = Status::where('in_reply_to_id', $status->id)->get();
|
||||
foreach($comments as $comment) {
|
||||
$comment->in_reply_to_id = null;
|
||||
$comment->save();
|
||||
Notification::whereItemType('App\Status')
|
||||
->whereItemId($comment->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$status->likes()->delete();
|
||||
Notification::whereItemType('App\Status')
|
||||
->whereItemId($status->id)
|
||||
->delete();
|
||||
StatusHashtag::whereStatusId($status->id)->delete();
|
||||
$status->delete();
|
||||
|
||||
|
|
|
@ -2,25 +2,32 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache;
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Media,
|
||||
Mention,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag
|
||||
};
|
||||
use App\Util\Lexer\Hashtag as HashtagLexer;
|
||||
use App\Util\Lexer\{Autolink, Extractor};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Jobs\MentionPipeline\MentionPipeline;
|
||||
|
||||
class StatusEntityLexer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
|
@ -39,36 +46,77 @@ class StatusEntityLexer implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->parseHashtags();
|
||||
$this->parseEntities();
|
||||
}
|
||||
|
||||
public function parseHashtags()
|
||||
public function parseEntities()
|
||||
{
|
||||
$this->extractEntities();
|
||||
}
|
||||
|
||||
public function extractEntities()
|
||||
{
|
||||
$this->entities = Extractor::create()->extract($this->status->caption);
|
||||
$this->autolinkStatus();
|
||||
}
|
||||
|
||||
public function autolinkStatus()
|
||||
{
|
||||
$this->autolink = Autolink::create()->autolink($this->status->caption);
|
||||
$this->storeEntities();
|
||||
}
|
||||
|
||||
public function storeEntities()
|
||||
{
|
||||
$this->storeHashtags();
|
||||
$this->storeMentions();
|
||||
DB::transaction(function () {
|
||||
$status = $this->status;
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
});
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
$text = e($status->caption);
|
||||
$tags = HashtagLexer::getHashtags($text);
|
||||
$rendered = $text;
|
||||
if(count($tags) > 0) {
|
||||
$rendered = HashtagLexer::replaceHashtagsWithLinks($text);
|
||||
}
|
||||
$status->rendered = $rendered;
|
||||
$status->save();
|
||||
|
||||
Cache::forever('post.' . $status->id, $status);
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$slug = str_slug($tag);
|
||||
|
||||
$htag = Hashtag::firstOrCreate(
|
||||
['name' => $tag],
|
||||
['slug' => $slug]
|
||||
);
|
||||
|
||||
$stag = new StatusHashtag;
|
||||
$stag->status_id = $status->id;
|
||||
$stag->hashtag_id = $htag->id;
|
||||
$stag->save();
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$slug = str_slug($tag);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
$mentions = array_unique($this->entities['mentions']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->firstOrFail();
|
||||
|
||||
if(empty($mentioned) || !isset($mentioned->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($status, $mentioned) {
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
});
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
10
app/Like.php
10
app/Like.php
|
@ -3,9 +3,19 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Like extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
|
|
34
app/Mail/ConfirmEmail.php
Normal file
34
app/Mail/ConfirmEmail.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\EmailVerification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class ConfirmEmail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(EmailVerification $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->markdown('emails.confirm_email')->with(['verify'=>$this->verify]);
|
||||
}
|
||||
}
|
|
@ -2,15 +2,32 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Media extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function url()
|
||||
{
|
||||
$path = $this->media_path;
|
||||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
{
|
||||
$path = $this->thumbnail_path;
|
||||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,18 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Mention extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
|
|
|
@ -3,28 +3,37 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'actor_id', 'id');
|
||||
}
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'actor_id', 'id');
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function item()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
public function item()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'item_id', 'id');
|
||||
}
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'item_id', 'id');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\{Profile, User};
|
||||
use App\{Profile, User, UserSetting};
|
||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||
|
||||
class UserObserver
|
||||
|
@ -36,6 +36,12 @@ class UserObserver
|
|||
|
||||
CreateAvatar::dispatch($profile);
|
||||
}
|
||||
|
||||
if(empty($user->settings)) {
|
||||
$settings = new UserSetting;
|
||||
$settings->user_id = $user->id;
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,9 +5,18 @@ namespace App;
|
|||
use Storage;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $hidden = [
|
||||
'private_key',
|
||||
];
|
||||
|
@ -20,6 +29,15 @@ class Profile extends Model
|
|||
}
|
||||
|
||||
public function url($suffix = '')
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return $this->remote_url;
|
||||
} else {
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
}
|
||||
|
||||
public function localUrl($suffix = '')
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
|
@ -115,4 +133,9 @@ class Profile extends Model
|
|||
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function statusCount()
|
||||
{
|
||||
return $this->statuses()->whereHas('media')->count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
|
|||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
],
|
||||
'auth.login' => [
|
||||
'App\Events\AuthLoginEvent',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
10
app/ReportComment.php
Normal file
10
app/ReportComment.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportComment extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/ReportLog.php
Normal file
10
app/ReportLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -2,12 +2,21 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Auth, Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
@ -25,7 +34,7 @@ class Status extends Model
|
|||
|
||||
public function thumb()
|
||||
{
|
||||
if($this->media->count() == 0) {
|
||||
if($this->media->count() == 0 || $this->is_nsfw) {
|
||||
return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
|
||||
}
|
||||
return url(Storage::url($this->firstMedia()->thumbnail_path));
|
||||
|
@ -43,6 +52,11 @@ class Status extends Model
|
|||
return url($path);
|
||||
}
|
||||
|
||||
public function editUrl()
|
||||
{
|
||||
return $this->url() . '/edit';
|
||||
}
|
||||
|
||||
public function mediaUrl()
|
||||
{
|
||||
$media = $this->firstMedia();
|
||||
|
@ -57,15 +71,39 @@ class Status extends Model
|
|||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
public function liked() : bool
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'in_reply_to_id');
|
||||
}
|
||||
|
||||
public function bookmarked()
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function shares()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'reblog_of_id');
|
||||
}
|
||||
|
||||
public function shared() : bool
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Status::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
if(!empty($this->in_reply_to_id)) {
|
||||
return Status::findOrFail($this->in_reply_to_id);
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if(!empty($parent)) {
|
||||
return Status::findOrFail($parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,6 +124,23 @@ class Status extends Model
|
|||
);
|
||||
}
|
||||
|
||||
public function mentions()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Mention::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function reportUrl()
|
||||
{
|
||||
return route('report.form') . "?type=post&id={$this->id}";
|
||||
}
|
||||
|
||||
public function toActivityStream()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
|
|
@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class StatusHashtag extends Model
|
||||
{
|
||||
protected $fillable = ['status_id', 'hashtag_id'];
|
||||
public $fillable = ['status_id', 'hashtag_id'];
|
||||
}
|
||||
|
|
33
app/Transformer/Api/AccountTransformer.php
Normal file
33
app/Transformer/Api/AccountTransformer.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class AccountTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
return [
|
||||
'id' => $profile->id,
|
||||
'username' => $profile->username,
|
||||
'acct' => $profile->username,
|
||||
'display_name' => $profile->name,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'created_at' => $profile->created_at->format('c'),
|
||||
'followers_count' => $profile->followerCount(),
|
||||
'following_count' => $profile->followingCount(),
|
||||
'statuses_count' => $profile->statusCount(),
|
||||
'note' => $profile->bio,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'avatar_static' => $profile->avatarUrl(),
|
||||
'header' => '',
|
||||
'header_static' => '',
|
||||
'moved' => null,
|
||||
'fields' => null,
|
||||
'bot' => null
|
||||
];
|
||||
}
|
||||
}
|
16
app/Transformer/Api/ApplicationTransformer.php
Normal file
16
app/Transformer/Api/ApplicationTransformer.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use League\Fractal;
|
||||
|
||||
class ApplicationTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform()
|
||||
{
|
||||
return [
|
||||
'name' => '',
|
||||
'website' => null
|
||||
];
|
||||
}
|
||||
}
|
18
app/Transformer/Api/HashtagTransformer.php
Normal file
18
app/Transformer/Api/HashtagTransformer.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Hashtag;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class HashtagTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Hashtag $hashtag)
|
||||
{
|
||||
return [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url(),
|
||||
];
|
||||
}
|
||||
}
|
24
app/Transformer/Api/MediaTransformer.php
Normal file
24
app/Transformer/Api/MediaTransformer.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Media;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class MediaTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Media $media)
|
||||
{
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'type' => 'image',
|
||||
'url' => $media->url(),
|
||||
'remote_url' => null,
|
||||
'preview_url' => $media->thumbnailUrl(),
|
||||
'text_url' => null,
|
||||
'meta' => null,
|
||||
'description' => null
|
||||
];
|
||||
}
|
||||
}
|
19
app/Transformer/Api/MentionTransformer.php
Normal file
19
app/Transformer/Api/MentionTransformer.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class MentionTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
return [
|
||||
'id' => $profile->id,
|
||||
'url' => $profile->url(),
|
||||
'username' => $profile->username,
|
||||
'acct' => $profile->username,
|
||||
];
|
||||
}
|
||||
}
|
69
app/Transformer/Api/StatusTransformer.php
Normal file
69
app/Transformer/Api/StatusTransformer.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'mentions',
|
||||
'media_attachments',
|
||||
'tags'
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => $status->id,
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
|
||||
// TODO: fixme
|
||||
'reblog' => null,
|
||||
|
||||
'content' => "<p>$status->rendered</p>",
|
||||
'created_at' => $status->created_at->format('c'),
|
||||
'emojis' => [],
|
||||
'reblogs_count' => $status->shares()->count(),
|
||||
'favourites_count' => $status->likes()->count(),
|
||||
'reblogged' => $status->shared(),
|
||||
'favourited' => $status->liked(),
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => '',
|
||||
'visibility' => $status->visibility,
|
||||
'application' => null,
|
||||
'language' => null,
|
||||
'pinned' => null
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
return $this->item($account, new AccountTransformer);
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
return $this->collection($mentions, new MentionTransformer);
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
$media = $status->media;
|
||||
return $this->collection($media, new MediaTransformer);
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
return $this->collection($tags, new HashtagTransformer);
|
||||
}
|
||||
}
|
15
app/User.php
15
app/User.php
|
@ -3,11 +3,19 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Notifiable;
|
||||
use Notifiable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at', 'email_verified_at'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
@ -36,4 +44,9 @@ class User extends Authenticatable
|
|||
{
|
||||
return url(config('app.url') . '/' . $this->username);
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
return $this->hasOne(UserSetting::class);
|
||||
}
|
||||
}
|
||||
|
|
10
app/UserFilter.php
Normal file
10
app/UserFilter.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserFilter extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/UserSetting.php
Normal file
10
app/UserSetting.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserSetting extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -118,6 +118,7 @@ class RestrictedNames {
|
|||
|
||||
// Static Assets
|
||||
"assets",
|
||||
"storage",
|
||||
|
||||
// Laravel Horizon
|
||||
"horizon",
|
||||
|
@ -127,14 +128,19 @@ class RestrictedNames {
|
|||
"api",
|
||||
"auth",
|
||||
"i",
|
||||
"dashboard",
|
||||
"discover",
|
||||
"docs",
|
||||
"home",
|
||||
"login",
|
||||
"logout",
|
||||
"media",
|
||||
"p",
|
||||
"password",
|
||||
"reports",
|
||||
"search",
|
||||
"settings",
|
||||
"statuses",
|
||||
"site",
|
||||
"timeline",
|
||||
"user",
|
||||
|
|
|
@ -103,6 +103,10 @@ class Image {
|
|||
$ratio = $this->getAspectRatio($file, $thumbnail);
|
||||
$aspect = $ratio['dimensions'];
|
||||
$orientation = $ratio['orientation'];
|
||||
if($media->mime === 'image/gif' && !$thumbnail)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$img = Intervention::make($file)->orientate();
|
||||
|
|
|
@ -31,13 +31,9 @@ class Webfinger {
|
|||
|
||||
public function generateAliases()
|
||||
{
|
||||
$host = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
$username = $this->user->username;
|
||||
$url = $this->user->url();
|
||||
|
||||
$this->aliases = [
|
||||
'acct:'.$username.'@'.$host,
|
||||
$url
|
||||
$this->user->url(),
|
||||
$this->user->permalink()
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
@ -55,24 +51,12 @@ class Webfinger {
|
|||
[
|
||||
'rel' => 'http://schemas.google.com/g/2010#updates-from',
|
||||
'type' => 'application/atom+xml',
|
||||
'href' => url("/users/{$user->username}.atom")
|
||||
'href' => $user->permalink('.atom')
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->permalink()
|
||||
],
|
||||
[
|
||||
'rel' => 'magic-public-key',
|
||||
'href' => null//$user->public_key
|
||||
],
|
||||
[
|
||||
'rel' => 'salmon',
|
||||
'href' => $user->permalink('/salmon')
|
||||
],
|
||||
[
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'href' => url('/main/ostatussub?profile={uri}')
|
||||
]
|
||||
];
|
||||
return $this;
|
||||
|
|
|
@ -6,18 +6,23 @@
|
|||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"99designs/http-signatures-guzzlehttp": "^2.0",
|
||||
"beyondcode/laravel-self-diagnosis": "^0.4.0",
|
||||
"bitverse/identicon": "^1.1",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"fideloper/proxy": "^4.0",
|
||||
"greggilbert/recaptcha": "dev-master",
|
||||
"intervention/image": "^2.4",
|
||||
"kitetail/zttp": "^0.3.0",
|
||||
"pixelfed/zttp": "^0.4",
|
||||
"laravel/framework": "5.6.*",
|
||||
"laravel/horizon": "^1.2",
|
||||
"laravel/passport": "^6.0",
|
||||
"laravel/tinker": "^1.0",
|
||||
"league/fractal": "^0.17.0",
|
||||
"moontoast/math": "^1.1",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
"pixelfed/dotenv-editor": "^2.0",
|
||||
"pixelfed/fractal": "^0.18.0",
|
||||
"pixelfed/google2fa-laravel": "^2.0",
|
||||
"pixelfed/http-signatures-guzzlehttp": "^4.0",
|
||||
"predis/predis": "^1.1",
|
||||
"spatie/laravel-backup": "^5.0.0",
|
||||
"spatie/laravel-image-optimizer": "^1.1",
|
||||
|
@ -25,6 +30,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.1",
|
||||
"beyondcode/laravel-er-diagram-generator": "^0.2.2",
|
||||
"filp/whoops": "^2.0",
|
||||
"fzaninotto/faker": "^1.4",
|
||||
"mockery/mockery": "^1.0",
|
||||
|
|
2148
composer.lock
generated
2148
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -65,7 +65,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -78,7 +78,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -91,7 +91,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'fallback_locale' => 'en',
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -151,6 +151,7 @@ return [
|
|||
* Package Service Providers...
|
||||
*/
|
||||
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
|
||||
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
|
@ -211,6 +212,7 @@ return [
|
|||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
||||
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
27
config/dotenv-editor.php
Normal file
27
config/dotenv-editor.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
return array(
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Auto backup mode
|
||||
|----------------------------------------------------------------------
|
||||
|
|
||||
| This value is used when you save your file content. If value is true,
|
||||
| the original file will be backed up before save.
|
||||
*/
|
||||
|
||||
'autoBackup' => true,
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Backup location
|
||||
|----------------------------------------------------------------------
|
||||
|
|
||||
| This value is used when you backup your file. This value is the sub
|
||||
| path from root folder of project application.
|
||||
*/
|
||||
|
||||
'backupPath' => base_path('storage/dotenv-editor/backups/')
|
||||
|
||||
);
|
|
@ -49,5 +49,5 @@ return [
|
|||
* If set to `true` all output of the optimizer binaries will be appended to the default log.
|
||||
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
|
||||
*/
|
||||
'log_optimizer_activity' => true,
|
||||
'log_optimizer_activity' => false,
|
||||
];
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your PixelFed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.1.0',
|
||||
'version' => '0.1.2',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -77,6 +77,17 @@ return [
|
|||
|
||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Account file size limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Update the max account size, the per user limit of files in KB.
|
||||
|
|
||||
|
|
||||
*/
|
||||
'max_account_size' => env('MAX_ACCOUNT_SIZE', 100000),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Photo file size limit
|
||||
|
@ -96,5 +107,25 @@ return [
|
|||
|
|
||||
*/
|
||||
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Album size limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The max number of photos allowed per post.
|
||||
|
|
||||
*/
|
||||
'max_album_length' => env('MAX_ALBUM_LENGTH', 4),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email Verification
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Require email verification before a new user can do anything.
|
||||
|
|
||||
*/
|
||||
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
|
||||
|
||||
];
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateWebSubsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('web_subs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('follower_id')->unsigned()->index();
|
||||
$table->bigInteger('following_id')->unsigned()->index();
|
||||
$table->string('profile_url')->index();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->unique(['follower_id', 'following_id', 'profile_url']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_subs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateImportJobsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('import_jobs', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->string('service')->default('instagram');
|
||||
$table->string('uuid')->nullable();
|
||||
$table->string('storage_path')->nullable();
|
||||
$table->tinyInteger('stage')->unsigned()->default(0);
|
||||
$table->text('media_json')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('import_jobs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddFiltersToMediaTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->string('filter_name')->nullable()->after('orientation');
|
||||
$table->string('filter_class')->nullable()->after('filter_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->dropColumn('filter_name');
|
||||
$table->dropColumn('filter_class');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSoftDeletesToModels extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('avatars', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('likes', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('media', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('mentions', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('notifications', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('profiles', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('statuses', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('users', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateEmailVerificationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('email_verifications', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('user_token')->index();
|
||||
$table->string('random_token')->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('email_verifications');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateReportCommentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_comments', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('report_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('user_id')->unsigned();
|
||||
$table->text('comment');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_comments');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateReportLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_logs', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('item_id')->unsigned()->nullable();
|
||||
$table->string('item_type')->nullable();
|
||||
$table->string('action')->nullable();
|
||||
$table->boolean('system_message')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_logs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateAccountLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('account_logs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->index();
|
||||
$table->bigInteger('item_id')->unsigned()->nullable();
|
||||
$table->string('item_type')->nullable();
|
||||
$table->string('action')->nullable();
|
||||
$table->string('message')->nullable();
|
||||
$table->string('link')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('user_agent')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('account_logs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateUserSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_settings', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->unique();
|
||||
$table->string('role')->default('user');
|
||||
$table->boolean('crawlable')->default(true);
|
||||
$table->boolean('show_guests')->default(true);
|
||||
$table->boolean('show_discover')->default(true);
|
||||
$table->boolean('public_dm')->default(false);
|
||||
$table->boolean('hide_cw_search')->default(true);
|
||||
$table->boolean('hide_blocked_search')->default(true);
|
||||
$table->boolean('always_show_cw')->default(false);
|
||||
$table->boolean('compose_media_descriptions')->default(false);
|
||||
$table->boolean('reduce_motion')->default(false);
|
||||
$table->boolean('optimize_screen_reader')->default(false);
|
||||
$table->boolean('high_contrast_mode')->default(false);
|
||||
$table->boolean('video_autoplay')->default(false);
|
||||
$table->boolean('send_email_new_follower')->default(false);
|
||||
$table->boolean('send_email_new_follower_request')->default(true);
|
||||
$table->boolean('send_email_on_share')->default(false);
|
||||
$table->boolean('send_email_on_like')->default(false);
|
||||
$table->boolean('send_email_on_mention')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_settings');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class Add2faToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('2fa_enabled')->default(false);
|
||||
$table->string('2fa_secret')->nullable();
|
||||
$table->json('2fa_backup_codes')->nullable();
|
||||
$table->timestamp('2fa_setup_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('2fa_enabled');
|
||||
$table->dropColumn('2fa_secret');
|
||||
$table->dropColumn('2fa_backup_codes');
|
||||
$table->dropColumn('2fa_setup_at');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateUserFiltersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_filters', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned()->index();
|
||||
$table->bigInteger('filterable_id')->unsigned();
|
||||
$table->string('filterable_type');
|
||||
$table->string('filter_type')->default('block')->index();
|
||||
$table->unique([
|
||||
'user_id',
|
||||
'filterable_id',
|
||||
'filterable_type',
|
||||
'filter_type'
|
||||
], 'filter_unique');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_filters');
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ services:
|
|||
- "mysql-data:/var/lib/mysql"
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
image: redis:4-alpine
|
||||
volumes:
|
||||
- "redis-data:/data"
|
||||
networks:
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/img/pixelfed-icon-black.svg
Normal file
BIN
public/img/pixelfed-icon-black.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
public/img/pixelfed-icon-color.svg
Normal file
BIN
public/img/pixelfed-icon-color.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
Binary file not shown.
1
resources/assets/js/bootstrap.js
vendored
1
resources/assets/js/bootstrap.js
vendored
|
@ -22,6 +22,7 @@ try {
|
|||
require('./components/commentform');
|
||||
require('./components/searchform');
|
||||
require('./components/bookmarkform');
|
||||
require('./components/statusform');
|
||||
} catch (e) {}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,12 +14,14 @@ $(document).ready(function() {
|
|||
let commenttext = commentform.val();
|
||||
let item = {item: id, comment: commenttext};
|
||||
|
||||
commentform.prop('disabled', true);
|
||||
axios.post('/i/comment', item)
|
||||
.then(function (res) {
|
||||
|
||||
var username = res.data.username;
|
||||
var permalink = res.data.url;
|
||||
var profile = res.data.profile;
|
||||
var reply = res.data.comment;
|
||||
|
||||
if($('.status-container').length == 1) {
|
||||
var comments = el.parents().eq(3).find('.comments');
|
||||
|
@ -27,12 +29,13 @@ $(document).ready(function() {
|
|||
var comments = el.parents().eq(1).find('.comments');
|
||||
}
|
||||
|
||||
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ commenttext + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
|
||||
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
|
||||
|
||||
comments.prepend(comment);
|
||||
|
||||
commentform.val('');
|
||||
commentform.blur();
|
||||
commentform.prop('disabled', false);
|
||||
|
||||
})
|
||||
.catch(function (res) {
|
||||
|
|
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal file
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="tokens.length > 0">
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">Authorized Applications</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Authorized Tokens -->
|
||||
<table class="table table-borderless mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Scopes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="token in tokens">
|
||||
<!-- Client Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ token.client.name }}
|
||||
</td>
|
||||
|
||||
<!-- Scopes -->
|
||||
<td style="vertical-align: middle;">
|
||||
<span v-if="token.scopes.length > 0">
|
||||
{{ token.scopes.join(', ') }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Revoke Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="revoke(token)">
|
||||
Revoke
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
tokens: []
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getTokens();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the authorized tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/oauth/tokens')
|
||||
.then(response => {
|
||||
this.tokens = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke the given token.
|
||||
*/
|
||||
revoke(token) {
|
||||
axios.delete('/oauth/tokens/' + token.id)
|
||||
.then(response => {
|
||||
this.getTokens();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
350
resources/assets/js/components/passport/Clients.vue
Normal file
350
resources/assets/js/components/passport/Clients.vue
Normal file
|
@ -0,0 +1,350 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
OAuth Clients
|
||||
</span>
|
||||
|
||||
<a class="action-link" tabindex="-1" @click="showCreateClientForm">
|
||||
Create New Client
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Current Clients -->
|
||||
<p class="mb-0" v-if="clients.length === 0">
|
||||
You have not created any OAuth clients.
|
||||
</p>
|
||||
|
||||
<table class="table table-borderless mb-0" v-if="clients.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Name</th>
|
||||
<th>Secret</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="client in clients">
|
||||
<!-- ID -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ client.id }}
|
||||
</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
|
||||
<!-- Secret -->
|
||||
<td style="vertical-align: middle;">
|
||||
<code>{{ client.secret }}</code>
|
||||
</td>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link" tabindex="-1" @click="edit(client)">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="destroy(client)">
|
||||
Delete
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Client Modal -->
|
||||
<div class="modal fade" id="modal-create-client" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Create Client
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="createForm.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in createForm.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Create Client Form -->
|
||||
<form role="form">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input id="create-client-name" type="text" class="form-control" autocomplete="off"
|
||||
@keyup.enter="store" v-model="createForm.name">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Something your users will recognize and trust.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect URL -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Redirect URL</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" name="redirect"
|
||||
@keyup.enter="store" v-model="createForm.redirect">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Your application's authorization callback URL.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Client Modal -->
|
||||
<div class="modal fade" id="modal-edit-client" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Edit Client
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="editForm.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in editForm.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Edit Client Form -->
|
||||
<form role="form">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input id="edit-client-name" type="text" class="form-control"
|
||||
@keyup.enter="update" v-model="editForm.name">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Something your users will recognize and trust.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect URL -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-3 col-form-label">Redirect URL</label>
|
||||
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" name="redirect"
|
||||
@keyup.enter="update" v-model="editForm.redirect">
|
||||
|
||||
<span class="form-text text-muted">
|
||||
Your application's authorization callback URL.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" @click="update">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
clients: [],
|
||||
|
||||
createForm: {
|
||||
errors: [],
|
||||
name: '',
|
||||
redirect: ''
|
||||
},
|
||||
|
||||
editForm: {
|
||||
errors: [],
|
||||
name: '',
|
||||
redirect: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getClients();
|
||||
|
||||
$('#modal-create-client').on('shown.bs.modal', () => {
|
||||
$('#create-client-name').focus();
|
||||
});
|
||||
|
||||
$('#modal-edit-client').on('shown.bs.modal', () => {
|
||||
$('#edit-client-name').focus();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the OAuth clients for the user.
|
||||
*/
|
||||
getClients() {
|
||||
axios.get('/oauth/clients')
|
||||
.then(response => {
|
||||
this.clients = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the form for creating new clients.
|
||||
*/
|
||||
showCreateClientForm() {
|
||||
$('#modal-create-client').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new OAuth client for the user.
|
||||
*/
|
||||
store() {
|
||||
this.persistClient(
|
||||
'post', '/oauth/clients',
|
||||
this.createForm, '#modal-create-client'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the given client.
|
||||
*/
|
||||
edit(client) {
|
||||
this.editForm.id = client.id;
|
||||
this.editForm.name = client.name;
|
||||
this.editForm.redirect = client.redirect;
|
||||
|
||||
$('#modal-edit-client').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the client being edited.
|
||||
*/
|
||||
update() {
|
||||
this.persistClient(
|
||||
'put', '/oauth/clients/' + this.editForm.id,
|
||||
this.editForm, '#modal-edit-client'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Persist the client to storage using the given form.
|
||||
*/
|
||||
persistClient(method, uri, form, modal) {
|
||||
form.errors = [];
|
||||
|
||||
axios[method](uri, form)
|
||||
.then(response => {
|
||||
this.getClients();
|
||||
|
||||
form.name = '';
|
||||
form.redirect = '';
|
||||
form.errors = [];
|
||||
|
||||
$(modal).modal('hide');
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof error.response.data === 'object') {
|
||||
form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
} else {
|
||||
form.errors = ['Something went wrong. Please try again.'];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the given client.
|
||||
*/
|
||||
destroy(client) {
|
||||
axios.delete('/oauth/clients/' + client.id)
|
||||
.then(response => {
|
||||
this.getClients();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal file
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal file
|
@ -0,0 +1,298 @@
|
|||
<style scoped>
|
||||
.action-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="card card-default mb-4">
|
||||
<div class="card-header font-weight-bold bg-white">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>
|
||||
Personal Access Tokens
|
||||
</span>
|
||||
|
||||
<a class="action-link" tabindex="-1" @click="showCreateTokenForm">
|
||||
Create New Token
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- No Tokens Notice -->
|
||||
<p class="mb-0" v-if="tokens.length === 0">
|
||||
You have not created any personal access tokens.
|
||||
</p>
|
||||
|
||||
<!-- Personal Access Tokens -->
|
||||
<table class="table table-borderless mb-0" v-if="tokens.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="token in tokens">
|
||||
<!-- Client Name -->
|
||||
<td style="vertical-align: middle;">
|
||||
{{ token.name }}
|
||||
</td>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<td style="vertical-align: middle;">
|
||||
<a class="action-link text-danger" @click="revoke(token)">
|
||||
Delete
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Token Modal -->
|
||||
<div class="modal fade" id="modal-create-token" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Create Token
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Form Errors -->
|
||||
<div class="alert alert-danger" v-if="form.errors.length > 0">
|
||||
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
|
||||
<br>
|
||||
<ul>
|
||||
<li v-for="error in form.errors">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Create Token Form -->
|
||||
<form role="form" @submit.prevent="store">
|
||||
<!-- Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-md-4 col-form-label">Name</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input id="create-token-name" type="text" class="form-control" name="name" v-model="form.name" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scopes -->
|
||||
<div class="form-group row" v-if="scopes.length > 0">
|
||||
<label class="col-md-4 col-form-label">Scopes</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-for="scope in scopes">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
@click="toggleScope(scope.id)"
|
||||
:checked="scopeIsAssigned(scope.id)">
|
||||
|
||||
{{ scope.id }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access Token Modal -->
|
||||
<div class="modal fade" id="modal-access-token" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
Personal Access Token
|
||||
</h4>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Here is your new personal access token. This is the only time it will be shown so don't lose it!
|
||||
You may now use this token to make API requests.
|
||||
</p>
|
||||
|
||||
<textarea class="form-control" rows="10">{{ accessToken }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/*
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
accessToken: null,
|
||||
|
||||
tokens: [],
|
||||
scopes: [],
|
||||
|
||||
form: {
|
||||
name: '',
|
||||
scopes: [],
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 1.x).
|
||||
*/
|
||||
ready() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component (Vue 2.x).
|
||||
*/
|
||||
mounted() {
|
||||
this.prepareComponent();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
prepareComponent() {
|
||||
this.getTokens();
|
||||
this.getScopes();
|
||||
|
||||
$('#modal-create-token').on('shown.bs.modal', () => {
|
||||
$('#create-token-name').focus();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the personal access tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/oauth/personal-access-tokens')
|
||||
.then(response => {
|
||||
this.tokens = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the available scopes.
|
||||
*/
|
||||
getScopes() {
|
||||
axios.get('/oauth/scopes')
|
||||
.then(response => {
|
||||
this.scopes = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the form for creating new tokens.
|
||||
*/
|
||||
showCreateTokenForm() {
|
||||
$('#modal-create-token').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new personal access token.
|
||||
*/
|
||||
store() {
|
||||
this.accessToken = null;
|
||||
|
||||
this.form.errors = [];
|
||||
|
||||
axios.post('/oauth/personal-access-tokens', this.form)
|
||||
.then(response => {
|
||||
this.form.name = '';
|
||||
this.form.scopes = [];
|
||||
this.form.errors = [];
|
||||
|
||||
this.tokens.push(response.data.token);
|
||||
|
||||
this.showAccessToken(response.data.accessToken);
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof error.response.data === 'object') {
|
||||
this.form.errors = _.flatten(_.toArray(error.response.data.errors));
|
||||
} else {
|
||||
this.form.errors = ['Something went wrong. Please try again.'];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the given scope in the list of assigned scopes.
|
||||
*/
|
||||
toggleScope(scope) {
|
||||
if (this.scopeIsAssigned(scope)) {
|
||||
this.form.scopes = _.reject(this.form.scopes, s => s == scope);
|
||||
} else {
|
||||
this.form.scopes.push(scope);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the given scope has been assigned to the token.
|
||||
*/
|
||||
scopeIsAssigned(scope) {
|
||||
return _.indexOf(this.form.scopes, scope) >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the given access token to the user.
|
||||
*/
|
||||
showAccessToken(accessToken) {
|
||||
$('#modal-create-token').modal('hide');
|
||||
|
||||
this.accessToken = accessToken;
|
||||
|
||||
$('#modal-access-token').modal('show');
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke the given token.
|
||||
*/
|
||||
revoke(token) {
|
||||
axios.delete('/oauth/personal-access-tokens/' + token.id)
|
||||
.then(response => {
|
||||
this.getTokens();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
110
resources/assets/js/components/statusform.js
vendored
Normal file
110
resources/assets/js/components/statusform.js
vendored
Normal file
|
@ -0,0 +1,110 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
$('#statusForm .btn-filter-select').on('click', function(e) {
|
||||
let el = $(this);
|
||||
});
|
||||
|
||||
pixelfed.create = {};
|
||||
pixelfed.filters = {};
|
||||
pixelfed.create.hasGeneratedSelect = false;
|
||||
pixelfed.create.selectedFilter = false;
|
||||
pixelfed.create.currentFilterName = false;
|
||||
pixelfed.create.currentFilterClass = false;
|
||||
|
||||
pixelfed.filters.list = [
|
||||
['1977','filter-1977'],
|
||||
['Aden','filter-aden'],
|
||||
['Amaro','filter-amaro'],
|
||||
['Ashby','filter-ashby'],
|
||||
['Brannan','filter-brannan'],
|
||||
['Brooklyn','filter-brooklyn'],
|
||||
['Charmes','filter-charmes'],
|
||||
['Clarendon','filter-clarendon'],
|
||||
['Crema','filter-crema'],
|
||||
['Dogpatch','filter-dogpatch'],
|
||||
['Earlybird','filter-earlybird'],
|
||||
['Gingham','filter-gingham'],
|
||||
['Ginza','filter-ginza'],
|
||||
['Hefe','filter-hefe'],
|
||||
['Helena','filter-helena'],
|
||||
['Hudson','filter-hudson'],
|
||||
['Inkwell','filter-inkwell'],
|
||||
['Kelvin','filter-kelvin'],
|
||||
['Kuno','filter-juno'],
|
||||
['Lark','filter-lark'],
|
||||
['Lo-Fi','filter-lofi'],
|
||||
['Ludwig','filter-ludwig'],
|
||||
['Maven','filter-maven'],
|
||||
['Mayfair','filter-mayfair'],
|
||||
['Moon','filter-moon'],
|
||||
['Nashville','filter-nashville'],
|
||||
['Perpetua','filter-perpetua'],
|
||||
['Poprocket','filter-poprocket'],
|
||||
['Reyes','filter-reyes'],
|
||||
['Rise','filter-rise'],
|
||||
['Sierra','filter-sierra'],
|
||||
['Skyline','filter-skyline'],
|
||||
['Slumber','filter-slumber'],
|
||||
['Stinson','filter-stinson'],
|
||||
['Sutro','filter-sutro'],
|
||||
['Toaster','filter-toaster'],
|
||||
['Valencia','filter-valencia'],
|
||||
['Vesper','filter-vesper'],
|
||||
['Walden','filter-walden'],
|
||||
['Willow','filter-willow'],
|
||||
['X-Pro II','filter-xpro-ii']
|
||||
];
|
||||
|
||||
function previewImage(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
$('.filterPreview').attr('src', e.target.result);
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function generateFilterSelect() {
|
||||
let filters = pixelfed.filters.list;
|
||||
for(var i = 0, len = filters.length; i < len; i++) {
|
||||
let filter = filters[i];
|
||||
let name = filter[0];
|
||||
let className = filter[1];
|
||||
let select = $('#filterSelectDropdown');
|
||||
var template = '<option value="' + className + '">' + name + '</option>';
|
||||
select.append(template);
|
||||
}
|
||||
pixelfed.create.hasGeneratedSelect = true;
|
||||
}
|
||||
|
||||
$('#fileInput').on('change', function() {
|
||||
previewImage(this);
|
||||
$('#statusForm .form-filters.d-none').removeClass('d-none');
|
||||
$('#statusForm .form-preview.d-none').removeClass('d-none');
|
||||
$('#statusForm #collapsePreview').collapse('show');
|
||||
if(!pixelfed.create.hasGeneratedSelect) {
|
||||
generateFilterSelect();
|
||||
}
|
||||
});
|
||||
|
||||
$('#filterSelectDropdown').on('change', function() {
|
||||
let el = $(this);
|
||||
let filter = el.val();
|
||||
let oldFilter = pixelfed.create.currentFilterClass;
|
||||
if(filter == 'none') {
|
||||
$('.filterContainer').removeClass(oldFilter);
|
||||
pixelfed.create.currentFilterClass = false;
|
||||
pixelfed.create.currentFilterName = 'None';
|
||||
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
|
||||
return;
|
||||
}
|
||||
$('.filterContainer').removeClass(oldFilter).addClass(filter);
|
||||
pixelfed.create.currentFilterClass = filter;
|
||||
pixelfed.create.currentFilterName = el.find(':selected').text();
|
||||
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
|
||||
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
|
||||
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
|
||||
});
|
||||
|
||||
});
|
2
resources/assets/sass/app.scss
vendored
2
resources/assets/sass/app.scss
vendored
|
@ -11,6 +11,8 @@
|
|||
|
||||
@import "custom";
|
||||
|
||||
@import "components/filters";
|
||||
|
||||
@import "components/typeahead";
|
||||
|
||||
@import "components/notifications";
|
||||
|
|
445
resources/assets/sass/components/filters.scss
vendored
Normal file
445
resources/assets/sass/components/filters.scss
vendored
Normal file
|
@ -0,0 +1,445 @@
|
|||
/*! Instagram.css v0.1.3 | MIT License | github.com/picturepan2/instagram.css */
|
||||
[class*="filter"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[class*="filter"]::before {
|
||||
display: block;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.filter-1977 {
|
||||
-webkit-filter: sepia(.5) hue-rotate(-30deg) saturate(1.4);
|
||||
filter: sepia(.5) hue-rotate(-30deg) saturate(1.4);
|
||||
}
|
||||
|
||||
.filter-aden {
|
||||
-webkit-filter: sepia(.2) brightness(1.15) saturate(1.4);
|
||||
filter: sepia(.2) brightness(1.15) saturate(1.4);
|
||||
}
|
||||
|
||||
.filter-aden::before {
|
||||
background: rgba(125, 105, 24, .1);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-amaro {
|
||||
-webkit-filter: sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3);
|
||||
filter: sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3);
|
||||
}
|
||||
|
||||
.filter-amaro::before {
|
||||
background: rgba(125, 105, 24, .2);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-ashby {
|
||||
-webkit-filter: sepia(.5) contrast(1.2) saturate(1.8);
|
||||
filter: sepia(.5) contrast(1.2) saturate(1.8);
|
||||
}
|
||||
|
||||
.filter-ashby::before {
|
||||
background: rgba(125, 105, 24, .35);
|
||||
content: "";
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.filter-brannan {
|
||||
-webkit-filter: sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg);
|
||||
filter: sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg);
|
||||
}
|
||||
|
||||
.filter-brooklyn {
|
||||
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg);
|
||||
filter: sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg);
|
||||
}
|
||||
|
||||
.filter-brooklyn::before {
|
||||
background: rgba(127, 187, 227, .2);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-charmes {
|
||||
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg);
|
||||
filter: sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg);
|
||||
}
|
||||
|
||||
.filter-charmes::before {
|
||||
background: rgba(125, 105, 24, .25);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-clarendon {
|
||||
-webkit-filter: sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg);
|
||||
filter: sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg);
|
||||
}
|
||||
|
||||
.filter-clarendon::before {
|
||||
background: rgba(127, 187, 227, .4);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-crema {
|
||||
-webkit-filter: sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg);
|
||||
filter: sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg);
|
||||
}
|
||||
|
||||
.filter-crema::before {
|
||||
background: rgba(125, 105, 24, .2);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-dogpatch {
|
||||
-webkit-filter: sepia(.35) saturate(1.1) contrast(1.5);
|
||||
filter: sepia(.35) saturate(1.1) contrast(1.5);
|
||||
}
|
||||
|
||||
.filter-earlybird {
|
||||
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg);
|
||||
filter: sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg);
|
||||
}
|
||||
|
||||
.filter-earlybird::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-gingham {
|
||||
-webkit-filter: contrast(1.1) brightness(1.1);
|
||||
filter: contrast(1.1) brightness(1.1);
|
||||
}
|
||||
|
||||
.filter-gingham::before {
|
||||
background: #e6e6e6;
|
||||
content: "";
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
.filter-ginza {
|
||||
-webkit-filter: sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg);
|
||||
filter: sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg);
|
||||
}
|
||||
|
||||
.filter-ginza::before {
|
||||
background: rgba(125, 105, 24, .15);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-hefe {
|
||||
-webkit-filter: sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg);
|
||||
filter: sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg);
|
||||
}
|
||||
|
||||
.filter-hefe::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-helena {
|
||||
-webkit-filter: sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35);
|
||||
filter: sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35);
|
||||
}
|
||||
|
||||
.filter-helena::before {
|
||||
background: rgba(158, 175, 30, .25);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-hudson {
|
||||
-webkit-filter: sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg);
|
||||
filter: sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg);
|
||||
}
|
||||
|
||||
.filter-hudson::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-inkwell {
|
||||
-webkit-filter: brightness(1.25) contrast(.85) grayscale(1);
|
||||
filter: brightness(1.25) contrast(.85) grayscale(1);
|
||||
}
|
||||
|
||||
.filter-juno {
|
||||
-webkit-filter: sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8);
|
||||
filter: sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8);
|
||||
}
|
||||
|
||||
.filter-juno::before {
|
||||
background: rgba(127, 187, 227, .2);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-kelvin {
|
||||
-webkit-filter: sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg);
|
||||
filter: sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg);
|
||||
}
|
||||
|
||||
.filter-kelvin::before {
|
||||
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-lark {
|
||||
-webkit-filter: sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25);
|
||||
filter: sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25);
|
||||
}
|
||||
|
||||
.filter-lofi {
|
||||
-webkit-filter: saturate(1.1) contrast(1.5);
|
||||
filter: saturate(1.1) contrast(1.5);
|
||||
}
|
||||
|
||||
.filter-ludwig {
|
||||
-webkit-filter: sepia(.25) contrast(1.05) brightness(1.05) saturate(2);
|
||||
filter: sepia(.25) contrast(1.05) brightness(1.05) saturate(2);
|
||||
}
|
||||
|
||||
.filter-ludwig::before {
|
||||
background: rgba(125, 105, 24, .1);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-maven {
|
||||
-webkit-filter: sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75);
|
||||
filter: sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75);
|
||||
}
|
||||
|
||||
.filter-maven::before {
|
||||
background: rgba(158, 175, 30, .25);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-mayfair {
|
||||
-webkit-filter: contrast(1.1) brightness(1.15) saturate(1.1);
|
||||
filter: contrast(1.1) brightness(1.15) saturate(1.1);
|
||||
}
|
||||
|
||||
.filter-mayfair::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-moon {
|
||||
-webkit-filter: brightness(1.4) contrast(.95) saturate(0) sepia(.35);
|
||||
filter: brightness(1.4) contrast(.95) saturate(0) sepia(.35);
|
||||
}
|
||||
|
||||
.filter-nashville {
|
||||
-webkit-filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
|
||||
filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
|
||||
}
|
||||
|
||||
.filter-nashville::before {
|
||||
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.filter-perpetua {
|
||||
-webkit-filter: contrast(1.1) brightness(1.25) saturate(1.1);
|
||||
filter: contrast(1.1) brightness(1.25) saturate(1.1);
|
||||
}
|
||||
|
||||
.filter-perpetua::before {
|
||||
background: linear-gradient(to bottom, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
|
||||
background: -o-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
|
||||
background: -moz-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
|
||||
background: -webkit-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 91, 154, .25)), to(rgba(230, 193, 61, .25)));
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.filter-poprocket {
|
||||
-webkit-filter: sepia(.15) brightness(1.2);
|
||||
filter: sepia(.15) brightness(1.2);
|
||||
}
|
||||
|
||||
.filter-poprocket::before {
|
||||
background: radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
|
||||
background: -o-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
|
||||
background: -moz-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
|
||||
content: "";
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.filter-reyes {
|
||||
-webkit-filter: sepia(.75) contrast(.75) brightness(1.25) saturate(1.4);
|
||||
filter: sepia(.75) contrast(.75) brightness(1.25) saturate(1.4);
|
||||
}
|
||||
|
||||
.filter-rise {
|
||||
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.2) saturate(.9);
|
||||
filter: sepia(.25) contrast(1.25) brightness(1.2) saturate(.9);
|
||||
}
|
||||
|
||||
.filter-rise::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.filter-sierra {
|
||||
-webkit-filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
|
||||
filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
|
||||
}
|
||||
|
||||
.filter-sierra::before {
|
||||
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.filter-skyline {
|
||||
-webkit-filter: sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2);
|
||||
filter: sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2);
|
||||
}
|
||||
|
||||
.filter-slumber {
|
||||
-webkit-filter: sepia(.35) contrast(1.25) saturate(1.25);
|
||||
filter: sepia(.35) contrast(1.25) saturate(1.25);
|
||||
}
|
||||
|
||||
.filter-slumber::before {
|
||||
background: rgba(125, 105, 24, .2);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-stinson {
|
||||
-webkit-filter: sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25);
|
||||
filter: sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25);
|
||||
}
|
||||
|
||||
.filter-stinson::before {
|
||||
background: rgba(125, 105, 24, .45);
|
||||
content: "";
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.filter-sutro {
|
||||
-webkit-filter: sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg);
|
||||
filter: sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg);
|
||||
}
|
||||
|
||||
.filter-sutro::before {
|
||||
background: radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
|
||||
background: -o-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
|
||||
background: -moz-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-toaster {
|
||||
-webkit-filter: sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg);
|
||||
filter: sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg);
|
||||
}
|
||||
|
||||
.filter-toaster::before {
|
||||
background: radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
|
||||
background: -o-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
|
||||
background: -moz-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
|
||||
background: -webkit-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
|
||||
content: "";
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.filter-valencia {
|
||||
-webkit-filter: sepia(.25) contrast(1.1) brightness(1.1);
|
||||
filter: sepia(.25) contrast(1.1) brightness(1.1);
|
||||
}
|
||||
|
||||
.filter-valencia::before {
|
||||
background: rgba(230, 193, 61, .1);
|
||||
content: "";
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.filter-vesper {
|
||||
-webkit-filter: sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3);
|
||||
filter: sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3);
|
||||
}
|
||||
|
||||
.filter-vesper::before {
|
||||
background: rgba(125, 105, 24, .25);
|
||||
content: "";
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
.filter-walden {
|
||||
-webkit-filter: sepia(.35) contrast(.8) brightness(1.25) saturate(1.4);
|
||||
filter: sepia(.35) contrast(.8) brightness(1.25) saturate(1.4);
|
||||
}
|
||||
|
||||
.filter-walden::before {
|
||||
background: rgba(229, 240, 128, .5);
|
||||
content: "";
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.filter-willow {
|
||||
-webkit-filter: brightness(1.2) contrast(.85) saturate(.05) sepia(.2);
|
||||
filter: brightness(1.2) contrast(.85) saturate(.05) sepia(.2);
|
||||
}
|
||||
|
||||
.filter-xpro-ii {
|
||||
-webkit-filter: sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg);
|
||||
filter: sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg);
|
||||
}
|
||||
|
||||
.filter-xpro-ii::before {
|
||||
background: radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -o-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -moz-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
|
||||
background: -webkit-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
|
||||
content: "";
|
||||
mix-blend-mode: multiply;
|
||||
}
|
29
resources/assets/sass/custom.scss
vendored
29
resources/assets/sass/custom.scss
vendored
|
@ -193,6 +193,13 @@ body, button, input, textarea {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
.card-md-rounded-0 {
|
||||
border-width: 1px 0;
|
||||
border-radius:0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-bar {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 100vw 0; }
|
||||
|
@ -234,3 +241,25 @@ body, button, input, textarea {
|
|||
height: 32px;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
|
||||
.status-photo img {
|
||||
object-fit: contain;
|
||||
max-height: calc(100vh - (6rem));
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-1.25em);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.details-animated[open] {
|
||||
animation-name: fadeInDown;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
|
|
13
resources/lang/de/navmenu.php
Normal file
13
resources/lang/de/navmenu.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'viewMyProfile' => 'Mein Profil anschauen',
|
||||
'myTimeline' => 'Meine Timeline',
|
||||
'publicTimeline' => 'Öffentliche Timeline',
|
||||
'remoteFollow' => 'Aus der Ferne folgen',
|
||||
'settings' => 'Einstellungen',
|
||||
'admin' => 'Administration',
|
||||
'logout' => 'Abmelden',
|
||||
|
||||
];
|
|
@ -5,5 +5,6 @@ return [
|
|||
'likedPhoto' => 'gefällt dein Foto.',
|
||||
'startedFollowingYou' => 'folgt dir nun.',
|
||||
'commented' => 'hat deinen Post kommentiert.',
|
||||
'mentionedYou' => 'hat dich erwähnt.'
|
||||
|
||||
];
|
|
@ -4,5 +4,5 @@ return [
|
|||
'emptyTimeline' => 'This user has no posts yet!',
|
||||
'emptyFollowers' => 'This user has no followers yet!',
|
||||
'emptyFollowing' => 'This user is not following anyone yet!',
|
||||
'savedWarning' => 'Only you can see what you\'ve saved',
|
||||
];
|
||||
'savedWarning' => 'Only you can see what you’ve saved',
|
||||
];
|
||||
|
|
13
resources/lang/he/navmenu.php
Normal file
13
resources/lang/he/navmenu.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'viewMyProfile' => 'צפה בפרופיל שלי',
|
||||
'myTimeline' => 'ציר הזמן שלי',
|
||||
'publicTimeline' => 'ציר הזמן הציבורי',
|
||||
'remoteFollow' => 'עקיבה מרחוק',
|
||||
'settings' => 'הגדרות',
|
||||
'admin' => 'מנהל',
|
||||
'logout' => 'התנתק',
|
||||
|
||||
];
|
|
@ -5,5 +5,6 @@ return [
|
|||
'likedPhoto' => 'אהבו את התמונה שלך.',
|
||||
'startedFollowingYou' => 'התחיל לעקוב אחריך.',
|
||||
'commented' => 'הגיב על הפוסט שלך.',
|
||||
'mentionedYou' => 'הזכיר אותך.'
|
||||
|
||||
];
|
||||
|
|
24
resources/views/account/verify_email.blade.php
Normal file
24
resources/views/account/verify_email.blade.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
@if (session('status'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="card">
|
||||
<div class="card-header font-weight-bold bg-white">Confirm Email Address</div>
|
||||
<div class="card-body">
|
||||
<p class="lead">You need to confirm your email address (<span class="font-weight-bold">{{Auth::user()->email}}</span>) before you can proceed.</p>
|
||||
<hr>
|
||||
<form method="post">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">Send Confirmation Email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -26,16 +26,16 @@
|
|||
<link rel="self" type="application/atom+xml" href="{{$profile->permalink('.atom')}}"/>
|
||||
@foreach($items as $item)
|
||||
<entry>
|
||||
<title><![CDATA[{{ $item->caption }}]]></title>
|
||||
<title>{{ $item->caption }}</title>
|
||||
<link rel="alternate" href="{{ $item->url() }}" />
|
||||
<id>{{ url($item->id) }}</id>
|
||||
<author>
|
||||
<name> <![CDATA[{{ $item->profile->username }}]]></name>
|
||||
</author>
|
||||
<summary type="html">
|
||||
<![CDATA[{!! $item->caption !!}]]>
|
||||
{{ $item->caption }}
|
||||
</summary>
|
||||
<updated>{{ $item->updated_at->toAtomString() }}</updated>
|
||||
</entry>
|
||||
@endforeach
|
||||
</feed>
|
||||
</feed>
|
||||
|
|
12
resources/views/emails/confirm_email.blade.php
Normal file
12
resources/views/emails/confirm_email.blade.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
@component('mail::message')
|
||||
# Email Confirmation
|
||||
|
||||
Please confirm your email address.
|
||||
|
||||
@component('mail::button', ['url' => $verify->url()])
|
||||
Confirm Email
|
||||
@endcomponent
|
||||
|
||||
Thanks,<br>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
|
@ -5,9 +5,9 @@
|
|||
<div class="error-page py-5 my-5">
|
||||
<div class="card mx-5">
|
||||
<div class="card-body p-5">
|
||||
<h1 class="text-center">403 - Forbidden</h1>
|
||||
<h1 class="text-center">403 – Forbidden</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<div class="error-page py-5 my-5">
|
||||
<div class="card mx-5">
|
||||
<div class="card-body p-5">
|
||||
<h1 class="text-center">404 - Page Not Found</h1>
|
||||
<h1 class="text-center">404 – Page Not Found</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
<div class="error-page py-5 my-5">
|
||||
<div class="card mx-5">
|
||||
<div class="card-body p-5 text-center">
|
||||
<h1>503 - Service Unavailable</h1>
|
||||
<h1>503 – Service Unavailable</h1>
|
||||
<p class="lead mb-0">Our services are overloaded at the moment, please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
<footer>
|
||||
<div class="container py-5">
|
||||
<p class="mb-0 text-uppercase font-weight-bold small">
|
||||
<a href="{{route('site.about')}}" class="text-primary pr-2">About Us</a>
|
||||
<a href="{{route('site.help')}}" class="text-primary pr-2">Support</a>
|
||||
<a href="{{route('site.opensource')}}" class="text-primary pr-2">Open Source</a>
|
||||
<a href="{{route('site.language')}}" class="text-primary pr-2">Language</a>
|
||||
<span class="px-2"></span>
|
||||
<a href="{{route('site.terms')}}" class="text-primary pr-2 pl-2">Terms</a>
|
||||
<a href="{{route('site.privacy')}}" class="text-primary pr-2">Privacy</a>
|
||||
<a href="{{route('site.platform')}}" class="text-primary pr-2">API</a>
|
||||
<span class="px-2"></span>
|
||||
<a href="#" class="text-primary pr-2 pl-2">Directory</a>
|
||||
<a href="#" class="text-primary pr-2">Profiles</a>
|
||||
<a href="#" class="text-primary">Hashtags</a>
|
||||
<p class="mb-0 text-uppercase font-weight-bold small text-justify">
|
||||
<a href="{{route('site.about')}}" class="text-primary pr-3">About Us</a>
|
||||
<a href="{{route('site.help')}}" class="text-primary pr-3">Support</a>
|
||||
<a href="{{route('site.opensource')}}" class="text-primary pr-3">Open Source</a>
|
||||
<a href="{{route('site.terms')}}" class="text-primary pr-3">Terms</a>
|
||||
<a href="{{route('site.privacy')}}" class="text-primary pr-3">Privacy</a>
|
||||
<a href="{{route('site.platform')}}" class="text-primary pr-3">API</a>
|
||||
<a href="#" class="text-primary pr-3">Directory</a>
|
||||
<a href="#" class="text-primary pr-3">Profiles</a>
|
||||
<a href="#" class="text-primary pr-3">Hashtags</a>
|
||||
<a href="{{route('site.language')}}" class="text-primary pr-3">Language</a>
|
||||
<a href="http://pixelfed.org" class="text-muted float-right" rel="noopener">Powered by PixelFed</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}" title="Logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
|
||||
<strong class="font-weight-bold">{{ config('app.name', 'Laravel') }}</strong>
|
||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
|
||||
<span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
@auth
|
||||
<ul class="navbar-nav ml-auto d-none d-md-block">
|
||||
<form class="form-inline search-form">
|
||||
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
|
||||
</form>
|
||||
</ul>
|
||||
@endauth
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@guest
|
||||
|
@ -21,7 +23,9 @@
|
|||
<a class="nav-link" href="{{route('discover')}}" title="Discover"><i class="far fa-compass fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{{route('notifications')}}" title="Notifications"><i class="far fa-heart fa-lg"></i></a>
|
||||
<a class="nav-link" href="{{route('notifications')}}" title="Notifications">
|
||||
<i class="far fa-heart fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown px-2">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre title="User Menu">
|
||||
|
@ -76,8 +80,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@auth
|
||||
<nav class="breadcrumb d-md-none d-flex">
|
||||
<form class="form-inline search-form mx-auto">
|
||||
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
|
||||
</form>
|
||||
</nav>
|
||||
@endauth
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app',['title' => $profile->username . "'s followers"])
|
||||
@extends('layouts.app',['title' => $profile->username . "’s followers"])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app',['title' => $profile->username . "'s follows"])
|
||||
@extends('layouts.app',['title' => $profile->username . "’s follows"])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue