pixelfed/pixelfed

View on GitHub
app/Http/Controllers/DirectMessageController.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

namespace App\Http\Controllers;

use Auth, Cache;
use Illuminate\Http\Request;
use App\{
    DirectMessage,
    Media,
    Notification,
    Profile,
    Status,
    User,
    UserFilter,
    UserSetting
};
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use Illuminate\Support\Str;
use App\Util\ActivityPub\Helpers;
use App\Services\WebfingerService;

class DirectMessageController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function browse(Request $request)
    {
        $this->validate($request, [
            'a' => 'nullable|string|in:inbox,sent,filtered',
            'page' => 'nullable|integer|min:1|max:99'
        ]);

        $profile = $request->user()->profile_id;
        $action = $request->input('a', 'inbox');
        $page = $request->input('page');

        if(config('database.default') == 'pgsql') {
            if($action == 'inbox') {
                $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
                ->whereToId($profile)
                ->with(['author','status'])
                ->whereIsHidden(false)
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->latest()
                ->get()
                ->unique('from_id')
                ->take(8)
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                })->values();
            }

            if($action == 'sent') {
                $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
                ->whereFromId($profile)
                ->with(['author','status'])
                ->orderBy('id', 'desc')
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->get()
                ->unique('to_id')
                ->take(8)
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                });
            }

            if($action == 'filtered') {
                $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
                ->whereToId($profile)
                ->with(['author','status'])
                ->whereIsHidden(true)
                ->orderBy('id', 'desc')
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->get()
                ->unique('from_id')
                ->take(8)
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                });
            }
        } elseif(config('database.default') == 'mysql') {
            if($action == 'inbox') {
                $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
                ->whereToId($profile)
                ->with(['author','status'])
                ->whereIsHidden(false)
                ->groupBy('from_id')
                ->latest()
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->limit(8)
                ->get()
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                });
            }

            if($action == 'sent') {
                $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
                ->whereFromId($profile)
                ->with(['author','status'])
                ->groupBy('to_id')
                ->orderBy('createdAt', 'desc')
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->limit(8)
                ->get()
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                });
            }

            if($action == 'filtered') {
                $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
                ->whereToId($profile)
                ->with(['author','status'])
                ->whereIsHidden(true)
                ->groupBy('from_id')
                ->orderBy('createdAt', 'desc')
                ->when($page, function($q, $page) {
                    if($page > 1) {
                        return $q->offset($page * 8 - 8);
                    }
                })
                ->limit(8)
                ->get()
                ->map(function($r) use($profile) {
                    return $r->from_id !== $profile ? [
                        'id' => (string) $r->from_id,
                        'name' => $r->author->name,
                        'username' => $r->author->username,
                        'avatar' => $r->author->avatarUrl(),
                        'url' => $r->author->url(),
                        'isLocal' => (bool) !$r->author->domain,
                        'domain' => $r->author->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ] : [
                        'id' => (string) $r->to_id,
                        'name' => $r->recipient->name,
                        'username' => $r->recipient->username,
                        'avatar' => $r->recipient->avatarUrl(),
                        'url' => $r->recipient->url(),
                        'isLocal' => (bool) !$r->recipient->domain,
                        'domain' => $r->recipient->domain,
                        'timeAgo' => $r->created_at->diffForHumans(null, true, true),
                        'lastMessage' => $r->status->caption,
                        'messages' => []
                    ];
                });
            }
        }

        return response()->json($dms->all());
    }

    public function create(Request $request)
    {
        $this->validate($request, [
            'to_id' => 'required',
            'message' => 'required|string|min:1|max:500',
            'type'  => 'required|in:text,emoji'
        ]);

        $profile = $request->user()->profile;
        $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));

        abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
        $msg = $request->input('message');

        if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
            if($recipient->follows($profile) == true) {
                $hidden = false;
            } else {
                $hidden = true;
            }
        } else {
            $hidden = false;
        }

        $status = new Status;
        $status->profile_id = $profile->id;
        $status->caption = $msg;
        $status->rendered = $msg;
        $status->visibility = 'direct';
        $status->scope = 'direct';
        $status->in_reply_to_profile_id = $recipient->id;
        $status->save();

        $dm = new DirectMessage;
        $dm->to_id = $recipient->id;
        $dm->from_id = $profile->id;
        $dm->status_id = $status->id;
        $dm->is_hidden = $hidden;
        $dm->type = $request->input('type');
        $dm->save();

        if(filter_var($msg, FILTER_VALIDATE_URL)) {
            if(Helpers::validateUrl($msg)) {
                $dm->type = 'link';
                $dm->meta = [
                    'domain' => parse_url($msg, PHP_URL_HOST),
                    'local' => parse_url($msg, PHP_URL_HOST) ==
                    parse_url(config('app.url'), PHP_URL_HOST)
                ];
                $dm->save();
            }
        }

        $nf = UserFilter::whereUserId($recipient->id)
        ->whereFilterableId($profile->id)
        ->whereFilterableType('App\Profile')
        ->whereFilterType('dm.mute')
        ->exists();

        if($recipient->domain == null && $hidden == false && !$nf) {
            $notification = new Notification();
            $notification->profile_id = $recipient->id;
            $notification->actor_id = $profile->id;
            $notification->action = 'dm';
            $notification->message = $dm->toText();
            $notification->rendered = $dm->toHtml();
            $notification->item_id = $dm->id;
            $notification->item_type = "App\DirectMessage";
            $notification->save();
        }

        if($recipient->domain) {
            $this->remoteDeliver($dm);
        }

        $res = [
            'id' => (string) $dm->id,
            'isAuthor' => $profile->id == $dm->from_id,
            'reportId' => (string) $dm->status_id,
            'hidden' => (bool) $dm->is_hidden,
            'type'  => $dm->type,
            'text' => $dm->status->caption,
            'media' => null,
            'timeAgo' => $dm->created_at->diffForHumans(null,null,true),
            'seen' => $dm->read_at != null,
            'meta' => $dm->meta
        ];

        return response()->json($res);
    }

    public function thread(Request $request)
    {
        $this->validate($request, [
            'pid' => 'required'
        ]);
        $uid = $request->user()->profile_id;
        $pid = $request->input('pid');
        $max_id = $request->input('max_id');
        $min_id = $request->input('min_id');

        $r = Profile::findOrFail($pid);

        if($min_id) {
            $res = DirectMessage::select('*')
            ->where('id', '>', $min_id)
            ->where(function($q) use($pid,$uid) {
                return $q->where([['from_id',$pid],['to_id',$uid]
            ])->orWhere([['from_id',$uid],['to_id',$pid]]);
            })
            ->latest()
            ->take(8)
            ->get();
        } else if ($max_id) {
            $res = DirectMessage::select('*')
            ->where('id', '<', $max_id)
            ->where(function($q) use($pid,$uid) {
                return $q->where([['from_id',$pid],['to_id',$uid]
            ])->orWhere([['from_id',$uid],['to_id',$pid]]);
            })
            ->latest()
            ->take(8)
            ->get();
        } else {
            $res = DirectMessage::where(function($q) use($pid,$uid) {
                return $q->where([['from_id',$pid],['to_id',$uid]
            ])->orWhere([['from_id',$uid],['to_id',$pid]]);
            })
            ->latest()
            ->take(8)
            ->get();
        }


        $res = $res->map(function($s) use ($uid){
            return [
                'id' => (string) $s->id,
                'hidden' => (bool) $s->is_hidden,
                'isAuthor' => $uid == $s->from_id,
                'type'  => $s->type,
                'text' => $s->status->caption,
                'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null,
                'timeAgo' => $s->created_at->diffForHumans(null,null,true),
                'seen' => $s->read_at != null,
                'reportId' => (string) $s->status_id,
                'meta' => json_decode($s->meta,true)
            ];
        });

        $w = [
            'id' => (string) $r->id,
            'name' => $r->name,
            'username' => $r->username,
            'avatar' => $r->avatarUrl(),
            'url' => $r->url(),
            'muted' => UserFilter::whereUserId($uid)
            ->whereFilterableId($r->id)
            ->whereFilterableType('App\Profile')
            ->whereFilterType('dm.mute')
            ->first() ? true : false,
            'isLocal' => (bool) !$r->domain,
            'domain' => $r->domain,
            'timeAgo' => $r->created_at->diffForHumans(null, true, true),
            'lastMessage' => '',
            'messages' => $res
        ];

        return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
    }

    public function delete(Request $request)
    {
        $this->validate($request, [
            'id' => 'required'
        ]);

        $sid = $request->input('id');
        $pid = $request->user()->profile_id;

        $dm = DirectMessage::whereFromId($pid)
        ->whereStatusId($sid)
        ->firstOrFail();

        $status = Status::whereProfileId($pid)
        ->findOrFail($dm->status_id);

        if($dm->recipient->domain) {
            $dmc = $dm;
            $this->remoteDelete($dmc);
        }

        $status->delete();
        $dm->delete();

        return [200];
    }

    public function get(Request $request, $id)
    {
        $pid = $request->user()->profile_id;
        $dm = DirectMessage::whereStatusId($id)->firstOrFail();
        abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
        return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
    }

    public function mediaUpload(Request $request)
    {
        $this->validate($request, [
            'file'      => function() {
                return [
                    'required',
                    'mimes:' . config_cache('pixelfed.media_types'),
                    'max:' . config_cache('pixelfed.max_photo_size'),
                ];
            },
            'to_id'     => 'required'
        ]);

        $user = $request->user();
        $profile = $user->profile;
        $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
        abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);

        if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
            if($recipient->follows($profile) == true) {
                $hidden = false;
            } else {
                $hidden = true;
            }
        } else {
            $hidden = false;
        }

        if(config_cache('pixelfed.enforce_account_limit') == true) {
            $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
                return Media::whereUserId($user->id)->sum('size') / 1000;
            });
            $limit = (int) config_cache('pixelfed.max_account_size');
            if ($size >= $limit) {
                abort(403, 'Account size limit reached.');
            }
        }
        $photo = $request->file('file');

        $mimes = explode(',', config_cache('pixelfed.media_types'));
        if(in_array($photo->getMimeType(), $mimes) == false) {
            abort(403, 'Invalid or unsupported mime type.');
        }

        $storagePath = MediaPathService::get($user, 2) . Str::random(8);
        $path = $photo->store($storagePath);
        $hash = \hash_file('sha256', $photo);

        abort_if(MediaBlocklistService::exists($hash) == true, 451);

        $status = new Status;
        $status->profile_id = $profile->id;
        $status->caption = null;
        $status->rendered = null;
        $status->visibility = 'direct';
        $status->scope = 'direct';
        $status->in_reply_to_profile_id = $recipient->id;
        $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->original_sha256 = $hash;
        $media->size = $photo->getSize();
        $media->mime = $photo->getMimeType();
        $media->caption = null;
        $media->filter_class = null;
        $media->filter_name = null;
        $media->save();

        $dm = new DirectMessage;
        $dm->to_id = $recipient->id;
        $dm->from_id = $profile->id;
        $dm->status_id = $status->id;
        $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo';
        $dm->is_hidden = $hidden;
        $dm->save();

        if($recipient->domain) {
            $this->remoteDeliver($dm);
        }

        return [
            'id' => $dm->id,
            'reportId' => (string) $dm->status_id,
            'type' => $dm->type,
            'url' => $media->url()
        ];
    }

    public function composeLookup(Request $request)
    {
        $this->validate($request, [
            'q' => 'required|string|min:2|max:50',
            'remote' => 'nullable',
        ]);

        $q = $request->input('q');
        $r = $request->input('remote', false);

        if($r && !Str::of($q)->contains('.')) {
            return [];
        }

        if($r && Helpers::validateUrl($q)) {
            Helpers::profileFetch($q);
        }

        if(Str::of($q)->startsWith('@')) {
            if(strlen($q) < 3) {
                return [];
            }
            if(substr_count($q, '@') == 2) {
                WebfingerService::lookup($q);
            }
            $q = mb_substr($q, 1);
        }

        $blocked = UserFilter::whereFilterableType('App\Profile')
        ->whereFilterType('block')
        ->whereFilterableId($request->user()->profile_id)
        ->pluck('user_id');

        $blocked->push($request->user()->profile_id);

        $results = Profile::select('id','domain','username')
        ->whereNotIn('id', $blocked)
        ->where('username','like','%'.$q.'%')
        ->orderBy('domain')
        ->limit(8)
        ->get()
        ->map(function($r) {
            return [
                'local' => (bool) !$r->domain,
                'id' => (string) $r->id,
                'name' => $r->username,
                'privacy' => true,
                'avatar' => $r->avatarUrl()
            ];
        });

        return $results;
    }

    public function read(Request $request)
    {
        $this->validate($request, [
            'pid' => 'required',
            'sid' => 'required'
        ]);

        $pid = $request->input('pid');
        $sid = $request->input('sid');

        $dms = DirectMessage::whereToId($request->user()->profile_id)
        ->whereFromId($pid)
        ->where('status_id', '>=', $sid)
        ->get();

        $now = now();
        foreach($dms as $dm) {
            $dm->read_at = $now;
            $dm->save();
        }

        return response()->json($dms->pluck('id'));
    }

    public function mute(Request $request)
    {
        $this->validate($request, [
            'id' => 'required'
        ]);

        $fid = $request->input('id');
        $pid = $request->user()->profile_id;

        UserFilter::firstOrCreate(
            [
                'user_id' => $pid,
                'filterable_id' => $fid,
                'filterable_type' => 'App\Profile',
                'filter_type' => 'dm.mute'
            ]
        );

        return [200];
    }

    public function unmute(Request $request)
    {
        $this->validate($request, [
            'id' => 'required'
        ]);

        $fid = $request->input('id');
        $pid = $request->user()->profile_id;

        $f = UserFilter::whereUserId($pid)
        ->whereFilterableId($fid)
        ->whereFilterableType('App\Profile')
        ->whereFilterType('dm.mute')
        ->firstOrFail();

        $f->delete();

        return [200];
    }

    public function remoteDeliver($dm)
    {
        $profile = $dm->author;
        $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;

        $tags = [
            [
                'type' => 'Mention',
                'href' => $dm->recipient->permalink(),
                'name' => $dm->recipient->emailUrl(),
            ]
        ];

        $body = [
            '@context' => [
                'https://www.w3.org/ns/activitystreams',
                'https://w3id.org/security/v1',
                [
                    'sc'                => 'http://schema.org#',
                    'Hashtag'           => 'as:Hashtag',
                    'sensitive'         => 'as:sensitive',
                ]
            ],
            'id'                    => $dm->status->permalink(),
            'type'                  => 'Create',
            'actor'                 => $dm->status->profile->permalink(),
            'published'             => $dm->status->created_at->toAtomString(),
            'to'                    => [$dm->recipient->permalink()],
            'cc'                    => [],
            'object' => [
                'id'                => $dm->status->url(),
                'type'              => 'Note',
                'summary'           => null,
                'content'           => $dm->status->rendered ?? $dm->status->caption,
                'inReplyTo'         => null,
                'published'         => $dm->status->created_at->toAtomString(),
                'url'               => $dm->status->url(),
                'attributedTo'      => $dm->status->profile->permalink(),
                'to'                => [$dm->recipient->permalink()],
                'cc'                => [],
                'sensitive'         => (bool) $dm->status->is_nsfw,
                'attachment'        => $dm->status->media()->orderBy('order')->get()->map(function ($media) {
                    return [
                        'type'      => $media->activityVerb(),
                        'mediaType' => $media->mime,
                        'url'       => $media->url(),
                        'name'      => $media->caption,
                    ];
                })->toArray(),
                'tag'               => $tags,
            ]
        ];

        Helpers::sendSignedObject($profile, $url, $body);
    }

    public function remoteDelete($dm)
    {
        $profile = $dm->author;
        $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;

        $body = [
            '@context' => [
                'https://www.w3.org/ns/activitystreams',
                'https://w3id.org/security/v1',
                [
                    'sc'                => 'http://schema.org#',
                    'Hashtag'            => 'as:Hashtag',
                    'sensitive'            => 'as:sensitive',
                ]
            ],
            'id' => $dm->status->permalink('#delete'),
            'to' => [
                'https://www.w3.org/ns/activitystreams#Public'
            ],
            'type' => 'Delete',
            'actor' => $dm->status->profile->permalink(),
            'object' => [
                'id' => $dm->status->url(),
                'type' => 'Tombstone'
            ]
        ];

        Helpers::sendSignedObject($profile, $url, $body);
    }
}