WebMonetization.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. /**
  20. * WebMonetization for GNU social
  21. *
  22. * @package GNUsocial
  23. * @category Plugin
  24. *
  25. * @author Phablulo <phablulo@gmail.com>
  26. * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
  27. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  28. */
  29. namespace Plugin\WebMonetization;
  30. use App\Core\Cache;
  31. use App\Core\DB\DB;
  32. use App\Core\Event;
  33. use App\Core\Form;
  34. use function App\Core\I18n\_m;
  35. use App\Core\Modules\Plugin;
  36. use App\Entity\Activity;
  37. use App\Entity\Actor;
  38. use App\Entity\LocalUser;
  39. use App\Util\Common;
  40. use App\Util\Exception\RedirectException;
  41. use App\Util\Formatting;
  42. use Plugin\WebMonetization\Entity\Wallet;
  43. use Plugin\WebMonetization\Entity\WebMonetization as Monetization;
  44. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  45. use Symfony\Component\Form\Extension\Core\Type\TextType;
  46. use Symfony\Component\HttpFoundation\Request;
  47. class WebMonetization extends Plugin
  48. {
  49. public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool
  50. {
  51. $user = Common::actor();
  52. if (\is_null($user)) {
  53. return Event::next;
  54. }
  55. if (\is_null($vars)) {
  56. return Event::next;
  57. }
  58. $is_self = null;
  59. $receiver_id = null;
  60. if ($vars['path'] === 'settings') {
  61. $is_self = true;
  62. } elseif ($vars['path'] === 'actor_view_nickname') {
  63. $is_self = $request->attributes->get('nickname') === $user->getNickname();
  64. if (!$is_self) {
  65. $receiver_id = DB::findOneBy(LocalUser::class, [
  66. 'nickname' => $request->attributes->get('nickname'),
  67. ], return_null: true)?->getId();
  68. }
  69. } elseif ($vars['path'] === 'actor_view_id') {
  70. $is_self = $request->attributes->get('id') == $user->getId();
  71. if (!$is_self) {
  72. $receiver_id = $request->attributes->get('id');
  73. }
  74. } else {
  75. return Event::next;
  76. }
  77. // if visiting self page, the user will see a form to add, remove or update his wallet
  78. if ($is_self) {
  79. $wallet = DB::findOneBy(Wallet::class, ['actor_id' => $user->getId()], return_null: true);
  80. $form = Form::create([
  81. ['address', TextType::class, [
  82. 'label' => _m('Wallet address'),
  83. 'attr' => [
  84. 'placeholder' => _m('Wallet address'),
  85. 'autocomplete' => 'off',
  86. 'value' => $wallet?->getAddress(),
  87. ],
  88. ]],
  89. ['webmonetizationsave', SubmitType::class, [
  90. 'label' => _m('Save'),
  91. 'attr' => [
  92. 'title' => _m('Save'),
  93. ],
  94. ]],
  95. ]);
  96. $form->handleRequest($request);
  97. if ($form->isSubmitted() && $form->isValid()) {
  98. if (\is_null($wallet)) {
  99. DB::persist(
  100. Wallet::create([
  101. 'actor_id' => $user->getId(),
  102. 'address' => $form->getData()['address'],
  103. ]),
  104. );
  105. } else {
  106. $wallet->setAddress($form->getData()['address']);
  107. }
  108. DB::flush();
  109. throw new RedirectException();
  110. }
  111. $res[] = Formatting::twigRenderFile(
  112. 'WebMonetization/widget.html.twig',
  113. ['user' => $user, 'the_form' => $form->createView()],
  114. );
  115. }
  116. // if visiting another user page, the user will see a form to start/stop donating to them
  117. else {
  118. $entry = DB::findOneBy(Monetization::class, ['sender' => $user->getId(), 'receiver' => $receiver_id], return_null: true);
  119. $label = $entry?->getActive() ? _m('Stop donating') : _m('Start donating');
  120. $form = Form::create([
  121. ['toggle', SubmitType::class, [
  122. 'label' => $label,
  123. 'attr' => [
  124. 'title' => $label,
  125. ],
  126. ]],
  127. ]);
  128. $res[] = Formatting::twigRenderFile(
  129. 'WebMonetization/widget.html.twig',
  130. ['user' => $user, 'the_form' => $form->createView()],
  131. );
  132. $form->handleRequest($request);
  133. if ($form->isSubmitted() && $form->isValid()) {
  134. if (\is_null($entry)) {
  135. $entry = Monetization::create(
  136. ['sender' => $user->getId(), 'receiver' => $receiver_id, 'active' => true, 'sent' => 0],
  137. );
  138. DB::persist($entry);
  139. } else {
  140. $entry->setActive(!$entry->getActive());
  141. }
  142. DB::flush();
  143. // notify receiver!
  144. $rwallet = DB::findOneBy(Wallet::class, ['actor_id' => $receiver_id], return_null: true);
  145. $message = null;
  146. if ($entry->getActive()) {
  147. if ($rwallet?->getAddress()) {
  148. $message = '{nickname} is now donating to you!';
  149. } else {
  150. $message = '{nickname} wants to donate to you. Configure a wallet address to receive donations!';
  151. }
  152. $activity = Activity::create([
  153. 'actor_id' => $user->getId(),
  154. 'verb' => 'offer',
  155. 'object_type' => 'webmonetization',
  156. 'object_id' => $entry->getId(),
  157. 'source' => 'web',
  158. ]);
  159. } else {
  160. $message = '{nickname} is no longer donating to you.';
  161. // find the old activity ...
  162. $activity = DB::findOneBy(Activity::class, [
  163. 'actor_id' => $user->getId(),
  164. 'verb' => 'offer',
  165. 'object_type' => 'webmonetization',
  166. 'object_id' => $entry->getId(),
  167. ], order_by: ['created' => 'DESC']);
  168. // ... and undo it
  169. $activity = Activity::create([
  170. 'actor_id' => $user->getId(),
  171. 'verb' => 'undo',
  172. 'object_type' => 'activity',
  173. 'object_id' => $activity->getId(),
  174. 'source' => 'web',
  175. ]);
  176. }
  177. DB::persist($activity);
  178. Event::handle('NewNotification', [
  179. $user,
  180. $activity,
  181. ['object' => [$receiver_id]],
  182. _m($message, ['{nickname}' => $user->getNickname()]),
  183. ]);
  184. DB::flush();
  185. // --
  186. throw new RedirectException();
  187. }
  188. }
  189. return Event::next;
  190. }
  191. public static function cacheKeys(int|LocalUser|Actor $id): array
  192. {
  193. if (!\is_int($id)) {
  194. $id = $id->getId();
  195. }
  196. return [
  197. 'wallets' => "webmonetization-wallets-sender-{$id}",
  198. ];
  199. }
  200. public function onAppendToHead(Request $request, &$res): bool
  201. {
  202. $user = Common::user();
  203. if (\is_null($user)) {
  204. return Event::next;
  205. }
  206. // donate to everyone!
  207. // Using Javascript, it can be improved to donate only
  208. // to actors owning notes rendered on current page.
  209. $entries = Cache::getList(
  210. self::cacheKeys($user->getId())['wallets'],
  211. fn () => DB::dql(
  212. <<<'EOF'
  213. SELECT wallet FROM webmonetizationWallet wallet
  214. INNER JOIN webmonetization wm
  215. WITH wallet.actor_id = wm.receiver
  216. WHERE wm.active = :active AND wm.sender = :sender
  217. EOF,
  218. ['sender' => $user->getId(), 'active' => true],
  219. ),
  220. );
  221. foreach ($entries as $entry) {
  222. $res[] = Formatting::twigRenderString(
  223. '<meta name="monetization" content="{{ address }}">',
  224. ['address' => $entry->getAddress()],
  225. );
  226. }
  227. return Event::next;
  228. }
  229. public function onActivityPubAddActivityStreamsTwoData(string $type_name, &$type): bool
  230. {
  231. if ($type_name === 'Person') {
  232. $actor = \Plugin\ActivityPub\ActivityPub::getActorByUri($type->getId());
  233. $wallet = DB::findOneBy(Wallet::class, ['actor_id' => $actor->getId()], return_null: true);
  234. if (!\is_null($address = $wallet?->getAddress())) {
  235. $type->set('gs:webmonetizationWallet', $address);
  236. }
  237. }
  238. return Event::next;
  239. }
  240. }