Skip to content

Commit

Permalink
Version 2.2
Browse files Browse the repository at this point in the history
  • Loading branch information
IAmTomahawkx committed Aug 20, 2023
1 parent 5ba2e4b commit 22187af
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 35 deletions.
70 changes: 56 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,11 @@ ___

## What's new?
See the [full changelog](#changelog). \
It's been awhile since I last worked on this script, and it's time for some updates! In this version, I've moved the
readme online, as you can tell, and done a few other things.
- Fixed geq `>=` and seq `<=` being treated backwards (thanks to lance lake for pointing this out)
- Added a new comparison, `indir`, which will allow you to check if a file exists in a certain directory
- Fixed backslashes (`\`) being displayed when used to escape commas
- Added $balance() inside the if response
This update adds in JSON parsing abilities.

___
Heads up when reading this readme!
Square brackets inside a parameter mark optional arguments. Ex. `$OBSSwapScene(scene[,delay])`. Do not actually write the brackets!
Square brackets inside a parameter mark optional arguments. Ex. `$OBSSwapScene(scene[,delay])`. Do not actually write the square brackets!

## Custom Variables
One of the things ive always found the chatbot is lacking is ways to store your own variables without having to use \
Expand Down Expand Up @@ -86,15 +81,16 @@ haspermission - returns true if the comparee (which must be $userid or equivalen
- Editor

## Other Parameters
the following are parameters (including obs/slobs) that can go inside the true-msg and false-msg, and will be run with if. \
- $setvar (see above) \
- $if (yes, they can go inside other ifs)\
- $add(userid,amount) OR $add(userid,amount,succeed,fail) - adds points to the target user\
- $remove(userid,amount) OR $remove(userid,amount,succeed,fail) - removes points from the user\
- $getapi(url)\
- $write(filepath,content) OR $write(filepath,content,succeed,fail)\
the following are parameters (including obs/slobs) that can go inside the true-msg and false-msg, and will be run with if.
- $setvar (see above)
- $if (yes, they can go inside other ifs)
- $add(userid,amount) OR $add(userid,amount,succeed,fail) - adds points to the target user
- $remove(userid,amount) OR $remove(userid,amount,succeed,fail) - removes points from the user
- $getapi(url)
- $write(filepath,content) OR $write(filepath,content,succeed,fail)
- $mathif(equation)
- $balance(user) - returns info on the specified user
- $parsejson(filepath)#anchor - See [JSON Parsing](#JSON Parsing)

## OBS & SLOBS parameters
- $OBSSwapScene(scene[,delay])\
Expand All @@ -109,15 +105,61 @@ the following are parameters (including obs/slobs) that can go inside the true-m
- $SLOBSFolderVisibility(folder,on/off[,scene])\
- $SLOBSTimedFolderVisibility(folder,on/off,delay[,scene])

## JSON Parsing
As of V2.2.0, this script now features $parsejson, which allows you to pull JSON data from a file, and grab a specific key from it.
This is a rather advanced feature, and if you don't know what this is, feel free to skip this section.
To use this, simply use the $parsejson parameter, and provide it with a filepath to a JSON file.
Afterwards, use an anchor to specify where in the json file to go.

### Anchors
Anchors are simply another way to provide arguments to the parsejson parameter. an anchor would look like this:
```
$parsejson(myfile.json)#abc
```
where `#abc` is the anchor. This allows you to navigate the JSON file in a chainable way, eg:
```
$parsejson(myfile.json)#abc.def
```
That way, when you have nested json, like this:
```json
{
"abc": {
"def": "ghi",
"jkl": [
"mno",
"pqr"
]
}
}
```
You can navigate anywhere in the file, as deep as you need to go.

You can also navigate through arrays using numbers:
```
$parsejson(myfile.json)#abc.jkl.0
```
Or you can pick a random item in the array using `!r`:
```
$parsejson(myfile.json)#abc.jkl.!r
```

___
## Contact
having problems? have questions? ideas/requests? head over to my [discord](https://discord.gg/VKp6zrs) \
I am well aware that I'm terrible at explaining things. if this readme was no help whatsoever,
feel free to jump into my discord server to ask!
You can also DM me on discord: @iamtomahawkx

___

# Changelog
2.2.0:
- Added JSON parsing ability
- Use $parsejson for this, along with an [anchor tag](#anchors)
- Fixed a bug where the parser got too eager on argument delimiters
- Fixed a bug where the parser would sometimes remove whitespace
- Fixed a bug where the parser would remove unknown $parameters

2.1.0:
- Fixed geq `>=` and seq `<=` being treated backwards (thanks to lance lake for pointing this out)
- Added a new comparison, `indir`, which will allow you to check if a file exists in a certain directory
Expand Down
60 changes: 51 additions & 9 deletions TMHK-IFParemeters/ArgumentParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,31 @@
B) you're new to scripting, and looking to gain some knowledge. if this is the case, I have fully commented and explained this code
to help you. BUT, dont just copy paste this into your code. pick it apart, learn what is actually going on, how it works.
"""
import re


class Adapter(object):
def __init__(self, brackets=(('(', ')'),), delimiters=(',',)):
def __init__(self, json, brackets=(('(', ')'),), delimiters=(',',)):
self._original_brackets = brackets
self._brackets = dict(brackets)
self.brackets_in = set([b[0] for b in brackets])
self.brackets_out = set([b[1] for b in brackets])
self.delimiters = set(delimiters)

self.json = json # type: JSONAdapter

def parse(self, buffer, maxdepth=0):
return self._actual_parse(buffer, 0, maxdepth)

def _actual_parse(self, buffer, depth, maxdepth):
collecting = False
collect_post_anchor = False
params = [""] # we start with an empty string so we can add anything before a parameter.
# if there are no parameters, we will just return the string, which is essentially returning the buffer
bracketlvl = 0
for index, char in enumerate(buffer):

iterator = iter(enumerate(buffer))

for index, char in iterator: # type: (int, str)
if isinstance(params[-1], dict):
params[-1]['raw'] += char

Expand All @@ -77,7 +84,7 @@ def _actual_parse(self, buffer, depth, maxdepth):

# if i'm here, there are brackets (and therfor arguments), so create a new 'param' dict
del brack
params.append({"name": "", "params": [""], "raw": "$"})
params.append({"name": "", "params": [""], "raw": "$", "post_anchor": ""})
collecting = True # enable name collection
continue

Expand Down Expand Up @@ -109,14 +116,21 @@ def _actual_parse(self, buffer, depth, maxdepth):
params[-1]['params'].pop()
params.append("")
continue # if it is, im going to continue, as to not add the bracket to the outer layer.

if char in self.delimiters and bracketlvl <= 1 and buffer[index-1]!="\\":

if char == self.json.anchor and bracketlvl == 0 and isinstance(params[-2], dict): # this is -2 because when the bracket ends a string gets added to the params
c = buffer[index:]
length, anchor = self.json.parse_full(c)
tuple(next(iterator) for _ in range(length)) # eat the anchor in the buffer
params[-2]['post_anchor'] = anchor
continue

elif char in self.delimiters and bracketlvl == 1 and buffer[index-1]!="\\":
# if we are here, this means that weve hit a delimiter.
params[-1]['params'][-1] = params[-1]['params'][-1].strip() # first, remove any lingering whitespace from the current argument
params[-1]['params'].append("") # create a new argument.
continue

elif char in self.delimiters and bracketlvl <= 1 and buffer[index-1] == "\\":
elif char in self.delimiters and bracketlvl == 1 and buffer[index-1] == "\\":
params[-1]['params'] = params[-1]['params'][0:-1]

#if i've hit this point, that means there is nothing special about the character, so add it to the current argument
Expand All @@ -142,8 +156,36 @@ def _actual_parse(self, buffer, depth, maxdepth):
return params

def copy(self):
return Adapter(self._original_brackets, self.delimiters)
return Adapter(self._original_brackets, self.delimiters, self.post_anchor, self.json_args)


class JSONAdapter(object):
def __init__(self, sep=".", anchor="#"):
self.sep = sep
self.anchor = anchor
cases = ["\"[a-zA-Z0-9!@#$%^&*()\-_+=|\[\]{}<>,?/'`~ ]+\"", "[a-zA-Z0-9]+", "!r"]

full = r"{anchor}({case})(?:\{sep}(?:{case}))*".format(anchor=anchor, sep=sep, case="|".join(cases))
self.full = re.compile(full)

def parse_full(self, string):
string = string # type: str
match = self.full.match(string, pos=0)
if not match:
return 0, None

# length, match
return match.span(0)[1], match.group(0)[1:]

def parse_anchor(self, string):
matches = string.split(self.sep) # type: list[str]
for index, match in enumerate(matches):
matches[index] = match.strip('"')

return matches


if __name__ == "__main__":
v = Adapter()
print(v.parse("$if(a,==,b,good$setvar(v,e) $setvar(e,v)job!,nope)"))
#print(v.parse("$if(a,==,b,good$setvar(v,e)#foo.bar.!r $setvar(e,v)job!,nope)"))
print(v.parse("$parsejson('file.txt')#anchor.foo.bar.!r yeetus"))
112 changes: 104 additions & 8 deletions TMHK-IFParemeters/Parameters_StreamlabsSystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@

variables = {}
last_save_time = time.time()
argparser = ArgumentParser.Adapter() # create my argparser instance.
controller = None # i will set this once we have the Parent at our disposal.
argparser = ArgumentParser.Adapter(ArgumentParser.JSONAdapter()) # create my argparser instance.
controller = None # type: scenecontrol.Broadcastcontrol # i will set this once we have the Parent at our disposal.

def send_message(message, data=None):
"""
Expand Down Expand Up @@ -240,6 +240,12 @@ def Parse(msg, userid, username, targetid, targetname, message):
if i['name'] == "if":
ret += parseif(i, userid, username, targetid, targetname)

elif i['name'] == "parsejson":
ret += parse_json(i)

else:
ret += " " + i['raw'] # try to correct for stripped whitespace

return ret
except ParsingError as e:
return e.message
Expand All @@ -251,7 +257,7 @@ def Parse(msg, userid, username, targetid, targetname, message):
traceback.print_exception(et, v, tb, file=f)
f.write("\n")

return ret
return ret or (msg + " <A fatal error occurred in the IF script. Please send the log file to the script developer>")

def parse_arg_parameters(string, msg):
v = view.StringView(msg)
Expand Down Expand Up @@ -322,10 +328,10 @@ def parseif(args, user, username, targetuser, targetname):
if i['name'] == "if":
ret += parseif(i, user, username, targetuser, targetname)

if i['name'] in ["add", "remove"]:
elif i['name'] in ["add", "remove"]:
ret += parse_currency(i)

if i['name'] == "getapi":
elif i['name'] == "getapi":
response = json.loads(i['params'][0])
if response['status'] != 200: # api error
ret += response['error']
Expand All @@ -334,17 +340,107 @@ def parseif(args, user, username, targetuser, targetname):
ret += response['response']
continue

if i['name'] == "write":
elif i['name'] == "write":
ret += writefile(i)

if i['name'] == "mathif":
elif i['name'] == "mathif":
ret += parsemath(i)

if i['name'] == "balance":
elif i['name'] == "balance":
ret += parse_balance(i)

elif i['name'] == "parsejson":
ret += parse_json(i)

else:
ret += " " + i["raw"]

return ret.strip()

def _assemble_anchor_error(where, err):
position = "#" + ".".join(where)
return "{{failed to parse json @ {pos} : {err}}}".format(pos=position, err=err)


def _parse_item_anchor(data, anchors, where): # type: (dict[str, str | int | dict | list] | list, list[str], list[str]) -> int | str | bool | list | dict
attr = anchors.pop()

if isinstance(data, list):
if not attr.isdigit() and attr != "!r":
return _assemble_anchor_error(where, "Attempted to access '{attr}' on an array".format(attr=attr))

elif attr == "!r":
digit = random.randint(0, len(data) - 1)
_where = "{}!r".format(digit)

else:
digit = int(attr)
_where = str(digit)

nxt = data[digit]
where.append(_where)

if anchors:
if isinstance(nxt, (str, int, bool)):
return _assemble_anchor_error(where, "There are still anchors to be parsed, but this item is not anchorable")

return _parse_item_anchor(nxt, anchors, where)

else:
return nxt # type: ignore

else:
where.append(attr)
try:
nxt = data[attr]
except KeyError:
return _assemble_anchor_error(where, "Item does not exist")

else:
if anchors:
if isinstance(nxt, (str, int, bool)):
return _assemble_anchor_error(where, "There are still anchors to be parsed, but this item is not anchorable")

return _parse_item_anchor(nxt, anchors, where)

else:
return nxt # type: ignore

def parse_json(arg):
if 0 >= len(arg["params"]) > 1:
return "{{$parsejson takes 1 argument, not {0}}}".format(len(arg["params"]))

if not arg["post_anchor"]:
return "{$parsejson expects an anchor, please read the docs}"

processed_anchor = argparser.json.parse_anchor(arg["post_anchor"])
processed_anchor.reverse() # process them working down the chain

fp = arg["params"][0]

try:
fp = os.path.abspath(fp)
t = time.time()

with open(fp) as f:
data = json.load(f) # hopefully no one throws a fuckoff massive json file at this

post = time.time() - t
if post > 3: # they threw a fuckoff massive json file at it
Parent.Log(ScriptName, "WARN: JSON processing of {0} took {1} seconds to complete, this WILL affect bot performance".format(fp, post))

except (OSError, WindowsError):
return "{{Could not open {0}}}".format(fp)
except ValueError as e:
return "{{Could not parse the JSON file: {0}}}".format(e.message)
except Exception as e:
Parent.Log(ScriptName, str((e, type(e))))
return "{{Something went wrong while processing {0}}}".format(fp)

attr = _parse_item_anchor(data, processed_anchor, [])
return str(attr)


def parse_balance(arg):
if len(arg['params']) != 1:
return "{{$balance takes 1 argument, not {0}}}".format(len(arg['params']))
Expand Down
Loading

0 comments on commit 22187af

Please sign in to comment.