diff --git a/plugins/WebMonetization/Entity/Wallet.php b/plugins/WebMonetization/Entity/Wallet.php new file mode 100644 index 0000000000..8310fa3940 --- /dev/null +++ b/plugins/WebMonetization/Entity/Wallet.php @@ -0,0 +1,65 @@ +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'], + ]; + } +} diff --git a/plugins/WebMonetization/Entity/WebMonetization.php b/plugins/WebMonetization/Entity/WebMonetization.php new file mode 100644 index 0000000000..45600866c9 --- /dev/null +++ b/plugins/WebMonetization/Entity/WebMonetization.php @@ -0,0 +1,105 @@ +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'], + ]; + } +} diff --git a/plugins/WebMonetization/WebMonetization.php b/plugins/WebMonetization/WebMonetization.php new file mode 100644 index 0000000000..f561eef91b --- /dev/null +++ b/plugins/WebMonetization/WebMonetization.php @@ -0,0 +1,221 @@ +. +// }}} +/** + * WebMonetization for GNU social + * + * @package GNUsocial + * @category Plugin + * + * @author Phablulo + * @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( + '', + ['address' => $entry->getAddress()], + ); + } + return Event::next; + } +} diff --git a/plugins/WebMonetization/templates/WebMonetization/widget.html.twig b/plugins/WebMonetization/templates/WebMonetization/widget.html.twig new file mode 100644 index 0000000000..9363a578e7 --- /dev/null +++ b/plugins/WebMonetization/templates/WebMonetization/widget.html.twig @@ -0,0 +1,12 @@ +
+
+ +

Web Monetization

+ {{ icon('arrow-down', 'icon icon-details-open') | raw }} +
+
+ {{ form(the_form) }} +
+
+
+