1
0

GroupActivityPubService.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. namespace App\Services\Groups;
  3. use App\Models\Group;
  4. use App\Models\GroupPost;
  5. use App\Models\GroupComment;
  6. use Cache;
  7. use Purify;
  8. use Illuminate\Support\Facades\Redis;
  9. use League\Fractal;
  10. use App\Util\ActivityPub\Helpers;
  11. use League\Fractal\Serializer\ArraySerializer;
  12. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  13. use App\Transformer\Api\GroupPostTransformer;
  14. use App\Services\ActivityPubFetchService;
  15. use Illuminate\Support\Facades\Validator;
  16. use App\Rules\ValidUrl;
  17. class GroupActivityPubService
  18. {
  19. const CACHE_KEY = 'pf:services:groups:ap:';
  20. public static function fetchGroup($url, $saveOnFetch = true)
  21. {
  22. $group = Group::where('remote_url', $url)->first();
  23. if($group) {
  24. return $group;
  25. }
  26. $res = ActivityPubFetchService::get($url);
  27. if(!$res) {
  28. return $res;
  29. }
  30. $json = json_decode($res, true);
  31. $group = self::validateGroup($json);
  32. if(!$group) {
  33. return false;
  34. }
  35. if($saveOnFetch) {
  36. return self::storeGroup($group);
  37. }
  38. return $group;
  39. }
  40. public static function fetchGroupPost($url, $saveOnFetch = true)
  41. {
  42. $group = GroupPost::where('remote_url', $url)->first();
  43. if($group) {
  44. return $group;
  45. }
  46. $res = ActivityPubFetchService::get($url);
  47. if(!$res) {
  48. return 'invalid res';
  49. }
  50. $json = json_decode($res, true);
  51. if(!$json) {
  52. return 'invalid json';
  53. }
  54. if(isset($json['inReplyTo'])) {
  55. $comment = self::validateGroupComment($json);
  56. return self::storeGroupComment($comment);
  57. }
  58. $group = self::validateGroupPost($json);
  59. if($saveOnFetch) {
  60. return self::storeGroupPost($group);
  61. }
  62. return $group;
  63. }
  64. public static function validateGroup($obj)
  65. {
  66. $validator = Validator::make($obj, [
  67. '@context' => 'required',
  68. 'id' => ['required', 'url', new ValidUrl],
  69. 'type' => 'required|in:Group',
  70. 'preferredUsername' => 'required',
  71. 'name' => 'required',
  72. 'url' => ['sometimes', 'url', new ValidUrl],
  73. 'inbox' => ['required', 'url', new ValidUrl],
  74. 'outbox' => ['required', 'url', new ValidUrl],
  75. 'followers' => ['required', 'url', new ValidUrl],
  76. 'attributedTo' => 'required',
  77. 'summary' => 'sometimes',
  78. 'publicKey' => 'required',
  79. 'publicKey.id' => 'required',
  80. 'publicKey.owner' => ['required', 'url', 'same:id', new ValidUrl],
  81. 'publicKey.publicKeyPem' => 'required',
  82. ]);
  83. if($validator->fails()) {
  84. return false;
  85. }
  86. return $validator->validated();
  87. }
  88. public static function validateGroupPost($obj)
  89. {
  90. $validator = Validator::make($obj, [
  91. '@context' => 'required',
  92. 'id' => ['required', 'url', new ValidUrl],
  93. 'type' => 'required|in:Page,Note',
  94. 'to' => 'required|array',
  95. 'to.*' => ['required', 'url', new ValidUrl],
  96. 'cc' => 'sometimes|array',
  97. 'cc.*' => ['sometimes', 'url', new ValidUrl],
  98. 'url' => ['sometimes', 'url', new ValidUrl],
  99. 'attributedTo' => 'required',
  100. 'name' => 'sometimes',
  101. 'target' => 'sometimes',
  102. 'audience' => 'sometimes',
  103. 'inReplyTo' => 'sometimes',
  104. 'content' => 'sometimes',
  105. 'mediaType' => 'sometimes',
  106. 'sensitive' => 'sometimes',
  107. 'attachment' => 'sometimes',
  108. 'published' => 'required',
  109. ]);
  110. if($validator->fails()) {
  111. return false;
  112. }
  113. return $validator->validated();
  114. }
  115. public static function validateGroupComment($obj)
  116. {
  117. $validator = Validator::make($obj, [
  118. '@context' => 'required',
  119. 'id' => ['required', 'url', new ValidUrl],
  120. 'type' => 'required|in:Note',
  121. 'to' => 'required|array',
  122. 'to.*' => ['required', 'url', new ValidUrl],
  123. 'cc' => 'sometimes|array',
  124. 'cc.*' => ['sometimes', 'url', new ValidUrl],
  125. 'url' => ['sometimes', 'url', new ValidUrl],
  126. 'attributedTo' => 'required',
  127. 'name' => 'sometimes',
  128. 'target' => 'sometimes',
  129. 'audience' => 'sometimes',
  130. 'inReplyTo' => 'sometimes',
  131. 'content' => 'sometimes',
  132. 'mediaType' => 'sometimes',
  133. 'sensitive' => 'sometimes',
  134. 'published' => 'required',
  135. ]);
  136. if($validator->fails()) {
  137. return $validator->errors();
  138. return false;
  139. }
  140. return $validator->validated();
  141. }
  142. public static function getGroupFromPostActivity($groupPost)
  143. {
  144. if(isset($groupPost['audience']) && is_string($groupPost['audience'])) {
  145. return $groupPost['audience'];
  146. }
  147. if(
  148. isset(
  149. $groupPost['target'],
  150. $groupPost['target']['type'],
  151. $groupPost['target']['attributedTo']
  152. ) && $groupPost['target']['type'] == 'Collection'
  153. ) {
  154. return $groupPost['target']['attributedTo'];
  155. }
  156. return false;
  157. }
  158. public static function getActorFromPostActivity($groupPost)
  159. {
  160. if(!isset($groupPost['attributedTo'])) {
  161. return false;
  162. }
  163. $field = $groupPost['attributedTo'];
  164. if(is_string($field)) {
  165. return $field;
  166. }
  167. if(is_array($field) && count($field) === 1) {
  168. if(
  169. isset(
  170. $field[0]['id'],
  171. $field[0]['type']
  172. ) &&
  173. $field[0]['type'] === 'Person' &&
  174. is_string($field[0]['id'])
  175. ) {
  176. return $field[0]['id'];
  177. }
  178. }
  179. return false;
  180. }
  181. public static function getCaptionFromPostActivity($groupPost)
  182. {
  183. if(!isset($groupPost['name']) && isset($groupPost['content'])) {
  184. return Purify::clean(strip_tags($groupPost['content']));
  185. }
  186. if(isset($groupPost['name'], $groupPost['content'])) {
  187. return Purify::clean(strip_tags($groupPost['name'])) . Purify::clean(strip_tags($groupPost['content']));
  188. }
  189. }
  190. public static function getSensitiveFromPostActivity($groupPost)
  191. {
  192. if(!isset($groupPost['sensitive'])) {
  193. return true;
  194. }
  195. if(isset($groupPost['sensitive']) && !is_bool($groupPost['sensitive'])) {
  196. return true;
  197. }
  198. return boolval($groupPost['sensitive']);
  199. }
  200. public static function storeGroup($activity)
  201. {
  202. $group = new Group;
  203. $group->profile_id = null;
  204. $group->category_id = 1;
  205. $group->name = $activity['name'] ?? 'Untitled Group';
  206. $group->description = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
  207. $group->is_private = false;
  208. $group->local_only = false;
  209. $group->metadata = [];
  210. $group->local = false;
  211. $group->remote_url = $activity['id'];
  212. $group->inbox_url = $activity['inbox'];
  213. $group->activitypub = true;
  214. $group->save();
  215. return $group;
  216. }
  217. public static function storeGroupPost($groupPost)
  218. {
  219. $groupUrl = self::getGroupFromPostActivity($groupPost);
  220. if(!$groupUrl) {
  221. return;
  222. }
  223. $group = self::fetchGroup($groupUrl, true);
  224. if(!$group) {
  225. return;
  226. }
  227. $actorUrl = self::getActorFromPostActivity($groupPost);
  228. $actor = Helpers::profileFetch($actorUrl);
  229. $caption = self::getCaptionFromPostActivity($groupPost);
  230. $sensitive = self::getSensitiveFromPostActivity($groupPost);
  231. $model = GroupPost::firstOrCreate(
  232. [
  233. 'remote_url' => $groupPost['id'],
  234. ], [
  235. 'group_id' => $group->id,
  236. 'profile_id' => $actor->id,
  237. 'type' => 'text',
  238. 'caption' => $caption,
  239. 'visibility' => 'public',
  240. 'is_nsfw' => $sensitive,
  241. ]
  242. );
  243. return $model;
  244. }
  245. public static function storeGroupComment($groupPost)
  246. {
  247. $groupUrl = self::getGroupFromPostActivity($groupPost);
  248. if(!$groupUrl) {
  249. return;
  250. }
  251. $group = self::fetchGroup($groupUrl, true);
  252. if(!$group) {
  253. return;
  254. }
  255. $actorUrl = self::getActorFromPostActivity($groupPost);
  256. $actor = Helpers::profileFetch($actorUrl);
  257. $caption = self::getCaptionFromPostActivity($groupPost);
  258. $sensitive = self::getSensitiveFromPostActivity($groupPost);
  259. $parentPost = self::fetchGroupPost($groupPost['inReplyTo']);
  260. $model = GroupComment::firstOrCreate(
  261. [
  262. 'remote_url' => $groupPost['id'],
  263. ], [
  264. 'group_id' => $group->id,
  265. 'profile_id' => $actor->id,
  266. 'status_id' => $parentPost->id,
  267. 'type' => 'text',
  268. 'caption' => $caption,
  269. 'visibility' => 'public',
  270. 'is_nsfw' => $sensitive,
  271. 'local' => $actor->private_key != null
  272. ]
  273. );
  274. return $model;
  275. }
  276. }