forked from GNUsocial/gnu-social
		
	[PLUGIN][WebMonetization] Adding Web Monetization plugin which allows for donations using the Web Monetizations protocol
This commit is contained in:
		
							
								
								
									
										65
									
								
								plugins/WebMonetization/Entity/Wallet.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								plugins/WebMonetization/Entity/Wallet.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| namespace Plugin\WebMonetization\Entity; | ||||
|  | ||||
| use App\Core\Entity; | ||||
|  | ||||
| class Wallet extends Entity | ||||
| { | ||||
|     // These tags are meant to be literally included and will be populated with the appropriate fields, setters and getters by `bin/generate_entity_fields` | ||||
|     // {{{ Autocode | ||||
|     // @codeCoverageIgnoreStart | ||||
|     private int $id; | ||||
|     private int $actor_id; | ||||
|     private string $address; | ||||
|  | ||||
|     public function setId(int $id): self | ||||
|     { | ||||
|         $this->id = $id; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getId(): int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function setActorId(int $actor_id): self | ||||
|     { | ||||
|         $this->actor_id = $actor_id; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getActorId(): int | ||||
|     { | ||||
|         return $this->actor_id; | ||||
|     } | ||||
|  | ||||
|     public function setAddress(string $address): self | ||||
|     { | ||||
|         $this->address = mb_substr($address, 0, 255); | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getAddress(): string | ||||
|     { | ||||
|         return $this->address; | ||||
|     } | ||||
|  | ||||
|     // @codeCoverageIgnoreEnd | ||||
|     // }}} Autocode | ||||
|     public static function schemaDef() | ||||
|     { | ||||
|         return [ | ||||
|             'name'   => 'webmonetizationWallet', | ||||
|             'fields' => [ | ||||
|                 'id'       => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], | ||||
|                 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to actor table'], | ||||
|                 'address'  => ['type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'wallet address'], | ||||
|             ], | ||||
|             'primary key' => ['id'], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										105
									
								
								plugins/WebMonetization/Entity/WebMonetization.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								plugins/WebMonetization/Entity/WebMonetization.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| namespace Plugin\WebMonetization\Entity; | ||||
|  | ||||
| use App\Core\Entity; | ||||
|  | ||||
| class WebMonetization extends Entity | ||||
| { | ||||
|     // These tags are meant to be literally included and will be populated with the appropriate fields, setters and getters by `bin/generate_entity_fields` | ||||
|     // {{{ Autocode | ||||
|     // @codeCoverageIgnoreStart | ||||
|     private int $id; | ||||
|     private int $sender; | ||||
|     private int $receiver; | ||||
|     private float $sent; | ||||
|     private bool $active; | ||||
|  | ||||
|     public function setId(int $id): self | ||||
|     { | ||||
|         $this->id = $id; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getId(): int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function setSender(int $sender): self | ||||
|     { | ||||
|         $this->sender = $sender; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getSender(): int | ||||
|     { | ||||
|         return $this->sender; | ||||
|     } | ||||
|  | ||||
|     public function setReceiver(int $receiver): self | ||||
|     { | ||||
|         $this->receiver = $receiver; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getReceiver(): int | ||||
|     { | ||||
|         return $this->receiver; | ||||
|     } | ||||
|  | ||||
|     public function setSent(float $sent): self | ||||
|     { | ||||
|         $this->sent = $sent; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getSent(): float | ||||
|     { | ||||
|         return $this->sent; | ||||
|     } | ||||
|  | ||||
|     public function setActive(bool $active): self | ||||
|     { | ||||
|         $this->active = $active; | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getActive(): bool | ||||
|     { | ||||
|         return $this->active; | ||||
|     } | ||||
|  | ||||
|     // @codeCoverageIgnoreEnd | ||||
|     // }}} Autocode | ||||
|     public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array | ||||
|     { | ||||
|         if (\array_key_exists('object', $ids_already_known)) { | ||||
|             $target_ids = $ids_already_known['object']; | ||||
|         } else { | ||||
|             $target_ids = [$this->getReceiver()]; | ||||
|         } | ||||
|         // Additional actors that should know about this | ||||
|         if ($include_additional && \array_key_exists('additional', $ids_already_known)) { | ||||
|             array_push($target_ids, ...$ids_already_known['additional']); | ||||
|             return array_unique($target_ids); | ||||
|         } | ||||
|         return $target_ids; | ||||
|     } | ||||
|     public static function schemaDef() | ||||
|     { | ||||
|         return [ | ||||
|             'name'   => 'webmonetization', | ||||
|             'fields' => [ | ||||
|                 'id'       => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], | ||||
|                 'sender'   => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor sending money'], | ||||
|                 'receiver' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor receiving money'], | ||||
|                 'sent'     => ['type' => 'float', 'not null' => true, 'description' => 'how much sender has sent to receiver'], | ||||
|                 'active'   => ['type' => 'bool', 'not null' => true, 'description' => 'whether it should donate'], | ||||
|             ], | ||||
|             'primary key' => ['id'], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										221
									
								
								plugins/WebMonetization/WebMonetization.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								plugins/WebMonetization/WebMonetization.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| // {{{ License | ||||
| // This file is part of GNU social - https://www.gnu.org/software/social | ||||
| // | ||||
| // GNU social is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // GNU social is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||
| // }}} | ||||
| /** | ||||
|  * WebMonetization for GNU social | ||||
|  * | ||||
|  * @package   GNUsocial | ||||
|  * @category  Plugin | ||||
|  * | ||||
|  * @author    Phablulo <phablulo@gmail.com> | ||||
|  * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org | ||||
|  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||
|  */ | ||||
|  | ||||
| namespace Plugin\WebMonetization; | ||||
|  | ||||
| use App\Core\DB\DB; | ||||
| use App\Core\Event; | ||||
| use App\Core\Form; | ||||
| use function App\Core\I18n\_m; | ||||
| use App\Core\Modules\Plugin; | ||||
| use App\Entity\Activity; | ||||
| use App\Entity\LocalUser; | ||||
| use App\Util\Common; | ||||
| use App\Util\Exception\RedirectException; | ||||
| use App\Util\Formatting; | ||||
| use Plugin\WebMonetization\Entity\Wallet; | ||||
| use Plugin\WebMonetization\Entity\WebMonetization as Monetization; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| class WebMonetization extends Plugin | ||||
| { | ||||
|     public function onAppendRightPanelBlock($vars, Request $request, &$res): bool | ||||
|     { | ||||
|         $user = Common::actor(); | ||||
|         if (\is_null($user)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|         if (\is_null($vars)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|  | ||||
|         $is_self     = null; | ||||
|         $receiver_id = null; | ||||
|  | ||||
|         if ($vars['path'] === 'settings') { | ||||
|             $is_self = true; | ||||
|         } elseif ($vars['path'] === 'actor_view_nickname') { | ||||
|             $is_self = $request->attributes->get('nickname') === $user->getNickname(); | ||||
|             if (!$is_self) { | ||||
|                 $receiver_id = DB::findOneBy(LocalUser::class, [ | ||||
|                     'nickname' => $request->attributes->get('nickname'), | ||||
|                 ], return_null: true)?->getId(); | ||||
|             } | ||||
|         } elseif ($vars['path'] === 'actor_view_id') { | ||||
|             $is_self = $request->attributes->get('id') == $user->getId(); | ||||
|             if (!$is_self) { | ||||
|                 $receiver_id = $request->attributes->get('id'); | ||||
|             } | ||||
|         } else { | ||||
|             return Event::next; | ||||
|         } | ||||
|         // if visiting self page, the user will see a form to add, remove or update his wallet | ||||
|         if ($is_self) { | ||||
|             $wallet = DB::findOneBy(Wallet::class, ['actor_id' => $user->getId()], return_null: true); | ||||
|             $form   = Form::create([ | ||||
|                 ['address', TextType::class, [ | ||||
|                     'label' => _m('Wallet address'), | ||||
|                     'attr'  => [ | ||||
|                         'placeholder'  => _m('Wallet address'), | ||||
|                         'autocomplete' => 'off', | ||||
|                         'value'        => $wallet?->getAddress(), | ||||
|                     ], | ||||
|                 ]], | ||||
|                 ['webmonetizationsave', SubmitType::class, [ | ||||
|                     'label' => _m('Save'), | ||||
|                     'attr'  => [ | ||||
|                         'title' => _m('Save'), | ||||
|                     ], | ||||
|                 ]], | ||||
|             ]); | ||||
|             $form->handleRequest($request); | ||||
|             if ($form->isSubmitted() && $form->isValid()) { | ||||
|                 if (\is_null($wallet)) { | ||||
|                     DB::persist( | ||||
|                         Wallet::create([ | ||||
|                             'actor_id' => $user->getId(), | ||||
|                             'address'  => $form->getData()['address'], | ||||
|                         ]), | ||||
|                     ); | ||||
|                 } else { | ||||
|                     $wallet->setAddress($form->getData()['address']); | ||||
|                 } | ||||
|                 DB::flush(); | ||||
|                 throw new RedirectException(); | ||||
|             } | ||||
|  | ||||
|             $res[] = Formatting::twigRenderFile( | ||||
|                 'WebMonetization/widget.html.twig', | ||||
|                 ['user' => $user, 'the_form' => $form->createView()], | ||||
|             ); | ||||
|         } | ||||
|         // if visiting another user page, the user will see a form to start/stop donating to them | ||||
|         else { | ||||
|             $entry = DB::findOneBy(Monetization::class, ['sender' => $user->getId(), 'receiver' => $receiver_id], return_null: true); | ||||
|             $label = $entry?->getActive() ? _m('Stop donating') : _m('Start donating'); | ||||
|             $form  = Form::create([ | ||||
|                 ['toggle', SubmitType::class, [ | ||||
|                     'label' => $label, | ||||
|                     'attr'  => [ | ||||
|                         'title' => $label, | ||||
|                     ], | ||||
|                 ]], | ||||
|             ]); | ||||
|             $res[] = Formatting::twigRenderFile( | ||||
|                 'WebMonetization/widget.html.twig', | ||||
|                 ['user' => $user, 'the_form' => $form->createView()], | ||||
|             ); | ||||
|             $form->handleRequest($request); | ||||
|             if ($form->isSubmitted() && $form->isValid()) { | ||||
|                 if (\is_null($entry)) { | ||||
|                     $entry = Monetization::create( | ||||
|                         ['sender' => $user->getId(), 'receiver' => $receiver_id, 'active' => true, 'sent' => 0], | ||||
|                     ); | ||||
|                     DB::persist($entry); | ||||
|                 } else { | ||||
|                     $entry->setActive(!$entry->getActive()); | ||||
|                 } | ||||
|                 DB::flush(); | ||||
|                 // notify receiver! | ||||
|                 $rwallet = DB::findOneBy(Wallet::class, ['actor_id' => $receiver_id], return_null: true); | ||||
|                 $message = null; | ||||
|                 if ($entry->getActive()) { | ||||
|                     if ($rwallet?->getAddress()) { | ||||
|                         $message = '{nickname} is now donating to you!'; | ||||
|                     } else { | ||||
|                         $message = '{nickname} wants to donate to you. Configure a wallet address to receive donations!'; | ||||
|                     } | ||||
|                     $activity = Activity::create([ | ||||
|                         'actor_id'    => $user->getId(), | ||||
|                         'verb'        => 'offer', | ||||
|                         'object_type' => 'webmonetization', | ||||
|                         'object_id'   => $entry->getId(), | ||||
|                         'source'      => 'web', | ||||
|                     ]); | ||||
|                 } else { | ||||
|                     $message = '{nickname} is no longer donating to you.'; | ||||
|                     // find the old activity ... | ||||
|                     $activity = DB::findOneBy(Activity::class, [ | ||||
|                         'actor_id'    => $user->getId(), | ||||
|                         'verb'        => 'offer', | ||||
|                         'object_type' => 'webmonetization', | ||||
|                         'object_id'   => $entry->getId(), | ||||
|                     ], order_by: ['created' => 'DESC']); | ||||
|                     // ... and undo it | ||||
|                     $activity = Activity::create([ | ||||
|                         'actor_id'    => $user->getId(), | ||||
|                         'verb'        => 'undo', | ||||
|                         'object_type' => 'activity', | ||||
|                         'object_id'   => $activity->getId(), | ||||
|                         'source'      => 'web', | ||||
|                     ]); | ||||
|                 } | ||||
|                 DB::persist($activity); | ||||
|                 Event::handle('NewNotification', [ | ||||
|                     $user, | ||||
|                     $activity, | ||||
|                     ['object' => [$receiver_id]], | ||||
|                     _m($message, ['{nickname}' => $user->getNickname()]), | ||||
|                 ]); | ||||
|                 DB::flush(); | ||||
|                 // -- | ||||
|  | ||||
|                 throw new RedirectException(); | ||||
|             } | ||||
|         } | ||||
|         return Event::next; | ||||
|     } | ||||
|     public function onAppendToHead(Request $request, &$res): bool | ||||
|     { | ||||
|         $user = Common::actor(); | ||||
|         if (\is_null($user)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|         // donate to everyone! | ||||
|         // Using Javascript, it can be improved to donate only | ||||
|         // to actors owning notes rendered on current page. | ||||
|         $entries = DB::dql(<<<'EOF' | ||||
|                 SELECT wallet FROM \Plugin\WebMonetization\Entity\Wallet wallet | ||||
|                     INNER JOIN \Plugin\WebMonetization\Entity\WebMonetization wm | ||||
|                     WITH wallet.actor_id = wm.receiver | ||||
|                 WHERE wm.active = :active AND wm.sender = :sender | ||||
|             EOF, ['sender' => $user->getId(), 'active' => true]); | ||||
|         foreach ($entries as $entry) { | ||||
|             $res[] = Formatting::twigRenderString( | ||||
|                 '<meta name="monetization" content="{{ address }}">', | ||||
|                 ['address' => $entry->getAddress()], | ||||
|             ); | ||||
|         } | ||||
|         return Event::next; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| <section class="section-widget"> | ||||
|     <details class="section-widget-title-details" title="Expand if you want to access more options."> | ||||
|         <summary class="section-title-summary"> | ||||
|             <h2>Web Monetization</h2> | ||||
|             {{ icon('arrow-down', 'icon icon-details-open') | raw }} | ||||
|         </summary> | ||||
|         <fieldset class="section-form"> | ||||
|             {{ form(the_form) }} | ||||
|         </fieldset> | ||||
|     </details> | ||||
| </section> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user