Welcome to the Second Life Forums Archive

These forums are CLOSED. Please visit the new forums HERE

Please critique this vendor script

Zyzzy Zarf
Registered User
Join date: 24 Jan 2007
Posts: 7
04-29-2007 10:59
Unable to find a simple 1-prim vendor script that did what I wanted, I wrote my own, based on the code I found here. I'm releasing it into the wild, and would really appreciate anyone's comments on it. As I'm somewhat nervous about setting out objects that can take money from me, I'm especially interested in any security problems you may see.

BTW, the logging script on my web server is a trivial perl script that just appends what it gets to a file.

I've obscured the URL in this code.

TIA.
---------------------
CODE

// ZyzzyVendor
//
// Simple single-prim vendor with logging.
//
// Written April 29, 2007 by Zyzzy Zarf
//
// Intended for objects that vend copies of themselves.
// One partner is allowed, who will get a cut of the proceeds.
// The first object (alphabetically) in the inventory of the vendor is the vended object.
//
// Some code derived from the 1-prim vendor by Hiro Pendragon.
//
// Licensed under Creative Commons Attribution 3.0
// see <http://creativecommons.org/licenses/by/3.0/>
// Any re-use or adaptation must credit Zyzzy Zarf as a source.

// a marker that there is no price. A high positive number to avoid bugs resulting in free objects.
integer kNoPrice = 9999999;
// this script will never give out more than this amount. Ensure that it's reasonable.
integer kMaxMoneyGive = 1000;
// log to this URL with POST calls
// ***** CHANGE THIS TO YOUR CGI URL *****
string kLoggingURL = "http://yourdomain.com/yourlogger.cgi";
// bump to distinguish versions in the field
string kVersionString = "ZyzzyVendor V1.0";
// channel on which to listen for commands
integer kCmdChan = 9;

key gOwnerKey = NULL_KEY;
key gPartnerKey = NULL_KEY;
string gPartnerName = "";
integer gSplitPercent = 0;
integer gPrice = kNoPrice;
key gLastToucherKey = NULL_KEY;
string gVendedObjectName = "";
// set to 1 when an HTTP request has been made, but not yet ack'd
integer gOutstandingAcks = 0;
// the location of the vendor
string gLocation = "";

// returns an integer location string
string simpleLoc()
{
vector myLocation = llGetPos();
integer xloc = (integer)myLocation.x;
integer yloc = (integer)myLocation.y;
integer zloc = (integer)myLocation.z;

return llGetRegionName() + " <" + (string)xloc + ", " + (string)yloc + ", " + (string)zloc + ">";
}

reset()
{
// user-set state
gPrice = kNoPrice;
gPartnerKey = NULL_KEY;
gPartnerName = "";
gSplitPercent = 0;
gLastToucherKey = NULL_KEY;
}

initialize()
{
gOwnerKey = llGetOwner();
gVendedObjectName = llGetInventoryName( INVENTORY_OBJECT, 0 );
gOutstandingAcks = 0;
reset();
gLocation = simpleLoc();
// so that we can split money
llRequestPermissions( gOwnerKey, PERMISSION_DEBIT );
}

integer checkStatusValidity()
{
// we must have an object to vend and a price to be usable
return gVendedObjectName != "" && gPrice != kNoPrice && gPrice >= 0 && gSplitPercent <= 100 && gSplitPercent >= 0;
}

// returns whether there's still a vendable object in inventory
integer checkChange( integer change )
{
if ( change != CHANGED_INVENTORY )
return TRUE;

gVendedObjectName = llGetInventoryName( INVENTORY_OBJECT, 0 );
return gVendedObjectName != "";
}

logEvent( string reason, key avKey, integer amtPaid )
{
if ( gOutstandingAcks > 0 )
llInstantMessage( gOwnerKey, llGetObjectName() + " - Missed an HTTP log response" );

// log the sale to my web server
string avName;
if ( avKey == NULL_KEY )
avName = "Testing Testing";
else if ( avKey == gPartnerKey ) {
// partner may not be in same sim
avName = gPartnerName;
}
else
avName = llKey2Name( avKey );
if ( avName == "" ) {
// shouldn't happen, but use key if no name
avName = (string)avKey;
}
// comment out this block if you're not doing HTTP logging
integer utime = llGetUnixTime();
string postData = "time=" + (string)utime + "&reason=" + reason + "&avname=" + avName + "&vendorname=" + llGetObjectName() + "&location=" + gLocation + "&paid=" + (string)amtPaid + "&price=" + (string)gPrice;
postData = llEscapeURL( postData );
gOutstandingAcks = 1;
llHTTPRequest( kLoggingURL, [HTTP_METHOD, "POST"], postData );

// IM the owner and partner about the event
string msg = reason + ", Av: " + avName + ", amount: L$" + (string)amtPaid + ", price: L$" + (string)gPrice + ", vendor: " + llGetObjectName() + ", location: " + gLocation;
llInstantMessage( gOwnerKey, msg );
if ( gPartnerKey != NULL_KEY )
llInstantMessage( gPartnerKey, msg );
}

