corrade-lsl-templates – Blame information for rev 4

Subversion Repositories:
Rev:
Rev Author Line No. Line
4 office 1 ///////////////////////////////////////////////////////////////////////////
2 // Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3 //
3 ///////////////////////////////////////////////////////////////////////////
4 //
5 // This project makes Corrade, the Second Life / OpenSim bot track all the
6 // visitors on a region and record information to the group database. More
7 // information on the Corrade Second Life / OpenSim scripted agent can be
8 // found at the URL: http://grimore.org/secondlife/scripted_agents/corrade
9 //
10 // The script works in combination with a "configuration" notecard that
11 // must be placed in the same primitive as this script. The purpose of this
12 // script is to illustrate the Corrade built-in group database features
13 // and you are free to use, change, and commercialize it under the terms
14 // of the GNU/GPLv3 license at: http://www.gnu.org/licenses/gpl.html
15 //
16 ///////////////////////////////////////////////////////////////////////////
17  
18 ///////////////////////////////////////////////////////////////////////////
19 // Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3 //
20 ///////////////////////////////////////////////////////////////////////////
21 string wasKeyValueGet(string k, string data) {
22 if(llStringLength(data) == 0) return "";
23 if(llStringLength(k) == 0) return "";
24 list a = llParseString2List(data, ["&", "="], []);
25 integer i = llListFindList(a, [ k ]);
26 if(i != -1) return llList2String(a, i+1);
27 return "";
28 }
29  
30 ///////////////////////////////////////////////////////////////////////////
31 // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 //
32 ///////////////////////////////////////////////////////////////////////////
33 string wasKeyValueEncode(list data) {
34 list k = llList2ListStrided(data, 0, -1, 2);
35 list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2);
36 data = [];
37 do {
38 data += llList2String(k, 0) + "=" + llList2String(v, 0);
39 k = llDeleteSubList(k, 0, 0);
40 v = llDeleteSubList(v, 0, 0);
41 } while(llGetListLength(k) != 0);
42 return llDumpList2String(data, "&");
43 }
44  
45 ///////////////////////////////////////////////////////////////////////////
46 // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 //
47 ///////////////////////////////////////////////////////////////////////////
48 // escapes a string in conformance with RFC1738
49 string wasURLEscape(string i) {
50 string o = "";
51 do {
52 string c = llGetSubString(i, 0, 0);
53 i = llDeleteSubString(i, 0, 0);
54 if(c == "") jump continue;
55 if(c == " ") {
56 o += "+";
57 jump continue;
58 }
59 if(c == "\n") {
60 o += "%0D" + llEscapeURL(c);
61 jump continue;
62 }
63 o += llEscapeURL(c);
64 @continue;
65 } while(i != "");
66 return o;
67 }
68  
69 ///////////////////////////////////////////////////////////////////////////
70 // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 //
71 ///////////////////////////////////////////////////////////////////////////
72 // unescapes a string in conformance with RFC1738
73 string wasURLUnescape(string i) {
74 return llUnescapeURL(
75 llDumpList2String(
76 llParseString2List(
77 llDumpList2String(
78 llParseString2List(
79 i,
80 ["+"],
81 []
82 ),
83 " "
84 ),
85 ["%0D%0A"],
86 []
87 ),
88 "\n"
89 )
90 );
91 }
92  
93 ///////////////////////////////////////////////////////////////////////////
94 // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 //
95 ///////////////////////////////////////////////////////////////////////////
96 list wasCSVToList(string csv) {
97 list l = [];
98 list s = [];
99 string m = "";
100 do {
101 string a = llGetSubString(csv, 0, 0);
102 csv = llDeleteSubString(csv, 0, 0);
103 if(a == ",") {
104 if(llList2String(s, -1) != "\"") {
105 l += m;
106 m = "";
107 jump continue;
108 }
109 m += a;
110 jump continue;
111 }
112 if(a == "\"" && llGetSubString(csv, 0, 0) == a) {
113 m += a;
114 csv = llDeleteSubString(csv, 0, 0);
115 jump continue;
116 }
117 if(a == "\"") {
118 if(llList2String(s, -1) != a) {
119 s += a;
120 jump continue;
121 }
122 s = llDeleteSubList(s, -1, -1);
123 jump continue;
124 }
125 m += a;
126 @continue;
127 } while(csv != "");
128 // postcondition: length(s) = 0
129 return l + m;
130 }
131  
132 // corrade data
133 key CORRADE = NULL_KEY;
134 string GROUP = "";
135 string PASSWORD = "";
136  
137 // for holding the callback URL
138 string callback = "";
139  
140 // for notecard reading
141 integer line = 0;
142  
143 // key-value data will be read into this list
144 list tuples = [];
145  
146  
147 default {
148 state_entry() {
149 if(llGetInventoryType("configuration") != INVENTORY_NOTECARD) {
150 llOwnerSay("Sorry, could not find a configuration inventory notecard.");
151 return;
152 }
153 // DEBUG
154 llOwnerSay("Reading configuration file...");
155 llGetNotecardLine("configuration", line);
156 }
157 dataserver(key id, string data) {
158 if(data == EOF) {
159 // invariant, length(tuples) % 2 == 0
160 if(llGetListLength(tuples) % 2 != 0) {
161 llOwnerSay("Error in configuration notecard.");
162 return;
163 }
164 CORRADE = llList2Key(
165 tuples,
166 llListFindList(
167 tuples,
168 [
169 "corrade"
170 ]
171 )
172 +1
173 );
174 if(CORRADE == NULL_KEY) {
175 llOwnerSay("Error in configuration notecard: corrade");
176 return;
177 }
178 GROUP = llList2String(
179 tuples,
180 llListFindList(
181 tuples,
182 [
183 "group"
184 ]
185 )
186 +1
187 );
188 if(GROUP == "") {
189 llOwnerSay("Error in configuration notecard: group");
190 return;
191 }
192 PASSWORD = llList2String(
193 tuples,
194 llListFindList(
195 tuples,
196 [
197 "password"
198 ]
199 )
200 +1
201 );
202 if(PASSWORD == "") {
203 llOwnerSay("Error in configuration notecard: password");
204 return;
205 }
206 // DEBUG
207 llOwnerSay("Read configuration notecard...");
208 tuples = [];
209 state url;
210 }
211 if(data == "") jump continue;
212 integer i = llSubStringIndex(data, "#");
213 if(i != -1) data = llDeleteSubString(data, i, -1);
214 list o = llParseString2List(data, ["="], []);
215 // get rid of starting and ending quotes
216 string k = llDumpList2String(
217 llParseString2List(
218 llStringTrim(
219 llList2String(
220 o,
221  
222 ),
223 STRING_TRIM),
224 ["\""], []
225 ), "\"");
226 string v = llDumpList2String(
227 llParseString2List(
228 llStringTrim(
229 llList2String(
230 o,
231 1
232 ),
233 STRING_TRIM),
234 ["\""], []
235 ), "\"");
236 if(k == "" || v == "") jump continue;
237 tuples += k;
238 tuples += v;
239 @continue;
240 llGetNotecardLine("configuration", ++line);
241 }
242 on_rez(integer num) {
243 llResetScript();
244 }
245 changed(integer change) {
246 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
247 llResetScript();
248 }
249 }
250 }
251  
252 state url {
253 state_entry() {
254 // DEBUG
255 llOwnerSay("Requesting URL...");
256 llRequestURL();
257 }
258 http_request(key id, string method, string body) {
259 if(method != URL_REQUEST_GRANTED) return;
260 callback = body;
261 // DEBUG
262 llOwnerSay("Got URL...");
263 state detect;
264 }
265 on_rez(integer num) {
266 llResetScript();
267 }
268 changed(integer change) {
269 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
270 llResetScript();
271 }
272 }
273 }
274  
275 state detect {
276 state_entry() {
277 // DEBUG
278 llOwnerSay("Detecting if Corrade is online...");
279 llSetTimerEvent(5);
280 }
281 timer() {
282 llRequestAgentData((key)CORRADE, DATA_ONLINE);
283 }
284 dataserver(key id, string data) {
285 if(data != "1") {
286 // DEBUG
287 llOwnerSay("Corrade is not online, sleeping...");
288 llSetTimerEvent(30);
289 return;
290 }
291 state initialize;
292 }
293 on_rez(integer num) {
294 llResetScript();
295 }
296 changed(integer change) {
297 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
298 llResetScript();
299 }
300 }
301 }
302  
303 state initialize {
304 state_entry() {
305 // DEBUG
306 llOwnerSay("Creating the database if it does not exist...");
307 llInstantMessage(
308 (key)CORRADE,
309 wasKeyValueEncode(
310 [
311 "command", "database",
312 "group", wasURLEscape(GROUP),
313 "password", wasURLEscape(PASSWORD),
314 "SQL", wasURLEscape(
315 "CREATE TABLE IF NOT EXISTS visitors (
316 'firstname' TEXT NOT NULL,
317 'lastname' TEXT NOT NULL,
318 'lastseen' TEXT NOT NULL,
319 'time' INTEGER NOT NULL,
320 'memory' INTEGER NOT NULL,
321 PRIMARY KEY ('firstname', 'lastname')
322 )"
323 ),
324 "callback", wasURLEscape(callback)
325 ]
326 )
327 );
328 // alarm 60
329 llSetTimerEvent(60);
330 }
331 timer() {
332 // DEBUG
333 llOwnerSay("Timeout creating table...");
334 llResetScript();
335 }
336 http_request(key id, string method, string body) {
337 llHTTPResponse(id, 200, "OK");
338 if(wasKeyValueGet("command", body) != "database") return;
339 if(wasKeyValueGet("success", body) != "True") {
340 // DEBUG
341 llOwnerSay("Failed to create the table: " +
342 wasURLUnescape(
343 wasKeyValueGet(
344 "error",
345 body
346 )
347 )
348 );
349 return;
350 }
351 // DEBUG
352 llOwnerSay("Table created...");
353 state show;
354 }
355 on_rez(integer num) {
356 llResetScript();
357 }
358 changed(integer change) {
359 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
360 llResetScript();
361 }
362 }
363 state_exit() {
364 llSetTimerEvent(0);
365 }
366 }
367  
368 state show {
369 state_entry() {
370 // DEBUG
371 llOwnerSay("Updating display with the number of recorded visitors...");
372 llInstantMessage(
373 (key)CORRADE,
374 wasKeyValueEncode(
375 [
376 "command", "database",
377 "group", wasURLEscape(GROUP),
378 "password", wasURLEscape(PASSWORD),
379 "SQL", wasURLEscape(
380 "SELECT
381 COUNT(*) AS 'Visits',
382 AVG(time) AS 'Time',
383 AVG(memory) AS 'Memory'
384 FROM visitors"
385 ),
386 "callback", wasURLEscape(callback)
387 ]
388 )
389 );
390 // alarm 60
391 llSetTimerEvent(60);
392 }
393 timer() {
394 // DEBUG
395 llOwnerSay("Timeout reading rows from visitors table...");
396 llResetScript();
397 }
398 http_request(key id, string method, string body) {
399 llHTTPResponse(id, 200, "OK");
400 if(wasKeyValueGet("command", body) != "database") return;
401 if(wasKeyValueGet("success", body) != "True") {
402 // DEBUG
403 llOwnerSay("Failed to enumerate visitors: " +
404 wasURLUnescape(
405 wasKeyValueGet(
406 "error",
407 body
408 )
409 )
410 );
411 llResetScript();
412 }
413 list data = wasCSVToList(
414 wasURLUnescape(
415 wasKeyValueGet(
416 "data",
417 body
418 )
419 )
420 );
421 integer visits = llList2Integer(
422 data,
423 llListFindList(
424 data,
425 (list)"Visits"
426 ) + 1
427 );
428 integer time = llList2Integer(
429 data,
430 llListFindList(
431 data,
432 (list)"Time"
433 ) + 1
434 );
435 integer memory = llList2Integer(
436 data,
437 llListFindList(
438 data,
439 (list)"Memory"
440 ) + 1
441 );
442 llMessageLinked(LINK_ROOT, 204000, "V:" + (string)visits, "0");
443 llMessageLinked(LINK_ROOT, 204000, "T:" + (string)time + "m", "1");
444 llMessageLinked(LINK_ROOT, 204000, "M:" + (string)memory + "k", "2");
445 state scan;
446 }
447 link_message(integer sender_num, integer num, string str, key id) {
448 if(str == "reset")
449 state reset;
450 if(str == "display") {
451 line = 0;
452 state display;
453 }
454 }
455 on_rez(integer num) {
456 llResetScript();
457 }
458 changed(integer change) {
459 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
460 llResetScript();
461 }
462 }
463 state_exit() {
464 llSetTimerEvent(0);
465 }
466 }
467  
468 state scan {
469 state_entry() {
470 // DEBUG
471 llOwnerSay("Scanning for visitors...");
472 // Scan for visitors every 60 seconds.
473 llSetTimerEvent(60);
474 }
475 timer() {
476 // Check if Corrade is online.
477 llRequestAgentData((key)CORRADE, DATA_ONLINE);
478 // Get agents
479 list as = llGetAgentList(AGENT_LIST_REGION, []);
480 do {
481 key a = llList2Key(as, 0);
482 as = llDeleteSubList(as, 0, 0);
483 list name = llParseString2List(llKey2Name(a), [" "], []);
484 if(llGetListLength(name) != 2) return;
485 string fn = llList2String(name, 0);
486 string ln = llList2String(name, 1);
487 // The command sent to Corrade responsible for adding a visitor
488 // or updating the data for the visitor in case the visitor is
489 // already entered into the visitors table. This is performed
490 // with an INSER OR REPLACE sqlite command given the first name
491 // and the last name of the avatar are unique primary keys.
492 llInstantMessage(
493 (key)CORRADE,
494 wasKeyValueEncode(
495 [
496 "command", "database",
497 "group", wasURLEscape(GROUP),
498 "password", wasURLEscape(PASSWORD),
499 "SQL", wasURLEscape(
500 "INSERT OR REPLACE INTO visitors (
501 firstname,
502 lastname,
503 lastseen,
504 time,
505 memory
506 ) VALUES(
507 '" + fn + "', '" + ln + "', '" + llGetTimestamp() + "',
508 COALESCE(
509 (
510 SELECT time FROM visitors WHERE
511 firstname='" + fn + "' AND lastname='" + ln + "'
512 ) + 1
513 ,
514 1
515 ), " +
516 (string)(
517 (integer)(
518 llList2Float(
519 llGetObjectDetails(
520 a,
521 [OBJECT_SCRIPT_MEMORY]
522 ),
523  
524 )
525 /
526 1024 /*in kib, to mib 1048576*/
527 )
528  
529 ) +
530 ")"
531 )
532 ]
533 )
534 );
535 } while(llGetListLength(as));
536 state show;
537 }
538 link_message(integer sender_num, integer num, string str, key id) {
539 if(str == "reset")
540 state reset;
541 if(str == "display") {
542 line = 0;
543 state display;
544 }
545 }
546 dataserver(key id, string data) {
547 if(data == "1") return;
548 // DEBUG
549 llOwnerSay("Corrade is not online, sleeping...");
550 // Switch to detect loop and wait there for Corrade to come online.
551 state detect;
552 }
553 on_rez(integer num) {
554 llResetScript();
555 }
556 changed(integer change) {
557 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
558 llResetScript();
559 }
560 }
561 state_exit() {
562 llSetTimerEvent(0);
563 }
564 }
565  
566 state display_trampoline {
567 state_entry() {
568 ++line;
569 state display;
570 }
571 link_message(integer sender_num, integer num, string str, key id) {
572 if(str == "reset")
573 state reset;
574 }
575 }
576  
577 state display {
578 state_entry() {
579 llInstantMessage(
580 (key)CORRADE,
581 wasKeyValueEncode(
582 [
583 "command", "database",
584 "group", wasURLEscape(GROUP),
585 "password", wasURLEscape(PASSWORD),
586 "SQL", wasURLEscape(
587 "SELECT * FROM visitors
588 ORDER BY lastseen DESC
589 LIMIT 1
590 OFFSET " + (string)line
591 ),
592 "callback", wasURLEscape(callback)
593 ]
594 )
595 );
596 // alarm 60
597 llSetTimerEvent(60);
598 }
599 http_request(key id, string method, string body) {
600 llHTTPResponse(id, 200, "OK");
601 if(wasKeyValueGet("command", body) != "database") return;
602 if(wasKeyValueGet("success", body) != "True") {
603 // DEBUG
604 llOwnerSay("Failed to query the table: " +
605 wasURLUnescape(
606 wasKeyValueGet(
607 "error",
608 body
609 )
610 )
611 );
612 return;
613 }
614 // Grab the data key if it exists.
615 string dataKey = wasURLUnescape(
616 wasKeyValueGet(
617 "data",
618 body
619 )
620 );
621  
622 // We got no more rows, so switch back to scanning.
623 if(dataKey == "")
624 state scan;
625  
626 list data = wasCSVToList(dataKey);
627  
628 string firstname = llList2String(
629 data,
630 llListFindList(
631 data,
632 (list)"firstname"
633 ) + 1
634 );
635 string lastname = llList2String(
636 data,
637 llListFindList(
638 data,
639 (list)"lastname"
640 ) + 1
641 );
642 string lastseen = llList2String(
643 data,
644 llListFindList(
645 data,
646 (list)"lastseen"
647 ) + 1
648 );
649  
650 llOwnerSay(firstname + " " + lastname + " @ " + lastseen);
651 state display_trampoline;
652 }
653 link_message(integer sender_num, integer num, string str, key id) {
654 if(str == "reset")
655 state reset;
656 }
657 timer() {
658 // DEBUG
659 llOwnerSay("Timeout reading rows from visitors table...");
660 llResetScript();
661 }
662 on_rez(integer num) {
663 llResetScript();
664 }
665 changed(integer change) {
666 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
667 llResetScript();
668 }
669 }
670 state_exit() {
671 llSetTimerEvent(0);
672 }
673 }
674  
675 state reset {
676 state_entry() {
677 // DEBUG
678 llOwnerSay("Resetting all visitors...");
679 llInstantMessage(
680 (key)CORRADE,
681 wasKeyValueEncode(
682 [
683 "command", "database",
684 "group", wasURLEscape(GROUP),
685 "password", wasURLEscape(PASSWORD),
686 "SQL", "DROP TABLE visitors",
687 "callback", wasURLEscape(callback)
688 ]
689 )
690 );
691 // alarm 60
692 llSetTimerEvent(60);
693 }
694 timer() {
695 // DEBUG
696 llOwnerSay("Timeout deleting database...");
697 llResetScript();
698 }
699 http_request(key id, string method, string body) {
700 llHTTPResponse(id, 200, "OK");
701 if(wasKeyValueGet("command", body) != "database") return;
702 if(wasKeyValueGet("success", body) != "True") {
703 // DEBUG
704 llOwnerSay("Failed to drop the visitors table: " +
705 wasURLUnescape(
706 wasKeyValueGet(
707 "error",
708 body
709 )
710 )
711 );
712 llResetScript();
713 }
714 // DEBUG
715 llOwnerSay("Table dropped...");
716 llResetScript();
717 }
718 on_rez(integer num) {
719 llResetScript();
720 }
721 changed(integer change) {
722 if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
723 llResetScript();
724 }
725 }
726 state_exit() {
727 llSetTimerEvent(0);
728 }
729 }