mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 13:03:16 +00:00
commit
c13826e44b
204 changed files with 22435 additions and 16 deletions
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait AdminGroupsController
|
||||
{
|
||||
public function groupsHome(Request $request)
|
||||
{
|
||||
$stats = $this->groupAdminStats();
|
||||
|
||||
return view('admin.groups.home', compact('stats'));
|
||||
}
|
||||
|
||||
protected function groupAdminStats()
|
||||
{
|
||||
return Cache::remember('admin:groups:stats', 3, function () {
|
||||
$res = [
|
||||
'total' => Group::count(),
|
||||
'local' => Group::whereLocal(true)->count(),
|
||||
];
|
||||
|
||||
$res['remote'] = $res['total'] - $res['local'];
|
||||
$res['categories'] = GroupCategory::count();
|
||||
$res['posts'] = GroupPost::count();
|
||||
$res['members'] = GroupMember::count();
|
||||
$res['interactions'] = GroupInteraction::count();
|
||||
$res['reports'] = GroupReport::count();
|
||||
|
||||
$res['local_30d'] = Cache::remember('admin:groups:stats:local_30d', 43200, function () {
|
||||
return Group::whereLocal(true)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
$res['remote_30d'] = Cache::remember('admin:groups:stats:remote_30d', 43200, function () {
|
||||
return Group::whereLocal(false)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
}
|
671
app/Http/Controllers/GroupController.php
Normal file
671
app/Http/Controllers/GroupController.php
Normal file
|
@ -0,0 +1,671 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Instance;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Storage;
|
||||
|
||||
class GroupController extends GroupFederationController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
abort_unless(config('groups.enabled'), 404);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function show(Request $request, $id, $path = false)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return $this->showGroupObject($group);
|
||||
}
|
||||
|
||||
return view('layouts.spa', compact('id', 'path'));
|
||||
}
|
||||
|
||||
public function showStatus(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::find($gid);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($group->is_private) {
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
}
|
||||
|
||||
$gp = GroupPost::whereGroupId($gid)
|
||||
->findOrFail($sid);
|
||||
|
||||
return view('layouts.spa', compact('group', 'gp'));
|
||||
}
|
||||
|
||||
public function getGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereNull('status')->findOrFail($id);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return response()->json($group, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showStatusLikes(Request $request, $id, $sid)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
$status = GroupPost::whereGroupId($id)->findOrFail($sid);
|
||||
$likes = GroupLike::whereStatusId($sid)
|
||||
->cursorPaginate(10)
|
||||
->map(function ($l) use ($group) {
|
||||
$account = AccountService::get($l->profile_id);
|
||||
$account['url'] = "/groups/{$group->id}/user/{$account['id']}";
|
||||
|
||||
return $account;
|
||||
})
|
||||
->filter(function ($l) {
|
||||
return $l && isset($l['id']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $likes;
|
||||
}
|
||||
|
||||
public function groupSettings(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return view('groups.settings', compact('group'));
|
||||
}
|
||||
|
||||
public function joinGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if($group->isMember($pid), 404);
|
||||
|
||||
if (! $request->user()->is_admin) {
|
||||
abort_if(GroupService::getRejoinTimeout($group->id, $pid), 422, 'Cannot re-join this group for 24 hours after leaving or cancelling a request to join');
|
||||
}
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'member';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->join_request = $group->is_private;
|
||||
$member->save();
|
||||
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
public function updateGroup(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:all,local,private',
|
||||
'avatar' => 'nullable',
|
||||
'header' => 'nullable',
|
||||
'discoverable' => 'required',
|
||||
'activitypub' => 'required',
|
||||
'is_nsfw' => 'required',
|
||||
'category' => 'required|string|in:'.implode(',', GroupService::categories()),
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::whereProfileId($pid)->findOrFail($id);
|
||||
$member = GroupMember::whereGroupId($group->id)->whereProfileId($pid)->firstOrFail();
|
||||
|
||||
abort_if($member->role != 'founder', 403, 'Invalid group permission');
|
||||
|
||||
$metadata = $group->metadata;
|
||||
$len = $group->is_private ? 12 : 4;
|
||||
|
||||
if ($request->hasFile('avatar')) {
|
||||
$avatar = $request->file('avatar');
|
||||
|
||||
if ($avatar) {
|
||||
if (isset($metadata['avatar']) &&
|
||||
isset($metadata['avatar']['path']) &&
|
||||
Storage::exists($metadata['avatar']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['avatar']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'avatar_'.strtolower(str_random($len)).'.'.$avatar->extension();
|
||||
$path = $avatar->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['avatar'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->hasFile('header')) {
|
||||
$header = $request->file('header');
|
||||
|
||||
if ($header) {
|
||||
if (isset($metadata['header']) &&
|
||||
isset($metadata['header']['path']) &&
|
||||
Storage::exists($metadata['header']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['header']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'header_'.strtolower(str_random($len)).'.'.$header->extension();
|
||||
$path = $header->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['header'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$cat = GroupService::categoryById($group->category_id);
|
||||
if ($request->category !== $cat['name']) {
|
||||
$group->category_id = GroupCategory::whereName($request->category)->first()->id;
|
||||
}
|
||||
|
||||
$changes = null;
|
||||
$group->description = e($request->input('description', null));
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->activitypub = $request->input('activitypub') == 'true';
|
||||
$group->discoverable = $request->input('discoverable') == 'true';
|
||||
$group->is_nsfw = $request->input('is_nsfw') == 'true';
|
||||
$group->metadata = $metadata;
|
||||
if ($group->isDirty()) {
|
||||
$changes = $group->getDirty();
|
||||
}
|
||||
$group->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:settings:updated',
|
||||
$changes
|
||||
);
|
||||
|
||||
GroupService::del($group->id);
|
||||
|
||||
$res = $this->toJson($group, $pid);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function groupLeave(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
|
||||
abort_if(! $group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function cancelJoinRequest(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
abort_if($group->isMember($pid), 422, 'Cannot cancel approved join request, please leave group instead.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function metaBlockSearch(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
||||
switch ($type) {
|
||||
case 'instance':
|
||||
$res = Instance::whereDomain($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereInstanceId($res->id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$res = Profile::whereUsername($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereProfileId($res->id)->exists(), 400);
|
||||
}
|
||||
if ($res->user_id != null) {
|
||||
abort_if(User::whereIsAdmin(true)->whereId($res->user_id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json((bool) $res, ($res ? 200 : 404));
|
||||
}
|
||||
|
||||
public function reportCreate(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$type = $request->input('type');
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'violence',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism',
|
||||
];
|
||||
|
||||
$gp = GroupPost::whereGroupId($group->id)->find($id);
|
||||
abort_if(! $gp, 422, 'Cannot report an invalid or deleted post');
|
||||
abort_if(! in_array($type, $types), 422, 'Invalid report type');
|
||||
abort_if($gp->profile_id === $pid, 422, 'Cannot report your own post');
|
||||
abort_if(
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereProfileId($pid)
|
||||
->whereItemType(GroupPost::class)
|
||||
->whereItemId($id)
|
||||
->exists(),
|
||||
422,
|
||||
'You already reported this'
|
||||
);
|
||||
|
||||
$report = new GroupReport();
|
||||
$report->group_id = $group->id;
|
||||
$report->profile_id = $pid;
|
||||
$report->type = $type;
|
||||
$report->item_type = GroupPost::class;
|
||||
$report->item_id = $id;
|
||||
$report->open = true;
|
||||
$report->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:report:create',
|
||||
[
|
||||
'type' => $type,
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $gp->status_id,
|
||||
'profile_id' => $gp->profile_id,
|
||||
'username' => optional(AccountService::get($gp->profile_id))['acct'],
|
||||
'gpid' => $gp->id,
|
||||
'url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response([200]);
|
||||
}
|
||||
|
||||
public function reportAction(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:cw,delete,ignore',
|
||||
'id' => 'required|string',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
|
||||
$report = GroupReport::whereGroupId($group->id)
|
||||
->findOrFail($id);
|
||||
$status = Status::findOrFail($report->item_id);
|
||||
$gp = GroupPost::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->firstOrFail();
|
||||
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'cw',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
|
||||
case 'ignore':
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'ignore',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
abort_if(! $group->isMember($profile_id), 404);
|
||||
$limits = GroupService::getInteractionLimits($group->id, $profile_id);
|
||||
|
||||
return response()->json($limits);
|
||||
}
|
||||
|
||||
public function updateMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|exists:profiles,id',
|
||||
'can_post' => 'required',
|
||||
'can_comment' => 'required',
|
||||
'can_like' => 'required',
|
||||
]);
|
||||
|
||||
$member = $request->input('profile_id');
|
||||
$can_post = $request->input('can_post');
|
||||
$can_comment = $request->input('can_comment');
|
||||
$can_like = $request->input('can_like');
|
||||
$account = AccountService::get($member);
|
||||
|
||||
abort_if(! $account, 422, 'Invalid profile');
|
||||
abort_if(! $group->isMember($member), 422, 'Invalid profile');
|
||||
|
||||
$limit = GroupLimit::firstOrCreate([
|
||||
'profile_id' => $member,
|
||||
'group_id' => $group->id,
|
||||
]);
|
||||
|
||||
if ($limit->wasRecentlyCreated) {
|
||||
abort_if(GroupLimit::whereGroupId($group->id)->count() >= 25, 422, 'limit_reached');
|
||||
}
|
||||
|
||||
$previousLimits = $limit->limits;
|
||||
|
||||
$limit->limits = [
|
||||
'can_post' => $can_post,
|
||||
'can_comment' => $can_comment,
|
||||
'can_like' => $can_like,
|
||||
];
|
||||
$limit->save();
|
||||
|
||||
GroupService::clearInteractionLimits($group->id, $member);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:member-limits:updated',
|
||||
[
|
||||
'profile_id' => $account['id'],
|
||||
'username' => $account['username'],
|
||||
'previousLimits' => $previousLimits,
|
||||
'newLimits' => $limit->limits,
|
||||
],
|
||||
GroupLimit::class,
|
||||
$limit->id
|
||||
);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function showProfile(Request $request, $id, $pid)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function showProfileByUsername(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
if (! $request->user()) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$group = Group::find($id);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if (! $group->isMember($cid)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$profile = Profile::whereUsername($pid)->first();
|
||||
|
||||
if (! $group->isMember($profile->id)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
if ($profile) {
|
||||
$url = url("/groups/{$id}/user/{$profile->id}");
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
abort(404, 'Invalid username');
|
||||
}
|
||||
|
||||
public function groupInviteLanding(Request $request, $id)
|
||||
{
|
||||
abort(404, 'Not yet implemented');
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
return view('groups.invite', compact('group'));
|
||||
}
|
||||
|
||||
public function groupShortLinkRedirect(Request $request, $hid)
|
||||
{
|
||||
$gid = HashidService::decode($hid);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
return redirect($group->url());
|
||||
}
|
||||
|
||||
public function groupInviteClaim(Request $request, $id)
|
||||
{
|
||||
$group = GroupService::get($id);
|
||||
abort_if(! $group || empty($group), 404);
|
||||
|
||||
return view('groups.invite-claim', compact('group'));
|
||||
}
|
||||
|
||||
public function groupMemberInviteCheck(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
$exists = GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists();
|
||||
|
||||
return response()->json([
|
||||
'gid' => $id,
|
||||
'can_join' => (bool) $exists,
|
||||
]);
|
||||
}
|
||||
|
||||
public function groupMemberInviteAccept(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
abort_if(! GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(), 422);
|
||||
|
||||
$gm = new GroupMember;
|
||||
$gm->group_id = $id;
|
||||
$gm->profile_id = $pid;
|
||||
$gm->role = 'member';
|
||||
$gm->local_group = $group->local;
|
||||
$gm->local_profile = true;
|
||||
$gm->join_request = false;
|
||||
$gm->save();
|
||||
|
||||
GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->delete();
|
||||
GroupService::del($id);
|
||||
GroupService::delSelf($id, $pid);
|
||||
|
||||
return ['next_url' => $group->url()];
|
||||
}
|
||||
|
||||
public function groupMemberInviteDecline(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
return ['next_url' => '/'];
|
||||
}
|
||||
}
|
103
app/Http/Controllers/GroupFederationController.php
Normal file
103
app/Http/Controllers/GroupFederationController.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Status;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Services\MediaService;
|
||||
|
||||
class GroupFederationController extends Controller
|
||||
{
|
||||
public function getGroupObject(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
|
||||
$res = $this->showGroupObject($group);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showGroupObject($group)
|
||||
{
|
||||
return Cache::remember('ap:groups:object:' . $group->id, 3600, function() use($group) {
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $group->url(),
|
||||
'inbox' => $group->permalink('/inbox'),
|
||||
'name' => $group->name,
|
||||
'outbox' => $group->permalink('/outbox'),
|
||||
'summary' => $group->description,
|
||||
'type' => 'Group',
|
||||
'attributedTo' => [
|
||||
'type' => 'Person',
|
||||
'id' => $group->admin->permalink()
|
||||
],
|
||||
// 'endpoints' => [
|
||||
// 'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
// ],
|
||||
'preferredUsername' => 'gid_' . $group->id,
|
||||
'publicKey' => [
|
||||
'id' => $group->permalink('#main-key'),
|
||||
'owner' => $group->permalink(),
|
||||
'publicKeyPem' => InstanceActor::first()->public_key,
|
||||
],
|
||||
'url' => $group->permalink()
|
||||
];
|
||||
|
||||
if($group->metadata && isset($group->metadata['avatar'])) {
|
||||
$res['icon'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['avatar']['url']
|
||||
];
|
||||
}
|
||||
|
||||
if($group->metadata && isset($group->metadata['header'])) {
|
||||
$res['image'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['header']['url']
|
||||
];
|
||||
}
|
||||
ksort($res);
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public function getStatusObject(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid);
|
||||
$gp = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
$status = Status::findOrFail($gp->status_id);
|
||||
// permission check
|
||||
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $gp->url(),
|
||||
|
||||
'type' => 'Note',
|
||||
|
||||
'summary' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'inReplyTo' => null,
|
||||
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'url' => $gp->url(),
|
||||
'attributedTo' => $status->profile->permalink(),
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
$group->permalink('/followers'),
|
||||
],
|
||||
'cc' => [],
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'attachment' => MediaService::activitypub($status->id),
|
||||
'target' => [
|
||||
'type' => 'Collection',
|
||||
'id' => $group->permalink('/wall'),
|
||||
'attributedTo' => $group->permalink()
|
||||
]
|
||||
];
|
||||
// ksort($res);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
10
app/Http/Controllers/GroupPostController.php
Normal file
10
app/Http/Controllers/GroupPostController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GroupPostController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
|
||||
class CreateGroupsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function checkCreatePermission(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$config = GroupService::config();
|
||||
if($request->user()->is_admin) {
|
||||
$allowed = true;
|
||||
} else {
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
$allowed = Group::whereProfileId($pid)->count() <= $max;
|
||||
}
|
||||
|
||||
return ['permission' => (bool) $allowed];
|
||||
}
|
||||
|
||||
public function storeGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:public,private,local'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$config = GroupService::config();
|
||||
abort_if($config['limits']['user']['create']['new'] == false && $request->user()->is_admin == false, 422, 'Invalid operation');
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
// abort_if(Group::whereProfileId($pid)->count() <= $max, 422, 'Group limit reached');
|
||||
|
||||
$group = new Group;
|
||||
$group->profile_id = $pid;
|
||||
$group->name = $request->input('name');
|
||||
$group->description = $request->input('description', null);
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->metadata = $request->input('configuration');
|
||||
$group->save();
|
||||
|
||||
GroupService::log($group->id, $pid, 'group:created');
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'founder';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $group->id,
|
||||
'url' => $group->url()
|
||||
];
|
||||
}
|
||||
}
|
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupReport;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
|
||||
class GroupsAdminController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getAdminTabs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
abort_if($pid !== $group->profile_id, 404);
|
||||
|
||||
$reqs = GroupMember::whereGroupId($group->id)->whereJoinRequest(true)->count();
|
||||
$mods = GroupReport::whereGroupId($group->id)->whereOpen(true)->count();
|
||||
$tabs = [
|
||||
'moderation_count' => $mods > 99 ? '99+' : $mods,
|
||||
'request_count' => $reqs > 99 ? '99+' : $reqs
|
||||
];
|
||||
|
||||
return response()->json($tabs);
|
||||
}
|
||||
|
||||
public function getInteractionLogs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$logs = GroupInteraction::whereGroupId($id)
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($log) use($group) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'profile' => GroupAccountService::get($group->id, $log->profile_id),
|
||||
'type' => $log->type,
|
||||
'metadata' => $log->metadata,
|
||||
'created_at' => $log->created_at->format('c')
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($logs, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->take(3)->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->take(3)->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->take(3)->pluck('name')
|
||||
];
|
||||
|
||||
return response()->json($blocks, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function exportBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->pluck('name')
|
||||
];
|
||||
|
||||
$blocks['_created_at'] = now()->format('c');
|
||||
$blocks['_version'] = '1.0.0';
|
||||
ksort($blocks);
|
||||
|
||||
return response()->streamDownload(function() use($blocks) {
|
||||
echo json_encode($blocks, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
}
|
||||
|
||||
public function addBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->profile_id = $profile->id;
|
||||
$gb->name = $profile->username;
|
||||
$gb->is_user = true;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = true;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function undoBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(false)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereProfileId($profile->id)
|
||||
->whereIsUser(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getReportList(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$scope = $request->input('scope', 'open');
|
||||
|
||||
$list = GroupReport::selectRaw('id, profile_id, item_type, item_id, type, created_at, count(*) as total')
|
||||
->whereGroupId($group->id)
|
||||
->groupBy('item_id')
|
||||
->when($scope == 'open', function($query, $scope) {
|
||||
return $query->whereOpen(true);
|
||||
})
|
||||
->latest()
|
||||
->simplePaginate(10)
|
||||
->map(function($report) use($group) {
|
||||
$res = [
|
||||
'id' => (string) $report->id,
|
||||
'profile' => GroupAccountService::get($group->id, $report->profile_id),
|
||||
'type' => $report->type,
|
||||
'created_at' => $report->created_at->format('c'),
|
||||
'total_count' => $report->total
|
||||
];
|
||||
|
||||
if($report->item_type === GroupPost::class) {
|
||||
$res['status'] = GroupPostService::get($group->id, $report->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
return response()->json($list, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
}
|
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
|
||||
class GroupsApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
return GroupService::config();
|
||||
}
|
||||
|
||||
public function getGroupAccount(Request $request, $gid, $pid)
|
||||
{
|
||||
$res = GroupAccountService::get($gid, $pid);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function getGroupCategories(Request $request)
|
||||
{
|
||||
$res = GroupService::categories();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupsByCategory(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$category = GroupCategory::whereName($name)->firstOrFail();
|
||||
$groups = Group::whereCategoryId($category->id)
|
||||
->simplePaginate(6)
|
||||
->map(function($group) {
|
||||
return GroupService::get($group->id);
|
||||
})
|
||||
->filter(function($group) {
|
||||
return $group;
|
||||
})
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getRecommendedGroups(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getSelfGroups(Request $request)
|
||||
{
|
||||
$selfOnly = $request->input('self') == true;
|
||||
$memberOnly = $request->input('member') == true;
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = GroupMember::whereProfileId($request->user()->profile_id)
|
||||
->when($selfOnly, function($q, $selfOnly) {
|
||||
return $q->whereRole('founder');
|
||||
})
|
||||
->when($memberOnly, function($q, $memberOnly) {
|
||||
return $q->whereRole('member');
|
||||
})
|
||||
->simplePaginate(4)
|
||||
->map(function($member) use($pid) {
|
||||
$group = $member->group;
|
||||
return $this->toJson($group, $pid);
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupCommentService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupComment;
|
||||
use Purify;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewCommentPipeline;
|
||||
use App\Jobs\GroupsPipeline\DeleteCommentPipeline;
|
||||
|
||||
class GroupsCommentController extends Controller
|
||||
{
|
||||
public function getComments(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required',
|
||||
'cid' => 'sometimes',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
$pid = optional($request->user())->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->has('cid') && $request->input('cid') == 1;
|
||||
$limit = $request->input('limit', 3);
|
||||
$maxId = $request->input('max_id', 0);
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if($group->is_private && !$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$status = $cid ? GroupComment::findOrFail($sid) : GroupPost::findOrFail($sid);
|
||||
|
||||
abort_if($status->group_id != $group->id, 400, 'Invalid group');
|
||||
|
||||
$replies = GroupComment::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->orderByDesc('id')
|
||||
->when($maxId, function($query, $maxId) {
|
||||
return $query->where('id', '<', $maxId);
|
||||
})
|
||||
->take($limit)
|
||||
->get()
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupCommentService::get($gp['group_id'], $gp['id']);
|
||||
$status['reply_count'] = $gp['reply_count'];
|
||||
$status['url'] = $gp->url();
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$gp['profile_id']}");
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $replies->toArray();
|
||||
}
|
||||
|
||||
public function storeComment(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'cid' => 'sometimes',
|
||||
'content' => 'required|string|min:1|max:1500'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->input('cid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = e($request->input('content'));
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
|
||||
|
||||
$parent = $cid == 1 ?
|
||||
GroupComment::findOrFail($sid) :
|
||||
GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
// $autolink = Purify::clean(Autolink::create()->autolink($caption));
|
||||
// $autolink = str_replace('/discover/tags/', '/groups/' . $gid . '/topics/', $autolink);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->status_id = $parent->id;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'public';
|
||||
$status->is_nsfw = false;
|
||||
$status->local = true;
|
||||
$status->save();
|
||||
|
||||
NewCommentPipeline::dispatch($parent, $status)->onQueue('groups');
|
||||
// todo: perform in job
|
||||
$parent->reply_count = $parent->reply_count ? $parent->reply_count + $parent->reply_count : 1;
|
||||
$parent->save();
|
||||
GroupPostService::del($parent->group_id, $parent->id);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:comment:created',
|
||||
[
|
||||
'type' => 'group:post:comment',
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$status->id
|
||||
);
|
||||
|
||||
//GroupCommentPipeline::dispatch($parent, $status, $gp);
|
||||
//NewStatusPipeline::dispatch($status, $gp);
|
||||
//GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
$s['pf_type'] = 'text';
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $status->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function storeCommentPhoto(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'photo' => 'required|image'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = $request->input('content');
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
$parent = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->status_id = $parent->id;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $parent->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
|
||||
// $gp = new GroupPost;
|
||||
// $gp->group_id = $group->id;
|
||||
// $gp->profile_id = $pid;
|
||||
// $gp->type = 'reply:photo';
|
||||
// $gp->status_id = $status->id;
|
||||
// $gp->in_reply_to_id = $parent->id;
|
||||
// $gp->save();
|
||||
|
||||
// GroupService::log(
|
||||
// $group->id,
|
||||
// $pid,
|
||||
// 'group:comment:created',
|
||||
// [
|
||||
// 'type' => $gp->type,
|
||||
// 'status_id' => $status->id
|
||||
// ],
|
||||
// GroupPost::class,
|
||||
// $gp->id
|
||||
// );
|
||||
|
||||
// todo: perform in job
|
||||
// $parent->reply_count = Status::whereInReplyToId($parent->id)->count();
|
||||
// $parent->save();
|
||||
// StatusService::del($parent->id);
|
||||
// GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
// $s['pf_type'] = 'text';
|
||||
// $s['visibility'] = 'public';
|
||||
// $s['url'] = $gp->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deleteComment(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupComment::whereGroupId($group->id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
|
||||
$parent = GroupPost::find($gp->status_id);
|
||||
abort_if(!$parent, 422, 'Invalid parent');
|
||||
|
||||
DeleteCommentPipeline::dispatch($parent, $gp)->onQueue('groups');
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $gp->id,
|
||||
],
|
||||
GroupComment::class,
|
||||
$gp->id
|
||||
);
|
||||
$gp->delete();
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect('/groups/feed');
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsDiscoverController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getDiscoverPopular(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::orderByDesc('member_count')
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getDiscoverNew(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::latest()
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
}
|
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsFeedController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getSelfFeed(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$limit = $request->input('limit', 5);
|
||||
$page = $request->input('page');
|
||||
$initial = $request->has('initial');
|
||||
|
||||
if($initial) {
|
||||
$res = Cache::remember('groups:self:feed:' . $pid, 900, function() use($pid) {
|
||||
return $this->getSelfFeedV0($pid, 5, null);
|
||||
});
|
||||
} else {
|
||||
abort_if($page && $page > 5, 422);
|
||||
$res = $this->getSelfFeedV0($pid, $limit, $page);
|
||||
}
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
protected function getSelfFeedV0($pid, $limit, $page)
|
||||
{
|
||||
return GroupPost::join('group_members', 'group_posts.group_id', 'group_members.group_id')
|
||||
->select('group_posts.*', 'group_members.group_id', 'group_members.profile_id')
|
||||
->where('group_members.profile_id', $pid)
|
||||
->whereIn('group_posts.type', ['text', 'photo', 'video'])
|
||||
->orderByDesc('group_posts.id')
|
||||
->limit($limit)
|
||||
// ->pluck('group_posts.status_id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = url("/groups/{$gp['group_id']}/p/{$gp['id']}");
|
||||
$status['group'] = GroupService::get($gp['group_id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
return $status;
|
||||
});
|
||||
}
|
||||
|
||||
public function getGroupProfileFeed(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$feed = GroupPost::whereGroupId($id)
|
||||
->whereProfileId($pid)
|
||||
->latest()
|
||||
->paginate(3)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
$status['account']['url'] = "/groups/{$gp['group_id']}/user/{$status['account']['id']}";
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
public function getGroupFeed(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = optional($user)->profile_id ?? false;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$max = $request->input('max_id');
|
||||
$limit = $request->limit ?? 3;
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
// $posts = GroupPost::whereGroupId($group->id)
|
||||
// ->when($maxId, function($q, $maxId) {
|
||||
// return $q->where('status_id', '<', $maxId);
|
||||
// })
|
||||
// ->whereNull('in_reply_to_id')
|
||||
// ->orderByDesc('status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
// $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
// return $status;
|
||||
// })->filter(function($status) {
|
||||
// return $status;
|
||||
// });
|
||||
// return $posts;
|
||||
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() use($id) {
|
||||
if(GroupFeedService::count($id) == 0) {
|
||||
GroupFeedService::warmCache($id, true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = GroupFeedService::getRankedMaxId($id, $max, $limit);
|
||||
} else {
|
||||
$feed = GroupFeedService::get($id, 0, $limit);
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user, $id) {
|
||||
$status = GroupPostService::get($id, $k);
|
||||
if($status && $user) {
|
||||
$pid = $user->profile_id;
|
||||
$sid = $status['account']['id'];
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $status['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($status['id']);
|
||||
$status['relationship'] = $pid == $sid ? [] : RelationshipService::get($pid, $sid);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupHashtagService;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinApprovedPipeline;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinRejectedPipeline;
|
||||
|
||||
class GroupsMemberController extends Controller
|
||||
{
|
||||
public function getGroupMembers(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$members = GroupMember::whereGroupId($gid)
|
||||
->whereJoinRequest(false)
|
||||
->simplePaginate(10)
|
||||
->map(function($member) use($pid) {
|
||||
$account = AccountService::get($member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$member->group_id}/user/{$member['profile_id']}");
|
||||
return $account;
|
||||
});
|
||||
|
||||
return response()->json($members->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberJoinRequests(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return GroupMember::whereGroupId($group->id)
|
||||
->whereJoinRequest(true)
|
||||
->whereNull('rejected_at')
|
||||
->paginate(10)
|
||||
->map(function($member) {
|
||||
return AccountService::get($member->profile_id);
|
||||
});
|
||||
}
|
||||
|
||||
public function handleGroupMemberJoinRequest(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
$mid = $request->input('pid');
|
||||
abort_if($group->isMember($mid), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required',
|
||||
'action' => 'required|in:approve,reject'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
||||
$member = GroupMember::whereGroupId($group->id)
|
||||
->whereProfileId($mid)
|
||||
->firstOrFail();
|
||||
|
||||
if($action == 'approve') {
|
||||
MemberJoinApprovedPipeline::dispatch($member)->onQueue('groups');
|
||||
} else if ($action == 'reject') {
|
||||
MemberJoinRejectedPipeline::dispatch($member)->onQueue('groups');
|
||||
}
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function getGroupMember(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$member_id = $request->input('pid');
|
||||
$member = GroupMember::whereGroupId($gid)
|
||||
->whereProfileId($member_id)
|
||||
->firstOrFail();
|
||||
|
||||
$account = GroupAccountService::get($group->id, $member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$gid}/user/{$member_id}");
|
||||
|
||||
return response()->json($account, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberCommonIntersections(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
// $this->validate($request, [
|
||||
// 'gid' => 'required',
|
||||
// 'pid' => 'required'
|
||||
// ]);
|
||||
|
||||
$gid = $request->input('gid');
|
||||
$pid = $request->input('pid');
|
||||
|
||||
if($pid === $cid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($cid), 404);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$self = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($cid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
$user = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($pid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
|
||||
$topics = $self->intersect($user)
|
||||
->values()
|
||||
->shuffle()
|
||||
->take(3)
|
||||
->map(function($id) use($group) {
|
||||
$tag = GroupHashtagService::get($id);
|
||||
$tag['url'] = url("/groups/{$group->id}/topics/{$tag['slug']}?src=upt");
|
||||
return $tag;
|
||||
});
|
||||
|
||||
// $friends = DB::table('followers as u')
|
||||
// ->join('followers as s', 'u.following_id', '=', 's.following_id')
|
||||
// ->where('s.profile_id', $cid)
|
||||
// ->where('u.profile_id', $pid)
|
||||
// ->inRandomOrder()
|
||||
// ->take(10)
|
||||
// ->pluck('s.following_id')
|
||||
// ->map(function($id) use($gid) {
|
||||
// $res = AccountService::get($id);
|
||||
// $res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
// return $res;
|
||||
// });
|
||||
$mutualGroups = GroupService::mutualGroups($cid, $pid, [$gid]);
|
||||
|
||||
$mutualFriends = collect(FollowerService::mutualIds($cid, $pid))
|
||||
->map(function($id) use($gid) {
|
||||
$res = AccountService::get($id);
|
||||
if(GroupService::isMember($gid, $id)) {
|
||||
$res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
} else if(!$res['local']) {
|
||||
$res['url'] = url("/i/web/profile/_/{$id}");
|
||||
}
|
||||
return $res;
|
||||
});
|
||||
$mutualFriendsCount = FollowerService::mutualCount($cid, $pid);
|
||||
|
||||
$res = [
|
||||
'groups_count' => $mutualGroups['count'],
|
||||
'groups' => $mutualGroups['groups'],
|
||||
'topics' => $topics,
|
||||
'friends_count' => $mutualFriendsCount,
|
||||
'friends' => $mutualFriends,
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
|
||||
class GroupsMetaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function deleteGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$group->status = "delete";
|
||||
$group->save();
|
||||
GroupService::del($group->id);
|
||||
return [200];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Notification;
|
||||
|
||||
class GroupsNotificationsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function selfGlobalNotifications(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$res = Notification::whereProfileId($pid)
|
||||
->where('action', 'like', 'group%')
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($n) {
|
||||
$res = [
|
||||
'id' => $n->id,
|
||||
'type' => $n->action,
|
||||
'account' => AccountService::get($n->actor_id),
|
||||
'object' => [
|
||||
'id' => $n->item_id,
|
||||
'type' => last(explode('\\', $n->item_type)),
|
||||
],
|
||||
'created_at' => $n->created_at->format('c')
|
||||
];
|
||||
|
||||
if($res['object']['type'] == 'Status' || in_array($n->action, ['group:comment'])) {
|
||||
$res['status'] = StatusService::get($n->item_id, false);
|
||||
$res['group'] = GroupService::get($res['status']['gid']);
|
||||
}
|
||||
|
||||
if($res['object']['type'] == 'Group') {
|
||||
$res['group'] = GroupService::get($n->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
|
@ -0,0 +1,420 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
|
||||
class GroupsPostController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function storePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'group_id' => 'required|exists:groups,id',
|
||||
'caption' => 'sometimes|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'pollOptions' => 'sometimes|array|min:1|max:4'
|
||||
]);
|
||||
|
||||
$group = Group::findOrFail($request->input('group_id'));
|
||||
$pid = $request->user()->profile_id;
|
||||
$caption = $request->input('caption');
|
||||
$type = $request->input('type', 'text');
|
||||
|
||||
abort_if(!GroupService::canPost($group->id, $pid), 422, 'You cannot create new posts at this time');
|
||||
|
||||
if($type == 'text') {
|
||||
abort_if(strlen(e($caption)) == 0, 403);
|
||||
}
|
||||
|
||||
$gp = new GroupPost;
|
||||
$gp->group_id = $group->id;
|
||||
$gp->profile_id = $pid;
|
||||
$gp->caption = e($caption);
|
||||
$gp->type = $type;
|
||||
$gp->visibility = 'draft';
|
||||
$gp->save();
|
||||
|
||||
$status = $gp;
|
||||
|
||||
NewPostPipeline::dispatchSync($gp);
|
||||
|
||||
// NewStatusPipeline::dispatch($status, $gp);
|
||||
|
||||
if($type == 'poll') {
|
||||
// Polls not supported yet
|
||||
// $poll = new Poll;
|
||||
// $poll->status_id = $status->id;
|
||||
// $poll->profile_id = $status->profile_id;
|
||||
// $poll->poll_options = $request->input('pollOptions');
|
||||
// $poll->expires_at = now()->addMinutes($request->input('expiry'));
|
||||
// $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
// return 0;
|
||||
// })->toArray();
|
||||
// $poll->save();
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'photo') {
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
// $storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
// $hash = \hash_file('sha256', $photo);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
// Bus::chain([
|
||||
// new ImageResizePipeline($media),
|
||||
// new ImageS3UploadPipeline($media),
|
||||
// ])->dispatch($media);
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
// ImageOptimize::dispatch($media);
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'video') {
|
||||
$video = $request->file('video');
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $video->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $video);
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->user_id = $request->user()->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $video->getSize();
|
||||
$media->mime = $video->getMimeType();
|
||||
$media->save();
|
||||
|
||||
VideoThumbnail::dispatch($media);
|
||||
sleep(15);
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:created',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$s = GroupPostService::get($status->group_id, $status->id);
|
||||
GroupFeedService::add($group->id, $gp->id);
|
||||
Cache::forget('groups:self:feed:' . $pid);
|
||||
|
||||
$s['pf_type'] = $type;
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $gp->url();
|
||||
|
||||
if($type == 'poll') {
|
||||
$s['poll'] = PollService::get($status->id);
|
||||
}
|
||||
|
||||
$group->last_active_at = now();
|
||||
$group->save();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deletePost(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupPost::whereGroupId($status->group_id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
$cached = GroupPostService::get($status->group_id, $status->id);
|
||||
|
||||
if($cached) {
|
||||
$cached = collect($cached)->filter(function($r, $k) {
|
||||
return in_array($k, [
|
||||
'id',
|
||||
'sensitive',
|
||||
'pf_type',
|
||||
'media_attachments',
|
||||
'content_text',
|
||||
'created_at'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$status->group_id,
|
||||
$request->user()->profile_id,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id,
|
||||
'original' => $cached
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
// if($status->profile_id != $user->profile->id &&
|
||||
// $user->is_admin == true &&
|
||||
// $status->uri == null
|
||||
// ) {
|
||||
// $media = $status->media;
|
||||
|
||||
// $ai = new AccountInterstitial;
|
||||
// $ai->user_id = $status->profile->user_id;
|
||||
// $ai->type = 'post.removed';
|
||||
// $ai->view = 'account.moderation.post.removed';
|
||||
// $ai->item_type = 'App\Status';
|
||||
// $ai->item_id = $status->id;
|
||||
// $ai->has_media = (bool) $media->count();
|
||||
// $ai->blurhash = $media->count() ? $media->first()->blurhash : null;
|
||||
// $ai->meta = json_encode([
|
||||
// 'caption' => $status->caption,
|
||||
// 'created_at' => $status->created_at,
|
||||
// 'type' => $status->type,
|
||||
// 'url' => $status->url(),
|
||||
// 'is_nsfw' => $status->is_nsfw,
|
||||
// 'scope' => $status->scope,
|
||||
// 'reblog' => $status->reblog_of_id,
|
||||
// 'likes_count' => $status->likes_count,
|
||||
// 'reblogs_count' => $status->reblogs_count,
|
||||
// ]);
|
||||
// $ai->save();
|
||||
|
||||
// $u = $status->profile->user;
|
||||
// $u->has_interstitial = true;
|
||||
// $u->save();
|
||||
// }
|
||||
|
||||
if($status->in_reply_to_id) {
|
||||
$parent = GroupPost::find($status->in_reply_to_id);
|
||||
if($parent) {
|
||||
$parent->reply_count = GroupPost::whereInReplyToId($parent->id)->count();
|
||||
$parent->save();
|
||||
GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
}
|
||||
}
|
||||
|
||||
GroupPostService::del($group->id, $gp->id);
|
||||
GroupFeedService::del($group->id, $gp->id);
|
||||
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
||||
// Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect($user->url());
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
// if (GroupLike::whereGroupId($gid)->whereStatusId($sid)->whereProfileId($pid)->exists()) {
|
||||
// $like = GroupLike::whereProfileId($pid)->whereStatusId($sid)->firstOrFail();
|
||||
// // UnlikePipeline::dispatch($like);
|
||||
// $count = $gp->likes_count - 1;
|
||||
// $action = 'group:unlike';
|
||||
// } else {
|
||||
// $count = $gp->likes_count;
|
||||
// $like = GroupLike::firstOrCreate([
|
||||
// 'group_id' => $gid,
|
||||
// 'profile_id' => $pid,
|
||||
// 'status_id' => $sid
|
||||
// ]);
|
||||
// if($like->wasRecentlyCreated == true) {
|
||||
// $count++;
|
||||
// $gp->likes_count = $count;
|
||||
// $like->save();
|
||||
// $gp->save();
|
||||
// // LikePipeline::dispatch($like);
|
||||
// $action = 'group:like';
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Cache::forget('status:'.$status->id.':likedby:userid:'.$request->user()->id);
|
||||
// StatusService::del($status->id);
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getGroupMedia(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'type' => 'required|in:photo,video'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$type = $request->input('type');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$media = GroupPost::whereGroupId($gid)
|
||||
->whereType($type)
|
||||
->latest()
|
||||
->simplePaginate(20)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
return $status;
|
||||
})->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($media->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\Groups\GroupActivityPubService;
|
||||
|
||||
class GroupsSearchController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function inviteFriendsToGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'uids' => 'required',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$uid = $request->input('uids');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(
|
||||
GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->count() >= 20,
|
||||
422,
|
||||
'Invite limit reached'
|
||||
);
|
||||
|
||||
$profiles = collect($uid)
|
||||
->map(function($u) {
|
||||
return Profile::find($u);
|
||||
})
|
||||
->filter(function($u) use($pid) {
|
||||
return $u &&
|
||||
$u->id != $pid &&
|
||||
isset($u->id) &&
|
||||
Follower::whereFollowingId($pid)
|
||||
->whereProfileId($u->id)
|
||||
->exists();
|
||||
})
|
||||
->filter(function($u) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($u->id)
|
||||
->exists() == false;
|
||||
})
|
||||
->each(function($u) use($gid, $pid) {
|
||||
$gi = new GroupInvitation;
|
||||
$gi->group_id = $gid;
|
||||
$gi->from_profile_id = $pid;
|
||||
$gi->to_profile_id = $u->id;
|
||||
$gi->to_local = true;
|
||||
$gi->from_local = $u->domain == null;
|
||||
$gi->save();
|
||||
// GroupMemberInvite::dispatch($gi);
|
||||
});
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function searchFriendsToInvite(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = Profile::where('username', 'like', "%{$q}%")
|
||||
->whereNull('profiles.domain')
|
||||
->join('followers', 'profiles.id', '=', 'followers.profile_id')
|
||||
->where('followers.following_id', $pid)
|
||||
->take(10)
|
||||
->get()
|
||||
->filter(function($p) use($group) {
|
||||
return $group->isMember($p->profile_id) == false;
|
||||
})
|
||||
->filter(function($p) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($p->profile_id)
|
||||
->exists() == false;
|
||||
})
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'id' => (string) $gm->profile_id,
|
||||
'username' => $a['acct'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchGlobalResults(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:140',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
|
||||
if(str_starts_with($q, 'https://')) {
|
||||
$res = Helpers::getSignedFetch($q);
|
||||
if($res && $res = json_decode($res, true)) {
|
||||
|
||||
}
|
||||
if($res && isset($res['type']) && in_array($res['type'], ['Group', 'Note', 'Page'])) {
|
||||
if($res['type'] === 'Group') {
|
||||
return GroupActivityPubService::fetchGroup($q, true);
|
||||
}
|
||||
$resp = GroupActivityPubService::fetchGroupPost($q, true);
|
||||
$resp['name'] = 'Group Post';
|
||||
$resp['url'] = '/groups/' . $resp['group_id'] . '/p/' . $resp['id'];
|
||||
return [$resp];
|
||||
}
|
||||
}
|
||||
return Group::whereNull('status')
|
||||
->where('name', 'like', '%' . $q . '%')
|
||||
->orderBy('id')
|
||||
->take(10)
|
||||
->pluck('id')
|
||||
->map(function($group) {
|
||||
return GroupService::get($group);
|
||||
});
|
||||
}
|
||||
|
||||
public function searchLocalAutocomplete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = GroupMember::whereGroupId($gid)
|
||||
->join('profiles', 'group_members.profile_id', '=', 'profiles.id')
|
||||
->where('profiles.username', 'like', "%{$q}%")
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'username' => $a['username'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchAddRecent(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
$ttl = now()->addDays(14);
|
||||
$res = Cache::get($key);
|
||||
if(!$res) {
|
||||
$val = json_encode([$q]);
|
||||
} else {
|
||||
$ex = collect(json_decode($res))
|
||||
->prepend($q)
|
||||
->unique('value')
|
||||
->slice(0, 3)
|
||||
->values()
|
||||
->all();
|
||||
$val = json_encode($ex);
|
||||
}
|
||||
Cache::put($key, $val, $ttl);
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function searchGetRecent(Request $request)
|
||||
{
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
return Cache::get($key);
|
||||
}
|
||||
}
|
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupPost;
|
||||
|
||||
class GroupsTopicController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function groupTopics(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$posts = GroupPostHashtag::join('group_hashtags', 'group_hashtags.id', '=', 'group_post_hashtags.hashtag_id')
|
||||
->selectRaw('group_hashtags.*, group_post_hashtags.*, count(group_post_hashtags.hashtag_id) as ht_count')
|
||||
->where('group_post_hashtags.group_id', $gid)
|
||||
->orderByDesc('ht_count')
|
||||
->limit(10)
|
||||
->pluck('group_post_hashtags.hashtag_id', 'ht_count')
|
||||
->map(function($id, $key) use ($gid) {
|
||||
$tag = GroupHashtag::find($id);
|
||||
return [
|
||||
'hid' => $id,
|
||||
'name' => $tag->name,
|
||||
'url' => url("/groups/{$gid}/topics/{$tag->slug}"),
|
||||
'count' => $key
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function groupTopicTag(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'name' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$name = $request->input('name');
|
||||
$hashtag = GroupHashtag::whereName($name)->first();
|
||||
|
||||
if(!$hashtag) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// $posts = GroupPost::whereGroupId($gid)
|
||||
// ->select('status_hashtags.*', 'group_posts.*')
|
||||
// ->where('status_hashtags.hashtag_id', $hashtag->id)
|
||||
// ->join('status_hashtags', 'group_posts.status_id', '=', 'status_hashtags.status_id')
|
||||
// ->orderByDesc('group_posts.status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
// return $status;
|
||||
// });
|
||||
|
||||
$posts = GroupPostHashtag::whereGroupId($gid)
|
||||
->whereHashtagId($hashtag->id)
|
||||
->orderByDesc('id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['status_id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['status_id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['status_id']);
|
||||
$status['pf_type'] = $status['pf_type'];
|
||||
$status['visibility'] = 'public';
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showTopicFeed(Request $request, $gid, $tag)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
$gid = $group->id;
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
return view('groups.topic-feed', compact('gid', 'tag'));
|
||||
}
|
||||
}
|
99
app/Jobs/GroupPipeline/GroupCommentPipeline.php
Normal file
99
app/Jobs/GroupPipeline/GroupCommentPipeline.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class GroupCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $comment;
|
||||
protected $groupPost;
|
||||
|
||||
public function __construct(Status $status, Status $comment, $groupPost = null)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->comment = $comment;
|
||||
$this->groupPost = $groupPost;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if($this->status->group_id == null || $this->comment->group_id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateParentReplyCount();
|
||||
$this->generateNotification();
|
||||
|
||||
if($this->groupPost) {
|
||||
$this->updateChildReplyCount();
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateParentReplyCount()
|
||||
{
|
||||
$parent = $this->status;
|
||||
$parent->reply_count = Status::whereInReplyToId($parent->id)->count();
|
||||
$parent->save();
|
||||
StatusService::del($parent->id);
|
||||
}
|
||||
|
||||
protected function updateChildReplyCount()
|
||||
{
|
||||
$gp = $this->groupPost;
|
||||
if($gp->reply_child_id) {
|
||||
$parent = GroupPost::whereStatusId($gp->reply_child_id)->first();
|
||||
if($parent) {
|
||||
$parent->reply_count++;
|
||||
$parent->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateNotification()
|
||||
{
|
||||
$status = $this->status;
|
||||
$comment = $this->comment;
|
||||
|
||||
$target = $status->profile;
|
||||
$actor = $comment->profile;
|
||||
|
||||
if ($actor->id == $target->id || $status->comments_disabled == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = DB::transaction(function() use($target, $actor, $comment) {
|
||||
$actorName = $actor->username;
|
||||
$actorUrl = $actor->url();
|
||||
$text = "{$actorName} commented on your group post.";
|
||||
$html = "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> commented on your group post.";
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:comment';
|
||||
$notification->item_id = $comment->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
return $notification;
|
||||
});
|
||||
|
||||
NotificationService::setNotification($notification);
|
||||
NotificationService::set($notification->profile_id, $notification->id);
|
||||
}
|
||||
}
|
57
app/Jobs/GroupPipeline/GroupMediaPipeline.php
Normal file
57
app/Jobs/GroupPipeline/GroupMediaPipeline.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Media;
|
||||
use Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
|
||||
class GroupMediaPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
MediaStorageService::store($this->media);
|
||||
}
|
||||
|
||||
protected function localToCloud($media)
|
||||
{
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
$thumb = storage_path('app/'.$media->thumbnail_path);
|
||||
|
||||
$p = explode('/', $media->media_path);
|
||||
$name = array_pop($p);
|
||||
$pt = explode('/', $media->thumbnail_path);
|
||||
$thumbname = array_pop($pt);
|
||||
$storagePath = implode('/', $p);
|
||||
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
$url = $disk->url($file);
|
||||
$thumbFile = $disk->putFileAs($storagePath, new File($thumb), $thumbname, 'public');
|
||||
$thumbUrl = $disk->url($thumbFile);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
$media->cdn_url = $url;
|
||||
$media->optimized_url = $url;
|
||||
$media->replicated_at = now();
|
||||
$media->save();
|
||||
if($media->status_id) {
|
||||
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
54
app/Jobs/GroupPipeline/GroupMemberInvite.php
Normal file
54
app/Jobs/GroupPipeline/GroupMemberInvite.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
|
||||
class GroupMemberInvite implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $invite;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupInvitation $invite)
|
||||
{
|
||||
$this->invite = $invite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$invite = $this->invite;
|
||||
$actor = Profile::find($invite->from_profile_id);
|
||||
$target = Profile::find($invite->to_profile_id);
|
||||
|
||||
if(!$actor || !$target) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:invite';
|
||||
$notification->item_id = $invite->group_id;
|
||||
$notification->item_type = 'App\Models\Group';
|
||||
$notification->save();
|
||||
}
|
||||
}
|
54
app/Jobs/GroupPipeline/JoinApproved.php
Normal file
54
app/Jobs/GroupPipeline/JoinApproved.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class JoinApproved implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->approved_at = now();
|
||||
$member->join_request = false;
|
||||
$member->role = 'member';
|
||||
$member->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $member->profile_id;
|
||||
$n->actor_id = $member->profile_id;
|
||||
$n->item_id = $member->group_id;
|
||||
$n->item_type = 'App\Models\Group';
|
||||
$n->save();
|
||||
|
||||
GroupService::del($member->group_id);
|
||||
GroupService::delSelf($member->group_id, $member->profile_id);
|
||||
}
|
||||
}
|
50
app/Jobs/GroupPipeline/JoinRejected.php
Normal file
50
app/Jobs/GroupPipeline/JoinRejected.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class JoinRejected implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->rejected_at = now();
|
||||
$member->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $member->profile_id;
|
||||
$n->actor_id = $member->profile_id;
|
||||
$n->item_id = $member->group_id;
|
||||
$n->item_type = 'App\Models\Group';
|
||||
$n->action = 'group.join.rejected';
|
||||
$n->save();
|
||||
}
|
||||
}
|
107
app/Jobs/GroupPipeline/LikePipeline.php
Normal file
107
app/Jobs/GroupPipeline/LikePipeline.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\Like as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class LikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if (!$status) {
|
||||
// Ignore notifications to deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
StatusService::refresh($status->id);
|
||||
|
||||
if($status->url && $actor->domain == null) {
|
||||
return $this->remoteLikeDeliver();
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('group:like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if ($actor->id === $status->profile_id || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $status->profile_id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:like';
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
public function remoteLikeDeliver()
|
||||
{
|
||||
$like = $this->like;
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($actor, $url, $activity);
|
||||
}
|
||||
}
|
130
app/Jobs/GroupPipeline/NewStatusPipeline.php
Normal file
130
app/Jobs/GroupPipeline/NewStatusPipeline.php
Normal file
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Hashtag;
|
||||
use App\Mention;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
|
||||
class NewStatusPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $gp;
|
||||
protected $tags;
|
||||
protected $mentions;
|
||||
|
||||
public function __construct(Status $status, GroupPost $gp)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->gp = $gp;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
|
||||
$autolink = Autolink::create()
|
||||
->setAutolinkActiveUsersOnly(true)
|
||||
->setBaseHashPath("/groups/{$status->group_id}/topics/")
|
||||
->setBaseUserPath("/groups/{$status->group_id}/username/")
|
||||
->autolink($status->caption);
|
||||
|
||||
$entities = Extractor::create()->extract($status->caption);
|
||||
|
||||
$autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink);
|
||||
|
||||
$status->rendered = nl2br($autolink);
|
||||
$status->entities = null;
|
||||
$status->save();
|
||||
|
||||
$this->tags = array_unique($entities['hashtags']);
|
||||
$this->mentions = array_unique($entities['mentions']);
|
||||
|
||||
if(count($this->tags)) {
|
||||
$this->storeHashtags();
|
||||
}
|
||||
|
||||
if(count($this->mentions)) {
|
||||
$this->storeMentions($this->mentions);
|
||||
}
|
||||
}
|
||||
|
||||
protected function storeHashtags()
|
||||
{
|
||||
$tags = $this->tags;
|
||||
$status = $this->status;
|
||||
$gp = $this->gp;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if(mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($status, $tag, $gp) {
|
||||
$slug = str_slug($tag, '-', false);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'group_id' => $status->group_id,
|
||||
'group_post_id' => $gp->id,
|
||||
'status_id' => $status->id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
]
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if(count($this->mentions)) {
|
||||
$this->storeMentions();
|
||||
}
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
|
||||
protected function storeMentions()
|
||||
{
|
||||
$mentions = $this->mentions;
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->first();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
}
|
109
app/Jobs/GroupPipeline/UnlikePipeline.php
Normal file
109
app/Jobs/GroupPipeline/UnlikePipeline.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class UnlikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if (!$status) {
|
||||
// Ignore notifications to deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$count = $status->likes_count > 1 ? $status->likes_count : $status->likes()->count();
|
||||
$status->likes_count = $count - 1;
|
||||
$status->save();
|
||||
|
||||
StatusService::del($status->id);
|
||||
|
||||
if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
|
||||
$this->remoteLikeDeliver();
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('group:like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->first();
|
||||
|
||||
if($exists) {
|
||||
$exists->delete();
|
||||
}
|
||||
|
||||
$like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first();
|
||||
|
||||
if(!$like) {
|
||||
return;
|
||||
}
|
||||
|
||||
$like->forceDelete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public function remoteLikeDeliver()
|
||||
{
|
||||
$like = $this->like;
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($actor, $url, $activity);
|
||||
}
|
||||
}
|
58
app/Jobs/GroupsPipeline/DeleteCommentPipeline.php
Normal file
58
app/Jobs/GroupsPipeline/DeleteCommentPipeline.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupComment;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class DeleteCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $parent;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($parent, $status)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$parent = $this->parent;
|
||||
$parent->reply_count = GroupComment::whereStatusId($parent->id)->count();
|
||||
$parent->save();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
89
app/Jobs/GroupsPipeline/ImageResizePipeline.php
Normal file
89
app/Jobs/GroupsPipeline/ImageResizePipeline.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Log;
|
||||
use Storage;
|
||||
use Image as Intervention;
|
||||
|
||||
class ImageResizePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Storage::exists($media->media_path) || $media->skip_optimize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $media->media_path;
|
||||
$file = storage_path('app/' . $path);
|
||||
$quality = config_cache('pixelfed.image_quality');
|
||||
|
||||
$orientations = [
|
||||
'square' => [
|
||||
'width' => 1080,
|
||||
'height' => 1080,
|
||||
],
|
||||
'landscape' => [
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
'portrait' => [
|
||||
'width' => 1080,
|
||||
'height' => 1350,
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$img = Intervention::make($file);
|
||||
$img->orientate();
|
||||
$width = $img->width();
|
||||
$height = $img->height();
|
||||
$aspect = $width / $height;
|
||||
$orientation = $aspect === 1 ? 'square' : ($aspect > 1 ? 'landscape' : 'portrait');
|
||||
$ratio = $orientations[$orientation];
|
||||
$img->resize($ratio['width'], $ratio['height']);
|
||||
$img->save($file, $quality);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
67
app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php
Normal file
67
app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Storage;
|
||||
use Illuminate\Http\File;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class ImageS3DeletePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
static $attempts = 1;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fs = Storage::disk(config('filesystems.cloud'));
|
||||
|
||||
if(!$fs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($fs->exists($media->media_path)) {
|
||||
$fs->delete($media->media_path);
|
||||
}
|
||||
}
|
||||
}
|
107
app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php
Normal file
107
app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Storage;
|
||||
use Illuminate\Http\File;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class ImageS3UploadPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
static $attempts = 1;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = storage_path('app/' . $media->media_path);
|
||||
|
||||
$p = explode('/', $media->media_path);
|
||||
$name = array_pop($p);
|
||||
$storagePath = implode('/', $p);
|
||||
|
||||
$url = (bool) config_cache('pixelfed.cloud_storage') && (bool) config('media.storage.remote.resilient_mode') ?
|
||||
self::handleResilientStore($storagePath, $path, $name) :
|
||||
self::handleStore($storagePath, $path, $name);
|
||||
|
||||
if($url && strlen($url) && str_starts_with($url, 'https://')) {
|
||||
$media->cdn_url = $url;
|
||||
$media->processed_at = now();
|
||||
$media->version = 11;
|
||||
$media->save();
|
||||
Storage::disk('local')->delete($media->media_path);
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleStore($storagePath, $path, $name)
|
||||
{
|
||||
return retry(3, function() use($storagePath, $path, $name) {
|
||||
$baseDisk = (bool) config_cache('pixelfed.cloud_storage') ? config('filesystems.cloud') : 'local';
|
||||
$disk = Storage::disk($baseDisk);
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
return $disk->url($file);
|
||||
}, random_int(100, 500));
|
||||
}
|
||||
|
||||
protected function handleResilientStore($storagePath, $path, $name)
|
||||
{
|
||||
$attempts = 0;
|
||||
return retry(4, function() use($storagePath, $path, $name, $attempts) {
|
||||
self::$attempts++;
|
||||
usleep(100000);
|
||||
$baseDisk = self::$attempts > 1 ? $this->getAltDriver() : config('filesystems.cloud');
|
||||
try {
|
||||
$disk = Storage::disk($baseDisk);
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
} catch (S3Exception | ClientException | ConnectException | UnableToWriteFile | Exception $e) {}
|
||||
return $disk->url($file);
|
||||
}, function (int $attempt, Exception $exception) {
|
||||
return $attempt * 200;
|
||||
});
|
||||
}
|
||||
|
||||
protected function getAltDriver()
|
||||
{
|
||||
return config('filesystems.cloud');
|
||||
}
|
||||
}
|
47
app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php
Normal file
47
app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class MemberJoinApprovedPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->approved_at = now();
|
||||
$member->join_request = false;
|
||||
$member->role = 'member';
|
||||
$member->save();
|
||||
|
||||
GroupService::del($member->group_id);
|
||||
GroupService::delSelf($member->group_id, $member->profile_id);
|
||||
}
|
||||
}
|
42
app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php
Normal file
42
app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class MemberJoinRejectedPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->rejected_at = now();
|
||||
$member->save();
|
||||
}
|
||||
}
|
115
app/Jobs/GroupsPipeline/NewCommentPipeline.php
Normal file
115
app/Jobs/GroupsPipeline/NewCommentPipeline.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupComment;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class NewCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $parent;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($parent, GroupComment $status)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->status->profile;
|
||||
$status = $this->status;
|
||||
|
||||
$parent = $this->parent;
|
||||
$parent->reply_count = GroupComment::whereStatusId($parent->id)->count();
|
||||
$parent->save();
|
||||
|
||||
if ($profile->no_autolink == false) {
|
||||
$this->parseEntities();
|
||||
}
|
||||
}
|
||||
|
||||
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->storeHashtags();
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$hashtag = GroupHashtag::firstOrCreate([
|
||||
'name' => $tag,
|
||||
]);
|
||||
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'status_id' => $status->id,
|
||||
'group_id' => $status->group_id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_visibility' => $status->visibility,
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
$this->storeMentions();
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
}
|
108
app/Jobs/GroupsPipeline/NewPostPipeline.php
Normal file
108
app/Jobs/GroupsPipeline/NewPostPipeline.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class NewPostPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupPost $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->status->profile;
|
||||
$status = $this->status;
|
||||
|
||||
if ($profile->no_autolink == false) {
|
||||
$this->parseEntities();
|
||||
}
|
||||
}
|
||||
|
||||
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->storeHashtags();
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$hashtag = GroupHashtag::firstOrCreate([
|
||||
'name' => $tag,
|
||||
]);
|
||||
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'status_id' => $status->id,
|
||||
'group_id' => $status->group_id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_visibility' => $status->visibility,
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
$this->storeMentions();
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
}
|
67
app/Models/Group.php
Normal file
67
app/Models/Group.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Profile;
|
||||
use App\Services\GroupService;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Group extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'json'
|
||||
];
|
||||
|
||||
public function url()
|
||||
{
|
||||
return url("/groups/{$this->id}");
|
||||
}
|
||||
|
||||
public function permalink($suffix = null)
|
||||
{
|
||||
if(!$this->local) {
|
||||
return $this->remote_url;
|
||||
}
|
||||
return $this->url() . $suffix;
|
||||
}
|
||||
|
||||
public function members()
|
||||
{
|
||||
return $this->hasMany(GroupMember::class);
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function isMember($id = false)
|
||||
{
|
||||
$id = $id ?? request()->user()->profile_id;
|
||||
// return $this->members()->whereProfileId($id)->whereJoinRequest(false)->exists();
|
||||
return GroupService::isMember($this->id, $id);
|
||||
}
|
||||
|
||||
public function getMembershipType()
|
||||
{
|
||||
return $this->is_private ? 'private' : ($this->is_local ? 'local' : 'all');
|
||||
}
|
||||
|
||||
public function selfRole($id = false)
|
||||
{
|
||||
$id = $id ?? request()->user()->profile_id;
|
||||
return optional($this->members()->whereProfileId($id)->first())->role ?? null;
|
||||
}
|
||||
}
|
11
app/Models/GroupActivityGraph.php
Normal file
11
app/Models/GroupActivityGraph.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupActivityGraph extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupBlock.php
Normal file
11
app/Models/GroupBlock.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupBlock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupCategory.php
Normal file
11
app/Models/GroupCategory.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
24
app/Models/GroupComment.php
Normal file
24
app/Models/GroupComment.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class GroupComment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return '/group/' . $this->group_id . '/c/' . $this->id;
|
||||
}
|
||||
}
|
11
app/Models/GroupEvent.php
Normal file
11
app/Models/GroupEvent.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupEvent extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
13
app/Models/GroupHashtag.php
Normal file
13
app/Models/GroupHashtag.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupHashtag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['name'];
|
||||
}
|
15
app/Models/GroupInteraction.php
Normal file
15
app/Models/GroupInteraction.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupInteraction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array'
|
||||
];
|
||||
}
|
11
app/Models/GroupInvitation.php
Normal file
11
app/Models/GroupInvitation.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupInvitation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
13
app/Models/GroupLike.php
Normal file
13
app/Models/GroupLike.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupLike extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['group_id', 'status_id', 'profile_id', 'comment_id'];
|
||||
}
|
21
app/Models/GroupLimit.php
Normal file
21
app/Models/GroupLimit.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupLimit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'limits' => 'json',
|
||||
'metadata' => 'json'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'profile_id',
|
||||
'group_id'
|
||||
];
|
||||
}
|
39
app/Models/GroupMedia.php
Normal file
39
app/Models/GroupMedia.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
|
||||
class GroupMedia extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'json',
|
||||
'processed_at' => 'datetime',
|
||||
'thumbnail_generated' => 'datetime'
|
||||
];
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
if($this->cdn_url) {
|
||||
return $this->cdn_url;
|
||||
}
|
||||
return Storage::url($this->media_path);
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
{
|
||||
return $this->thumbnail_url;
|
||||
}
|
||||
}
|
16
app/Models/GroupMember.php
Normal file
16
app/Models/GroupMember.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupMember extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
}
|
57
app/Models/GroupPost.php
Normal file
57
app/Models/GroupPost.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Services\HashidService;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
|
||||
class GroupPost extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, HasFactory;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $fillable = [
|
||||
'remote_url',
|
||||
'group_id',
|
||||
'profile_id',
|
||||
'type',
|
||||
'caption',
|
||||
'visibility',
|
||||
'is_nsfw'
|
||||
];
|
||||
|
||||
public function mediaPath()
|
||||
{
|
||||
return 'public/g/_v1/' . $this->group_id . '/' . $this->id;
|
||||
}
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return '/groups/' . $this->group_id . '/p/' . $this->id;
|
||||
}
|
||||
}
|
22
app/Models/GroupPostHashtag.php
Normal file
22
app/Models/GroupPostHashtag.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupPostHashtag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = [
|
||||
'group_id',
|
||||
'group_post_id',
|
||||
'status_id',
|
||||
'hashtag_id',
|
||||
'profile_id',
|
||||
'nsfw'
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
}
|
11
app/Models/GroupReport.php
Normal file
11
app/Models/GroupReport.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupReport extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupRole.php
Normal file
11
app/Models/GroupRole.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupRole extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupStore.php
Normal file
11
app/Models/GroupStore.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupStore extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
21
app/Rules/ValidUrl.php
Normal file
21
app/Rules/ValidUrl.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidUrl implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (!str_starts_with(strtolower($value), 'https://')) {
|
||||
$fail('The :attribute must start with https://.');
|
||||
}
|
||||
}
|
||||
}
|
88
app/Services/GroupFeedService.php
Normal file
88
app/Services/GroupFeedService.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class GroupFeedService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:feed:';
|
||||
|
||||
const FEED_LIMIT = 400;
|
||||
|
||||
public static function get($gid, $start = 0, $stop = 10)
|
||||
{
|
||||
if ($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_KEY.$gid, $start, $stop);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($gid, $start = null, $limit = 10)
|
||||
{
|
||||
if (! $start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRankedMinId($gid, $end = null, $limit = 10)
|
||||
{
|
||||
if (! $end) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function add($gid, $val)
|
||||
{
|
||||
if (self::count($gid) > self::FEED_LIMIT) {
|
||||
if (config('database.redis.client') === 'phpredis') {
|
||||
Redis::zpopmin(self::CACHE_KEY.$gid);
|
||||
}
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_KEY.$gid, $val, $val);
|
||||
}
|
||||
|
||||
public static function rem($gid, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY.$gid, $val);
|
||||
}
|
||||
|
||||
public static function del($gid, $val)
|
||||
{
|
||||
return self::rem($gid, $val);
|
||||
}
|
||||
|
||||
public static function count($gid)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_KEY.$gid);
|
||||
}
|
||||
|
||||
public static function warmCache($gid, $force = false, $limit = 100)
|
||||
{
|
||||
if (self::count($gid) == 0 || $force == true) {
|
||||
Redis::del(self::CACHE_KEY.$gid);
|
||||
$ids = GroupPost::whereGroupId($gid)
|
||||
->orderByDesc('id')
|
||||
->limit($limit)
|
||||
->pluck('id');
|
||||
foreach ($ids as $id) {
|
||||
self::add($gid, $id);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
49
app/Services/GroupPostService.php
Normal file
49
app/Services/GroupPostService.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
use Cache;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class GroupPostService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:post:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY.$gid.':'.$pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function () use ($gid, $pid) {
|
||||
$gp = GroupPost::whereGroupId($gid)->find($pid);
|
||||
|
||||
if (! $gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = $gp['type'];
|
||||
$res['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
}
|
366
app/Services/GroupService.php
Normal file
366
app/Services/GroupService.php
Normal file
|
@ -0,0 +1,366 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use App\Http\Resources\Groups\GroupResource;
|
||||
|
||||
class GroupService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:';
|
||||
|
||||
protected static function key($name)
|
||||
{
|
||||
return self::CACHE_KEY . $name;
|
||||
}
|
||||
|
||||
public static function get($id, $pid = false)
|
||||
{
|
||||
$res = Cache::remember(
|
||||
self::key($id),
|
||||
1209600,
|
||||
function() use($id, $pid) {
|
||||
$group = (new Group)->withoutRelations()->whereNull('status')->find($id);
|
||||
|
||||
if(!$group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$admin = $group->profile_id ? AccountService::get($group->profile_id) : null;
|
||||
|
||||
return [
|
||||
'id' => (string) $group->id,
|
||||
'name' => $group->name,
|
||||
'description' => $group->description,
|
||||
'short_description' => str_limit(strip_tags($group->description), 120),
|
||||
'category' => self::categoryById($group->category_id),
|
||||
'local' => (bool) $group->local,
|
||||
'url' => $group->url(),
|
||||
'shorturl' => url('/g/'.HashidService::encode($group->id)),
|
||||
'membership' => $group->getMembershipType(),
|
||||
'member_count' => $group->members()->whereJoinRequest(false)->count(),
|
||||
'verified' => false,
|
||||
'self' => null,
|
||||
'admin' => $admin,
|
||||
'config' => [
|
||||
'recommended' => (bool) $group->recommended,
|
||||
'discoverable' => (bool) $group->discoverable,
|
||||
'activitypub' => (bool) $group->activitypub,
|
||||
'is_nsfw' => (bool) $group->is_nsfw,
|
||||
'dms' => (bool) $group->dms
|
||||
],
|
||||
'metadata' => $group->metadata,
|
||||
'created_at' => $group->created_at->toAtomString(),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
if($pid) {
|
||||
$res['self'] = self::getSelf($id, $pid);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public static function del($id)
|
||||
{
|
||||
Cache::forget('ap:groups:object:' . $id);
|
||||
return Cache::forget(self::key($id));
|
||||
}
|
||||
|
||||
public static function getSelf($gid, $pid)
|
||||
{
|
||||
return Cache::remember(
|
||||
self::key('self:gid-' . $gid . ':pid-' . $pid),
|
||||
3600,
|
||||
function() use($gid, $pid) {
|
||||
$group = Group::find($gid);
|
||||
|
||||
if(!$gid || !$pid) {
|
||||
return [
|
||||
'is_member' => false,
|
||||
'role' => null,
|
||||
'is_requested' => null
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'is_member' => $group->isMember($pid),
|
||||
'role' => $group->selfRole($pid),
|
||||
'is_requested' => optional($group->members()->whereProfileId($pid)->first())->join_request ?? false
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function delSelf($gid, $pid)
|
||||
{
|
||||
Cache::forget(self::key("is_member:{$gid}:{$pid}"));
|
||||
return Cache::forget(self::key('self:gid-' . $gid . ':pid-' . $pid));
|
||||
}
|
||||
|
||||
public static function sidToGid($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key('s2gid:' . $gid . ':' . $pid), 3600, function() use($gid, $pid) {
|
||||
return optional(GroupPost::whereGroupId($gid)->whereStatusId($pid)->first())->id;
|
||||
});
|
||||
}
|
||||
|
||||
public static function membershipsByPid($pid)
|
||||
{
|
||||
return Cache::remember(self::key("mbpid:{$pid}"), 3600, function() use($pid) {
|
||||
return GroupMember::whereProfileId($pid)->pluck('group_id');
|
||||
});
|
||||
}
|
||||
|
||||
public static function config()
|
||||
{
|
||||
return [
|
||||
'enabled' => config('exp.gps') ?? false,
|
||||
'limits' => [
|
||||
'group' => [
|
||||
'max' => 999,
|
||||
'federation' => false,
|
||||
],
|
||||
|
||||
'user' => [
|
||||
'create' => [
|
||||
'new' => true,
|
||||
'max' => 10
|
||||
],
|
||||
'join' => [
|
||||
'max' => 10
|
||||
],
|
||||
'invite' => [
|
||||
'max' => 20
|
||||
]
|
||||
]
|
||||
],
|
||||
'guest' => [
|
||||
'public' => false
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function fetchRemote($url)
|
||||
{
|
||||
// todo: refactor this demo
|
||||
$res = Helpers::fetchFromUrl($url);
|
||||
|
||||
if(!$res || !isset($res['type']) || $res['type'] != 'Group') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group = Group::whereRemoteUrl($url)->first();
|
||||
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$group = new Group;
|
||||
$group->remote_url = $res['url'];
|
||||
$group->name = $res['name'];
|
||||
$group->inbox_url = $res['inbox'];
|
||||
$group->metadata = [
|
||||
'header' => [
|
||||
'url' => $res['icon']['image']['url']
|
||||
]
|
||||
];
|
||||
$group->description = Purify::clean($res['summary']);
|
||||
$group->local = false;
|
||||
$group->save();
|
||||
|
||||
return $group->url();
|
||||
}
|
||||
|
||||
public static function log(
|
||||
string $groupId,
|
||||
string $profileId,
|
||||
string $type = null,
|
||||
array $meta = null,
|
||||
string $itemType = null,
|
||||
string $itemId = null
|
||||
)
|
||||
{
|
||||
// todo: truncate (some) metadata after XX days in cron/queue
|
||||
$log = new GroupInteraction;
|
||||
$log->group_id = $groupId;
|
||||
$log->profile_id = $profileId;
|
||||
$log->type = $type;
|
||||
$log->item_type = $itemType;
|
||||
$log->item_id = $itemId;
|
||||
$log->metadata = $meta;
|
||||
$log->save();
|
||||
}
|
||||
|
||||
public static function getRejoinTimeout($gid, $pid)
|
||||
{
|
||||
$key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid);
|
||||
return Cache::has($key);
|
||||
}
|
||||
|
||||
public static function setRejoinTimeout($gid, $pid)
|
||||
{
|
||||
// todo: allow group admins to manually remove timeout
|
||||
$key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid);
|
||||
return Cache::put($key, 1, 86400);
|
||||
}
|
||||
|
||||
public static function getMemberInboxes($id)
|
||||
{
|
||||
// todo: cache this, maybe add join/leave methods to this service to handle cache invalidation
|
||||
$group = (new Group)->withoutRelations()->findOrFail($id);
|
||||
if(!$group->local) {
|
||||
return [];
|
||||
}
|
||||
$members = GroupMember::whereGroupId($id)->whereLocalProfile(false)->pluck('profile_id');
|
||||
return Profile::find($members)->map(function($u) {
|
||||
return $u->sharedInbox ?? $u->inbox_url;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public static function getInteractionLimits($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key(":il:{$gid}:{$pid}"), 3600, function() use($gid, $pid) {
|
||||
$limit = GroupLimit::whereGroupId($gid)->whereProfileId($pid)->first();
|
||||
if(!$limit) {
|
||||
return [
|
||||
'limits' => [
|
||||
'can_post' => true,
|
||||
'can_comment' => true,
|
||||
'can_like' => true
|
||||
],
|
||||
'updated_at' => null
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'limits' => $limit->limits,
|
||||
'updated_at' => $limit->updated_at->format('c')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public static function clearInteractionLimits($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key(":il:{$gid}:{$pid}"));
|
||||
}
|
||||
|
||||
public static function canPost($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_post'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function canComment($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_comment'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function canLike($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_like'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function categories($onlyActive = true)
|
||||
{
|
||||
return Cache::remember(self::key(':categories'), 2678400, function() use($onlyActive) {
|
||||
return GroupCategory::when($onlyActive, function($q, $onlyActive) {
|
||||
return $q->whereActive(true);
|
||||
})
|
||||
->orderBy('order')
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function categoryById($id)
|
||||
{
|
||||
return Cache::remember(self::key(':categorybyid:'.$id), 2678400, function() use($id) {
|
||||
$category = GroupCategory::find($id);
|
||||
if($category) {
|
||||
return [
|
||||
'name' => $category->name,
|
||||
'url' => url("/groups/explore/category/{$category->slug}")
|
||||
];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public static function isMember($gid = false, $pid = false)
|
||||
{
|
||||
if(!$gid || !$pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = self::key("is_member:{$gid}:{$pid}");
|
||||
return Cache::remember($key, 3600, function() use($gid, $pid) {
|
||||
return GroupMember::whereGroupId($gid)
|
||||
->whereProfileId($pid)
|
||||
->whereJoinRequest(false)
|
||||
->exists();
|
||||
});
|
||||
}
|
||||
|
||||
public static function mutualGroups($cid = false, $pid = false, $exclude = [])
|
||||
{
|
||||
if(!$cid || !$pid) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'groups' => []
|
||||
];
|
||||
}
|
||||
|
||||
$self = self::membershipsByPid($cid);
|
||||
$user = self::membershipsByPid($pid);
|
||||
|
||||
if(!$self->count() || !$user->count()) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'groups' => []
|
||||
];
|
||||
}
|
||||
|
||||
$intersect = $self->intersect($user);
|
||||
$count = $intersect->count();
|
||||
$groups = $intersect
|
||||
->values()
|
||||
->filter(function($id) use($exclude) {
|
||||
return !in_array($id, $exclude);
|
||||
})
|
||||
->shuffle()
|
||||
->take(1)
|
||||
->map(function($id) {
|
||||
return self::get($id);
|
||||
});
|
||||
|
||||
return [
|
||||
'count' => $count,
|
||||
'groups' => $groups
|
||||
];
|
||||
}
|
||||
}
|
51
app/Services/Groups/GroupAccountService.php
Normal file
51
app/Services/Groups/GroupAccountService.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupMember;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class GroupAccountService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:accounts-v0:';
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
$group = GroupService::get($gid);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account = AccountService::get($pid, true);
|
||||
if(!$account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = self::CACHE_KEY . $gid . ':' . $pid;
|
||||
$account['group'] = Cache::remember($key, 3600, function() use($gid, $pid) {
|
||||
$membership = GroupMember::whereGroupId($gid)->whereProfileId($pid)->first();
|
||||
if(!$membership) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'joined' => $membership->created_at->format('c'),
|
||||
'role' => $membership->role,
|
||||
'local_group' => (bool) $membership->local_group,
|
||||
'local_profile' => (bool) $membership->local_profile,
|
||||
];
|
||||
});
|
||||
return $account;
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
$key = self::CACHE_KEY . $gid . ':' . $pid;
|
||||
return Cache::forget($key);
|
||||
}
|
||||
}
|
311
app/Services/Groups/GroupActivityPubService.php
Normal file
311
app/Services/Groups/GroupActivityPubService.php
Normal file
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupComment;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
use App\Services\ActivityPubFetchService;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Rules\ValidUrl;
|
||||
|
||||
class GroupActivityPubService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:ap:';
|
||||
|
||||
public static function fetchGroup($url, $saveOnFetch = true)
|
||||
{
|
||||
$group = Group::where('remote_url', $url)->first();
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$res = ActivityPubFetchService::get($url);
|
||||
if(!$res) {
|
||||
return $res;
|
||||
}
|
||||
$json = json_decode($res, true);
|
||||
$group = self::validateGroup($json);
|
||||
if(!$group) {
|
||||
return false;
|
||||
}
|
||||
if($saveOnFetch) {
|
||||
return self::storeGroup($group);
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function fetchGroupPost($url, $saveOnFetch = true)
|
||||
{
|
||||
$group = GroupPost::where('remote_url', $url)->first();
|
||||
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$res = ActivityPubFetchService::get($url);
|
||||
if(!$res) {
|
||||
return 'invalid res';
|
||||
}
|
||||
$json = json_decode($res, true);
|
||||
if(!$json) {
|
||||
return 'invalid json';
|
||||
}
|
||||
if(isset($json['inReplyTo'])) {
|
||||
$comment = self::validateGroupComment($json);
|
||||
return self::storeGroupComment($comment);
|
||||
}
|
||||
|
||||
$group = self::validateGroupPost($json);
|
||||
if($saveOnFetch) {
|
||||
return self::storeGroupPost($group);
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function validateGroup($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Group',
|
||||
'preferredUsername' => 'required',
|
||||
'name' => 'required',
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'inbox' => ['required', 'url', new ValidUrl],
|
||||
'outbox' => ['required', 'url', new ValidUrl],
|
||||
'followers' => ['required', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'summary' => 'sometimes',
|
||||
'publicKey' => 'required',
|
||||
'publicKey.id' => 'required',
|
||||
'publicKey.owner' => ['required', 'url', 'same:id', new ValidUrl],
|
||||
'publicKey.publicKeyPem' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function validateGroupPost($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Page,Note',
|
||||
'to' => 'required|array',
|
||||
'to.*' => ['required', 'url', new ValidUrl],
|
||||
'cc' => 'sometimes|array',
|
||||
'cc.*' => ['sometimes', 'url', new ValidUrl],
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'name' => 'sometimes',
|
||||
'target' => 'sometimes',
|
||||
'audience' => 'sometimes',
|
||||
'inReplyTo' => 'sometimes',
|
||||
'content' => 'sometimes',
|
||||
'mediaType' => 'sometimes',
|
||||
'sensitive' => 'sometimes',
|
||||
'attachment' => 'sometimes',
|
||||
'published' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function validateGroupComment($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Note',
|
||||
'to' => 'required|array',
|
||||
'to.*' => ['required', 'url', new ValidUrl],
|
||||
'cc' => 'sometimes|array',
|
||||
'cc.*' => ['sometimes', 'url', new ValidUrl],
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'name' => 'sometimes',
|
||||
'target' => 'sometimes',
|
||||
'audience' => 'sometimes',
|
||||
'inReplyTo' => 'sometimes',
|
||||
'content' => 'sometimes',
|
||||
'mediaType' => 'sometimes',
|
||||
'sensitive' => 'sometimes',
|
||||
'published' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return $validator->errors();
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function getGroupFromPostActivity($groupPost)
|
||||
{
|
||||
if(isset($groupPost['audience']) && is_string($groupPost['audience'])) {
|
||||
return $groupPost['audience'];
|
||||
}
|
||||
|
||||
if(
|
||||
isset(
|
||||
$groupPost['target'],
|
||||
$groupPost['target']['type'],
|
||||
$groupPost['target']['attributedTo']
|
||||
) && $groupPost['target']['type'] == 'Collection'
|
||||
) {
|
||||
return $groupPost['target']['attributedTo'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getActorFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['attributedTo'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$field = $groupPost['attributedTo'];
|
||||
|
||||
if(is_string($field)) {
|
||||
return $field;
|
||||
}
|
||||
|
||||
if(is_array($field) && count($field) === 1) {
|
||||
if(
|
||||
isset(
|
||||
$field[0]['id'],
|
||||
$field[0]['type']
|
||||
) &&
|
||||
$field[0]['type'] === 'Person' &&
|
||||
is_string($field[0]['id'])
|
||||
) {
|
||||
return $field[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getCaptionFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['name']) && isset($groupPost['content'])) {
|
||||
return Purify::clean(strip_tags($groupPost['content']));
|
||||
}
|
||||
|
||||
if(isset($groupPost['name'], $groupPost['content'])) {
|
||||
return Purify::clean(strip_tags($groupPost['name'])) . Purify::clean(strip_tags($groupPost['content']));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSensitiveFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['sensitive'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(isset($groupPost['sensitive']) && !is_bool($groupPost['sensitive'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return boolval($groupPost['sensitive']);
|
||||
}
|
||||
|
||||
public static function storeGroup($activity)
|
||||
{
|
||||
$group = new Group;
|
||||
$group->profile_id = null;
|
||||
$group->category_id = 1;
|
||||
$group->name = $activity['name'] ?? 'Untitled Group';
|
||||
$group->description = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
|
||||
$group->is_private = false;
|
||||
$group->local_only = false;
|
||||
$group->metadata = [];
|
||||
$group->local = false;
|
||||
$group->remote_url = $activity['id'];
|
||||
$group->inbox_url = $activity['inbox'];
|
||||
$group->activitypub = true;
|
||||
$group->save();
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function storeGroupPost($groupPost)
|
||||
{
|
||||
$groupUrl = self::getGroupFromPostActivity($groupPost);
|
||||
if(!$groupUrl) {
|
||||
return;
|
||||
}
|
||||
$group = self::fetchGroup($groupUrl, true);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
$actorUrl = self::getActorFromPostActivity($groupPost);
|
||||
$actor = Helpers::profileFetch($actorUrl);
|
||||
$caption = self::getCaptionFromPostActivity($groupPost);
|
||||
$sensitive = self::getSensitiveFromPostActivity($groupPost);
|
||||
$model = GroupPost::firstOrCreate(
|
||||
[
|
||||
'remote_url' => $groupPost['id'],
|
||||
], [
|
||||
'group_id' => $group->id,
|
||||
'profile_id' => $actor->id,
|
||||
'type' => 'text',
|
||||
'caption' => $caption,
|
||||
'visibility' => 'public',
|
||||
'is_nsfw' => $sensitive,
|
||||
]
|
||||
);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public static function storeGroupComment($groupPost)
|
||||
{
|
||||
$groupUrl = self::getGroupFromPostActivity($groupPost);
|
||||
if(!$groupUrl) {
|
||||
return;
|
||||
}
|
||||
$group = self::fetchGroup($groupUrl, true);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
$actorUrl = self::getActorFromPostActivity($groupPost);
|
||||
$actor = Helpers::profileFetch($actorUrl);
|
||||
$caption = self::getCaptionFromPostActivity($groupPost);
|
||||
$sensitive = self::getSensitiveFromPostActivity($groupPost);
|
||||
$parentPost = self::fetchGroupPost($groupPost['inReplyTo']);
|
||||
$model = GroupComment::firstOrCreate(
|
||||
[
|
||||
'remote_url' => $groupPost['id'],
|
||||
], [
|
||||
'group_id' => $group->id,
|
||||
'profile_id' => $actor->id,
|
||||
'status_id' => $parentPost->id,
|
||||
'type' => 'text',
|
||||
'caption' => $caption,
|
||||
'visibility' => 'public',
|
||||
'is_nsfw' => $sensitive,
|
||||
'local' => $actor->private_key != null
|
||||
]
|
||||
);
|
||||
return $model;
|
||||
}
|
||||
}
|
50
app/Services/Groups/GroupCommentService.php
Normal file
50
app/Services/Groups/GroupCommentService.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\GroupComment;
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
|
||||
class GroupCommentService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:comment:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY . $gid . ':' . $pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function() use($gid, $pid) {
|
||||
$gp = GroupComment::whereGroupId($gid)->find($pid);
|
||||
|
||||
if(!$gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = 'group:post:comment';
|
||||
$res['url'] = $gp->url();
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
}
|
95
app/Services/Groups/GroupFeedService.php
Normal file
95
app/Services/Groups/GroupFeedService.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class GroupFeedService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:feed:';
|
||||
const FEED_LIMIT = 400;
|
||||
|
||||
public static function get($gid, $start = 0, $stop = 10)
|
||||
{
|
||||
if($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_KEY . $gid, $start, $stop);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($gid, $start = null, $limit = 10)
|
||||
{
|
||||
if(!$start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRankedMinId($gid, $end = null, $limit = 10)
|
||||
{
|
||||
if(!$end) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function add($gid, $val)
|
||||
{
|
||||
if(self::count($gid) > self::FEED_LIMIT) {
|
||||
if(config('database.redis.client') === 'phpredis') {
|
||||
Redis::zpopmin(self::CACHE_KEY . $gid);
|
||||
}
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_KEY . $gid, $val, $val);
|
||||
}
|
||||
|
||||
public static function rem($gid, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY . $gid, $val);
|
||||
}
|
||||
|
||||
public static function del($gid, $val)
|
||||
{
|
||||
return self::rem($gid, $val);
|
||||
}
|
||||
|
||||
public static function count($gid)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_KEY . $gid);
|
||||
}
|
||||
|
||||
public static function warmCache($gid, $force = false, $limit = 100)
|
||||
{
|
||||
if(self::count($gid) == 0 || $force == true) {
|
||||
Redis::del(self::CACHE_KEY . $gid);
|
||||
$ids = GroupPost::whereGroupId($gid)
|
||||
->orderByDesc('id')
|
||||
->limit($limit)
|
||||
->pluck('id');
|
||||
foreach($ids as $id) {
|
||||
self::add($gid, $id);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
28
app/Services/Groups/GroupHashtagService.php
Normal file
28
app/Services/Groups/GroupHashtagService.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
|
||||
class GroupHashtagService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups-v1:hashtags:';
|
||||
|
||||
public static function get($id)
|
||||
{
|
||||
return Cache::remember(self::CACHE_KEY . $id, 3600, function() use($id) {
|
||||
$tag = GroupHashtag::find($id);
|
||||
if(!$tag) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'slug' => Str::slug($tag->name),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
114
app/Services/Groups/GroupMediaService.php
Normal file
114
app/Services/Groups/GroupMediaService.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\HashidService;
|
||||
|
||||
class GroupMediaService
|
||||
{
|
||||
const CACHE_KEY = 'groups:media:';
|
||||
|
||||
public static function path($gid, $pid, $sid = false)
|
||||
{
|
||||
if(!$gid || !$pid) {
|
||||
return;
|
||||
}
|
||||
$groupHashid = HashidService::encode($gid);
|
||||
$monthHash = HashidService::encode(date('Y').date('n'));
|
||||
$pid = HashidService::encode($pid);
|
||||
$sid = $sid ? HashidService::encode($sid) : false;
|
||||
$path = $sid ?
|
||||
"public/g1/{$groupHashid}/{$pid}/{$monthHash}/{$sid}" :
|
||||
"public/g1/{$groupHashid}/{$pid}/{$monthHash}";
|
||||
return $path;
|
||||
}
|
||||
|
||||
public static function get($statusId)
|
||||
{
|
||||
return Cache::remember(self::CACHE_KEY.$statusId, 21600, function() use($statusId) {
|
||||
$media = GroupMedia::whereStatusId($statusId)->orderBy('order')->get();
|
||||
if(!$media) {
|
||||
return [];
|
||||
}
|
||||
$medias = $media->map(function($media) {
|
||||
return [
|
||||
'id' => (string) $media->id,
|
||||
'type' => 'Document',
|
||||
'url' => $media->url(),
|
||||
'preview_url' => $media->url(),
|
||||
'remote_url' => $media->url,
|
||||
'description' => $media->cw_summary,
|
||||
'blurhash' => $media->blurhash ?? 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay'
|
||||
];
|
||||
});
|
||||
return $medias->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getMastodon($id)
|
||||
{
|
||||
$media = self::get($id);
|
||||
if(!$media) {
|
||||
return [];
|
||||
}
|
||||
$medias = collect($media)
|
||||
->map(function($media) {
|
||||
$mime = $media['mime'] ? explode('/', $media['mime']) : false;
|
||||
unset(
|
||||
$media['optimized_url'],
|
||||
$media['license'],
|
||||
$media['is_nsfw'],
|
||||
$media['orientation'],
|
||||
$media['filter_name'],
|
||||
$media['filter_class'],
|
||||
$media['mime'],
|
||||
$media['hls_manifest']
|
||||
);
|
||||
|
||||
$media['type'] = $mime ? strtolower($mime[0]) : 'unknown';
|
||||
return $media;
|
||||
})
|
||||
->filter(function($m) {
|
||||
return $m && isset($m['url']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $medias->toArray();
|
||||
}
|
||||
|
||||
public static function del($statusId)
|
||||
{
|
||||
return Cache::forget(self::CACHE_KEY . $statusId);
|
||||
}
|
||||
|
||||
public static function activitypub($statusId)
|
||||
{
|
||||
$status = self::get($statusId);
|
||||
if(!$status) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($status)->map(function($s) {
|
||||
$license = isset($s['license']) && $s['license']['title'] ? $s['license']['title'] : null;
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $s['mime'],
|
||||
'url' => $s['url'],
|
||||
'name' => $s['description'],
|
||||
'summary' => $s['description'],
|
||||
'blurhash' => $s['blurhash'],
|
||||
'license' => $license
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
83
app/Services/Groups/GroupPostService.php
Normal file
83
app/Services/Groups/GroupPostService.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
|
||||
class GroupPostService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:post:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY . $gid . ':' . $pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function() use($gid, $pid) {
|
||||
$gp = GroupPost::whereGroupId($gid)->find($pid);
|
||||
|
||||
if(!$gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = $gp['type'];
|
||||
$res['url'] = $gp->url();
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
|
||||
public function getStatus(Request $request)
|
||||
{
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
if($group->is_private) {
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
}
|
||||
|
||||
$gp = GroupPost::whereGroupId($group->id)->whereId($sid)->firstOrFail();
|
||||
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['reply_count'] = $gp['reply_count'];
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
$status['account']['url'] = url("/groups/{$gp->group_id}/user/{$gp->profile_id}");
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
85
app/Services/Groups/GroupsLikeService.php
Normal file
85
app/Services/Groups/GroupsLikeService.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Models\GroupLike;
|
||||
|
||||
class GroupsLikeService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:group-likes:ids:';
|
||||
const CACHE_SET_KEY = 'pf:services:group-likes:set:';
|
||||
const CACHE_POST_KEY = 'pf:services:group-likes:count:';
|
||||
|
||||
public static function add($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
Cache::increment(self::CACHE_POST_KEY . $statusId);
|
||||
//Cache::forget('pf:services:likes:liked_by:'.$statusId);
|
||||
self::setAdd($profileId, $statusId);
|
||||
return Cache::put($key, true, 86400);
|
||||
}
|
||||
|
||||
public static function setAdd($profileId, $statusId)
|
||||
{
|
||||
if(self::setCount($profileId) > 400) {
|
||||
Redis::zpopmin(self::CACHE_SET_KEY . $profileId);
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_SET_KEY . $profileId, $statusId, $statusId);
|
||||
}
|
||||
|
||||
public static function setCount($id)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_SET_KEY . $id);
|
||||
}
|
||||
|
||||
public static function setRem($profileId, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_SET_KEY . $profileId, $val);
|
||||
}
|
||||
|
||||
public static function get($profileId, $start = 0, $stop = 10)
|
||||
{
|
||||
if($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_SET_KEY . $profileId, $start, $stop);
|
||||
}
|
||||
|
||||
public static function remove($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
Cache::decrement(self::CACHE_POST_KEY . $statusId);
|
||||
//Cache::forget('pf:services:likes:liked_by:'.$statusId);
|
||||
self::setRem($profileId, $statusId);
|
||||
return Cache::put($key, false, 86400);
|
||||
}
|
||||
|
||||
public static function liked($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
return Cache::remember($key, 900, function() use($profileId, $statusId) {
|
||||
return GroupLike::whereProfileId($profileId)->whereStatusId($statusId)->exists();
|
||||
});
|
||||
}
|
||||
|
||||
public static function likedBy($status)
|
||||
{
|
||||
$empty = [
|
||||
'username' => null,
|
||||
'others' => false
|
||||
];
|
||||
|
||||
return $empty;
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
return Cache::get(self::CACHE_POST_KEY . $id, 0);
|
||||
}
|
||||
|
||||
}
|
59
app/Transformer/Api/GroupPostTransformer.php
Normal file
59
app/Transformer/Api/GroupPostTransformer.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusLabelService;
|
||||
use App\Services\StatusMentionService;
|
||||
use App\Services\PollService;
|
||||
use App\Models\CustomEmoji;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Purify;
|
||||
|
||||
class GroupPostTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform($status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'gid' => $status->group_id ? (string) $status->group_id : null,
|
||||
'url' => '/groups/' . $status->group_id . '/p/' . $status->id,
|
||||
'content' => $status->caption,
|
||||
'content_text' => $status->caption,
|
||||
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'reblogs_count' => $status->reblogs_count ?? 0,
|
||||
'favourites_count' => $status->likes_count ?? 0,
|
||||
'reblogged' => null,
|
||||
'favourited' => null,
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'language' => null,
|
||||
'pf_type' => $status->type,
|
||||
'reply_count' => (int) $status->reply_count ?? 0,
|
||||
'comments_disabled' => (bool) $status->comments_disabled,
|
||||
'thread' => false,
|
||||
'media_attachments' => GroupMediaService::get($status->id),
|
||||
'replies' => [],
|
||||
'parent' => [],
|
||||
'place' => null,
|
||||
'local' => (bool) !$status->remote_url,
|
||||
'account' => AccountService::get($status->profile_id, true),
|
||||
'poll' => [],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -858,6 +858,11 @@ class Helpers
|
|||
return self::profileFirstOrNew($url);
|
||||
}
|
||||
|
||||
public static function getSignedFetch($url)
|
||||
{
|
||||
return ActivityPubFetchService::get($url);
|
||||
}
|
||||
|
||||
public static function sendSignedObject($profile, $url, $body)
|
||||
{
|
||||
if (app()->environment() !== 'production') {
|
||||
|
|
13
config/groups.php
Normal file
13
config/groups.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => env('GROUPS_ENABLED', false),
|
||||
'federation' => env('GROUPS_FEDERATION', true),
|
||||
|
||||
'acl' => [
|
||||
'create_group' => [
|
||||
'admins' => env('GROUPS_ACL_CREATE_ADMINS', true),
|
||||
'users' => env('GROUPS_ACL_CREATE_USERS', true),
|
||||
]
|
||||
]
|
||||
];
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupRolesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->string('name');
|
||||
$table->string('slug')->nullable();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->unique(['group_id', 'slug']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_roles');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupInteractionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_interactions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('item_type')->nullable()->index();
|
||||
$table->string('item_id')->nullable()->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_interactions');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupReportsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('item_type')->nullable()->index();
|
||||
$table->string('item_id')->nullable()->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->boolean('open')->default(true)->index();
|
||||
$table->unique(['group_id', 'profile_id', 'item_type', 'item_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_reports');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupBlocksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_blocks', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('admin_id')->unsigned()->nullable();
|
||||
$table->bigInteger('profile_id')->nullable()->unsigned()->index();
|
||||
$table->bigInteger('instance_id')->nullable()->unsigned()->index();
|
||||
$table->string('name')->nullable()->index();
|
||||
$table->string('reason')->nullable();
|
||||
$table->boolean('is_user')->index();
|
||||
$table->boolean('moderated')->default(false)->index();
|
||||
$table->unique(['group_id', 'profile_id', 'instance_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_blocks');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupLimitsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_limits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->json('limits')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->unique(['group_id', 'profile_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_limits');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\GroupCategory;
|
||||
|
||||
class CreateGroupCategoriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('group_categories');
|
||||
|
||||
Schema::create('group_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique()->index();
|
||||
$table->string('slug')->unique()->index();
|
||||
$table->boolean('active')->default(true)->index();
|
||||
$table->tinyInteger('order')->unsigned()->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$default = [
|
||||
'General',
|
||||
'Photography',
|
||||
'Fediverse',
|
||||
'CompSci & Programming',
|
||||
'Causes & Movements',
|
||||
'Humor',
|
||||
'Science & Tech',
|
||||
'Travel',
|
||||
'Buy & Sell',
|
||||
'Business',
|
||||
'Style',
|
||||
'Animals',
|
||||
'Sports & Fitness',
|
||||
'Education',
|
||||
'Arts',
|
||||
'Entertainment',
|
||||
'Faith & Spirituality',
|
||||
'Relationships & Identity',
|
||||
'Parenting',
|
||||
'Hobbies & Interests',
|
||||
'Food & Drink',
|
||||
'Vehicles & Commutes',
|
||||
'Civics & Community',
|
||||
];
|
||||
|
||||
for ($i=1; $i <= 23; $i++) {
|
||||
$cat = new GroupCategory;
|
||||
$cat->name = $default[$i - 1];
|
||||
$cat->slug = str_slug($cat->name);
|
||||
$cat->active = true;
|
||||
$cat->order = $i;
|
||||
$cat->save();
|
||||
}
|
||||
|
||||
Schema::table('groups', function (Blueprint $table) {
|
||||
$table->unsignedInteger('category_id')->default(1)->index()->after('id');
|
||||
$table->unsignedInteger('member_count')->nullable();
|
||||
$table->boolean('recommended')->default(false)->index();
|
||||
$table->boolean('discoverable')->default(false)->index();
|
||||
$table->boolean('activitypub')->default(false);
|
||||
$table->boolean('is_nsfw')->default(false);
|
||||
$table->boolean('dms')->default(false);
|
||||
$table->boolean('autospam')->default(false);
|
||||
$table->boolean('verified')->default(false);
|
||||
$table->timestamp('last_active_at')->nullable();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_categories');
|
||||
|
||||
Schema::table('groups', function (Blueprint $table) {
|
||||
$table->dropColumn('category_id');
|
||||
$table->dropColumn('member_count');
|
||||
$table->dropColumn('recommended');
|
||||
$table->dropColumn('activitypub');
|
||||
$table->dropColumn('is_nsfw');
|
||||
$table->dropColumn('discoverable');
|
||||
$table->dropColumn('dms');
|
||||
$table->dropColumn('autospam');
|
||||
$table->dropColumn('verified');
|
||||
$table->dropColumn('last_active_at');
|
||||
$table->dropColumn('deleted_at');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupHashtagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_hashtags', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('name')->unique()->index();
|
||||
$table->string('formatted')->nullable();
|
||||
$table->boolean('recommended')->default(false);
|
||||
$table->boolean('sensitive')->default(false);
|
||||
$table->boolean('banned')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_hashtags');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupPostHashtagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_post_hashtags', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('hashtag_id')->unsigned()->index();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('status_id')->unsigned()->nullable();
|
||||
$table->string('status_visibility')->nullable();
|
||||
$table->boolean('nsfw')->default(false);
|
||||
$table->unique(['hashtag_id', 'group_id', 'profile_id', 'status_id'], 'group_post_hashtags_gda_unique');
|
||||
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
|
||||
$table->foreign('profile_id')->references('id')->on('profiles')->onDelete('cascade');
|
||||
$table->foreign('hashtag_id')->references('id')->on('group_hashtags')->onDelete('cascade');
|
||||
$table->foreign('status_id')->references('id')->on('group_posts')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_post_hashtags');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupStoresTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_stores', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->nullable()->index();
|
||||
$table->string('store_key')->index();
|
||||
$table->json('store_value')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->unique(['group_id', 'store_key']);
|
||||
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_stores');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupEventsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_events', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->nullable()->index();
|
||||
$table->string('name')->nullable();
|
||||
$table->string('type')->index();
|
||||
$table->json('tags')->nullable();
|
||||
$table->json('location')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->boolean('open')->default(false)->index();
|
||||
$table->boolean('comments_open')->default(false);
|
||||
$table->boolean('show_guest_list')->default(false);
|
||||
$table->timestamp('start_at')->nullable();
|
||||
$table->timestamp('end_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_events');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupActivityGraphsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_activity_graphs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('instance_id')->nullable()->index();
|
||||
$table->bigInteger('actor_id')->nullable()->index();
|
||||
$table->string('verb')->nullable()->index();
|
||||
$table->string('id_url')->nullable()->unique()->index();
|
||||
$table->json('payload')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_activity_graphs');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('group_posts', function (Blueprint $table) {
|
||||
$table->dropColumn('status_id');
|
||||
$table->dropColumn('reply_child_id');
|
||||
$table->dropColumn('in_reply_to_id');
|
||||
$table->dropColumn('reblog_of_id');
|
||||
$table->text('caption')->nullable();
|
||||
$table->string('visibility')->nullable();
|
||||
$table->boolean('is_nsfw')->default(false);
|
||||
$table->unsignedInteger('likes_count')->default(0);
|
||||
$table->text('cw_summary')->nullable();
|
||||
$table->json('media_ids')->nullable();
|
||||
$table->boolean('comments_disabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('group_posts', function (Blueprint $table) {
|
||||
$table->bigInteger('status_id')->unsigned()->unique()->nullable();
|
||||
$table->bigInteger('reply_child_id')->unsigned()->nullable();
|
||||
$table->bigInteger('in_reply_to_id')->unsigned()->nullable();
|
||||
$table->bigInteger('reblog_of_id')->unsigned()->nullable();
|
||||
$table->dropColumn('caption');
|
||||
$table->dropColumn('is_nsfw');
|
||||
$table->dropColumn('visibility');
|
||||
$table->dropColumn('likes_count');
|
||||
$table->dropColumn('cw_summary');
|
||||
$table->dropColumn('media_ids');
|
||||
$table->dropColumn('comments_disabled');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('group_comments', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('group_id')->index();
|
||||
$table->unsignedBigInteger('profile_id')->nullable();
|
||||
$table->unsignedBigInteger('status_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('in_reply_to_id')->nullable()->index();
|
||||
$table->string('remote_url')->nullable()->unique()->index();
|
||||
$table->text('caption')->nullable();
|
||||
$table->boolean('is_nsfw')->default(false);
|
||||
$table->string('visibility')->nullable();
|
||||
$table->unsignedInteger('likes_count')->default(0);
|
||||
$table->unsignedInteger('replies_count')->default(0);
|
||||
$table->text('cw_summary')->nullable();
|
||||
$table->json('media_ids')->nullable();
|
||||
$table->string('status')->nullable();
|
||||
$table->string('type')->default('text')->nullable();
|
||||
$table->boolean('local')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_comments');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('group_likes', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('group_id');
|
||||
$table->unsignedBigInteger('profile_id')->index();
|
||||
$table->unsignedBigInteger('status_id')->nullable();
|
||||
$table->unsignedBigInteger('comment_id')->nullable();
|
||||
$table->boolean('local')->default(true);
|
||||
$table->unique(['group_id', 'profile_id', 'status_id', 'comment_id'], 'group_likes_gpsc_unique');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_likes');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('group_media', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('group_id');
|
||||
$table->unsignedBigInteger('profile_id');
|
||||
$table->unsignedBigInteger('status_id')->nullable()->index();
|
||||
$table->string('media_path')->unique();
|
||||
$table->text('thumbnail_url')->nullable();
|
||||
$table->text('cdn_url')->nullable();
|
||||
$table->text('url')->nullable();
|
||||
$table->string('mime')->nullable();
|
||||
$table->unsignedInteger('size')->nullable();
|
||||
$table->text('cw_summary')->nullable();
|
||||
$table->string('license')->nullable();
|
||||
$table->string('blurhash')->nullable();
|
||||
$table->tinyInteger('order')->unsigned()->default(1);
|
||||
$table->unsignedInteger('width')->nullable();
|
||||
$table->unsignedInteger('height')->nullable();
|
||||
$table->boolean('local_user')->default(true);
|
||||
$table->boolean('is_cached')->default(false);
|
||||
$table->boolean('is_comment')->default(false)->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->string('version')->default(1);
|
||||
$table->boolean('skip_optimize')->default(false);
|
||||
$table->timestamp('processed_at')->nullable();
|
||||
$table->timestamp('thumbnail_generated')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_media');
|
||||
}
|
||||
};
|
Binary file not shown.
BIN
public/js/compose.chunk.28539d85e4c112c4.js
vendored
Normal file
BIN
public/js/compose.chunk.28539d85e4c112c4.js
vendored
Normal file
Binary file not shown.
BIN
public/js/compose.chunk.47ba00abaa827b26.js
vendored
BIN
public/js/compose.chunk.47ba00abaa827b26.js
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/group-status.js
vendored
Normal file
BIN
public/js/group-status.js
vendored
Normal file
Binary file not shown.
BIN
public/js/group-topic-feed.js
vendored
Normal file
BIN
public/js/group-topic-feed.js
vendored
Normal file
Binary file not shown.
BIN
public/js/group.create.9836b689acf0fc1b.js
vendored
Normal file
BIN
public/js/group.create.9836b689acf0fc1b.js
vendored
Normal file
Binary file not shown.
BIN
public/js/groups-page-about.150f2f899988e65c.js
vendored
Normal file
BIN
public/js/groups-page-about.150f2f899988e65c.js
vendored
Normal file
Binary file not shown.
BIN
public/js/groups-page-media.a57186ce36fd8972.js
vendored
Normal file
BIN
public/js/groups-page-media.a57186ce36fd8972.js
vendored
Normal file
Binary file not shown.
BIN
public/js/groups-page-members.20f9217256d06bf3.js
vendored
Normal file
BIN
public/js/groups-page-members.20f9217256d06bf3.js
vendored
Normal file
Binary file not shown.
BIN
public/js/groups-page-topics.c856bf15dc42b2fb.js
vendored
Normal file
BIN
public/js/groups-page-topics.c856bf15dc42b2fb.js
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue