SearchSubPlugin.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. defined('GNUSOCIAL') || die();
  17. /**
  18. * SearchSub plugin main class
  19. *
  20. * @category Plugin
  21. * @package SearchSubPlugin
  22. * @author Brion Vibber <brionv@status.net>
  23. * @copyright 2011-2019 Free Software Foundation, Inc http://www.fsf.org
  24. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  25. */
  26. class SearchSubPlugin extends Plugin
  27. {
  28. const PLUGIN_VERSION = '0.1.0';
  29. /**
  30. * Database schema setup
  31. *
  32. * @return bool hook value; true means continue processing, false means stop.
  33. * @throws PEAR_Exception
  34. * @see Schema
  35. *
  36. */
  37. public function onCheckSchema(): bool
  38. {
  39. $schema = Schema::get();
  40. $schema->ensureTable('searchsub', SearchSub::schemaDef());
  41. return true;
  42. }
  43. /**
  44. * Map URLs to actions
  45. *
  46. * @param URLMapper $m path-to-action mapper
  47. *
  48. * @return bool hook value; true means continue processing, false means stop.
  49. * @throws Exception
  50. */
  51. public function onRouterInitialized(URLMapper $m): bool
  52. {
  53. $m->connect(
  54. 'search/:search/subscribe',
  55. ['action' => 'searchsub'],
  56. ['search' => Router::REGEX_TAG]
  57. );
  58. $m->connect(
  59. 'search/:search/unsubscribe',
  60. ['action' => 'searchunsub'],
  61. ['search' => Router::REGEX_TAG]
  62. );
  63. $m->connect(
  64. ':nickname/search-subscriptions',
  65. ['action' => 'searchsubs'],
  66. ['nickname' => Nickname::DISPLAY_FMT]
  67. );
  68. return true;
  69. }
  70. /**
  71. * Module version data
  72. *
  73. * @param array &$versions array of version data
  74. *
  75. * @return bool
  76. * @throws Exception
  77. */
  78. public function onPluginVersion(array &$versions): bool
  79. {
  80. $versions[] = array('name' => 'SearchSub',
  81. 'version' => self::PLUGIN_VERSION,
  82. 'author' => 'Brion Vibber',
  83. 'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/SearchSub',
  84. 'rawdescription' =>
  85. // TRANS: Module description.
  86. _m('Module to allow following all messages with a given search.'));
  87. return true;
  88. }
  89. /**
  90. * Hook inbox delivery setup so search subscribers receive all
  91. * notices with that search in their inbox.
  92. *
  93. * Currently makes no distinction between local messages and
  94. * remote ones which happen to come in to the system. Remote
  95. * notices that don't come in at all won't ever reach this.
  96. *
  97. * @param Notice $notice
  98. * @param array $ni in/out map of profile IDs to inbox constants
  99. * @return bool hook result
  100. */
  101. public function onStartNoticeWhoGets(Notice $notice, array &$ni): bool
  102. {
  103. // Warning: this is potentially very slow
  104. // with a lot of searches!
  105. $sub = new SearchSub();
  106. $sub->groupBy('search');
  107. $sub->selectAdd();
  108. $sub->selectAdd('search');
  109. $sub->find();
  110. while ($sub->fetch()) {
  111. $search = $sub->search;
  112. if ($this->matchSearch($notice, $search)) {
  113. // Match? Find all those who subscribed to this
  114. // search term and get our delivery on...
  115. $searchsub = new SearchSub();
  116. $searchsub->search = $search;
  117. $searchsub->find();
  118. while ($searchsub->fetch()) {
  119. // These constants are currently not actually used, iirc
  120. $ni[$searchsub->profile_id] = NOTICE_INBOX_SOURCE_SUB;
  121. }
  122. }
  123. }
  124. return true;
  125. }
  126. /**
  127. * Does the given notice match the given fulltext search query?
  128. *
  129. * Warning: not guaranteed to match other search engine behavior, etc.
  130. * Currently using a basic case-insensitive substring match, which
  131. * probably fits with the 'LIKE' search but not the default MySQL
  132. * or Sphinx search backends.
  133. *
  134. * @param Notice $notice
  135. * @param string $search
  136. * @return bool
  137. */
  138. public function matchSearch(Notice $notice, $search): bool
  139. {
  140. return (mb_stripos($notice->content, $search) !== false);
  141. }
  142. /**
  143. *
  144. * @param NoticeSearchAction $action
  145. * @param string $q
  146. * @param Notice $notice
  147. * @return bool hook result
  148. */
  149. public function onStartNoticeSearchShowResults($action, $q, $notice): bool
  150. {
  151. $user = common_current_user();
  152. if ($user) {
  153. $search = $q;
  154. $searchsub = SearchSub::pkeyGet(array('search' => $search,
  155. 'profile_id' => $user->id));
  156. if ($searchsub) {
  157. $form = new SearchUnsubForm($action, $search);
  158. } else {
  159. $form = new SearchSubForm($action, $search);
  160. }
  161. $action->elementStart('div', 'entity_actions');
  162. $action->elementStart('ul');
  163. $action->elementStart('li', 'entity_subscribe');
  164. $form->show();
  165. $action->elementEnd('li');
  166. $action->elementEnd('ul');
  167. $action->elementEnd('div');
  168. }
  169. return true;
  170. }
  171. /**
  172. * Menu item for personal subscriptions/groups area
  173. *
  174. * @param Widget $widget Widget being executed
  175. *
  176. * @return bool hook return
  177. * @throws Exception
  178. */
  179. public function onEndSubGroupNav($widget): bool
  180. {
  181. $action = $widget->out;
  182. $action_name = $action->trimmed('action');
  183. $action->menuItem(
  184. common_local_url('searchsubs', array('nickname' => $action->user->nickname)),
  185. // TRANS: SearchSub plugin menu item on user settings page.
  186. _m('MENU', 'Searches'),
  187. // TRANS: SearchSub plugin tooltip for user settings menu item.
  188. _m('Configure search subscriptions'),
  189. $action_name == 'searchsubs' && $action->arg('nickname') == $action->user->nickname
  190. );
  191. return true;
  192. }
  193. /**
  194. * Replace the built-in stub track commands with ones that control
  195. * search subscriptions.
  196. *
  197. * @param CommandInterpreter $cmd
  198. * @param string $arg
  199. * @param User $user
  200. * @param Command $result
  201. * @return bool hook result
  202. */
  203. public function onEndInterpretCommand($cmd, $arg, $user, &$result): bool
  204. {
  205. if ($result instanceof TrackCommand) {
  206. $result = new SearchSubTrackCommand($user, $arg);
  207. return false;
  208. } elseif ($result instanceof TrackOffCommand) {
  209. $result = new SearchSubTrackOffCommand($user);
  210. return false;
  211. } elseif ($result instanceof TrackingCommand) {
  212. $result = new SearchSubTrackingCommand($user);
  213. return false;
  214. } elseif ($result instanceof UntrackCommand) {
  215. $result = new SearchSubUntrackCommand($user, $arg);
  216. return false;
  217. } else {
  218. return true;
  219. }
  220. }
  221. public function onHelpCommandMessages($cmd, &$commands): void
  222. {
  223. // TRANS: Help message for IM/SMS command "track <word>"
  224. $commands["track <word>"] = _m('COMMANDHELP', "Start following notices matching the given search query.");
  225. // TRANS: Help message for IM/SMS command "untrack <word>"
  226. $commands["untrack <word>"] = _m('COMMANDHELP', "Stop following notices matching the given search query.");
  227. // TRANS: Help message for IM/SMS command "track off"
  228. $commands["track off"] = _m('COMMANDHELP', "Disable all tracked search subscriptions.");
  229. // TRANS: Help message for IM/SMS command "untrack all"
  230. $commands["untrack all"] = _m('COMMANDHELP', "Disable all tracked search subscriptions.");
  231. // TRANS: Help message for IM/SMS command "tracks"
  232. $commands["tracks"] = _m('COMMANDHELP', "List all your search subscriptions.");
  233. // TRANS: Help message for IM/SMS command "tracking"
  234. $commands["tracking"] = _m('COMMANDHELP', "List all your search subscriptions.");
  235. }
  236. public function onEndDefaultLocalNav($menu, $user): bool
  237. {
  238. $user = common_current_user();
  239. if (!empty($user)) {
  240. $searches = SearchSub::forProfile($user->getProfile());
  241. if (!empty($searches) && count($searches) > 0) {
  242. $searchSubMenu = new SearchSubMenu($menu->out, $user, $searches);
  243. // TRANS: Sub menu for searches.
  244. $menu->submenu(_m('MENU', 'Searches'), $searchSubMenu);
  245. }
  246. }
  247. return true;
  248. }
  249. }