]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/moveuser.php
first example of moving a user
[quix0rs-gnu-social.git] / scripts / moveuser.php
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
20 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
21
22 $shortoptions = 'i:n:r:w:';
23 $longoptions = array('id=', 'nickname=', 'remote=', 'password=');
24
25 $helptext = <<<END_OF_MOVEUSER_HELP
26 moveuser.php [options]
27 Move a local user to a remote account.
28
29   -i --id       ID of user to move
30   -n --nickname nickname of the user to move
31   -r --remote   Full ID of remote users
32   -w --password Password of remote user
33
34 Remote user identity must be a Webfinger (nickname@example.com) or 
35 an HTTP or HTTPS URL (http://example.com/social/site/user/nickname).
36
37 END_OF_MOVEUSER_HELP;
38
39 require_once INSTALLDIR.'/scripts/commandline.inc';
40 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
41
42 class ActivitySink
43 {
44     protected $svcDocUrl   = null;
45     protected $username    = null;
46     protected $password    = null;
47     protected $collections = array();
48
49     function __construct($svcDocUrl, $username, $password)
50     {
51         $this->svcDocUrl = $svcDocUrl;
52         $this->username  = $username;
53         $this->password  = $password;
54
55         $this->_parseSvcDoc();
56     }
57
58     private function _parseSvcDoc()
59     {
60         $client   = new HTTPClient();
61         $response = $client->get($this->svcDocUrl);
62
63         if ($response->getStatus() != 200) {
64             throw new Exception("Can't get {$this->svcDocUrl}; response status " . $response->getStatus());
65         }
66
67         $xml = $response->getBody();
68
69         $dom = new DOMDocument();
70
71         // We don't want to bother with white spaces
72         $dom->preserveWhiteSpace = false;
73
74         // Don't spew XML warnings to output
75         $old = error_reporting();
76         error_reporting($old & ~E_WARNING);
77         $ok = $dom->loadXML($xml);
78         error_reporting($old);
79
80         $path = new DOMXPath($dom);
81
82         $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
83         $path->registerNamespace('app', 'http://www.w3.org/2007/app');
84         $path->registerNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
85
86         $collections = $path->query('//app:collection');
87
88         for ($i = 0; $i < $collections->length; $i++) {
89             $collection = $collections->item($i);
90             $url = $collection->getAttribute('href');
91             $takesEntries = false;
92             $accepts = $path->query('app:accept', $collection);
93             for ($j = 0; $j < $accepts->length; $j++) {
94                 $accept = $accepts->item($j);
95                 $acceptValue = $accept->nodeValue;
96                 if (preg_match('#application/atom\+xml(;\s*type=entry)?#', $acceptValue)) {
97                     $takesEntries = true;
98                     break;
99                 }
100             }
101             if (!$takesEntries) {
102                 continue;
103             }
104             $verbs = $path->query('activity:verb', $collection);
105             if ($verbs->length == 0) {
106                 $this->_addCollection(ActivityVerb::POST, $url);
107             } else {
108                 for ($k = 0; $k < $verbs->length; $k++) {
109                     $verb = $verbs->item($k);
110                     $this->_addCollection($verb->nodeValue, $url);
111                 }
112             }
113         }
114     }
115
116     private function _addCollection($verb, $url)
117     {
118         if (array_key_exists($verb, $this->collections)) {
119             $this->collections[$verb][] = $url;
120         } else {
121             $this->collections[$verb] = array($url);
122         }
123         return;
124     }
125
126     function postActivity($activity)
127     {
128         if (!array_key_exists($activity->verb, $this->collections)) {
129             throw new Exception("No collection for verb {$activity->verb}");
130         } else {
131             if (count($this->collections[$activity->verb]) > 1) {
132                 common_log(LOG_NOTICE, "More than one collection for verb {$activity->verb}");
133             }
134             $this->postToCollection($this->collections[$activity->verb][0], $activity);
135         }
136     }
137
138     function postToCollection($url, $activity)
139     {
140         $client = new HTTPClient($url);
141
142         $client->setMethod('POST');
143         $client->setAuth($this->username, $this->password);
144         $client->setHeader('Content-Type', 'application/atom+xml;type=entry');
145         $client->setBody($activity->asString(true, true, true));
146
147         $response = $client->send();
148     }
149 }
150
151 function getServiceDocument($remote)
152 {
153     $discovery = new Discovery();
154
155     $xrd = $discovery->lookup($remote);
156
157     if (empty($xrd)) {
158         throw new Exception("Can't find XRD for $remote");
159     } 
160
161     $svcDocUrl = null;
162     $username  = null;
163
164     foreach ($xrd->links as $link) {
165         if ($link['rel'] == 'http://apinamespace.org/atom' &&
166             $link['type'] == 'application/atomsvc+xml') {
167             $svcDocUrl = $link['href'];
168             if (!empty($link['property'])) {
169                 foreach ($link['property'] as $property) {
170                     if ($property['type'] == 'http://apinamespace.org/atom/username') {
171                         $username = $property['value'];
172                         break;
173                     }
174                 }
175             }
176             break;
177         }
178     }
179
180     if (empty($svcDocUrl)) {
181         throw new Exception("No AtomPub API service for $remote.");
182     }
183
184     return array($svcDocUrl, $username);
185 }
186
187 class AccountMover
188 {
189     private $_user    = null;
190     private $_profile = null;
191     private $_remote  = null;
192     private $_sink    = null;
193     
194     function __construct($user, $remote, $password)
195     {
196         $this->_user    = $user;
197         $this->_profile = $user->getProfile();
198
199         $oprofile = Ostatus_profile::ensureProfileURI($remote);
200
201         if (empty($oprofile)) {
202             throw new Exception("Can't locate account {$remote}");
203         }
204
205         $this->_remote = $oprofile->localProfile();
206
207         list($svcDocUrl, $username) = getServiceDocument($remote);
208
209         $this->_sink = new ActivitySink($svcDocUrl, $username, $password);
210     }
211
212     function move()
213     {
214         $stream = new UserActivityStream($this->_user);
215
216         $acts = array_reverse($stream->activities);
217
218         // Reverse activities to run in correct chron order
219
220         foreach ($acts as $act) {
221             $this->_moveActivity($act);
222         }
223     }
224
225     private function _moveActivity($act)
226     {
227         switch ($act->verb) {
228         case ActivityVerb::FAVORITE:
229             // push it, then delete local
230             $this->_sink->postActivity($act);
231             $notice = Notice::staticGet('uri', $act->objects[0]->id);
232             if (!empty($notice)) {
233                 $fave = Fave::pkeyGet(array('user_id' => $this->_user->id,
234                                             'notice_id' => $notice->id));
235                 $fave->delete();
236             }
237             break;
238         case ActivityVerb::POST:
239             // XXX: send a reshare, not a post
240             common_log(LOG_INFO, "Pushing notice {$act->objects[0]->id} to {$this->_remote->getURI()}");
241             $this->_sink->postActivity($act);
242             $notice = Notice::staticGet('uri', $act->objects[0]->id);
243             if (!empty($notice)) {
244                 $notice->delete();
245             }
246             break;
247         case ActivityVerb::JOIN:
248             $this->_sink->postActivity($act);
249             $group = User_group::staticGet('uri', $act->objects[0]->id);
250             if (!empty($group)) {
251                 Group_member::leave($group->id, $this->_user->id);
252             }
253             break;
254         case ActivityVerb::FOLLOW:
255             if ($act->actor->id == $this->_user->uri) {
256                 $this->_sink->postActivity($act);
257                 $other = Profile::fromURI($act->objects[0]->id);
258                 if (!empty($other)) {
259                     Subscription::cancel($this->_profile, $other);
260                 }
261             } else {
262                 $otherUser = User::staticGet('uri', $act->actor->id);
263                 if (!empty($otherUser)) {
264                     $otherProfile = $otherUser->getProfile();
265                     Subscription::start($otherProfile, $this->_remote);
266                     Subscription::cancel($otherProfile, $this->_user->getProfile());
267                 } else {
268                     // It's a remote subscription. Do something here!
269                 }
270             }
271             break;
272         }
273     }
274 }
275
276 try {
277
278     $user = getUser();
279
280     $remote = get_option_value('r', 'remote');
281
282     if (empty($remote)) {
283         show_help();
284         exit(1);
285     }
286
287     $password = get_option_value('w', 'password');
288
289     $mover = new AccountMover($user, $remote, $password);
290
291     $mover->move();
292
293 } catch (Exception $e) {
294     print $e->getMessage()."\n";
295     exit(1);
296 }