Add NuBux link to index page
[loomclient.git] / LoomClient.php
blob0e631d5fadf40f23cb1b6390189f50eb3222913b
1 <?php
2 require_once "bcbitwise.php";
3 require_once "Socket.php";
4 require_once "LoomRandom.php";
6 /*
7 * Client to the webapp at http://loom.cc/
8 * See https://loom.cc/?function=help&topic=grid_tutorial&mode=advanced
9 */
11 class LoomClient
13 var $url_prefix; // Who you gonna call?
14 var $bits128; // 128 1-bits = 2**128 - 1
15 var $socket; // our connection to the Loom server
16 var $random; // An instance of LoomRandom
18 function LoomClient($prefix = 'https://loom.cc/') {
19 if (substr($prefix, -1) != '/') $prefix .= '/';
20 $this->url_prefix = $prefix;
21 $this->bits128 = bcsub(bcleftshift(1, 128), 1);
22 $this->socket = FALSE;
23 $this->random = new LoomRandom();
26 // This is all you really need to call
27 // The functions below are just syntactic sugar for calling get()
28 /* Save the old non-persistent connection version
29 function get($keys, &$url) {
30 $url = $this->url($this->url_prefix, $keys);
31 $kv = @file_get_contents($url);
32 return $this->parsekv($kv);
36 // The base of the Loom interface
37 // Send an HTTP GET, with args in $keys.
38 // Return whatever Loom returns.
39 function rawget($keys, &$url) {
40 return $this->rawGetOrPost($keys, 'get', $url);
43 function rawpost($keys, &$url) {
44 return $this->rawGetOrPost($keys, 'post', $url);
47 function rawGetOrPost($keys, $getOrPost, &$url) {
48 if (!$this->socket) {
49 // Should make this less brittle
50 if (substr($this->url_prefix, 0, 5) == 'https') {
51 $host = substr($this->url_prefix, 8, -1);
52 $ssl = TRUE;
53 } else {
54 $host = substr($this->url_prefix, 7, -1);
55 $ssl = FALSE;
57 $this->socket = new Socket($host, $ssl);
58 $this->socket->connect();
60 $uri = $this->url('', $keys);
61 $url = $this->url_prefix . $uri;
62 $uri = '/' . $uri;
63 if ($getOrPost == 'post') $this->socket->post($uri);
64 else $this->socket->get($uri);
65 return $this->socket->body;
68 // This is all you really need to call
69 // The functions below are just syntactic sugar for calling get()
70 function get($keys, &$url) {
71 $kv = $this->rawget($keys, $url);
72 return $this->parsekv($kv);
76 function buy($type, $location, $usage, &$url) {
77 return $this->get(array('function' => 'grid',
78 'action' => 'buy',
79 'type' => $type,
80 'loc' => $location,
81 'usage' => $usage),
82 $url);
85 function sell($type, $location, $usage, &$url) {
86 return $this->get(array('function' => 'grid',
87 'action' => 'sell',
88 'type' => $type,
89 'loc' => $location,
90 'usage' => $usage),
91 $url);
94 function issuer($type, $orig, $dest, &$url) {
95 return $this->get(array('function' => 'grid',
96 'action' => 'issuer',
97 'type' => $type,
98 'orig' => $orig,
99 'dest' => $dest),
100 $url);
103 function touch($type, $location, &$url) {
104 return $this->get(array('function' => 'grid',
105 'action' => 'touch',
106 'type' => $type,
107 'loc' => $location),
108 $url);
111 function look($type, $hash, &$url) {
112 return $this->get(array('function' => 'grid',
113 'action' => 'look',
114 'type' => $type,
115 'hash' => $hash),
116 $url);
119 function move($type, $quantity, $origin, $destination, &$url) {
120 return $this->get(array('function' => 'grid',
121 'action' => 'move',
122 'type' => $type,
123 'qty' => $quantity,
124 'orig' => $origin,
125 'dest' => $destination),
126 $url);
129 // $locs and $types are lists of space-separated hex IDs.
130 // If $zeroes is true, will return 0 values.
131 function scan($locs, $types, $zeroes, &$url) {
132 $a = array('function' => 'grid',
133 'action' => 'scan',
134 'locs' => $locs,
135 'types' => $types);
136 if ($zeroes) $a['zeroes'] = '1';
137 return $this->get($a, $url);
140 // $locs is array(locname => id, ...)
141 // $types is array(typename => array('id' => id,
142 // 'name' => typename,
143 // 'min_precision' => min_precision,
144 // 'scale' => scale),
145 // ...)
146 // Returns array(locname => array(typename => value, ...), ...)
147 // Returns FALSE if it gets an error from the loom server
148 function namedScan($locs, $types, $zeroes, &$url) {
149 $loca = array();
150 $locstring = "";
151 foreach ($locs as $locname => $id) {
152 if ($locstring != '') $locstring .= ' ';
153 $locstring .= $id;
154 $loca[$id] = $locname;
157 $typea = array();
158 $typestring = "";
159 foreach ($types as $typename => $attributes) {
160 $id = $attributes['id'];
161 if ($typestring != '') $typestring .= ' ';
162 $typestring .= $id;
163 $typea[$id] = $attributes;
166 $res = $this->scan($locstring, $typestring, $zeroes, $url);
168 $resa = array();
169 foreach ($loca as $id => $locname) {
170 $vals = explode(' ', $res["loc/$id"]);
171 $vala = array();
172 foreach ($vals as $val) {
173 $val = explode(':', $val);
174 $value = $val[0];
175 $id = $val[1];
176 $attributes = $typea[$id];
177 $typename = $attributes['name'];
178 $min_precision = $attributes['min_precision'];
179 $scale = $attributes['scale'];
180 $vala[$typename] = $this->applyScale($value, $min_precision, $scale);
182 $resa[$locname] = $vala;
184 return $resa;
187 function applyScale($value, $min_precision, $scale) {
188 if ($value < 0) $value = bcadd($value, 1);
189 if ($scale > 0) $value = bcdiv($value, bcpow(10, $scale, 0), $scale);
191 $dotpos = strpos($value, '.');
193 if ($dotpos > 0) {
194 while (substr($value, -1) == '0') {
195 $value = substr($value, 0, strlen($value)-1);
197 if (substr($value, -1) == '.') {
198 $value = substr($value, 0, strlen($value)-1);
199 $dotpos = 0;
203 if ($min_precision > 0) {
204 if ($dotpos == 0) {
205 $value .= ".";
206 $dotpos = strlen($value);
207 } else $dotpos++;
208 $places = strlen($value) - $dotpos;
209 if ($min_precision > $places) {
210 $value .= str_repeat("0", $min_precision - $places);
213 return $value;
216 function unscale($value, $scale) {
217 if ($scale >= 0) return bcmul($value, bcpow(10, $scale, 0), 0);
218 return $value;
221 function buy_archive($loc, $usage, &$url) {
222 return $this->get(array('function' => 'archive',
223 'action' => 'buy',
224 'loc' => $loc,
225 'usage' => $usage),
226 $url);
229 function sell_archive($loc, $usage, &$url) {
230 return $this->get(array('function' => 'archive',
231 'action' => 'sell',
232 'loc' => $loc,
233 'usage' => $usage),
234 $url);
237 function touch_archive($loc, &$url) {
238 return $this->get(array('function' => 'archive',
239 'action' => 'touch',
240 'loc' => $loc),
241 $url);
244 function look_archive($hash, &$url) {
245 return $this->get(array('function' => 'archive',
246 'action' => 'look',
247 'hash' => $hash),
248 $url);
251 function write_archive($loc, $usage, $content, &$url) {
252 return $this->get(array('function' => 'archive',
253 'action' => 'write',
254 'loc' => $loc,
255 'usage' => $usage,
256 'content' => $content),
257 $url);
260 // Vending machines aren't implemented yet at Loom.cc,
261 // So fake it.
262 // Returns four different machines for first byte of $id mod 4 being 0, 1, 2, or 3
263 // Machine id of all 0 is invalid.
264 function query_vend($id, $key, &$url) {
265 $res = array('function' => 'queryvend',
266 'id' => $id,
267 'status' => 'success');
268 if ($id == 0) {
269 $res['status'] = 'error';
270 return;
272 $val = 3 & hexdec(substr($id, 0, 1));
273 $assets = $this->knownAssets();
274 $dollars = $assets['Cartwheel USD'];
275 $goldgrams = $assets['Patrick GoldGrams'];
276 $tokens = $assets['usage tokens'];
277 if ($val == 0) {
278 $res['input'] = $this->unscale(30, $dollars['scale']) . ':' .
279 $dollars['id'] . ' ' .
280 $this->unscale(15, $tokens['scale']) . ':' . $tokens['id'];
281 $res['output'] = $this->unscale(1, $goldgrams['scale']) . ':' .
282 $goldgrams['id'];
283 } else if ($val == 1) {
284 $res['input'] = $this->unscale(1, $goldgrams['scale']) . ':' .
285 $goldgrams['id'];
286 $res['output'] = $this->unscale(30, $dollars['scale']) . ':' .
287 $dollars['id'];
288 } else if ($val == 2) {
289 $res['input'] = $this->unscale(1, $dollars['scale']) . ':' .
290 $dollars['id'];
291 $res['output'] = $this->unscale(100, $tokens['scale']) . ':' .
292 $tokens['id'];
293 } else {
294 $res['input'] = $this->unscale(100, $tokens['scale']) . ':' .
295 $tokens['id'];
296 $res['output'] = $this->unscale(1, $dollars['scale']) . ':' .
297 $dollars['id'];
299 return $res;
302 // From https://loom.cc//?function=view&hash=db53fd011eb53ee9c4dbb5a88198cb3e34e1778feeaf3b2a118e619425ecbf6a
303 // Returns an array mapping the id and name of each known asset to
304 // an array mapping 'id', 'scale', 'precision', and 'name' to that asset's
305 // properties.
306 function knownAssets() {
307 $curs = array(array('id' => '00000000000000000000000000000000',
308 'scale' => 0,
309 'precision' => 0,
310 'name' => 'usage tokens'),
311 array('id' => 'a67b4f153e85d7cc6c846893dac1155c',
312 'scale' => 2,
313 'precision' => 2,
314 'name' => 'Cartwheel USD'),
315 array('id' => '1650f617c024d6441461b2538c6d9540',
316 'scale' => 7,
317 'precision' => 3,
318 'name' => 'GoldNowBanc GoldGrams'),
319 array('id' => 'c9c5ccc9957c3c8c0bec320c16da451a',
320 'scale' => 2,
321 'precision' => 2,
322 'name' => 'GoldNowBanc USD'),
323 array('id' => '40c28abac43d0001fd2aa5b705929852',
324 'scale' => 2,
325 'precision' => 0,
326 'name' => 'MetroPipe Tunneler Pro Tokens 1 Month'),
327 array('id' => 'd43b8563ea999c1f53279b60142be5fe',
328 'scale' => 2,
329 'precision' => 2,
330 'name' => 'Capulin Currency $'),
331 array('id' => 'f0d4c8bd09e9c8c276af9ddd7c514495',
332 'scale' => 7,
333 'precision' => 3,
334 'name' => 'Wontongold Grams'),
335 array('id' => '26ef701a952fe3d641a69bf859db71c2',
336 'scale' => 7,
337 'precision' => 3,
338 'name' => 'Patrick GoldGrams'));
339 $res = array();
340 foreach ($curs as $cur) {
341 $res[$cur['id']] = $cur;
342 $res[$cur['name']] = $cur;
344 return $res;
347 function url($prefix, $keys) {
348 $str = $prefix;
349 $delim = '?';
350 foreach($keys as $key => $value) {
351 $str .= $delim . $key . '=' . urlencode($value);
352 $delim = '&';
354 return $str;
357 function parseFolder($location, $folder) {
358 global $client;
360 $paren_pos = strpos($folder, "(");
361 if ($paren_pos || $folder[0] == '(') {
362 $kv = substr($folder, $paren_pos);
363 $res = array();
364 $keys = $client->parsekv($kv);
365 $keytypes = explode(' ', $keys['list_type']);
366 $types = array();
367 foreach ($keytypes as $keytype) {
368 $type = array('name' => $keys["type_name.$keytype"],
369 'id' => $keytype,
370 'min_precision' => blankToZero($keys["type_min_precision.$keytype"]),
371 'scale' => blankToZero($keys["type_scale.$keytype"]));
372 //$types[$keytype] = $type;
373 $types[$type['name']] = $type;
375 ksort($types);
376 $res['types'] = $types;
377 $keylocs = explode(' ', $keys['list_loc']);
378 $locs = array();
379 foreach ($keylocs as $keyloc) {
380 $name = $keys["loc_name.$keyloc"];
381 if ($name != '') {
382 if ($keyloc == $location) {
383 $folder_name = $name;
385 $locs[$name] = $keyloc;
386 //$locs[$keyloc] = $name;
389 ksort($locs);
390 $res['locs'] = $locs;
391 $res['name'] = $folder_name;
392 $res['loc'] = $location;
393 return $res;
395 return FALSE;
398 // Convert our array() representation of the folder
399 // into the string to write into the archive
400 // Not used. We let Loom itself munge the folder string.
401 function folderArchiveString($folder) {
402 $res = "Content-type: loom/folder\n\n(\n";
403 $types = "";
404 $ids = "";
405 foreach ($folder['types'] as $name => $type) {
406 $id = $type['id'];
407 if ($ids != '') $ids .= ' ';
408 $ids .= $id;
409 $min_precision = $type['min_precision'];
410 $scale = $type['scale'];
411 $types .= ":type_name.$id\n=" . $this->quote_cstring($name) . "\n";
412 if ($min_precision != '0') {
413 $types .= ":type_min_precision.$id\n=$min_precision\n";
415 if ($scale != '0') {
416 $types .= ":type_scale.$id\n=$scale\n";
419 $res .= ":list_type\n=$ids\n";
420 $res .= $types;
422 $ids = "";
423 $locations = "";
424 foreach ($folder['locs'] as $name => $location) {
425 if ($ids != '') $ids .= ' ';
426 $ids .= $location;
427 $locations .= ":loc_name.$location\n=" . $this->quote_cstring($name) . "\n";
429 $res .= ":list_loc\n=$ids\n";
430 $res .= $locations;
432 $res .= ")\n";
434 return $res;
437 // Return the session associated with a folder location
438 // Create a new session if one doesn't already exist,
439 // buying the location to store it as necessary.
440 // Returns false if it can't buy the session location
441 function folderSession($folder_location) {
442 $loc = $this->leftPadHex(bcxorhex($folder_location, "1"), 32);
443 $res = $this->touch_archive($loc, $url);
444 if ($res['status'] == 'success') {
445 return $res['content'];
447 $session = $this->random->random_id();
448 $res = $this->buy_archive($session, $folder_location, $url);
449 if ($res['status'] != 'success') return false;
450 // Probably don't need this, but you never know
451 $this->buy_archive($loc, $folder_location, $url);
452 $res = $this->write_archive($loc, $folder_location, $session, $url);
453 if ($res['status'] != 'success') return false;
454 return $session;
457 // This doesn't yet test that it worked.
458 // I'll wait for Patrick to make a KV-returning version,
459 // instead of attempting to parse the returned HTML
460 function renameFolderLocation($session, $oldname, $newname) {
461 return $this->rawget(array('function' => 'folder_locations',
462 'session' => $session,
463 'old_name' => $oldname,
464 'new_name' => $newname,
465 'save' => 'Save'),
466 $url);
469 // This doesn't yet test that it worked.
470 // I'll wait for Patrick to make a KV-returning version,
471 // instead of attempting to parse the returned HTML
472 function newFolderLocation($session, $newname, $newlocation) {
473 return $this->rawget(array('function' => 'folder_locations',
474 'session' => $session,
475 'add_location' => '1',
476 'loc' => $newlocation,
477 'name' => $newname,
478 'save' => 'Save'),
479 $url);
482 // Logout from Loom, destroying the old session
483 function logout($session) {
484 return $this->rawget(array('function' => 'folder',
485 'logout' => '1',
486 'session' => $session),
487 $url);
490 function parsekv($kv, $recursive=FALSE) {
491 $lines = explode("\n", $kv);
492 $first = true;
493 $res = array();
494 $stackptr = 0;
495 $stack = array();
496 foreach ($lines as $line) {
497 $line = trim($line);
498 //echo "$line<br>\n";
499 if ($first && ($line != '(')) {
500 return $res; // Could throw exception in PHP 5
502 $first = false;
503 if ($line == ')') {
504 if (!$recursive || $stackptr == 0) return $res;
505 $child = $res;
506 $res = $stack[--$stackptr];
507 $key = $stack[--$stackptr];
508 $res[$key] = $child;
509 //echo "popped: $stackptr<pre>\n"; print_r($res); echo "</pre>\n";
510 } else {
511 if (substr($line, 0, 1) == ':') $key = substr($line, 1);
512 elseif (substr($line, 0, 1) == '=') {
513 $value = substr($line, 1);
514 if ($recursive && $value == "(") {
515 $child = array();
516 $stack[$stackptr++] = $key;
517 $stack[$stackptr++] = $res;
518 //echo "pushed: $stackptr<br>\n";
519 $res = $child;
520 } else {
521 $value = $this->unquote_cstring($value);
522 $res[$key] = $value;
529 function array2kv($array, $res="(\n") {
530 foreach ($array as $key => $value) {
531 $res .= ":$key\n";
532 if (is_array($value)) $res = $this->array2kv($value, $res . "=(\n");
533 else $res .= "=" . $this->quote_cstring($value) . "\n";
535 $res .= ")\n";
536 return $res;
539 function quote_cstring($cstring) {
540 $res = '';
541 for ($i=0; $i<strlen($cstring); $i++) {
542 $chr = substr($cstring, $i, 1);
543 if ($chr == "\n") $res .= '\n';
544 elseif ($chr == '"') $res .= '\"';
545 elseif ($chr == "\t") $res .= '\t';
546 elseif ($chr == "\\") $res .= "\\\\";
547 elseif ($chr < ' ' || $chr > '~') $res .= '\\'.sprintf('%03o', ord($chr));
548 else $res .= $chr;
550 return $res;
553 function unquote_cstring($cstring) {
554 $res = '';
555 $len = strlen($cstring);
556 $i = 0;
557 while ($i<$len) {
558 $chr = substr($cstring, $i, 1);
559 if ($chr == "\\") {
560 $i++;
561 if ($i >= $len) {
562 $res .= $chr;
563 break;
565 $chr = substr($cstring, $i, 1);
566 if ($chr == 'n') $res .= "\n";
567 elseif ($chr == '"') $res .= '"';
568 elseif ($chr == 't') $res .= "\t";
569 elseif ($chr == "\\") $res .= "\\";
570 elseif ($chr >= '0' and $chr <= '9') {
571 if ($len < ($i + 3)) {
572 $res .= substr($cstring, $i-1);
573 break;
575 sscanf(substr($cstring, $i, 3), '%o', $n);
576 $res .= chr($n);
577 $i += 2;
579 else $res .= "\\" . $chr;
581 else {
582 $res .= $chr;
584 $i++;
586 return $res;
589 // Return the sha256 hash of a string.
590 // The result is encoded as hex, and guaranteed to be 64 charaacters,
591 // with leading zeroes added, if necessary.
592 function sha256($str) {
593 if (function_exists('hash_init')) {
594 // Modern PHP
595 $ctx = hash_init('sha256');
596 hash_update($ctx, $str);
597 $hash = hash_final($ctx);
598 } else if (function_exists('mhash')) {
599 // Old PHP with mhash compiled in
600 $hash = bin2hex(mhash(MHASH_SHA256, $str));
601 } else {
602 // Not a hash, really, but the best we can do
603 $hash = bin2hex($str);
604 if (strlen($hash) > 64) $hash = substr($hash, 1, 64);
606 return $this->leftPadHex($hash, 64);
609 // PHP has bin2hex($x). An easier to remember name for pack("H*", $x)
610 // Note that this does NOT get you a string that looks like a decimal number.
611 // It's raw bits, 8 bits per characetr.
612 function hex2bin($x) {
613 return pack("H*", $x);
616 // Loom changes an SHA256 hash to a location by xoring the two halves
617 // Input and output are both encoded as hex
618 // Won't work correctly
619 function hash2location($hash) {
620 $value = bchexdec($hash);
621 $bits128 = $this->bits128;
622 $location = bcxor(bcrightshift($value, 128), bcand($value, $bits128));
623 return $this->leftPadHex(bcdechex($location), 32);
626 function leftPadHex($hex, $chars) {
627 if (strlen($hex) < $chars) {
628 $hex = str_repeat("0", $chars - strlen($hex)) . $hex;
630 return $hex;
633 function xorLocations($l1, $l2) {
634 return $this->leftPadHex(bcxorhex($l1, $l2), 32);
637 // Returns true if $location is occupied for $type, i.e. if touch() will succeed
638 function isLocationOccuppied($type, $location) {
639 $res = $this->touch($type, $location, $url);
640 return $res['status'] == 'success';
643 // Returns true if $location is vacant for $type,
644 // i.e. if touch() fails with vacant
645 function isLocationVacant($type, $location) {
646 $res = $this->touch($type, $location, $url);
647 return ($res['status'] == 'fail') && ($res['error_loc'] == 'vacant');
650 // Return a random vacant location.
651 // If can't find one after 10 tries, return false.
652 function randomVacantLocation($type) {
653 for ($i=0; $i<10; $i++) {
654 $id = $this->random->random_id();
655 if ($this->isLocationVacant($type, $id)) return $id;
657 return false;
660 // Test that an ID is a valid 32-character hex string
661 function isValidID($id) {
662 return (strlen($id) == 32) && preg_match('/[a-f0-9]{32}/', $id);
665 } // End of LoomClient class
668 /* Testing code. Uncomment to run.
669 $api = new LoomClient();
670 $values = $api->move('12345678123456781234567812345678',
672 '22345678123456781234567812345678',
673 '32345678123456781234567812345678',
674 &$url);
675 echo $url . "\n";
676 print_r($values);
679 /* ***** BEGIN LICENSE BLOCK *****
680 * Version: MPL 1.1/GPL 2.0/LGPL 2.1/Apache 2.0
682 * The contents of this file are subject to the Mozilla Public License Version
683 * 1.1 (the "License"); you may not use this file except in compliance with
684 * the License. You may obtain a copy of the License at
685 * http://www.mozilla.org/MPL/
687 * Software distributed under the License is distributed on an "AS IS" basis,
688 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
689 * for the specific language governing rights and limitations under the
690 * License.
692 * The Original Code is LoomClient PHP library
694 * The Initial Developer of the Original Code is
695 * Bill St. Clair.
696 * Portions created by the Initial Developer are Copyright (C) 2008
697 * the Initial Developer. All Rights Reserved.
699 * Contributor(s):
700 * Bill St. Clair <bill@billstclair.com>
702 * Alternatively, the contents of this file may be used under the
703 * terms of the GNU General Public License Version 2 or later (the
704 * "GPL"), the GNU Lesser General Public License Version 2.1 or later
705 * (the "LGPL"), or The Apache License Version 2.0 (the "AL"), in
706 * which case the provisions of the GPL, LGPL, or AL are applicable
707 * instead of those above. If you wish to allow use of your version of
708 * this file only under the terms of the GPL, the LGPL, or the AL, and
709 * not to allow others to use your version of this file under the
710 * terms of the MPL, indicate your decision by deleting the provisions
711 * above and replace them with the notice and other provisions
712 * required by the GPL or the LGPL. If you do not delete the
713 * provisions above, a recipient may use your version of this file
714 * under the terms of any one of the MPL, the GPL the LGPL, or the AL.
715 ****** END LICENSE BLOCK ***** */