From d63569c1204274df2b7397a48b4b0dac728ea57c Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Wed, 18 Nov 2020 14:19:02 -0700
Subject: [PATCH] Add Direct Messages
---
app/DirectMessage.php | 23 +-
app/Http/Controllers/AccountController.php | 13 +-
.../Controllers/DirectMessageController.php | 631 ++++++++++++++++-
app/Http/Controllers/FederationController.php | 6 +-
.../Controllers/Settings/PrivacySettings.php | 7 +
.../Api/NotificationTransformer.php | 1 +
app/Util/ActivityPub/Inbox.php | 138 +++-
app/Util/ActivityPub/Validator/Add.php | 41 ++
app/Util/Lexer/PrettyNumber.php | 18 +-
app/Util/Media/Image.php | 3 +
config/cors.php | 4 +-
resources/assets/js/components/Activity.vue | 8 +
.../assets/js/components/ComposeModal.vue | 14 +-
resources/assets/js/components/Direct.vue | 400 +++++++++++
.../assets/js/components/DirectMessage.vue | 648 ++++++++++++++++++
.../assets/js/components/NotificationCard.vue | 5 +
resources/assets/js/components/Profile.vue | 13 +-
.../assets/js/components/SearchResults.vue | 2 +-
resources/assets/js/direct.js | 5 +
resources/views/account/direct.blade.php | 7 +-
.../views/account/directmessage.blade.php | 12 +
resources/views/layouts/partial/nav.blade.php | 6 +
resources/views/settings/privacy.blade.php | 10 +-
webpack.mix.js | 2 +-
24 files changed, 1938 insertions(+), 79 deletions(-)
create mode 100644 app/Util/ActivityPub/Validator/Add.php
create mode 100644 resources/assets/js/components/Direct.vue
create mode 100644 resources/assets/js/components/DirectMessage.vue
create mode 100644 resources/views/account/directmessage.blade.php
diff --git a/app/DirectMessage.php b/app/DirectMessage.php
index accc92f7f..1900d326f 100644
--- a/app/DirectMessage.php
+++ b/app/DirectMessage.php
@@ -14,7 +14,7 @@ class DirectMessage extends Model
public function url()
{
- return url('/i/message/' . $this->to_id . '/' . $this->id);
+ return config('app.url') . '/account/direct/m/' . $this->status_id;
}
public function author()
@@ -22,8 +22,29 @@ class DirectMessage extends Model
return $this->hasOne(Profile::class, 'id', 'from_id');
}
+ public function recipient()
+ {
+ return $this->hasOne(Profile::class, 'id', 'to_id');
+ }
+
public function me()
{
return Auth::user()->profile->id === $this->from_id;
}
+
+ public function toText()
+ {
+ $actorName = $this->author->username;
+
+ return "{$actorName} sent a direct message.";
+ }
+
+ public function toHtml()
+ {
+ $actorName = $this->author->username;
+ $actorUrl = $this->author->url();
+ $url = $this->url();
+
+ return "{$actorName} sent a direct message .";
+ }
}
diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php
index 09f4ba1c9..b7af749f3 100644
--- a/app/Http/Controllers/AccountController.php
+++ b/app/Http/Controllers/AccountController.php
@@ -13,6 +13,7 @@ use Illuminate\Http\Request;
use PragmaRX\Google2FA\Google2FA;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\{
+ DirectMessage,
EmailVerification,
Follower,
FollowRequest,
@@ -114,19 +115,17 @@ class AccountController extends Controller
}
}
- public function messages()
- {
- return view('account.messages');
- }
-
public function direct()
{
return view('account.direct');
}
- public function showMessage(Request $request, $id)
+ public function directMessage(Request $request, $id)
{
- return view('account.message');
+ $profile = Profile::where('id', '!=', $request->user()->profile_id)
+ // ->whereNull('domain')
+ ->findOrFail($id);
+ return view('account.directmessage', compact('id'));
}
public function mute(Request $request)
diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php
index 1bc47d24f..00f780b19 100644
--- a/app/Http/Controllers/DirectMessageController.php
+++ b/app/Http/Controllers/DirectMessageController.php
@@ -2,57 +2,616 @@
namespace App\Http\Controllers;
-use Auth;
+use Auth, Cache;
use Illuminate\Http\Request;
use App\{
DirectMessage,
+ Media,
+ Notification,
Profile,
- Status
+ 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;
class DirectMessageController extends Controller
{
- public function __construct()
- {
- $this->middleware('auth');
- }
+ public function __construct()
+ {
+ $this->middleware('auth');
+ }
- public function inbox(Request $request)
- {
- $profile = Auth::user()->profile;
- $inbox = DirectMessage::selectRaw('*, max(created_at) as createdAt')
- ->whereToId($profile->id)
- ->with(['author','status'])
- ->orderBy('createdAt', 'desc')
- ->groupBy('from_id')
- ->paginate(12);
- return view('account.messages', compact('inbox'));
+ 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');
- public function show(Request $request, int $pid, $mid)
- {
- $profile = Auth::user()->profile;
+ 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($pid !== $profile->id) {
- abort(403);
- }
+ 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' => []
+ ];
+ });
+ }
- $msg = DirectMessage::whereToId($profile->id)
- ->findOrFail($mid);
+ 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' => []
+ ];
+ });
+ }
- $thread = DirectMessage::whereIn('to_id', [$profile->id, $msg->from_id])
- ->whereIn('from_id', [$profile->id,$msg->from_id])
- ->orderBy('created_at', 'desc')
- ->paginate(30);
+ return response()->json($dms);
+ }
- $thread = $thread->reverse();
+ public function create(Request $request)
+ {
+ $this->validate($request, [
+ 'to_id' => 'required',
+ 'message' => 'required|string|min:1|max:500',
+ 'type' => 'required|in:text,emoji'
+ ]);
- return view('account.message', compact('msg', 'profile', 'thread'));
- }
+ $profile = $request->user()->profile;
+ $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
- public function compose(Request $request)
- {
- $profile = Auth::user()->profile;
- }
+ 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,
+ '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);
+ // $r = Profile::whereNull('domain')->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,
+ '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)
+ ->findOrFail($sid);
+
+ $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('pixelfed.media_types'),
+ 'max:' . config('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('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('pixelfed.max_account_size');
+ if ($size >= $limit) {
+ abort(403, 'Account size limit reached.');
+ }
+ }
+ $photo = $request->file('file');
+
+ $mimes = explode(',', config('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 [
+ 'type' => $dm->type,
+ 'url' => $media->url()
+ ];
+ }
+
+ public function composeLookup(Request $request)
+ {
+ $this->validate($request, [
+ 'username' => 'required'
+ ]);
+ $username = $request->input('username');
+ $profile = Profile::whereUsername($username)->firstOrFail();
+
+ return ['id' => (string)$profile->id];
+ }
+
+ 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->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->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);
+ }
}
diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php
index 09116f1f4..b26bf1a1c 100644
--- a/app/Http/Controllers/FederationController.php
+++ b/app/Http/Controllers/FederationController.php
@@ -34,7 +34,8 @@ class FederationController extends Controller
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
- return response()->json(Nodeinfo::wellKnown());
+ return response()->json(Nodeinfo::wellKnown())
+ ->header('Access-Control-Allow-Origin','*');
}
public function nodeinfo()
@@ -62,7 +63,8 @@ class FederationController extends Controller
}
$webfinger = (new Webfinger($profile))->generate();
- return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
+ return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT)
+ ->header('Access-Control-Allow-Origin','*');
}
public function hostMeta(Request $request)
diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php
index dff6636eb..91f1ac43c 100644
--- a/app/Http/Controllers/Settings/PrivacySettings.php
+++ b/app/Http/Controllers/Settings/PrivacySettings.php
@@ -34,6 +34,7 @@ trait PrivacySettings
$fields = [
'is_private',
'crawlable',
+ 'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
];
@@ -56,6 +57,12 @@ trait PrivacySettings
} else {
$settings->{$field} = true;
}
+ } elseif ($field == 'public_dm') {
+ if ($form == 'on') {
+ $settings->{$field} = true;
+ } else {
+ $settings->{$field} = false;
+ }
} else {
if ($form == 'on') {
$settings->{$field} = true;
diff --git a/app/Transformer/Api/NotificationTransformer.php b/app/Transformer/Api/NotificationTransformer.php
index 812b250e6..cc75bfd83 100644
--- a/app/Transformer/Api/NotificationTransformer.php
+++ b/app/Transformer/Api/NotificationTransformer.php
@@ -50,6 +50,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
public function replaceTypeVerb($verb)
{
$verbs = [
+ 'dm' => 'direct',
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',
diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php
index 6ccbd508b..ccc8d74f2 100644
--- a/app/Util/ActivityPub/Inbox.php
+++ b/app/Util/ActivityPub/Inbox.php
@@ -19,6 +19,7 @@ use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
+use App\Util\ActivityPub\Validator\Add as AddValidator;
use App\Util\ActivityPub\Validator\Announce as AnnounceValidator;
use App\Util\ActivityPub\Validator\Follow as FollowValidator;
use App\Util\ActivityPub\Validator\Like as LikeValidator;
@@ -57,6 +58,12 @@ class Inbox
{
$verb = (string) $this->payload['type'];
switch ($verb) {
+
+ case 'Add':
+ if(AddValidator::validate($this->payload) == false) { return; }
+ $this->handleAddActivity();
+ break;
+
case 'Create':
$this->handleCreateActivity();
break;
@@ -121,6 +128,11 @@ class Inbox
return Helpers::profileFetch($actorUrl);
}
+ public function handleAddActivity()
+ {
+ // stories ;)
+ }
+
public function handleCreateActivity()
{
$activity = $this->payload['object'];
@@ -157,6 +169,15 @@ class Inbox
if(!$actor || $actor->domain == null) {
return;
}
+ $to = $activity['to'];
+ $cc = $activity['cc'];
+ if(count($to) == 1 &&
+ count($cc) == 0 &&
+ parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
+ ) {
+ $this->handleDirectMessage();
+ return;
+ }
if($actor->followers()->count() == 0) {
return;
@@ -170,6 +191,103 @@ class Inbox
return;
}
+ public function handleDirectMessage()
+ {
+ $activity = $this->payload['object'];
+ $actor = $this->actorFirstOrCreate($this->payload['actor']);
+ $profile = Profile::whereNull('domain')
+ ->whereUsername(array_last(explode('/', $activity['to'][0])))
+ ->firstOrFail();
+
+ if(in_array($actor->id, $profile->blockedIds()->toArray())) {
+ return;
+ }
+
+ $msg = $activity['content'];
+ $msgText = strip_tags($activity['content']);
+
+ if($profile->user->settings->public_dm == false || $profile->is_private) {
+ if($profile->follows($actor) == true) {
+ $hidden = false;
+ } else {
+ $hidden = true;
+ }
+ } else {
+ $hidden = false;
+ }
+
+ $status = new Status;
+ $status->profile_id = $actor->id;
+ $status->caption = $msgText;
+ $status->rendered = $msg;
+ $status->visibility = 'direct';
+ $status->scope = 'direct';
+ $status->in_reply_to_profile_id = $profile->id;
+ $status->save();
+
+ $dm = new DirectMessage;
+ $dm->to_id = $profile->id;
+ $dm->from_id = $actor->id;
+ $dm->status_id = $status->id;
+ $dm->is_hidden = $hidden;
+ $dm->type = $request->input('type');
+ $dm->save();
+
+ if(count($activity['attachment'])) {
+ $allowed = explode(',', config('pixelfed.media_types'));
+ foreach($activity['attachment'] as $a) {
+ $type = $a['mediaType'];
+ $url = $a['url'];
+ $valid = self::validateUrl($url);
+ if(in_array($type, $allowed) == false || $valid == false) {
+ continue;
+ }
+
+ $media = new Media();
+ $media->remote_media = true;
+ $media->status_id = $status->id;
+ $media->profile_id = $status->profile_id;
+ $media->user_id = null;
+ $media->media_path = $url;
+ $media->remote_url = $url;
+ $media->mime = $type;
+ $media->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($profile->id)
+ ->whereFilterableId($actor->id)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('dm.mute')
+ ->exists();
+
+ if($profile->domain == null && $hidden == false && !$nf) {
+ $notification = new Notification();
+ $notification->profile_id = $profile->id;
+ $notification->actor_id = $actor->id;
+ $notification->action = 'dm';
+ $notification->message = $dm->toText();
+ $notification->rendered = $dm->toHtml();
+ $notification->item_id = $dm->id;
+ $notification->item_type = "App\DirectMessage";
+ $notification->save();
+ }
+
+ return;
+ }
+
public function handleFollowActivity()
{
$actor = $this->actorFirstOrCreate($this->payload['actor']);
@@ -305,7 +423,20 @@ class Inbox
}
$actor = $this->payload['actor'];
$obj = $this->payload['object'];
- if(is_string($obj) == true) {
+ if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) {
+ $profile = Profile::whereRemoteUrl($obj)->first();
+ if(!$profile || $profile->private_key != null) {
+ return;
+ }
+ Notification::whereActorId($profile->id)->delete();
+ $profile->avatar()->delete();
+ $profile->followers()->delete();
+ $profile->following()->delete();
+ $profile->likes()->delete();
+ $profile->media()->delete();
+ $profile->hashtags()->delete();
+ $profile->statuses()->delete();
+ $profile->delete();
return;
}
$type = $this->payload['object']['type'];
@@ -319,9 +450,7 @@ class Inbox
$id = $this->payload['object']['id'];
switch ($type) {
case 'Person':
- // todo: fix race condition
- return;
- $profile = Helpers::profileFetch($actor);
+ $profile = Profile::whereRemoteUrl($actor)->first();
if(!$profile || $profile->private_key != null) {
return;
}
@@ -331,6 +460,7 @@ class Inbox
$profile->following()->delete();
$profile->likes()->delete();
$profile->media()->delete();
+ $profile->hashtags()->delete();
$profile->statuses()->delete();
$profile->delete();
return;
diff --git a/app/Util/ActivityPub/Validator/Add.php b/app/Util/ActivityPub/Validator/Add.php
new file mode 100644
index 000000000..89a7ec729
--- /dev/null
+++ b/app/Util/ActivityPub/Validator/Add.php
@@ -0,0 +1,41 @@
+ 'required',
+ 'id' => 'required|string',
+ 'type' => [
+ 'required',
+ Rule::in(['Add'])
+ ],
+ 'actor' => 'required|url',
+ 'object' => 'required',
+ 'object.id' => 'required|url',
+ 'object.type' => [
+ 'required',
+ Rule::in(['Story'])
+ ],
+ 'object.attributedTo' => 'required|url|same:actor',
+ 'object.attachment' => 'required',
+ 'object.attachment.type' => [
+ 'required',
+ Rule::in(['Image'])
+ ],
+ 'object.attachment.url' => 'required|url',
+ 'object.attachment.mediaType' => [
+ 'required',
+ Rule::in(['image/jpeg', 'image/png'])
+ ]
+ ])->passes();
+
+ return $valid;
+ }
+}
\ No newline at end of file
diff --git a/app/Util/Lexer/PrettyNumber.php b/app/Util/Lexer/PrettyNumber.php
index 2dfa86e6c..4e6a4eba6 100644
--- a/app/Util/Lexer/PrettyNumber.php
+++ b/app/Util/Lexer/PrettyNumber.php
@@ -4,19 +4,23 @@ namespace App\Util\Lexer;
class PrettyNumber
{
- public static function convert($expression)
+ public static function convert($number)
{
+ if(!is_integer($number)) {
+ return $number;
+ }
+
$abbrevs = [12 => 'T', 9 => 'B', 6 => 'M', 3 => 'K', 0 => ''];
foreach ($abbrevs as $exponent => $abbrev) {
- if ($expression >= pow(10, $exponent)) {
- $display_num = $expression / pow(10, $exponent);
- $num = number_format($display_num, 0).$abbrev;
-
- return $num;
+ if(abs($number) >= pow(10, $exponent)) {
+ $display = $number / pow(10, $exponent);
+ $decimals = ($exponent >= 3 && round($display) < 100) ? 1 : 0;
+ $number = number_format($display, $decimals).$abbrev;
+ break;
}
}
- return $expression;
+ return $number;
}
public static function size($expression, $kb = false)
diff --git a/app/Util/Media/Image.php b/app/Util/Media/Image.php
index 4cbfaca16..bbc877ffa 100644
--- a/app/Util/Media/Image.php
+++ b/app/Util/Media/Image.php
@@ -165,6 +165,8 @@ class Image
$quality = config('pixelfed.image_quality');
$img->save($newPath, $quality);
+ $media->width = $img->width();
+ $media->height = $img->height();
$img->destroy();
if (!$thumbnail) {
$media->orientation = $orientation;
@@ -178,6 +180,7 @@ class Image
$media->mime = $img->mime;
}
+
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
Cache::forget('status:thumb:'.$media->status_id);
diff --git a/config/cors.php b/config/cors.php
index e33f4c445..92b4b8e8c 100644
--- a/config/cors.php
+++ b/config/cors.php
@@ -21,7 +21,9 @@ return [
* You can enable CORS for 1 or multiple paths.
* Example: ['api/*']
*/
- 'paths' => [],
+ 'paths' => [
+ '.well-known/*'
+ ],
/*
* Matches the request method. `[*]` allows all methods.
diff --git a/resources/assets/js/components/Activity.vue b/resources/assets/js/components/Activity.vue
index d2e7e0012..ff4d6ccac 100644
--- a/resources/assets/js/components/Activity.vue
+++ b/resources/assets/js/components/Activity.vue
@@ -52,6 +52,11 @@
{{truncate(n.account.username)}} tagged you in a post .
+
{{timeAgo(n.created_at)}}
@@ -249,6 +254,9 @@ export default {
case 'tagged':
return n.tagged.post_url;
break;
+ case 'direct':
+ return '/account/direct/t/'+n.account.id;
+ break
}
return '/';
},
diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue
index 8f36f5ec4..f934527c6 100644
--- a/resources/assets/js/components/ComposeModal.vue
+++ b/resources/assets/js/components/ComposeModal.vue
@@ -237,7 +237,7 @@
@@ -271,7 +271,7 @@
Audience
- {{visibilityTag}}
+ {{visibilityTag}}
@@ -632,12 +632,13 @@ export default {
methods: {
fetchProfile() {
+ let self = this;
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
- this.profile = res.data;
+ self.profile = res.data;
window.pixelfed.currentUser = res.data;
if(res.data.locked == true) {
- this.visibility = 'private';
- this.visibilityTag = 'Followers Only';
+ self.visibility = 'private';
+ self.visibilityTag = 'Followers Only';
}
}).catch(err => {
});
@@ -663,6 +664,9 @@ export default {
let self = this;
self.uploading = true;
let io = document.querySelector('#pf-dz');
+ if(!io.files.length) {
+ self.uploading = false;
+ }
Array.prototype.forEach.call(io.files, function(io, i) {
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
diff --git a/resources/assets/js/components/Direct.vue b/resources/assets/js/components/Direct.vue
new file mode 100644
index 000000000..f56f395bd
--- /dev/null
+++ b/resources/assets/js/components/Direct.vue
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+ Prev
+ Next
+
+
+ Prev
+ Next
+
+
+ Prev
+ Next
+
+
+
+
+
+
+
+
+
+
+
+ To:
+
+
+
+ Select a username to send a message to.
+
+
+
+
+ You cannot message remote accounts yet.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/assets/js/components/DirectMessage.vue b/resources/assets/js/components/DirectMessage.vue
new file mode 100644
index 000000000..23826b561
--- /dev/null
+++ b/resources/assets/js/components/DirectMessage.vue
@@ -0,0 +1,648 @@
+
+
+
+
+
+
+
View Original
+
Play
+
+
+ Navigate to
+
+
+ {{this.ctxContext.meta.domain}}
+
+
+
Copy
+
Report
+
Delete
+
Cancel
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/assets/js/components/NotificationCard.vue b/resources/assets/js/components/NotificationCard.vue
index 2eb3034c6..f44205317 100644
--- a/resources/assets/js/components/NotificationCard.vue
+++ b/resources/assets/js/components/NotificationCard.vue
@@ -62,6 +62,11 @@
{{truncate(n.account.username)}} tagged you in a post .
+
We cannot display this notification at this time.
diff --git a/resources/assets/js/components/Profile.vue b/resources/assets/js/components/Profile.vue
index f002b5c0d..9a86874f4 100644
--- a/resources/assets/js/components/Profile.vue
+++ b/resources/assets/js/components/Profile.vue
@@ -102,17 +102,18 @@
- FOLLOWING
+ Message
+
- FOLLOW
+ Follow
Edit Profile
-
+
--}}
- {{--
{{--
diff --git a/webpack.mix.js b/webpack.mix.js
index 315527f8c..a9864ef34 100644
--- a/webpack.mix.js
+++ b/webpack.mix.js
@@ -35,7 +35,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/profile-directory.js', 'public/js')
.js('resources/assets/js/story-compose.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
-// .js('resources/assets/js/direct.js', 'public/js')
+ .js('resources/assets/js/direct.js', 'public/js')
// .js('resources/assets/js/admin.js', 'public/js')
// .js('resources/assets/js/micro.js', 'public/js')
.js('resources/assets/js/rempro.js', 'public/js')