// add some sanity checks to giveMoney so I don't go broke because of a bug
giveMoney( string reason, key recipient, integer amount )
{
if ( amount < 1 )
return;

if ( amount > kMaxMoneyGive || amount > gPrice ) {
logEvent( "InvalidPayoutError for " + reason, recipient, amount );
return;
}
logEvent( reason, recipient, amount );
llGiveMoney( recipient, amount );
}

default
{
state_entry()
{
initialize();

state setupMode;
}

on_rez( integer param )
{
initialize();

state setupMode;
}
}

state setupMode
{
state_entry()
{
// listen only to owner, and on channel 3
// listens are automatically removed when exiting a state
llListen( kCmdChan, "", gOwnerKey, "" );
gLastToucherKey = NULL_KEY;

llOwnerSay( kVersionString );
llOwnerSay( llGetObjectName() + " is listening for vendor commands on channel " + (string)kCmdChan + ". Say '/" + (string)kCmdChan + " v help' for help." );
// revert to run if possible and owner has not given any commands
llSetTimerEvent( 120 );
}

// ensure that we start fresh when rezzed
on_rez( integer param )
{
state default;
}

listen( integer channel, string name, key id, string message )
{
// all our commands start with 'v '
if ( llGetSubString( message, 0, 1 ) != "v " )
return;

// turn off the timer since the owner is clearly entering commands
llSetTimerEvent( 0 );

message = llGetSubString( message, 2, -1 );
string cmd = message;
string args = "";

// find the first space char, which delimits the command
integer delimiterPos = llSubStringIndex( message, " " );
if ( delimiterPos != -1 ) {
cmd = llGetSubString( message, 0, delimiterPos - 1 );
args = llGetSubString( message, delimiterPos + 1, -1 );
}
cmd = llToLower( cmd );

if ( cmd == "help" ) {
llOwnerSay( kVersionString );
llOwnerSay( "Vendor commands. Chat on channel " + (string)kCmdChan );
llOwnerSay( "v help - this message" );
llOwnerSay( "v price <num> - set price to num" );
llOwnerSay( "v partner - sets the partner to get a split. Partner must touch before command.");
llOwnerSay( "v split <%> - sets % of revenue split. % is an integer from 1 to 100." );
llOwnerSay( "v nosplit - turns off splitting" );
llOwnerSay( "v reset - resets all settings" );
llOwnerSay( "v status - lists settings" );
llOwnerSay( "v test - tests logger" );
llOwnerSay( "v run - stop listening and vend. Owner touch to start listening again" );
return;
}
if ( cmd == "run" ) {
if ( checkStatusValidity() ) {
llOwnerSay( "Running vendor" );
state runMode;
}
llOwnerSay( "Invalid setup. Can't run.");
return;
}
if ( cmd == "price" ) {
gPrice = (integer)args;
llOwnerSay( "Price is now " + (string)gPrice );
return;
}
if ( cmd == "nosplit" ) {
gPartnerKey = NULL_KEY;
gSplitPercent = 0;
llOwnerSay( "Removed partner and split" );
return;
}
if ( cmd == "partner" ) {
if ( gLastToucherKey == NULL_KEY ) {
llOwnerSay( "Partner must touch vendor before you give the partner command" );
return;
}
gPartnerKey = gLastToucherKey;
gPartnerName = llKey2Name( gPartnerKey );
llOwnerSay( "Partner set to " + gPartnerName );
// let the partner hear this
llWhisper( 0, gPartnerName + " will receive " + (string)gSplitPercent + "% of each sale" );
return;
}
if ( cmd == "split" ) {
integer splitPercent = (integer)args;

if ( splitPercent < 1 || splitPercent > 100 ) {
llOwnerSay( "Split percent must be an integer between 1 and 100" );
return;
}
gSplitPercent = splitPercent;
string partner = "Partner";
if ( gPartnerKey != NULL_KEY )
partner = gPartnerName;
llOwnerSay( partner + "will receive " + (string)gSplitPercent + "% of each sale" );
return;
}
if ( cmd == "reset" ) {
reset();
llOwnerSay( "All settings reset" );
return;
}
if ( cmd == "status" ) {
string str;
llOwnerSay( "Status:" );
str = "No object to vend in contents";
if ( gVendedObjectName != "" )
str = "Vending object named " + gVendedObjectName;
llOwnerSay( str );
str = "No price set";
if ( gPrice != kNoPrice )
str = "Price: " + (string)gPrice;
llOwnerSay( str );
str = "No partner set";
if ( gPartnerKey != NULL_KEY )
str = gPartnerName + " will receive " + (string)gSplitPercent + "% of each sale";
llOwnerSay( str );
return;
}
if ( cmd == "test" ) {
logEvent( "Test", NULL_KEY, 12345 );
return;
}
llOwnerSay( "Unrecognized command: '" + message + "'" );
}

touch_start( integer param )
{
key toucher = llDetectedKey( 0 );

// ignore owner touches - can't be partner too
if ( toucher == gOwnerKey )
return;

gLastToucherKey = toucher;

// tell that we've been set for a partner
llWhisper( 0, "Ready to partner with " + llKey2Name( gLastToucherKey ));
}

changed( integer change )
{
checkChange( change );
}

http_response( key request_id, integer status, list metadata, string body )
{
gOutstandingAcks = 0;

llOwnerSay( llGetObjectName() + " - HTTP response: " + (string)status );
}

timer()
{
// turn off the timer
llSetTimerEvent( 0 );

// if we can, switch back to run mode
if ( checkStatusValidity() ) {
llOwnerSay( "No commands seen after 2 minutes. Running vendor" );
state runMode;
}
}
}

