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