yammerrunner.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2010, StatusNet, Inc.
  5. *
  6. * This program 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. * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. if (!defined('STATUSNET')) {
  20. exit(1);
  21. }
  22. /**
  23. * State machine for running through Yammer import.
  24. *
  25. * @package YammerImportPlugin
  26. * @author Brion Vibber <brion@status.net>
  27. */
  28. class YammerRunner
  29. {
  30. private $state;
  31. private $client;
  32. private $importer;
  33. /**
  34. * Normalize our singleton state and give us a YammerRunner object to play with!
  35. *
  36. * @return YammerRunner
  37. */
  38. public static function init()
  39. {
  40. $state = Yammer_state::getKV('id', 1);
  41. if (!$state) {
  42. $state = self::initState();
  43. }
  44. return new YammerRunner($state);
  45. }
  46. private static function initState()
  47. {
  48. $state = new Yammer_state();
  49. $state->id = 1;
  50. $state->state = 'init';
  51. $state->created = common_sql_now();
  52. $state->modified = common_sql_now();
  53. $state->insert();
  54. return $state;
  55. }
  56. private function __construct($state)
  57. {
  58. $this->state = $state;
  59. $this->client = new SNYammerClient(
  60. common_config('yammer', 'consumer_key'),
  61. common_config('yammer', 'consumer_secret'),
  62. $this->state->oauth_token,
  63. $this->state->oauth_secret);
  64. $this->importer = new YammerImporter($this->client);
  65. }
  66. /**
  67. * Check which state we're in
  68. *
  69. * @return string
  70. */
  71. public function state()
  72. {
  73. return $this->state->state;
  74. }
  75. /**
  76. * Is the import done, finished, complete, finito?
  77. *
  78. * @return boolean
  79. */
  80. public function isDone()
  81. {
  82. $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
  83. return ($this->state() == 'done');
  84. }
  85. /**
  86. * Check if we have work to do in iterate().
  87. *
  88. * @return boolean
  89. */
  90. public function hasWork()
  91. {
  92. $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
  93. return in_array($this->state(), $workStates);
  94. }
  95. /**
  96. * Blow away any current state!
  97. */
  98. public function reset()
  99. {
  100. $this->state->delete();
  101. $this->state = self::initState();
  102. }
  103. /**
  104. * Start the authentication process! If all goes well, we'll get back a URL.
  105. * Have the user visit that URL, log in on Yammer and verify the importer's
  106. * permissions. They'll get back a verification code, which needs to be passed
  107. * on to saveAuthToken().
  108. *
  109. * @return string URL
  110. */
  111. public function requestAuth()
  112. {
  113. if ($this->state->state != 'init') {
  114. // TRANS: Server exception thrown if a Yammer authentication request is already present.
  115. throw new ServerException(_m('Cannot request Yammer auth; already there!'));
  116. }
  117. $data = $this->client->requestToken();
  118. $old = clone($this->state);
  119. $this->state->state = 'requesting-auth';
  120. $this->state->oauth_token = $data['oauth_token'];
  121. $this->state->oauth_secret = $data['oauth_token_secret'];
  122. $this->state->modified = common_sql_now();
  123. $this->state->update($old);
  124. return $this->getAuthUrl();
  125. }
  126. /**
  127. * When already in requesting-auth state, grab the URL to send the user to
  128. * to complete OAuth setup.
  129. *
  130. * @return string URL
  131. */
  132. function getAuthUrl()
  133. {
  134. if ($this->state() == 'requesting-auth') {
  135. return $this->client->authorizeUrl($this->state->oauth_token);
  136. } else {
  137. // TRANS: Server exception thrown when requesting a Yammer authentication URL while in an incorrect state.
  138. throw new ServerException(_m('Cannot get Yammer auth URL when not in requesting-auth state!'));
  139. }
  140. }
  141. /**
  142. * Now that the user's given us this verification code from Yammer, we can
  143. * request a final OAuth token/secret pair which we can use to access the
  144. * API.
  145. *
  146. * After success here, we'll be ready to move on and run through iterate()
  147. * until the import is complete.
  148. *
  149. * @param string $verifier
  150. * @return boolean success
  151. */
  152. public function saveAuthToken($verifier)
  153. {
  154. if ($this->state->state != 'requesting-auth') {
  155. // TRANS: Server exception thrown if a Yammer authentication token could not be saved in a certain import state.
  156. // TRANS: %s is the import state in the which the error occurred.
  157. throw new ServerException(_m('Cannot save auth token in Yammer import state %s.',$this->state->state));
  158. }
  159. $data = $this->client->accessToken($verifier);
  160. $old = clone($this->state);
  161. $this->state->state = 'import-users';
  162. $this->state->oauth_token = $data['oauth_token'];
  163. $this->state->oauth_secret = $data['oauth_token_secret'];
  164. $this->state->modified = common_sql_now();
  165. $this->state->update($old);
  166. return true;
  167. }
  168. /**
  169. * Once authentication is complete, we need to call iterate() a bunch of times
  170. * until state() returns 'done'.
  171. *
  172. * @return boolean success
  173. */
  174. public function iterate()
  175. {
  176. switch($this->state())
  177. {
  178. case 'init':
  179. case 'requesting-auth':
  180. // Neither of these should reach our background state!
  181. common_log(LOG_ERR, "Non-background YammerImport state '$state->state' during import run!");
  182. return false;
  183. case 'import-users':
  184. return $this->iterateUsers();
  185. case 'import-groups':
  186. return $this->iterateGroups();
  187. case 'fetch-messages':
  188. return $this->iterateFetchMessages();
  189. case 'save-messages':
  190. return $this->iterateSaveMessages();
  191. default:
  192. common_log(LOG_ERR, "Invalid YammerImport state '$state->state' during import run!");
  193. return false;
  194. }
  195. }
  196. /**
  197. * Trundle through one 'page' return of up to 50 user accounts retrieved
  198. * from the Yammer API, importing them as we go.
  199. *
  200. * When we run out of users, move on to groups.
  201. *
  202. * @return boolean success
  203. */
  204. private function iterateUsers()
  205. {
  206. $old = clone($this->state);
  207. $page = intval($this->state->users_page) + 1;
  208. $data = $this->client->users(array('page' => $page));
  209. if (count($data) == 0) {
  210. common_log(LOG_INFO, "Finished importing Yammer users; moving on to groups.");
  211. $this->state->state = 'import-groups';
  212. } else {
  213. foreach ($data as $item) {
  214. $user = $this->importer->importUser($item);
  215. common_log(LOG_INFO, "Imported Yammer user " . $item['id'] . " as $user->nickname ($user->id)");
  216. }
  217. $this->state->users_page = $page;
  218. }
  219. $this->state->modified = common_sql_now();
  220. $this->state->update($old);
  221. return true;
  222. }
  223. /**
  224. * Trundle through one 'page' return of up to 20 user groups retrieved
  225. * from the Yammer API, importing them as we go.
  226. *
  227. * When we run out of groups, move on to messages.
  228. *
  229. * @return boolean success
  230. */
  231. private function iterateGroups()
  232. {
  233. $old = clone($this->state);
  234. $page = intval($this->state->groups_page) + 1;
  235. $data = $this->client->groups(array('page' => $page));
  236. if (count($data) == 0) {
  237. common_log(LOG_INFO, "Finished importing Yammer groups; moving on to messages.");
  238. $this->state->state = 'fetch-messages';
  239. } else {
  240. foreach ($data as $item) {
  241. $group = $this->importer->importGroup($item);
  242. common_log(LOG_INFO, "Imported Yammer group " . $item['id'] . " as $group->nickname ($group->id)");
  243. }
  244. $this->state->groups_page = $page;
  245. }
  246. $this->state->modified = common_sql_now();
  247. $this->state->update($old);
  248. return true;
  249. }
  250. /**
  251. * Trundle through one 'page' return of up to 20 public messages retrieved
  252. * from the Yammer API, saving them to our stub table for future import in
  253. * correct chronological order.
  254. *
  255. * When we run out of messages to fetch, move on to saving the messages.
  256. *
  257. * @return boolean success
  258. */
  259. private function iterateFetchMessages()
  260. {
  261. $old = clone($this->state);
  262. $oldest = intval($this->state->messages_oldest);
  263. if ($oldest) {
  264. $params = array('older_than' => $oldest);
  265. } else {
  266. $params = array();
  267. }
  268. $data = $this->client->messages($params);
  269. $messages = $data['messages'];
  270. if (count($messages) == 0) {
  271. common_log(LOG_INFO, "Finished fetching Yammer messages; moving on to save messages.");
  272. $this->state->state = 'save-messages';
  273. } else {
  274. foreach ($messages as $item) {
  275. $stub = Yammer_notice_stub::getKV($item['id']);
  276. if (!$stub) {
  277. Yammer_notice_stub::record($item['id'], $item);
  278. }
  279. $oldest = $item['id'];
  280. }
  281. $this->state->messages_oldest = $oldest;
  282. }
  283. $this->state->modified = common_sql_now();
  284. $this->state->update($old);
  285. return true;
  286. }
  287. private function iterateSaveMessages()
  288. {
  289. $old = clone($this->state);
  290. $newest = intval($this->state->messages_newest);
  291. $stub = new Yammer_notice_stub();
  292. if ($newest) {
  293. $stub->whereAdd('id > ' . $newest);
  294. }
  295. $stub->limit(20);
  296. $stub->orderBy('id');
  297. $stub->find();
  298. if ($stub->N == 0) {
  299. common_log(LOG_INFO, "Finished saving Yammer messages; import complete!");
  300. $this->state->state = 'done';
  301. } else {
  302. while ($stub->fetch()) {
  303. $item = $stub->getData();
  304. $notice = $this->importer->importNotice($item);
  305. common_log(LOG_INFO, "Imported Yammer notice " . $item['id'] . " as $notice->id");
  306. $newest = $item['id'];
  307. }
  308. $this->state->messages_newest = $newest;
  309. }
  310. $this->state->modified = common_sql_now();
  311. $this->state->update($old);
  312. return true;
  313. }
  314. /**
  315. * Count the number of Yammer users we've mapped into our system!
  316. *
  317. * @return int
  318. */
  319. public function countUsers()
  320. {
  321. $map = new Yammer_user();
  322. return $map->count();
  323. }
  324. /**
  325. * Count the number of Yammer groups we've mapped into our system!
  326. *
  327. * @return int
  328. */
  329. public function countGroups()
  330. {
  331. $map = new Yammer_group();
  332. return $map->count();
  333. }
  334. /**
  335. * Count the number of Yammer notices we've pulled down for pending import...
  336. *
  337. * @return int
  338. */
  339. public function countFetchedNotices()
  340. {
  341. $map = new Yammer_notice_stub();
  342. return $map->count();
  343. }
  344. /**
  345. * Count the number of Yammer notices we've mapped into our system!
  346. *
  347. * @return int
  348. */
  349. public function countSavedNotices()
  350. {
  351. $map = new Yammer_notice();
  352. return $map->count();
  353. }
  354. /**
  355. * Start running import work in the background queues...
  356. */
  357. public function startBackgroundImport()
  358. {
  359. $qm = QueueManager::get();
  360. $qm->enqueue('YammerImport', 'yammer');
  361. }
  362. /**
  363. * Record an error condition from a background run, which we should
  364. * display in progress state for the admin.
  365. *
  366. * @param string $msg
  367. */
  368. public function recordError($msg)
  369. {
  370. // HACK HACK HACK
  371. try {
  372. $temp = new Yammer_state();
  373. $temp->query('ROLLBACK');
  374. } catch (Exception $e) {
  375. common_log(LOG_ERR, 'Exception while confirming rollback while recording error: ' . $e->getMessage());
  376. }
  377. $old = clone($this->state);
  378. $this->state->last_error = $msg;
  379. $this->state->update($old);
  380. }
  381. /**
  382. * Clear the error state.
  383. */
  384. public function clearError()
  385. {
  386. $this->recordError('');
  387. }
  388. /**
  389. * Get the last recorded background error message, if any.
  390. *
  391. * @return string
  392. */
  393. public function lastError()
  394. {
  395. return $this->state->last_error;
  396. }
  397. }