]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub.php
Activitity pub - first commit with much test code
[friendica.git] / src / Protocol / ActivityPub.php
1 <?php
2 /**
3  * @file src/Protocol/ActivityPub.php
4  */
5 namespace Friendica\Protocol;
6
7 use Friendica\Database\DBA;
8 use Friendica\Core\System;
9 use Friendica\BaseObject;
10 use Friendica\Util\Network;
11 use Friendica\Util\HTTPSignature;
12 use Friendica\Core\Protocol;
13 use Friendica\Model\Item;
14 use Friendica\Model\User;
15 use Friendica\Util\DateTimeFormat;
16 use Friendica\Util\Crypto;
17 use Friendica\Content\Text\BBCode;
18
19 /**
20  * @brief ActivityPub Protocol class
21  * The ActivityPub Protocol is a message exchange protocol defined by the W3C.
22  * https://www.w3.org/TR/activitypub/
23  * https://www.w3.org/TR/activitystreams-core/
24  * https://www.w3.org/TR/activitystreams-vocabulary/
25  *
26  * https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
27  * https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
28  */
29 class ActivityPub
30 {
31         const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
32
33         public static function transmit($content, $target, $uid)
34         {
35                 $owner = User::getOwnerDataById($uid);
36
37                 if (!$owner) {
38                         return;
39                 }
40
41                 $host = parse_url($target, PHP_URL_HOST);
42                 $path = parse_url($target, PHP_URL_PATH);
43                 $date = date('r');
44
45                 $headers = ['Host: ' . $host, 'Date: ' . $date];
46
47                 $signed_data = "(request-target): post " . $path . "\nhost: " . $host . "\ndate: " . $date;
48
49                 $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256'));
50
51                 $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",headers="(request-target) host date",signature="' . $signature . '"';
52                 $headers[] = 'Content-Type: application/activity+json';
53 //print_r($headers);
54 //die($signed_data);
55 //$headers = [];
56 //              $headers = HTTPSignature::createSig('', $headers, $owner['uprvkey'], $owner['url'] . '#main-key', false, false, 'sha256');
57
58                 Network::post($target, $content, $headers);
59                 $return_code = BaseObject::getApp()->get_curl_code();
60 echo $return_code."\n";
61                 print_r(BaseObject::getApp()->get_curl_headers());
62                 print_r($headers);
63         }
64
65         /**
66          * Return the ActivityPub profile of the given user
67          *
68          * @param integer $uid User ID
69          * @return array
70          */
71         public static function profile($uid)
72         {
73                 $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application'];
74
75                 $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false,
76                         'account_removed' => false, 'verified' => true];
77                 $fields = ['guid', 'nickname', 'pubkey', 'account-type'];
78                 $user = DBA::selectFirst('user', $fields, $condition);
79                 if (!DBA::isResult($user)) {
80                         return [];
81                 }
82
83                 $fields = ['locality', 'region', 'country-name'];
84                 $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]);
85                 if (!DBA::isResult($profile)) {
86                         return [];
87                 }
88
89                 $fields = ['name', 'url', 'location', 'about', 'avatar'];
90                 $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
91                 if (!DBA::isResult($contact)) {
92                         return [];
93                 }
94
95                 $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1',
96                         ['uuid' => 'http://schema.org/identifier', 'sensitive' => 'as:sensitive',
97                         'vcard' => 'http://www.w3.org/2006/vcard/ns#']]];
98
99                 $data['id'] = $contact['url'];
100                 $data['uuid'] = $user['guid'];
101                 $data['type'] = $accounttype[$user['account-type']];
102                 $data['following'] = System::baseUrl() . '/following/' . $user['nickname'];
103                 $data['followers'] = System::baseUrl() . '/followers/' . $user['nickname'];
104                 $data['inbox'] = System::baseUrl() . '/inbox/' . $user['nickname'];
105                 $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname'];
106                 $data['preferredUsername'] = $user['nickname'];
107                 $data['name'] = $contact['name'];
108                 $data['vcard:hasAddress'] = ['@type' => 'Home', 'vcard:country-name' => $profile['country-name'],
109                         'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']];
110                 $data['summary'] = $contact['about'];
111                 $data['url'] = $contact['url'];
112                 $data['manuallyApprovesFollowers'] = false;
113                 $data['publicKey'] = ['id' => $contact['url'] . '#main-key',
114                         'owner' => $contact['url'],
115                         'publicKeyPem' => $user['pubkey']];
116                 $data['endpoints'] = ['sharedInbox' => System::baseUrl() . '/inbox'];
117                 $data['icon'] = ['type' => 'Image',
118                         'url' => $contact['avatar']];
119
120                 // tags: https://kitty.town/@inmysocks/100656097926961126.json
121                 return $data;
122         }
123
124         public static function createActivityFromItem($item_id)
125         {
126                 $item = Item::selectFirst([], ['id' => $item_id]);
127
128                 $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1',
129                         ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri',
130                         'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri',
131                         'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive',
132                         'toot' => 'http://joinmastodon.org/ns#']]];
133
134                 $data['type'] = 'Create';
135                 $data['id'] = $item['plink'];
136                 $data['actor'] = $item['author-link'];
137                 $data['to'] = 'https://www.w3.org/ns/activitystreams#Public';
138                 $data['object'] = self::createNote($item);
139 //              print_r($data);
140 //              print_r($item);
141                 return $data;
142         }
143
144         public static function createNote($item)
145         {
146                 $data = [];
147                 $data['type'] = 'Note';
148                 $data['id'] = $item['plink'];
149                 //$data['context'] = $data['conversation'] = $item['parent-uri'];
150                 $data['actor'] = $item['author-link'];
151 //              if (!$item['private']) {
152 //                      $data['to'] = [];
153 //                      $data['to'][] = '"https://pleroma.soykaf.com/users/heluecht"';
154                         $data['to'] = 'https://www.w3.org/ns/activitystreams#Public';
155 //                      $data['cc'] = 'https://pleroma.soykaf.com/users/heluecht';
156 //              }
157                 $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM);
158                 $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM);
159                 $data['attributedTo'] = $item['author-link'];
160                 $data['title'] = BBCode::convert($item['title'], false, 7);
161                 $data['content'] = BBCode::convert($item['body'], false, 7);
162                 //$data['summary'] = '';
163                 //$data['sensitive'] = false;
164                 //$data['emoji'] = [];
165                 //$data['tag'] = [];
166                 //$data['attachment'] = [];
167                 return $data;
168         }
169
170         /**
171          * Fetches ActivityPub content from the given url
172          *
173          * @param string $url content url
174          * @return array
175          */
176         public static function fetchContent($url)
177         {
178                 $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json']);
179
180                 if (!$ret['success'] || empty($ret['body'])) {
181                         return;
182                 }
183
184                 return json_decode($ret['body'], true);
185         }
186
187         /**
188          * Resolves the profile url from the address by using webfinger
189          *
190          * @param string $addr profile address (user@domain.tld)
191          * @return string url
192          */
193         private static function addrToUrl($addr)
194         {
195                 $addr_parts = explode('@', $addr);
196                 if (count($addr_parts) != 2) {
197                         return false;
198                 }
199
200                 $webfinger = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr);
201
202                 $ret = Network::curl($webfinger, false, $redirects, ['accept_content' => 'application/jrd+json,application/json']);
203                 if (!$ret['success'] || empty($ret['body'])) {
204                         return false;
205                 }
206
207                 $data = json_decode($ret['body'], true);
208
209                 if (empty($data['links'])) {
210                         return false;
211                 }
212
213                 foreach ($data['links'] as $link) {
214                         if (empty($link['href']) || empty($link['rel']) || empty($link['type'])) {
215                                 continue;
216                         }
217
218                         if (($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
219                                 return $link['href'];
220                         }
221                 }
222
223                 return false;
224         }
225
226         /**
227          * Fetches a profile from the given url
228          *
229          * @param string $url profile url
230          * @return array
231          */
232         public static function fetchProfile($url)
233         {
234                 if (empty(parse_url($url, PHP_URL_SCHEME))) {
235                         $url = self::addrToUrl($url);
236                         if (empty($url)) {
237                                 return false;
238                         }
239                 }
240
241                 $data = self::fetchContent($url);
242
243                 if (empty($data) || empty($data['id']) || empty($data['inbox'])) {
244                         return false;
245                 }
246
247                 $profile = ['network' => Protocol::ACTIVITYPUB];
248                 $profile['nick'] = $data['preferredUsername'];
249                 $profile['name'] = defaults($data, 'name', $profile['nick']);
250                 $profile['guid'] = defaults($data, 'uuid', null);
251                 $profile['url'] = $data['id'];
252                 $profile['alias'] = self::processElement($data, 'url', 'href');
253
254                 $parts = parse_url($profile['url']);
255                 unset($parts['scheme']);
256                 unset($parts['path']);
257                 $profile['addr'] = $profile['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts));
258
259                 $profile['photo'] = self::processElement($data, 'icon', 'url');
260                 $profile['about'] = defaults($data, 'summary', '');
261                 $profile['batch'] = self::processElement($data, 'endpoints', 'sharedInbox');
262                 $profile['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem');
263                 $profile['notify'] = $data['inbox'];
264                 $profile['poll'] = $data['outbox'];
265
266                 // Check if the address is resolvable
267                 if (self::addrToUrl($profile['addr']) == $profile['url']) {
268                         $parts = parse_url($profile['url']);
269                         unset($parts['path']);
270                         $profile['baseurl'] = Network::unparseURL($parts);
271                 } else {
272                         unset($profile['addr']);
273                 }
274
275                 if ($profile['url'] == $profile['alias']) {
276                         unset($profile['alias']);
277                 }
278
279                 // Remove all "null" fields
280                 foreach ($profile as $field => $content) {
281                         if (is_null($content)) {
282                                 unset($profile[$field]);
283                         }
284                 }
285
286                 // Handled
287                 unset($data['id']);
288                 unset($data['inbox']);
289                 unset($data['outbox']);
290                 unset($data['preferredUsername']);
291                 unset($data['name']);
292                 unset($data['summary']);
293                 unset($data['url']);
294                 unset($data['publicKey']);
295                 unset($data['endpoints']);
296                 unset($data['icon']);
297                 unset($data['uuid']);
298
299                 // To-Do
300                 unset($data['type']);
301                 unset($data['manuallyApprovesFollowers']);
302
303                 // Unhandled
304                 unset($data['@context']);
305                 unset($data['tag']);
306                 unset($data['attachment']);
307                 unset($data['image']);
308                 unset($data['nomadicLocations']);
309                 unset($data['signature']);
310                 unset($data['following']);
311                 unset($data['followers']);
312                 unset($data['featured']);
313                 unset($data['movedTo']);
314                 unset($data['liked']);
315                 unset($data['sharedInbox']); // Misskey
316                 unset($data['isCat']); // Misskey
317                 unset($data['kroeg:blocks']); // Kroeg
318                 unset($data['updated']); // Kroeg
319
320 /*              if (!empty($data)) {
321                         print_r($data);
322                         die();
323                 }
324 */
325                 return $profile;
326         }
327
328         public static function fetchOutbox($url)
329         {
330                 $data = self::fetchContent($url);
331                 if (empty($data)) {
332                         return;
333                 }
334
335                 if (!empty($data['orderedItems'])) {
336                         $items = $data['orderedItems'];
337                 } elseif (!empty($data['first']['orderedItems'])) {
338                         $items = $data['first']['orderedItems'];
339                 } elseif (!empty($data['first'])) {
340                         self::fetchOutbox($data['first']);
341                         return;
342                 } else {
343                         $items = [];
344                 }
345
346                 foreach ($items as $activity) {
347                         self::processActivity($activity, $url);
348                 }
349         }
350
351         function processActivity($activity, $url)
352         {
353                 if (empty($activity['type'])) {
354                         return;
355                 }
356
357                 if (empty($activity['object'])) {
358                         return;
359                 }
360
361                 if (empty($activity['actor'])) {
362                         return;
363                 }
364
365                 $actor = self::processElement($activity, 'actor', 'id');
366                 if (empty($actor)) {
367                         return;
368                 }
369
370                 if (is_string($activity['object'])) {
371                         $object_url = $activity['object'];
372                 } elseif (!empty($activity['object']['id'])) {
373                         $object_url = $activity['object']['id'];
374                 } else {
375                         return;
376                 }
377
378                 $receivers = self::getReceivers($activity);
379                 if (empty($receivers)) {
380                         return;
381                 }
382
383                 // ----------------------------------
384                 // unhandled
385                 unset($activity['@context']);
386                 unset($activity['id']);
387
388                 // Non standard
389                 unset($activity['title']);
390                 unset($activity['atomUri']);
391                 unset($activity['context_id']);
392                 unset($activity['statusnetConversationId']);
393
394                 $structure = $activity;
395
396                 // To-Do?
397                 unset($activity['context']);
398                 unset($activity['location']);
399
400                 // handled
401                 unset($activity['to']);
402                 unset($activity['cc']);
403                 unset($activity['bto']);
404                 unset($activity['bcc']);
405                 unset($activity['type']);
406                 unset($activity['actor']);
407                 unset($activity['object']);
408                 unset($activity['published']);
409                 unset($activity['updated']);
410                 unset($activity['instrument']);
411                 unset($activity['inReplyTo']);
412
413                 if (!empty($activity)) {
414                         echo "Activity\n";
415                         print_r($activity);
416                         die($url."\n");
417                 }
418
419                 $activity = $structure;
420                 // ----------------------------------
421
422                 $item = self::fetchObject($object_url, $url);
423                 if (empty($item)) {
424                         return;
425                 }
426
427                 $item = self::addActivityFields($item, $activity);
428
429                 $item['owner'] = $actor;
430
431                 $item['receiver'] = array_merge($item['receiver'], $receivers);
432
433                 switch ($activity['type']) {
434                         case 'Create':
435                         case 'Update':
436                                 self::createItem($item);
437                                 break;
438
439                         case 'Announce':
440                                 self::announceItem($item);
441                                 break;
442
443                         case 'Like':
444                         case 'Dislike':
445                                 self::activityItem($item);
446                                 break;
447
448                         case 'Follow':
449                                 break;
450
451                         default:
452                                 echo "Unknown activity: ".$activity['type']."\n";
453                                 print_r($item);
454                                 die();
455                                 break;
456                 }
457         }
458
459         private static function getReceivers($activity)
460         {
461                 $receivers = [];
462
463                 $elements = ['to', 'cc', 'bto', 'bcc'];
464                 foreach ($elements as $element) {
465                         if (empty($activity[$element])) {
466                                 continue;
467                         }
468
469                         // The receiver can be an arror or a string
470                         if (is_string($activity[$element])) {
471                                 $activity[$element] = [$activity[$element]];
472                         }
473
474                         foreach ($activity[$element] as $receiver) {
475                                 if ($receiver == self::PUBLIC) {
476                                         $receivers[$receiver] = 0;
477                                 }
478
479                                 $condition = ['self' => true, 'nurl' => normalise_link($receiver)];
480                                 $contact = DBA::selectFirst('contact', ['id'], $condition);
481                                 if (!DBA::isResult($contact)) {
482                                         continue;
483                                 }
484                                 $receivers[$receiver] = $contact['id'];
485                         }
486                 }
487                 return $receivers;
488         }
489
490         private static function addActivityFields($item, $activity)
491         {
492                 if (!empty($activity['published']) && empty($item['published'])) {
493                         $item['published'] = $activity['published'];
494                 }
495
496                 if (!empty($activity['updated']) && empty($item['updated'])) {
497                         $item['updated'] = $activity['updated'];
498                 }
499
500                 if (!empty($activity['inReplyTo']) && empty($item['parent-uri'])) {
501                         $item['parent-uri'] = self::processElement($activity, 'inReplyTo', 'id');
502                 }
503
504                 if (!empty($activity['instrument'])) {
505                         $item['service'] = self::processElement($activity, 'instrument', 'name', 'Service');
506                 }
507
508                 // Remove all "null" fields
509                 foreach ($item as $field => $content) {
510                         if (is_null($content)) {
511                                 unset($item[$field]);
512                         }
513                 }
514
515                 return $item;
516         }
517
518         private static function fetchObject($object_url, $url)
519         {
520                 $data = self::fetchContent($object_url);
521                 if (empty($data)) {
522                         return false;
523                 }
524
525                 if (empty($data['type'])) {
526                         return false;
527                 } else {
528                         $type = $data['type'];
529                 }
530
531                 if (in_array($type, ['Note', 'Article', 'Video'])) {
532                         $common = self::processCommonData($data, $url);
533                 }
534
535                 switch ($type) {
536                         case 'Note':
537                                 return array_merge($common, self::processNote($data, $url));
538                         case 'Article':
539                                 return array_merge($common, self::processArticle($data, $url));
540                         case 'Video':
541                                 return array_merge($common, self::processVideo($data, $url));
542
543                         case 'Announce':
544                                 if (empty($data['object'])) {
545                                         return false;
546                                 }
547                                 return self::fetchObject($data['object'], $url);
548
549                         case 'Person':
550                         case 'Tombstone':
551                                 break;
552
553                         default:
554                                 echo "Unknown object type: ".$data['type']."\n";
555                                 print_r($data);
556                                 die($url."\n");
557                                 break;
558                 }
559         }
560
561         private static function processCommonData(&$object, $url)
562         {
563                 if (empty($object['id']) || empty($object['attributedTo'])) {
564                         return false;
565                 }
566
567                 $item = [];
568                 $item['uri'] = $object['id'];
569
570                 if (!empty($object['inReplyTo'])) {
571                         $item['reply-to-uri'] = self::processElement($object, 'inReplyTo', 'id');
572                 } else {
573                         $item['reply-to-uri'] = $item['uri'];
574                 }
575
576                 $item['published'] = defaults($object, 'published', null);
577                 $item['updated'] = defaults($object, 'updated', $item['published']);
578
579                 if (empty($item['published']) && !empty($item['updated'])) {
580                         $item['published'] = $item['updated'];
581                 }
582
583                 $item['uuid'] = defaults($object, 'uuid', null);
584                 $item['owner'] = $item['author'] = self::processElement($object, 'attributedTo', 'id');
585                 $item['context'] = defaults($object, 'context', null);
586                 $item['conversation'] = defaults($object, 'conversation', null);
587                 $item['sensitive'] = defaults($object, 'sensitive', null);
588                 $item['name'] = defaults($object, 'name', null);
589                 $item['title'] = defaults($object, 'title', null);
590                 $item['content'] = defaults($object, 'content', null);
591                 $item['summary'] = defaults($object, 'summary', null);
592                 $item['location'] = self::processElement($object, 'location', 'name', 'Place');
593                 $item['attachments'] = defaults($object, 'attachment', null);
594                 $item['tags'] = defaults($object, 'tag', null);
595                 $item['service'] = self::processElement($object, 'instrument', 'name', 'Service');
596                 $item['alternate-url'] = self::processElement($object, 'url', 'href');
597                 $item['receiver'] = self::getReceivers($object);
598
599                 // handled
600                 unset($object['id']);
601                 unset($object['inReplyTo']);
602                 unset($object['published']);
603                 unset($object['updated']);
604                 unset($object['uuid']);
605                 unset($object['attributedTo']);
606                 unset($object['context']);
607                 unset($object['conversation']);
608                 unset($object['sensitive']);
609                 unset($object['name']);
610                 unset($object['title']);
611                 unset($object['content']);
612                 unset($object['summary']);
613                 unset($object['location']);
614                 unset($object['attachment']);
615                 unset($object['tag']);
616                 unset($object['instrument']);
617                 unset($object['url']);
618                 unset($object['to']);
619                 unset($object['cc']);
620                 unset($object['bto']);
621                 unset($object['bcc']);
622
623                 // To-Do
624                 unset($object['source']);
625
626                 // Unhandled
627                 unset($object['@context']);
628                 unset($object['type']);
629                 unset($object['actor']);
630                 unset($object['signature']);
631                 unset($object['mediaType']);
632                 unset($object['duration']);
633                 unset($object['replies']);
634                 unset($object['icon']);
635
636                 /*
637                 audience, preview, endTime, startTime, generator, image
638                 */
639
640                 return $item;
641         }
642
643         private static function processNote($object, $url)
644         {
645                 $item = [];
646
647                 // To-Do?
648                 unset($object['emoji']);
649                 unset($object['atomUri']);
650                 unset($object['inReplyToAtomUri']);
651
652                 // Unhandled
653                 unset($object['contentMap']);
654                 unset($object['announcement_count']);
655                 unset($object['announcements']);
656                 unset($object['context_id']);
657                 unset($object['likes']);
658                 unset($object['like_count']);
659                 unset($object['inReplyToStatusId']);
660                 unset($object['shares']);
661                 unset($object['quoteUrl']);
662                 unset($object['statusnetConversationId']);
663
664                 if (empty($object))
665                         return $item;
666
667                 echo "Unknown Note\n";
668                 print_r($object);
669                 print_r($item);
670                 die($url."\n");
671
672                 return [];
673         }
674
675         private static function processArticle($object, $url)
676         {
677                 $item = [];
678
679                 if (empty($object))
680                         return $item;
681
682                 echo "Unknown Article\n";
683                 print_r($object);
684                 print_r($item);
685                 die($url."\n");
686
687                 return [];
688         }
689
690         private static function processVideo($object, $url)
691         {
692                 $item = [];
693
694                 // To-Do?
695                 unset($object['category']);
696                 unset($object['licence']);
697                 unset($object['language']);
698                 unset($object['commentsEnabled']);
699
700                 // Unhandled
701                 unset($object['views']);
702                 unset($object['waitTranscoding']);
703                 unset($object['state']);
704                 unset($object['support']);
705                 unset($object['subtitleLanguage']);
706                 unset($object['likes']);
707                 unset($object['dislikes']);
708                 unset($object['shares']);
709                 unset($object['comments']);
710
711                 if (empty($object))
712                         return $item;
713
714                 echo "Unknown Video\n";
715                 print_r($object);
716                 print_r($item);
717                 die($url."\n");
718
719                 return [];
720         }
721
722         private static function processElement($array, $element, $key, $type = null)
723         {
724                 if (empty($array)) {
725                         return false;
726                 }
727
728                 if (empty($array[$element])) {
729                         return false;
730                 }
731
732                 if (is_string($array[$element])) {
733                         return $array[$element];
734                 }
735
736                 if (is_null($type)) {
737                         if (!empty($array[$element][$key])) {
738                                 return $array[$element][$key];
739                         }
740
741                         if (!empty($array[$element][0][$key])) {
742                                 return $array[$element][0][$key];
743                         }
744
745                         return false;
746                 }
747
748                 if (!empty($array[$element][$key]) && !empty($array[$element]['type']) && ($array[$element]['type'] == $type)) {
749                         return $array[$element][$key];
750                 }
751
752                 /// @todo Add array search
753
754                 return false;
755         }
756
757         private static function createItem($item)
758         {
759 //              print_r($item);
760         }
761
762         private static function announceItem($item)
763         {
764 //              print_r($item);
765         }
766
767         private static function activityItem($item)
768         {
769         //      print_r($item);
770         }
771
772 }