state runMode
{
state_entry()
{
llOwnerSay( llGetObjectName() + " has stopped listening for commands. Touch to listen again." );

// allow only one pay price
llSetPayPrice( PAY_HIDE, [gPrice, PAY_HIDE, PAY_HIDE, PAY_HIDE] );
}

// ensure that we start fresh when rezzed
on_rez( integer param )
{
state default;
}

touch_start( integer param )
{
key toucher = llDetectedKey( 0 );

// only the owner's touch makes it listen
if ( toucher != gOwnerKey )
return;

state setupMode;
}

changed( integer change )
{
if ( !checkChange( change )) {
llOwnerSay( "No object to vend" );
state setupMode;
}
}

money( key giver, integer amount )
{
if ( gOutstandingAcks > 0 )
llInstantMessage( gOwnerKey, llGetObjectName() + " - Missed an HTTP log response" );

if ( amount < gPrice ) {
// underpayment, return money
llWhisper( 0, "This item costs L$" + (string)gPrice );
llWhisper( 0, "You paid only L$" + (string)amount );
giveMoney( "Underpayment", giver, amount );
return;
}
string giverName = llKey2Name( giver );
string firstName = "Unknown";
firstName = llGetSubString( giverName, 0, llSubStringIndex( giverName, " " ));
llWhisper( 0, "Please enjoy your new " + gVendedObjectName + ", " + firstName);
llGiveInventory( giver, gVendedObjectName );

if ( amount > gPrice ) {
// overpayment, return change
integer overpayment = amount - gPrice;
llWhisper( 0, "You overpaid by L$" + (string)overpayment + " - returning change" );
giveMoney( "Overpayment", giver, overpayment );
}
// log the sale to my web server
logEvent( "Sale", giver, amount );

// it would be bad to split if the owner paid
if ( giver == gOwnerKey )
return;

if ( gPartnerKey != NULL_KEY ) {
// note that this keeps the fractional L$ for the owner
integer splitAmount = (integer)(((float)gPrice * gSplitPercent) / 100.0);
giveMoney( "Split", gPartnerKey, splitAmount );
}
}

http_response( key request_id, integer status, list metadata, string body )
{
gOutstandingAcks = 0;

if ( status < 200 || status > 299 )
llInstantMessage( gOwnerKey, llGetObjectName() + " - HTTP error response: " + (string)status );
}
}
Sys Slade
Registered User
Join date: 15 Feb 2007
Posts: 626
04-29-2007 11:23
Taken a very quick look, but you aren't removing the listeners when they aren't in use.
Assign the output of the llListen command to an integer handle, then llListenRemove(handle) before jumping out of the setup state.

CODE
integer Handle;
Handle=llListen(kCmdChan, "", gOwnerKey, "");
llListenRemove(Handle);