Repository navigation
Custom Command Quickstart Guide
The custom commands module of Red Star allows its users to create simple scripts for Red Star to run, the titular custom commands. This guide will introduce you to a few usecases of this module and hopefully allow you to begin using it by providing simple examples which explore different functions.
Before we can begin writing custom commands, you need to know how to create them in the bot.
Creation of custom commands is handled by the !newcc command. Be mindful of the command prefix set up on your server of choice.
The !newcc custom command can be used in a few ways:
!newcc cc_name cc_code
!newcc
+ cc_file.json
!newcc -s cc_name
+ cc_file.lisp
!newcc -s
+ cc_name.lisp
Let's examine them closer:
-
!newcc cc_name cc_code
In this case, you call the command with a one-word name for your custom command, followed by the code you want that custom command to interpret. If the following string is not wrapped in parenthesis(), the command will automatically convert it to a script that simply outputs that string. -
!newcc + cc_file.json
This is a way to port commands between servers using Red Star, as well as a way to upload backed up versions of commands. Simply call the command itself and attach the.jsonfile to it. Such file can be generated by the!dumpcc cc_namecommand. -
!newcc -s cc_name + cc_file.lisp
Similar to uploading the json file, calling the command with a file and an-sflag will make it interpret the contents of the uploaded text file as the text of the new custom command. If no name is given, it will take the name of the file as the cc name.
Similarly, there are !editcc and !delcc commands that use similar syntax.
As is tradition, the first script to be made is the one that outputs a simple string.
This can be done in a few ways using RSLisp:
!newcc helloworld Hello world.
If called this way, the !newcc command will interpret the given string as what it is: a string.
If the script given to the command is not wrapped into () parenthesis, it will wrap it into "" quotes, which will make the string evaluate as plain text.
(quote "Hello world.")This is technically how RSLisp treats the strings internally. The (quote) function takes one argument and returns it as is, without any further evaluation.
For example,
(quote (+ 1 2))Will evaluate to a list containing a string "+" and numbers 1 and 2, similar to
(list "+" 1 2)Note that you do not need to write (quote "string") in your code if you wish to put a string in. Simply wrapping the string in quotes will do.
The quote operation can be reversed using the aptly named (unquote).
(reduce + (imap chr (list 72 101 108 108 111 32 119 111 114 108 100 46)))As is tradition, here is a more involved version of the script, with the same output.
While it is not as complicated as some, it shows some of the features of the language. Let's walk through it.
(list 72 101 108 108 111 32 119 111 114 108 100 46) Stands for the ordinal values of the characters in the string "Hello world.". Which is a fancy way of saying "the numbers of the unicode characters".
(imap chr ()) is a function that takes that list and applies function (chr) to each value, which is the opposite of (ord) that was used to get the values in the first place and as such will return the characters we need. Note that in this case we use (imap) instead of (map), because we will be feeding the result to yet another function that takes an iterator and don't really need it to return a fully-featured list of values.
(reduce + imap) is the final function in this script. While (imap) did convert the numbers to characters, it gives us a list, not a string. Here is where (reduce) comes in handy - as the name implies, this function reduces the given iterable through applying some operation to each of it's members and a special value called the accumulator.
In this case, we did not give it an extra value to set the accumulator up, so it will simply take the first item from the iterable, the character "H", and begin applying the + function - first adding "H" and "e" and storing it into the accumulator, then adding "He" and "l" and so on.
As such, this is a script that first converts all those numbers into correspnding characters and then adds them all together to get our desired output.
Hello world.
Now, let's look at a more common application of the custom commands: generating strings with a degree of randomness, for fun and entertainment.
The simplest possible command of such nature is...
The classic magic 8-ball included 20 responses:
10 positive:
- It is certain.
- It is decidedly so.
- Without a doubt.
- Yes - definitely.
- You may rely on it.
- As I see it, yes.
- Most likely.
- Outlook good.
- Yes.
- Signs point to yes.
5 non-commital:
- Reply hazy, try again
- Ask again later.
- Better not tell you now.
- Cannot predict now.
- Concentrate and ask again.
5 negative:
- Don't count on it.
- My reply is no.
- My sources say no
- Outlook not so good.
- Very doubtful.
The simplest script to do that for us would be the one to take a number of different strings and randomly choose one of them. Luckily for us, there is just the function for that in RSLisp:
(ezchoice "yes" "yes" "reply hazy" "no")To keep the code manageable and easy to understand, let's divide those numbers by 5 and use just four options.
(ezchoice) is a function that takes any number of arguments (in this case, four) and returns one of them, chosen randomly. It's called "easy choice" because it is meant to make it easier for you to make such a script - the alternative function - (choice) - requires a list, which would make the above code look like this:
(choice (list "yes" "yes" "reply hazy" "no"))But, it does offer some advantages - it can take a resulting list from some other function, and also can select the items using weights. For example, since we have two "yes" options in our code above, we could instead give the function some weights:
(choice (list "yes" "reply hazy" "no")(list 2 1 1))Note that you can substitute
(list x y z)for(l x y z), to cut down on all the typing a little.
Weights can be used to make some items more likely to be chosen than the others, which can be used to make some sort of a raffle or simply add some rarely occuring result to your random pick without copy-pasting options a hundred times.
But, that does not improve our 8-ball much. Regardless of how we choose, this script will simply return a plain text reply. Maybe we could make it look a little better? Something like... 🎱 It is certain 🎱?
We could achieve that by adding the little 8-balls to each option, but a more efficient solution would be to add them after, and what do you know, there is just a function for that:
(f "🎱 `" (ezchoice "yes" "yes" "reply hazy" "no") "` 🎱")(f) is a helper function, built into RSLisp to make it easier to do this kind of formatting: it takes any number of arguments, converts them to strings and then sticks them all together. In this case, it will add some cute little 8-balls to our response, as well as wrapping it into some inline code formatting.
Here's a final, fully-featured version:
(f "🎱 `" (ezchoice "It is certain.""It is decidedly so.""Without a doubt.""Yes - definitely.""You may rely on it.""As I see it, yes.""Most likely.""Outlook good.""Yes.""Signs point to yes.""Reply hazy, try again""Ask again later.""Better not tell you now.""Cannot predict now.""Concentrate and ask again.""Don't count on it.""My reply is no.""My sources say no""Outlook not so good.""Very doubtful.") "` 🎱")You don't actually need to put spaces between strings, the parser does not need them. But they do help readability of your code.
So far, we've been making scripts that output some value based on internal variables, without any input. What better way to change that than writing a nice command to hug people with?
Let's look at we want to achieve:
<Username> hugs <user input> in some way.
- Get the name of the user calling the command and put it at the front
- Add "hugged", or perhaps some randomly selected version of it
- Get whatever the user typed after the command and add it. Maybe add something default if the user didn't put anything.
- Add some randomly selected ending, like "tightly" or "cutely"
We already know how to select strings randomly and add them togeher, but to get the input we need to use some inbuilt variables, namely usernick and argstring.
RSLisp offer a number of ways to acces whatever the user has entered after the custom command.
argstringis a variable that just has everything in one chunk.argsis a variable holding that string split up into a list. And finally,(args)function gives a nice way of requesting a certain argument by number without getting an error if the user doesn't input it for some reason.
Using what we learned in the previous example, the basic code would look like this:
(f
usernick
" hugs "
argstring
" "
(ezchoice "tightly" "firmly" "cutely" "warmly")
)Putting arguments on separate lines with indentation indicating which function they belong to is a nice trick to make your script more readable while you're still working on it. The parser doesn't care what sort of whitespace there is so long as it can tell one symbol from another.
Here you can see how referencing variables is done - simply by writing in their name!
Now, we also wanted to add some default plug in case user doesn't put anything after the command.
For that, we will need to use a conditional function - simply called (if).
(if) is a function that looks at it's first argument to see if it evaluates to True, and then evaluates either the second or the third of it's arguments.
True and False are boolean values, usually resulting from some sort of a check. For example, (== 1 2) will evaluate to False, since 1 is not equal to 2. Other things can be interpreted as boolean if needed - an empty string, an empty list or a number 0 is pretty much the same as False, while a string or list of any length and any positive number is True.
It is good practice to use actual booleans, because it is harder to find errors in a more tricky code.
In our case, the property of empty strings being False is very useful, because we can write our check like this:
(if argstring argstring "someone nice")Which will check if argstring is a thing and just return argstring if it is. Otherwise, return "someone nice".
So, putting it all together with some pretties gives us:
(f
"`" usernick " "
(ezchoice "hugs" "glomps" "cuddles") " "
(if argstring argstring "someone nice") " "
(ezchoice "tightly" "firmly" "cutely" "warmly" "lovingly" "and gropes their butt") ".`"
)Note that you do need to put in some spaces into all those strings.
Now, what can be more fun than taking some regular old text and making it fancier?
Also known as "translation" - this is the process of taking a string and then translating it's characters into other characters. For example, you can make your text all 𝒻𝒶𝓃𝒸𝓎 by using (transcode "fancy text here" "script").
Let's look at how it works in the most elite way possible - by writing a custom command that turns whatever is given to it into 1337speak.
There are two inbuilt functions that can accomplish transcoding in RSLisp - (transcode) and (str "translate"). The former is much more user-friendly, but is not as powerful. The latter allows things like deleting characters and translating them to more than one character long strings, but it is a bit more involved.
Let's start with (transcode).
For 1337speak, we want to make simple transformations: i into 1, e into 3, a into 4, t into 7, b into 8 and s into 5; which can be accomplished with the following code:
(transcode argstring "iIeEaAtTbBsS" "113344778855")Note that to and from strings are the same lengths, as each symbol from the first is turned into the symbol that takes same position in the second.
Easy enough - we put in both lower and uppercase letters, then put their replacements into the other string.
Let's see how it looks like when we use (str) methods.
(str "translate" argstring (str "maketrans" "iIeEaAtTbBsS" "113344778855"))
(str "translate" argstring (dict (zip (imap ord "iIeEaAtTbBsS") "113344778855")))Not as pretty and streamlined, but it does expose the inner workings of the (transcode) function.
First, we make a translation mapping - a data structure in which the characters are mapped to the other characters, then we feed it to the translate method.
Pig latin is a rather simple word game, so what does it take to implement it in a custom command?
Answer: regular expressions!
The rules of Pig latin are pretty simple - take all consonants from the beginning of the word, add them to the end, then slap an "ay" behind them. How do we achieve that?
Answer: regular expressions!
([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]*)([^\s]+)
Looks intimidating? Let's walk through it.
First thing you need to know about regular expressions is what the () brackets are doing. In regexp, those are called "capture groups", and they do exactly that - capture strings inside them for future use. Our regexp has two of those, and we will use them later as \1 and \2.
Second thing is what the [] brackets are doing. Those actually represent a single character from the list inside of them. Specifically, that first one is representing all latin consonants. b-d stands "all characters between b and d inclusive", and could be replaced by bcd. The second one has a special character ^ in the beginning, which tells regexp to invert it, matching characters that are not inside the brackets. The \s stands for "whitespace", so that one is meant to match anything until we run out of word.
But, those are each just one character. Capturing the whole word requires quite a bit of those, and the regexp does it by using quantifiers. Namely, the * and the + you see after the bracket. The asterisk means "any amount of the character/group before" and the plus means "one or more of the character/group before".
So, that regexp just means "start by finding all the consonants you can find, remember them, then remember everything after them until you hit a whitespace".
And now, this is how the code will look like to use it:
(resub "([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]*)([^\s]+)" "\2\1ay" argstring)resub takes three arguments - pattern, replace string and argstring
You can see us using the contents of the groups in the replace string: \2\1 flips the text we found, and then ay is added to the end.
So, what other text transformations could we do? How about a custom command that makes your text go LiKe tHiS?
Well, let's do it!
The algorithm seems fairly easy: go over the characters and make them upper or lower case, toggling between the two. There are a few ways to accomplish that.
(map) function can be used to go over some iterable, applying some function to each item. It happens that strings are an iterable, so we can use it.
But before that, we need to learn another part of the language: making our own functions.
(lambda) function can be used to create new functions, that can either be assigned to some name, or just passed directly to another function. So for example (lambda (x) x) would be just a function that returns exactly the same value as it gets.
Another useful thing for us to know would be "how do we make new variables" and "how do we assign to them", which is handled by the (def name value) and (:= name value) functions.
So then, let's have a look at what we can do with a (map):
(map spork argstring)We can have some function spork going over the argstring. Okay. What should that function do?
We need it to remember if it was doing upper or lower case last time...
Time for another new function: (do)! (do) exists to execute our code in order, without really doing anything fancy. And we need it here, because (map) won't be telling us where in the argstring we are. So, we will make a new variable that will simply toggle between True and False, outside the (map).
(do
(def toggle False)
(map (lambda (symbol) (do
(:= toggle (not toggle))
(str (if toggle "upper" "lower") symbol)
))
argstring)
)Note that you can nest expressions as much as you like, in most cases. Special cases include the
(quote)function, which will not actually evaluate it's argument, and the first argument of(lambda), which must be a list of argument names.
So, let's run it aaaand...
['p', 'E', 'n', 'G', 'u', 'I', 'n', 'O', 'f', 'D', 'o', 'O', 'm']
Not quite what we wanted. (map) doesn't output strings, it outputs lists. So how can we fix this?
First, we could join the symbols together. (str) has a method for that. Second, we could make another variable to store our result and append the symbols to it instead of putting them into the list. Third, we could learn a way to use (map) to work on the string by index of it's letters.
Here are those three implementations:
Joining:
(do
(def toggle False)
(str "join" ""
(map (lambda (symbol) (do
(:= toggle (not toggle))
(str (if toggle "upper" "lower") symbol)
))
argstring))
)
Extra variable:
(do
(def toggle False)
(def output "")
(map (lambda (symbol) (do
(:= toggle (not toggle))
(:= output (+ output (str (if toggle "upper" "lower") symbol)))
))
argstring)
output
)
Index:
(do
(map
(lambda (index)
(:= argstring:index (str (if (% index 2) "lower" "upper") argstring:index))
)
(range (len argstring)))
argstring
)Be sure to check out the Custom Command reference page to see what unfamiliar functions are doing.
Note that (do) returns the value of the last argument, which is why we put output there.
And, whoops - it seems like there is some more black magic happening in that third one!
In the third version, you can see us using argstring:index notation, as well as (range) and (len).
argstring:index is a way of accessing the specific character of the string, or more generally - a specific item in a list. The ability of an object to return an item when given an index is called "being subscriptable". Do note that the first item in any iterable will have an index of 0, not one.
Which works great for us, because (range) is a special function that returns a special iterable, called a range, which is pretty much a list of numbers. We only give it the "stop" value, so it's a list of all integer numbers starting from 0 and going up to the value we give it, non inclusive.
And the value we give it is (len argstring), which is just the length of the argument string. That way, the (map) is simply going over all the indices of the string, as opposed to letters.
So, we solved the problem one way. But there is another:
As mentioned above in our "Hello world" script, (reduce) is a function similar to (map) - it goes over the given list and applies some function to each.
But, there is a difference - (reduce) reduces the iterable to a single value. And the function that it applies:
(map) takes a function with one argument, the output of which is put back into the list.
(reduce) takes a function with two arguments, one of which is called an "accumulator". It is called that because that is the value that (reduce) returns eventually, and is also the value that stores the result of the function each time, accumulating the operations.
The starting accumulator value is set either to the first item of the given iterable, or to whatever value we give it.
Which means, we can get it to return a string without any extra steps! Let's apply that to our "toggle" script:
(do
(def toggle False)
(reduce (lambda (accumulator symbol) (do
(:= toggle (not toggle))
(+ accumulator (str (if toggle "upper" "lower") symbol))
))
argstring
"")
)Now, that works just as well, but we can do it more elegantly. But how?
Answer: regular expressions!
A "more elegant" solution in this case would involve us removing that (do) and the (def) - and we can do that by improving the data that we give to (reduce). And by improve, I mean "give it two letters at once". And for that, we will use yet another regexp function: (refindall).
As the name suggests, this function will find all the instances of the pattern it can.
Since we just want it to find two letters, in a way that lets us access first and second one, the regexp will look like this:
(.)(.?)
Very simple this time - the . stands for "any symbol", aside from newline. The ? is a quantifier that means "one or no symbols", and it is there because we won't always get a string that is even length, and we still want the regexp to capture that last bit.
Calling
(refindall)this way will return a list of lists, which will contain the capture group data. But that is not guaranteed if there is some plain text before the groups. Check the output before writing a whole script around it.
Now, with that regex at our disposal, our script will look like this:
(reduce
(lambda (string letters)
(+ string (+ (str "upper" letters:0)(str "lower" letters:1)))
)
(refindall "(.)(.?)" argstring)
""
)