Fix position of balance output with set
[tpg/opendispense2.git] / src / client / main.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  * - Dispense Client
5  *
6  * main.c - Core and Initialisation
7  *
8  * This file is licenced under the 3-clause BSD Licence. See the file
9  * COPYING for full details.
10  */
11 #include <stdlib.h>
12 #include <stdio.h>
13 #include <string.h>
14 #include <ctype.h>      // isspace
15 #include <pwd.h>        // getpwuids
16 #include <unistd.h>     // close/getuid
17 #include <limits.h>     // INT_MIN/INT_MAX
18 #include "common.h"
19
20 #define USE_NCURSES_INTERFACE   0
21 #define DEBUG_TRACE_SERVER      0
22 #define USE_AUTOAUTH    1
23
24 #define MAX_TXT_ARGS    5       // Maximum number of textual arguments (including command)
25 #define DISPENSE_MULTIPLE_MAX   20      // Maximum argument to -c
26
27 // === TYPES ===
28
29 // === PROTOTYPES ===
30 void    ShowUsage(void);
31  int    main(int argc, char *argv[]);
32  int    ParseArguments(int argc, char *argv[]);
33 // --- Coke Server Communication ---
34 // --- Helpers ---
35 char    *trim(char *string);
36  int    RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage);
37 void    CompileRegex(regex_t *regex, const char *pattern, int flags);
38
39 // === GLOBALS ===
40 char    *gsDispenseServer = "merlo.ucc.gu.uwa.edu.au";
41  int    giDispensePort = 11020;
42
43 tItem   *gaItems;
44  int    giNumItems;
45 regex_t gArrayRegex, gItemRegex, gSaltRegex, gUserInfoRegex, gUserItemIdentRegex;
46  int    gbIsAuthenticated = 0;
47
48 char    *gsItemPattern; //!< Item pattern
49 char    *gsEffectiveUser;       //!< '-u' Dispense as another user
50
51 enum eUI_Modes  giUIMode = UI_MODE_STANDARD;
52  int    gbDryRun = 0;   //!< '-n' Read-only
53  int    gbDisallowSelectWithoutBalance = 1;     //!< Don't allow items to be hilighted if not affordable
54
55  int    giMinimumBalance = INT_MIN;     //!< '-m' Minumum balance for `dispense acct`
56  int    giMaximumBalance = INT_MAX;     //!< '-M' Maximum balance for `dispense acct`
57
58  char   *gsUserName;    //!< User that dispense will happen as
59 char    *gsUserFlags;   //!< User's flag set
60  int    giUserBalance = -1;     //!< User balance (set by Authenticate)
61  int    giDispenseCount = 1;    //!< Number of dispenses to do
62
63 char    *gsTextArgs[MAX_TXT_ARGS];
64  int    giTextArgc;
65
66 // === CODE ===
67 void ShowUsage(void)
68 {
69         printf( "Usage:\n" );
70         if( giTextArgc == 0 )
71                 printf(
72                         "  == Everyone ==\n"
73                         "    dispense\n"
74                         "        Show interactive list\n"
75                         "    dispense <name>|<index>|<itemid>\n"
76                         "        Dispense named item (<name> matches if it is a unique prefix)\n"
77                         "    dispense finger\n"
78                         "        Show the finger output\n"
79                         );
80         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "give") == 0 )
81                 printf(
82                         "    dispense give <user> <amount> \"<reason>\"\n"
83                         "        Give money to another user\n"
84                         );
85         
86         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "donate") == 0 )
87                 printf(
88                         "    dispense donate <amount> \"<reason>\"\n"
89                         "        Donate to the club\n"
90                         );
91         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "iteminfo") == 0 )
92                 printf(
93                         "    dispense iteminfo <itemid>\n"
94                         "        Get the name and price for an item\n"
95                         );
96 //      if( giTextArgc == 0 || strcmp(gsTextArgs[0], "enumitems") == 0 )
97 //              printf(
98 //                      "    dispense enumitems\n"
99 //                      "        List avaliable items\n"
100 //                      );
101         if( giTextArgc == 0 )
102                 printf("  == Coke members == \n");
103         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "acct") == 0 )
104                 printf(
105                         "    dispense acct [<user>]\n"
106                         "        Show user balances\n"
107                         "    dispense acct <user> [+-]<amount> \"<reason>\"\n"
108                         "        Alter a account value\n"
109                         "    dispense acct <user> =<amount> \"<reason>\"\n"
110                         "        Set an account balance\n"
111                         );
112         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "refund") == 0 )
113                 printf(
114                         "    dispense refund <user> <itemid> [<price>]\n"
115                         "        Refund an item to a user (with optional price override)\n"
116                         "        Item IDs can be seen in the cokelog (in the brackets after the item name)\n"
117                         "        e.g. coke:6 for a coke, snack:33 for slot 33 of the snack machine\n"
118                         );
119         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "slot") == 0 )
120                 printf(
121                         "    dispense slot <itemid> <price> <name>\n"
122                         "        Rename/Re-price a slot\n"
123                         );
124         if( giTextArgc == 0 )
125                 printf("  == Dispense administrators ==\n");
126         if( giTextArgc == 0 || strcmp(gsTextArgs[0], "user") == 0 )
127                 printf(
128                         "    dispense user add <user>\n"
129                         "        Create new account\n"
130                         "    dispense user type <user> <flags> <reason>\n"
131                         "        Alter a user's flags\n"
132                         "        <flags> is a comma-separated list of user, coke, admin, internal or disabled\n"
133                         "        Flags are removed by preceding the name with '-' or '!'\n"
134                         );
135         if( giTextArgc == 0 )
136                 printf( "\n"
137                         "General Options:\n"
138                         "    -c <count>\n"
139                         "        Dispense multiple times\n"
140                         "    -u <username>\n"
141                         "        Set a different user (Coke members only)\n"
142                         "    -h / -?\n"
143                         "        Show help text\n"
144                         "    -G\n"
145                         "        Use simple textual interface (instead of ncurses)\n"
146                         "    -n\n"
147                         "        Dry run - Do not actually do dispenses\n"
148                         "    -m <min balance>\n"
149                         "    -M <max balance>\n"
150                         "        Set the Maximum/Minimum balances shown in `dispense acct`\n"
151                         "Definitions:\n"
152                         "    <itemid>\n"
153                         "        Item ID of the form <type>:<num> where <type> is a non-empty string of alpha-numeric characters, and <num> is a non-negative integer\n"
154 //                      "    <user>\n"
155 //                      "        Account name\n"
156                         );
157 }
158
159 //
160 // `dispense finger`
161 // - Display [email protected] finger output
162 //
163 int subcommand_finger(void)
164 {
165         // Connect to server
166         int sock = OpenConnection(gsDispenseServer, giDispensePort);
167         if( sock < 0 )  return RV_SOCKET_ERROR;
168
169         // Get items
170         PopulateItemList(sock);
171
172         printf("The UCC Coke machine.\n\n");
173
174         // Only get coke slot statuses
175         for( int i = 0; i <= 6; i ++ )
176         {
177                 const char *status;
178                 switch(gaItems[i].Status)
179                 {
180                 case 0: status = "Avail";       break;
181                 case 1: status = "Sold ";       break;
182                 default:
183                         status = "Error";
184                         break;
185                 }
186                 printf("%i - %s %3i %s\n", gaItems[i].ID, status, gaItems[i].Price, gaItems[i].Desc);
187         }
188
189         printf("\nMay your pink fish bing into the distance.\n");
190
191         return 0;
192 }
193
194 //
195 // `dispense acct`
196 // - Display/manipulate account balances
197 //
198 int subcommand_acct(void)
199 {
200         int ret = 0;
201
202         // Connect to server
203         int sock = OpenConnection(gsDispenseServer, giDispensePort);
204         if( sock < 0 )  return RV_SOCKET_ERROR;
205         // List accounts?
206         if( giTextArgc == 1 ) {
207                 ret = Dispense_EnumUsers(sock);
208                 close(sock);
209                 return ret;
210         }
211                 
212         // gsTextArgs[1]: Username
213         
214         // Alter account?
215         if( giTextArgc != 2 )
216         {
217                 if( giTextArgc != 4 ) {
218                         fprintf(stderr, "`dispense acct` requires a reason\n");
219                         ShowUsage();
220                         return RV_ARGUMENTS;
221                 }
222                 
223                 // Authentication required
224                 ret = Authenticate(sock);
225                 if(ret) return ret;
226                 
227                 // gsTextArgs[1]: Username
228                 // gsTextArgs[2]: Ammount
229                 // gsTextArgs[3]: Reason
230                  char   *tmp = NULL;
231                 long int balance = strtol(gsTextArgs[2]+(gsTextArgs[2][0] == '='), &tmp, 10);
232                 if(!tmp || *tmp != '\0') {
233                         fprintf(stderr, "dispense acct: Value must be a decimal number of cents\n");
234                         return RV_ARGUMENTS;
235                 }
236                 
237                 if( gsTextArgs[2][0] == '=' ) {
238                         // Set balance
239                         ret = Dispense_ShowUser(sock, gsTextArgs[1]);
240                         ret = Dispense_SetBalance(sock, gsTextArgs[1], balance, gsTextArgs[3]);
241                 }
242                 else {
243                         // Alter balance
244                         ret = Dispense_AlterBalance(sock, gsTextArgs[1], balance, gsTextArgs[3]);
245                 }
246         }
247         // On error, quit
248         if( ret ) {
249                 close(sock);
250                 return ret;
251         }
252         
253         // Show user information
254         ret = Dispense_ShowUser(sock, gsTextArgs[1]);
255         
256         close(sock);
257         return ret;
258 }
259
260 //
261 // `dispense give`
262 // - Transfer credit from the current user to another account
263 //
264 // "Here, have some money."
265 //
266 int subcommand_give(int argc, char *args[])
267 {
268         int ret;
269         
270         if( argc != 3 ) {
271                 fprintf(stderr, "`dispense give` takes three arguments\n");
272                 ShowUsage();
273                 return RV_ARGUMENTS;
274         }
275         
276         const char *dst_acct = args[0];
277         const char *amt_str = args[1];
278         const char *message = args[2];
279         
280         // Connect to server
281         int sock = OpenConnection(gsDispenseServer, giDispensePort);
282         if( sock < 0 )  return RV_SOCKET_ERROR;
283         
284         // Authenticate
285         ret = Authenticate(sock);
286         if(ret) return ret;
287
288         char *end = NULL;
289         long amt = strtol(amt_str, &end, 10);
290         if( !end || *end != '\0' ) {
291                 fprintf(stderr, "dispense give: Balance is invalid, must be decimal number of cents");
292                 return RV_ARGUMENTS;
293         }
294         ret = Dispense_Give(sock, dst_acct, amt, message);
295
296         close(sock);
297         return ret;
298 }
299
300 // 
301 // `dispense user`
302 // - User administration (Admin Only)
303 //
304 int subcommand_user(int argc, char *args[])
305 {
306          int    ret;
307         
308         // Check argument count
309         if( argc == 0 ) {
310                 fprintf(stderr, "Error: `dispense user` requires arguments\n");
311                 ShowUsage();
312                 return RV_ARGUMENTS;
313         }
314         
315         // Connect to server
316         int sock = OpenConnection(gsDispenseServer, giDispensePort);
317         if( sock < 0 )  return RV_SOCKET_ERROR;
318         
319         // Attempt authentication
320         ret = Authenticate(sock);
321         if(ret) return ret;
322         
323         // Add new user?
324         if( strcmp(args[0], "add") == 0 )
325         {
326                 if( giTextArgc != 3 ) {
327                         fprintf(stderr, "Error: `dispense user add` requires an argument\n");
328                         ShowUsage();
329                         return RV_ARGUMENTS;
330                 }
331                 
332                 ret = Dispense_AddUser(sock, args[1]);
333         }
334         // Update a user
335         else if( strcmp(args[0], "type") == 0 || strcmp(args[0], "flags") == 0 )
336         {
337                 if( argc < 3 || argc > 4 ) {
338                         fprintf(stderr, "Error: `dispense user type` requires two arguments\n");
339                         ShowUsage();
340                         return RV_ARGUMENTS;
341                 }
342                 
343                 ret = Dispense_SetUserType(sock, args[1], args[2], (argc == 3 ? "" : args[3]));
344         }
345         else
346         {
347                 fprintf(stderr, "Error: Unknown sub-command for `dispense user`\n");
348                 ShowUsage();
349                 return RV_ARGUMENTS;
350         }
351         close(sock);
352         return ret;
353 }
354
355 //
356 // `dispense donate`
357 // - Donate money to the club
358 //
359 int subcommand_donate(int argc, char *args[])
360 {
361          int    ret;
362         
363         // Check argument count
364         if( argc != 2 ) {
365                 fprintf(stderr, "Error: `dispense donate` requires two arguments\n");
366                 ShowUsage();
367                 return RV_ARGUMENTS;
368         }
369         
370         // Connect to server
371         int sock = OpenConnection(gsDispenseServer, giDispensePort);
372         if( sock < 0 )  return RV_SOCKET_ERROR;
373         
374         // Attempt authentication
375         ret = Authenticate(sock);
376         if(ret) return ret;
377         
378         // Do donation
379         ret = Dispense_Donate(sock, atoi(args[0]), args[1]);
380                         
381         close(sock);
382
383         return ret;
384 }
385
386 //
387 // `dispense refund`
388 // - Refund a purchased item
389 //
390 // "Well excuuuuse me, princess"
391 //
392 int subcommand_refund(int argc, char *args[])
393 {
394          int    ret;
395
396         // Check argument count
397         if( argc != 2 && argc != 3 ) {
398                fprintf(stderr, "Error: `dispense refund` takes 2 or 3 arguments\n");
399                ShowUsage();
400                return RV_ARGUMENTS;
401         }
402
403         // Connect to server
404         int sock = OpenConnection(gsDispenseServer, giDispensePort);
405         if(sock < 0)    return RV_SOCKET_ERROR; 
406
407         // Attempt authentication
408         ret = Authenticate(sock);
409         if(ret) return ret;
410
411          int     price = 0;
412         if( argc > 2 ) {
413                price = atoi(args[2]);
414                if( price <= 0 ) {
415                        fprintf(stderr, "Error: Override price is invalid (should be > 0)\n");
416                        return RV_ARGUMENTS;
417                }
418         }
419
420         // Username, Item, cost
421         ret = Dispense_Refund(sock, args[0], args[1], price);
422
423         // TODO: More
424         close(sock);
425         return ret;
426 }
427
428 //
429 // `dispense iteminfo`
430 // - Get the state of an item
431 //
432 int subcommand_iteminfo(int argc, char *args[])
433 {
434          int    ret;
435
436          // Check argument count
437         if( argc != 1 ) {
438                 fprintf(stderr, "Error: `dispense iteminfo` takes one argument\n");
439                 ShowUsage();
440                 return RV_ARGUMENTS;
441         }
442
443         char *item_id = args[0];
444
445         regmatch_t matches[3];
446         // Parse item ID
447         if( RunRegex(&gUserItemIdentRegex, item_id, 3, matches, NULL) != 0 ) {
448                 fprintf(stderr, "Error: Invalid item ID passed (<type>:<id> expected)\n");
449                 return RV_ARGUMENTS;
450         }
451         char    *type = item_id + matches[1].rm_so;
452         item_id[ matches[1].rm_eo ] = '\0';
453          int    id = atoi( item_id + matches[2].rm_so );
454
455         int sock = OpenConnection(gsDispenseServer, giDispensePort);
456         if( sock < 0 )  return RV_SOCKET_ERROR;
457         
458         ret = Dispense_ItemInfo(sock, type, id);
459         close(sock);
460         return ret;
461 }
462
463 //
464 // `dispense slot`
465 // - Update the name/price of an item
466 //
467 int subcommand_slot(int argc, char *args[])
468 {
469          int    ret;
470         
471         // Check arguments
472         if( argc != 3 ) {
473                 fprintf(stderr, "Error: `dispense slot` takes three arguments\n");
474                 ShowUsage();
475                 return RV_ARGUMENTS;
476         }
477         char *slot_id = args[0];
478         char *price_str = args[1];
479         char *newname = args[2];
480         
481         // Parse arguments
482         regmatch_t matches[3];
483         if( RunRegex(&gUserItemIdentRegex, slot_id, 3, matches, NULL) != 0 ) {
484                 fprintf(stderr, "Error: Invalid item ID passed (<type>:<id> expected)\n");
485                 return RV_ARGUMENTS;
486         }
487         const char *item_type = slot_id + matches[1].rm_so;
488         slot_id[ matches[1].rm_eo ] = '\0';
489         int item_id = atoi( slot_id + matches[2].rm_so );
490
491         // - Price
492         char *end;
493         int price = strtol( price_str, &end, 0 );
494         if( price < 0 || *end != '\0' ) {
495                 fprintf(stderr, "Error: Invalid price passed (must be >= 0)\n");
496                 return RV_ARGUMENTS;
497         }
498         
499         // -- Sanity
500         for( char *pos = newname; *pos; pos ++ )
501         {
502                 if( !isalnum(*pos) && *pos != ' ' ) {
503                         fprintf(stderr, "Error: You should only have letters, numbers and spaces in an item name\n");
504                         return RV_ARGUMENTS;
505                 }
506         }
507         
508         // Connect & Authenticate
509         int sock = OpenConnection(gsDispenseServer, giDispensePort);
510         if( sock < 0 )  return RV_SOCKET_ERROR;
511         
512         ret = Authenticate(sock);
513         if(ret) return ret;
514         
515         // Update the slot
516         ret = Dispense_SetItem(sock, item_type, item_id, price, newname);
517         
518         close(sock);
519         return ret;
520 }
521
522 //
523 // `dispense pincheck`
524 // - Validate a user's pin (used by the snack machine)
525 //
526 int subcommand_pincheck(int argc, char *args[])
527 {
528          int    ret;
529         
530         if( argc < 1 || argc > 2 ) {
531                 fprintf(stderr, "Error: `dispense pincheck` takes one/two arguments\n");
532                 ShowUsage();
533                 return RV_ARGUMENTS;
534         }
535
536         struct passwd   *pwd = getpwuid( getuid() );
537         gsUserName = strdup(pwd->pw_name);
538         
539         const char *pin = args[0];
540         const char *user = (argc > 1 ? args[1] : gsUserName);
541         
542         int sock = OpenConnection(gsDispenseServer, giDispensePort);
543         if( sock < 0 )  return RV_SOCKET_ERROR;
544         
545         ret = Authenticate(sock);
546         if(ret) return ret;
547         
548         ret = DispenseCheckPin(sock, user, pin);
549         
550         close(sock);
551         return ret;
552 }
553
554 //
555 // `dispense pinset`
556 // - Set the pin of the current account
557 //
558 int subcommand_pinset(int argc, char *args[])
559 {
560          int    ret;
561         
562         if( argc != 1 ) {
563                 fprintf(stderr, "Error: `dispense pinset` takes one argument\n");
564                 ShowUsage();
565                 return RV_ARGUMENTS;
566         }
567         
568         const char *pin = args[0];
569         
570         int sock = OpenConnection(gsDispenseServer, giDispensePort);
571         if( sock < 0 )  return RV_SOCKET_ERROR;
572
573         ret = Authenticate(sock);
574         if(ret) return ret;
575
576         ret = DispenseSetPin(sock, pin);
577         
578         close(sock);
579         return ret;
580 }
581
582 int main(int argc, char *argv[])
583 {
584          int    i, ret = 0;
585         char    buffer[BUFSIZ];
586         
587         gsTextArgs[0] = "";
588
589         // -- Create regular expressions
590         // > Code Type Count ...
591         CompileRegex(&gArrayRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([0-9]+)", REG_EXTENDED);     //
592         // > Code Type Ident Status Price Desc
593         CompileRegex(&gItemRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([A-Za-z]+):([0-9]+)\\s+(avail|sold|error)\\s+([0-9]+)\\s+(.+)$", REG_EXTENDED);
594         // > Code 'SALT' salt
595         CompileRegex(&gSaltRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+(.+)$", REG_EXTENDED);
596         // > Code 'User' Username Balance Flags
597         CompileRegex(&gUserInfoRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([^ ]+)\\s+(-?[0-9]+)\\s+(.+)$", REG_EXTENDED);
598         // > Item Ident
599         CompileRegex(&gUserItemIdentRegex, "^([A-Za-z]+):([0-9]+)$", REG_EXTENDED);
600
601         // Parse Arguments
602         ret = ParseArguments(argc, argv);
603         if( ret )
604                 return ret;
605
606         // Sub-commands
607         if( strcmp(gsTextArgs[0], "finger") == 0 ) {
608                 return subcommand_finger();
609         }
610         else if( strcmp(gsTextArgs[0], "acct") == 0 ) {
611                 return subcommand_acct();
612         }
613         else if( strcmp(gsTextArgs[0], "give") == 0 ) {
614                 return subcommand_give(giTextArgc-1, gsTextArgs+1);
615         }
616         else if( strcmp(gsTextArgs[0], "user") == 0 ) {
617                 return subcommand_user(giTextArgc-1, gsTextArgs+1);
618         }
619         else if( strcmp(gsTextArgs[0], "donate") == 0 ) {
620                 return subcommand_donate(giTextArgc-1, gsTextArgs+1);
621         }
622         else if( strcmp(gsTextArgs[0], "refund") == 0 ) {
623                 return subcommand_refund(giTextArgc-1, gsTextArgs+1);
624         }
625         else if( strcmp(gsTextArgs[0], "iteminfo") == 0 ) {
626                 return subcommand_iteminfo(giTextArgc-1, gsTextArgs+1);
627         }
628         else if( strcmp(gsTextArgs[0], "slot") == 0 ) {
629                 return subcommand_slot(giTextArgc-1, gsTextArgs+1);
630         }
631         else if(strcmp(gsTextArgs[0], "pincheck") == 0) {
632                 return subcommand_pincheck(giTextArgc-1, gsTextArgs+1);
633         }
634         else if(strcmp(gsTextArgs[0], "pinset") == 0) {
635                 return subcommand_pinset(giTextArgc-1, gsTextArgs+1);
636         }
637         else {
638                 // Item name / pattern
639                 gsItemPattern = gsTextArgs[0];
640         }
641         
642         // Connect to server
643         int sock = OpenConnection(gsDispenseServer, giDispensePort);
644         if( sock < 0 )  return RV_SOCKET_ERROR;
645
646         // Get the user's balance
647         ret = GetUserBalance(sock);
648         if(ret) return ret;
649
650         // Get items
651         PopulateItemList(sock);
652         
653         // Disconnect from server
654         close(sock);
655         
656         if( gsItemPattern && gsItemPattern[0] )
657         {
658                 regmatch_t matches[3];
659                 // Door (hard coded)
660                 if( strcmp(gsItemPattern, "door") == 0 )
661                 {
662                         // Connect, Authenticate, dispense and close
663                         sock = OpenConnection(gsDispenseServer, giDispensePort);
664                         if( sock < 0 )  return RV_SOCKET_ERROR;
665                         ret = Authenticate(sock);
666                         if(ret) return ret;
667                         ret = DispenseItem(sock, "door", 0);
668                         close(sock);
669                         return ret;
670                 }
671                 // Item id (<type>:<num>)
672                 else if( RunRegex(&gUserItemIdentRegex, gsItemPattern, 3, matches, NULL) == 0 )
673                 {
674                         char    *ident;
675                          int    id;
676                         
677                         // Get and finish ident
678                         ident = gsItemPattern + matches[1].rm_so;
679                         gsItemPattern[matches[1].rm_eo] = '\0';
680                         // Get ID
681                         id = atoi( gsItemPattern + matches[2].rm_so );
682                         
683                         // Connect, Authenticate, dispense and close
684                         sock = OpenConnection(gsDispenseServer, giDispensePort);
685                         if( sock < 0 )  return RV_SOCKET_ERROR;
686                         
687                         Dispense_ItemInfo(sock, ident, id);
688                         
689                         ret = Authenticate(sock);
690                         if(ret) return ret;
691                         ret = DispenseItem(sock, ident, id);
692                         close(sock);
693                         return ret;
694                 }
695                 // Item number (6 = coke)
696                 else if( strcmp(gsItemPattern, "0") == 0 || atoi(gsItemPattern) > 0 )
697                 {
698                         i = atoi(gsItemPattern);
699                 }
700                 // Item prefix
701                 else
702                 {
703                          int    j;
704                          int    best = -1;
705                         for( i = 0; i < giNumItems; i ++ )
706                         {
707                                 // Prefix match (with case-insensitive match)
708                                 for( j = 0; gsItemPattern[j]; j ++ )
709                                 {
710                                         if( gaItems[i].Desc[j] == gsItemPattern[j] )
711                                                 continue;
712                                         if( tolower(gaItems[i].Desc[j]) == tolower(gsItemPattern[j]) )
713                                                 continue;
714                                         break;
715                                 }
716                                 // Check if the prefix matched
717                                 if( gsItemPattern[j] != '\0' )
718                                         continue;
719                                 
720                                 // Prefect match
721                                 if( gaItems[i].Desc[j] == '\0' ) {
722                                         best = i;
723                                         break;
724                                 }
725                                 
726                                 // Only one match allowed
727                                 if( best == -1 ) {
728                                         best = i;
729                                 }
730                                 else {
731                                         // TODO: Allow ambiguous matches?
732                                         // or just print a wanrning
733                                         printf("Warning - Ambiguous pattern, stopping\n");
734                                         return RV_BAD_ITEM;
735                                 }
736                         }
737                         
738                         // Was a match found?
739                         if( best == -1 )
740                         {
741                                 fprintf(stderr, "No item matches the passed string\n");
742                                 return RV_BAD_ITEM;
743                         }
744                         
745                         i = best;
746                 }
747         }
748         else if( giUIMode != UI_MODE_BASIC )
749         {
750                 i = ShowNCursesUI();
751         }
752         else
753         {
754                 // Very basic dispense interface
755                 for( i = 0; i < giNumItems; i ++ ) {
756                         // Add a separator
757                         if( i && strcmp(gaItems[i].Type, gaItems[i-1].Type) != 0 )
758                                 printf("   ---\n");
759                         
760                         printf("%2i %s:%i\t%3i %s\n", i, gaItems[i].Type, gaItems[i].ID,
761                                 gaItems[i].Price, gaItems[i].Desc);
762                 }
763                 printf(" q Quit\n");
764                 for(;;)
765                 {
766                         char    *buf;
767                         
768                         i = -1;
769                         
770                         fgets(buffer, BUFSIZ, stdin);
771                         
772                         buf = trim(buffer);
773                         
774                         if( buf[0] == 'q' )     break;
775                         
776                         i = atoi(buf);
777                         
778                         if( i != 0 || buf[0] == '0' )
779                         {
780                                 if( i < 0 || i >= giNumItems ) {
781                                         printf("Bad item %i (should be between 0 and %i)\n", i, giNumItems);
782                                         continue;
783                                 }
784                                 break;
785                         }
786                 }
787         }
788         
789         
790         // Check for a valid item ID
791         if( i >= 0 )
792         {
793                  int j;
794                 // Connect, Authenticate, dispense and close
795                 sock = OpenConnection(gsDispenseServer, giDispensePort);
796                 if( sock < 0 )  return RV_SOCKET_ERROR;
797                         
798                 ret = Dispense_ItemInfo(sock, gaItems[i].Type, gaItems[i].ID);
799                 if(ret) return ret;
800                 
801                 ret = Authenticate(sock);
802                 if(ret) return ret;
803                 
804                 for( j = 0; j < giDispenseCount; j ++ ) {
805                         ret = DispenseItem(sock, gaItems[i].Type, gaItems[i].ID);
806                         if( ret )       break;
807                 }
808                 if( j > 1 ) {
809                         printf("%i items dispensed\n", j);
810                 }
811                 Dispense_ShowUser(sock, gsUserName);
812                 close(sock);
813
814         }
815
816         return ret;
817 }
818
819 int ParseArguments(int argc, char *argv[])
820 {
821         for( int i = 1; i < argc; i ++ )
822         {
823                 char    *arg = argv[i];
824                 
825                 if( arg[0] == '-' )
826                 {                       
827                         switch(arg[1])
828                         {
829                         case 'h':
830                         case '?':
831                                 ShowUsage();
832                                 return 0;
833                                         
834                         case 'c':
835                                 if( i > 2 && strcmp(argv[i-1], "type") == 0 )
836                                         goto _default;
837                                 if( i + 1 >= argc ) {
838                                         fprintf(stderr, "%s: -c takes an argument\n", argv[0]);
839                                         ShowUsage();
840                                         return -1;
841                                 }
842                                 giDispenseCount = atoi(argv[++i]);
843                                 if( giDispenseCount < 1 || giDispenseCount > DISPENSE_MULTIPLE_MAX ) {
844                                         fprintf(stderr, "Sorry, only 1-20 can be passed to -c (safety)\n");
845                                         return -1;
846                                 }
847                                 
848                                 break ;
849         
850                         case 'm':       // Minimum balance
851                                 if( i + 1 >= argc ) {
852                                         fprintf(stderr, "%s: -m takes an argument\n", argv[0]);
853                                         ShowUsage();
854                                         return RV_ARGUMENTS;
855                                 }
856                                 giMinimumBalance = atoi(argv[++i]);
857                                 break;
858                         case 'M':       // Maximum balance
859                                 if( i + 1 >= argc ) {
860                                         fprintf(stderr, "%s: -M takes an argument\n", argv[0]);
861                                         ShowUsage();
862                                         return RV_ARGUMENTS;
863                                 }
864                                 giMaximumBalance = atoi(argv[++i]);
865                                 break;
866                         
867                         case 'u':       // Override User
868                                 if( i + 1 >= argc ) {
869                                         fprintf(stderr, "%s: -u takes an argument\n", argv[0]);
870                                         ShowUsage();
871                                         return RV_ARGUMENTS;
872                                 }
873                                 gsEffectiveUser = argv[++i];
874                                 break;
875                         
876                         case 'H':       // Override remote host
877                                 if( i + 1 >= argc ) {
878                                         fprintf(stderr, "%s: -H takes an argument\n", argv[0]);
879                                         ShowUsage();
880                                         return RV_ARGUMENTS;
881                                 }
882                                 gsDispenseServer = argv[++i];
883                                 break;
884                         case 'P':       // Override remote port
885                                 if( i + 1 >= argc ) {
886                                         fprintf(stderr, "%s: -P takes an argument\n", argv[0]);
887                                         ShowUsage();
888                                         return RV_ARGUMENTS;
889                                 }
890                                 giDispensePort = atoi(argv[++i]);
891                                 break;
892                         
893                         // Set slot name/price
894                         case 's':
895                                 if( giTextArgc != 0 ) {
896                                         fprintf(stderr, "%s: -s must appear before other arguments\n", argv[0]);
897                                         ShowUsage();
898                                         return RV_ARGUMENTS;
899                                 }
900                                 gsTextArgs[0] = "slot"; // HACK!!
901                                 giTextArgc ++;
902                                 break;
903                         
904                         case 'G':       // Don't use GUI
905                                 giUIMode = UI_MODE_BASIC;
906                                 break;
907                         case 'D':       // Drinks only
908                                 giUIMode = UI_MODE_DRINKSONLY;
909                                 break;
910                         case 'n':       // Dry Run / read-only
911                                 gbDryRun = 1;
912                                 break;
913                         case '-':
914                                 if( strcmp(argv[i], "--help") == 0 ) {
915                                         ShowUsage();
916                                         return 0;
917                                 }
918                                 else if( strcmp(argv[i], "--dry-run") == 0 ) {
919                                         gbDryRun = 1;
920                                 }
921                                 else if( strcmp(argv[i], "--drinks-only") == 0 ) {
922                                         giUIMode = UI_MODE_DRINKSONLY;
923                                 }
924                                 else if( strcmp(argv[i], "--can-select-all") == 0 ) {
925                                         gbDisallowSelectWithoutBalance = 0;
926                                 }
927                                 else {
928                                         fprintf(stderr, "%s: Unknown switch '%s'\n", argv[0], argv[i]);
929                                         ShowUsage();
930                                         return RV_ARGUMENTS;
931                                 }
932                                 break;
933                         default: _default:
934                                 // The first argument is not allowed to begin with 'i'
935                                 // (catches most bad flags)
936                                 if( giTextArgc == 0 ) {
937                                         fprintf(stderr, "%s: Unknown switch '%s'\n", argv[0], argv[i]);
938                                         ShowUsage();
939                                         return RV_ARGUMENTS;
940                                 }
941                                 if( giTextArgc == MAX_TXT_ARGS )
942                                 {
943                                         fprintf(stderr, "ERROR: Too many arguments\n");
944                                         return RV_ARGUMENTS;
945                                 }
946                                 gsTextArgs[giTextArgc++] = argv[i];
947                                 break;
948                         }
949
950                         continue;
951                 }
952
953                 if( giTextArgc == MAX_TXT_ARGS )
954                 {
955                         fprintf(stderr, "ERROR: Too many arguments\n");
956                         return RV_ARGUMENTS;
957                 }
958         
959                 gsTextArgs[giTextArgc++] = argv[i];
960         
961         }
962         return 0;
963 }
964
965 // ---------------
966 // --- Helpers ---
967 // ---------------
968 char *trim(char *string)
969 {
970          int    i;
971         
972         while( isspace(*string) )
973                 string ++;
974         
975         for( i = strlen(string); i--; )
976         {
977                 if( isspace(string[i]) )
978                         string[i] = '\0';
979                 else
980                         break;
981         }
982         
983         return string;
984 }
985
986 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
987 {
988          int    ret;
989         
990         ret = regexec(regex, string, nMatches, matches, 0);
991         if( ret && errorMessage ) {
992                 size_t  len = regerror(ret, regex, NULL, 0);
993                 char    errorStr[len];
994                 regerror(ret, regex, errorStr, len);
995                 printf("string = '%s'\n", string);
996                 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
997                 exit(-1);
998         }
999         
1000         return ret;
1001 }
1002
1003 void CompileRegex(regex_t *regex, const char *pattern, int flags)
1004 {
1005          int    ret = regcomp(regex, pattern, flags);
1006         if( ret ) {
1007                 size_t  len = regerror(ret, regex, NULL, 0);
1008                 char    errorStr[len];
1009                 regerror(ret, regex, errorStr, len);
1010                 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);
1011                 exit(-1);
1012         }
1013 }

UCC git Repository :: git.ucc.asn.au