bTagScript 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. bTagScript/__init__.py +66 -0
  2. bTagScript/adapter/__init__.py +16 -0
  3. bTagScript/adapter/discord_adapters.py +275 -0
  4. bTagScript/adapter/function_adapter.py +33 -0
  5. bTagScript/adapter/int_adapter.py +30 -0
  6. bTagScript/adapter/object_adapter.py +41 -0
  7. bTagScript/adapter/string_adapter.py +69 -0
  8. bTagScript/block/__init__.py +67 -0
  9. bTagScript/block/break_block.py +41 -0
  10. bTagScript/block/case_block.py +62 -0
  11. bTagScript/block/comment_block.py +33 -0
  12. bTagScript/block/control_block.py +161 -0
  13. bTagScript/block/counting_blocks.py +88 -0
  14. bTagScript/block/digitshorthand_block.py +38 -0
  15. bTagScript/block/discord_blocks/__init__.py +20 -0
  16. bTagScript/block/discord_blocks/command_block.py +52 -0
  17. bTagScript/block/discord_blocks/cooldown_block.py +101 -0
  18. bTagScript/block/discord_blocks/delete_block.py +47 -0
  19. bTagScript/block/discord_blocks/embed_block.py +248 -0
  20. bTagScript/block/discord_blocks/override_block.py +61 -0
  21. bTagScript/block/discord_blocks/react_block.py +49 -0
  22. bTagScript/block/discord_blocks/redirect_block.py +42 -0
  23. bTagScript/block/discord_blocks/requirement_blocks.py +83 -0
  24. bTagScript/block/helpers.py +121 -0
  25. bTagScript/block/math_blocks.py +257 -0
  26. bTagScript/block/random_block.py +65 -0
  27. bTagScript/block/range_block.py +54 -0
  28. bTagScript/block/replace_block.py +110 -0
  29. bTagScript/block/stop_block.py +38 -0
  30. bTagScript/block/strf_block.py +70 -0
  31. bTagScript/block/url_blocks.py +76 -0
  32. bTagScript/block/util_blocks/__init__.py +3 -0
  33. bTagScript/block/util_blocks/debug_block.py +107 -0
  34. bTagScript/block/var_block.py +49 -0
  35. bTagScript/block/vargetter_blocks.py +91 -0
  36. bTagScript/exceptions.py +139 -0
  37. bTagScript/interface/__init__.py +4 -0
  38. bTagScript/interface/adapter.py +43 -0
  39. bTagScript/interface/block.py +136 -0
  40. bTagScript/interpreter.py +614 -0
  41. bTagScript/utils.py +65 -0
  42. bTagScript/verb.py +129 -0
  43. btagscript-5.0.0.dist-info/METADATA +109 -0
  44. btagscript-5.0.0.dist-info/RECORD +47 -0
  45. btagscript-5.0.0.dist-info/WHEEL +5 -0
  46. btagscript-5.0.0.dist-info/licenses/LICENSE +1 -0
  47. btagscript-5.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,248 @@
1
+ import json
2
+ from inspect import ismethod
3
+ from typing import Optional, Union
4
+
5
+ from discord import Colour, Embed
6
+
7
+ from ...exceptions import BadColourArgument, EmbedParseError
8
+ from ...interface import Block
9
+ from ...interpreter import Context
10
+ from ..helpers import helper_split, implicit_bool
11
+
12
+
13
+ def string_to_color(argument: str) -> Colour:
14
+ """
15
+ Converts a string to a discord.Colour object
16
+ """
17
+ arg = argument.replace("0x", "").lower()
18
+
19
+ if arg[0] == "#":
20
+ arg = arg[1:]
21
+ try:
22
+ value = int(arg, base=16)
23
+ if not (0 <= value <= 0xFFFFFF):
24
+ raise BadColourArgument(arg)
25
+ return Colour(value=value)
26
+ except ValueError:
27
+ arg = arg.replace(" ", "_")
28
+ method = getattr(Colour, arg, None)
29
+ if arg.startswith("from_") or method is None or not ismethod(method):
30
+ raise BadColourArgument(arg) # pylint: disable=raise-missing-from
31
+ return method()
32
+
33
+
34
+ def set_color(embed: Embed, attribute: str, value: str) -> None:
35
+ """
36
+ Sets the colour of the embed given
37
+ """
38
+ value = string_to_color(value)
39
+ setattr(embed, attribute, value)
40
+
41
+
42
+ def set_dynamic_url(embed: Embed, attribute: str, value: str) -> None:
43
+ """
44
+ Dynamically sets the url of the embed
45
+ """
46
+ method = getattr(embed, f"set_{attribute}")
47
+ method(url=value)
48
+
49
+
50
+ def add_field(embed: Embed, _: str, payload: str) -> None:
51
+ """
52
+ Adds a field to the embed
53
+ """
54
+ parts = helper_split(payload, False)
55
+ if parts is None or len(parts) not in (2, 3):
56
+ raise EmbedParseError("`add_field` payload was not split by |")
57
+ name, value = parts[0], parts[1]
58
+ inline = False
59
+ if len(parts) == 3:
60
+ inline = implicit_bool(parts[2])
61
+ if inline is None:
62
+ raise EmbedParseError(
63
+ f"`inline` argument for `add_field` is not a boolean value ({parts[2]})"
64
+ )
65
+ embed.add_field(name=name, value=value, inline=inline)
66
+
67
+
68
+ class EmbedBlock(Block):
69
+ """
70
+ An embed block will send an embed in the tag response.
71
+ There are two ways to use the embed block, either by using properly
72
+ formatted embed JSON from an embed generator or manually inputting
73
+ the accepted embed attributes.
74
+
75
+ **JSON**
76
+
77
+ Using JSON to create an embed offers complete embed customization.
78
+ Multiple embed generators are available online to visualize and generate
79
+ embed JSON.
80
+
81
+ **Usage:** ``{embed(<json>)}``
82
+
83
+ **Payload:** ``None``
84
+
85
+ **Parameter:** ``json``
86
+
87
+ .. tagscript::
88
+
89
+ {embed({"title":"Hello!", "description":"This is a test embed."})}
90
+ {embed({
91
+ "title":"Here's a random duck!",
92
+ "image":{"url":"https://random-d.uk/api/randomimg"},
93
+ "color":15194415
94
+ })}
95
+
96
+ **Manual**
97
+
98
+ The following embed attributes can be set manually:
99
+
100
+ * ``title``
101
+ * ``description``
102
+ * ``color``
103
+ * ``url``
104
+ * ``thumbnail``
105
+ * ``image``
106
+ * ``field`` - (See below)
107
+
108
+ Adding a field to an embed requires the payload to be split by ``|``, into
109
+ either 2 or 3 parts. The first part is the name of the field, the second is
110
+ the text of the field, and the third optionally specifies whether the field
111
+ should be inline.
112
+
113
+ **Usage:** ``{embed(<attribute>):<value>}``
114
+
115
+ **Payload:** ``value``
116
+
117
+ **Parameter:** ``attribute``
118
+
119
+ .. tagscript::
120
+ {embed(color):#37b2cb}
121
+ {embed(title):Rules}
122
+ {embed(description):Follow these rules to ensure a good experience in our server!}
123
+ {embed(field):Rule 1|Respect everyone you speak to.|false}
124
+
125
+ Both methods can be combined to create an embed in a tag.
126
+ The following tagscript uses JSON to create an embed with fields and later
127
+ set the embed title.
128
+
129
+ :: tagscript::
130
+
131
+ {embed({{"fields":[{"name":"Field 1","value":"field description","inline":false}]})}
132
+ {embed(title):my embed title}
133
+ """
134
+
135
+ ACCEPTED_NAMES = ("embed",)
136
+
137
+ ATTRIBUTE_HANDLERS = {
138
+ "description": setattr,
139
+ "title": setattr,
140
+ "color": set_color,
141
+ "colour": set_color,
142
+ "url": setattr,
143
+ "thumbnail": set_dynamic_url,
144
+ "image": set_dynamic_url,
145
+ "field": add_field,
146
+ }
147
+
148
+ @staticmethod
149
+ def get_embed(ctx: Context) -> Embed:
150
+ """
151
+ Gets the embed object
152
+ """
153
+ return ctx.response.actions.get("embed", Embed())
154
+
155
+ @staticmethod
156
+ def value_to_color(value: Optional[Union[int, str]]) -> Colour:
157
+ """
158
+ Converts a value to a discord.Colour object
159
+ """
160
+ if value is None or isinstance(value, Colour):
161
+ return value
162
+ if isinstance(value, int):
163
+ return Colour(value)
164
+ elif isinstance(value, str):
165
+ return string_to_color(value)
166
+ else:
167
+ raise EmbedParseError("Received invalid type for color key (expected string or int)")
168
+
169
+ def text_to_embed(self, text: str) -> Embed:
170
+ """
171
+ Converts json to an embed
172
+ """
173
+ try:
174
+ data = json.loads(text)
175
+ except json.decoder.JSONDecodeError as error:
176
+ raise EmbedParseError(error) from error
177
+
178
+ if data.get("embed"):
179
+ data = data["embed"]
180
+ if data.get("timestamp"):
181
+ data["timestamp"] = data["timestamp"].strip("Z")
182
+
183
+ color = data.pop("color", data.pop("colour", None))
184
+
185
+ try:
186
+ embed = Embed.from_dict(data)
187
+ except Exception as error:
188
+ raise EmbedParseError(error) from error
189
+ else:
190
+ if color := self.value_to_color(color):
191
+ embed.color = color
192
+ return embed
193
+
194
+ @classmethod
195
+ def update_embed(cls, embed: Embed, attribute: str, value: str) -> Embed:
196
+ """
197
+ Update the embed with all attributes
198
+ """
199
+ handler = cls.ATTRIBUTE_HANDLERS[attribute]
200
+ try:
201
+ handler(embed, attribute, value)
202
+ except Exception as error:
203
+ raise EmbedParseError(error) from error
204
+ return embed
205
+
206
+ @staticmethod
207
+ def return_error(error: Exception) -> str:
208
+ """
209
+ Return an error message
210
+ """
211
+ return f"Embed Parse Error: {error}"
212
+
213
+ @staticmethod
214
+ def return_embed(ctx: Context, embed: Embed) -> str:
215
+ """
216
+ Returns the embed
217
+ """
218
+ try:
219
+ length = len(embed)
220
+ except Exception as error: # pylint: disable=broad-except
221
+ return str(error)
222
+ if length > 6000:
223
+ return f"`MAX EMBED LENGTH REACHED ({length}/6000)`"
224
+ ctx.response.actions["embed"] = embed
225
+ return ""
226
+
227
+ def process(self, ctx: Context) -> Optional[str]:
228
+ """
229
+ Process the block
230
+ """
231
+ if not ctx.verb.parameter:
232
+ return self.return_embed(ctx, self.get_embed(ctx))
233
+
234
+ lowered = ctx.verb.parameter.lower()
235
+ try:
236
+ if ctx.verb.parameter.strip().startswith("{") and ctx.verb.parameter.strip().endswith(
237
+ "}"
238
+ ):
239
+ embed = self.text_to_embed(ctx.verb.parameter)
240
+ elif lowered in self.ATTRIBUTE_HANDLERS and ctx.verb.payload:
241
+ embed = self.get_embed(ctx)
242
+ embed = self.update_embed(embed, lowered, ctx.verb.payload)
243
+ else:
244
+ return
245
+ except EmbedParseError as error:
246
+ return self.return_error(error)
247
+
248
+ return self.return_embed(ctx, embed)
@@ -0,0 +1,61 @@
1
+ from typing import Optional
2
+
3
+ from ...interface import Block
4
+ from ...interpreter import Context
5
+
6
+
7
+ class OverrideBlock(Block):
8
+ """
9
+ Override a command's permission requirements. This can override
10
+ mod, admin, or general user permission requirements when running commands
11
+ with the :ref:`Command Block`. Passing no parameter will default to overriding
12
+ all permissions.
13
+
14
+ In order to add a tag with the override block, the tag author must have ``Manage
15
+ Server`` permissions.
16
+
17
+ This will not override bot owner commands or command checks.
18
+
19
+ **Usage:** ``{override(["admin"|"mod"|"permissions"]):[command]}``
20
+
21
+ **Aliases:** ``bypass``
22
+
23
+ **Payload:** ``command``
24
+
25
+ **Parameter:** ``"admin", "mod", "permissions"``
26
+
27
+ **Examples:**
28
+
29
+ .. tagscript::
30
+
31
+ {override}
32
+ overrides all commands and permissions
33
+
34
+ {override(admin)}
35
+ overrides commands that require the admin role
36
+
37
+ {bypass(permissions)}
38
+ {bypass(mod)}
39
+ overrides commands that require the mod role or have user permission requirements
40
+ """
41
+
42
+ ACCEPTED_NAMES = ("override", "bypass")
43
+
44
+ def process(self, ctx: Context) -> Optional[str]:
45
+ """
46
+ Process the block and update response.actions with correct overrides
47
+ """
48
+ param = ctx.verb.parameter
49
+ if not param:
50
+ ctx.response.actions["overrides"] = {"admin": True, "mod": True, "permissions": True}
51
+ return ""
52
+
53
+ param = param.strip().lower()
54
+ if param not in ("admin", "mod", "permissions"):
55
+ return None
56
+ overrides = ctx.response.actions.get(
57
+ "overrides", {"admin": False, "mod": False, "permissions": False}
58
+ )
59
+ overrides[param] = True
60
+ ctx.response.actions["overrides"] = overrides
61
+ return ""
@@ -0,0 +1,49 @@
1
+ from typing import Optional
2
+
3
+ from ...interface import verb_required_block
4
+ from ...interpreter import Context
5
+
6
+
7
+ class ReactBlock(verb_required_block(True, payload=True)):
8
+ """
9
+ The react block will set the actions "react" key to a list of reactions.
10
+
11
+ .. note::
12
+
13
+ You must set the behaviour manually.
14
+
15
+ **Usage:** ``{react:<emojis>}``
16
+
17
+ **Aliases:** ``None``
18
+
19
+ **Payload:** ``emojis``
20
+
21
+ **Parameter:** ``None``
22
+
23
+ .. tagscript::
24
+
25
+ {react:💩}
26
+ {react:💩,:)}
27
+ {react:💩~:)~:D}
28
+ """
29
+
30
+ ACCEPTED_NAMES = ("react",)
31
+
32
+ def __init__(self, limit: int = 5) -> None:
33
+ """
34
+ Initialize the block
35
+
36
+ Limit is the maximum number of reactions the block will add
37
+ """
38
+ self.limit = limit
39
+ super().__init__()
40
+
41
+ def process(self, ctx: Context) -> Optional[str]:
42
+ """
43
+ Process the reactions
44
+ """
45
+ reactions = ctx.verb.payload.strip().split("~" if "~" in ctx.verb.payload else ",")
46
+ if len(reactions) > self.limit:
47
+ return f"`Reaction Limit Reached ({self.limit})`"
48
+ ctx.response.actions["reactions"] = reactions
49
+ return ""
@@ -0,0 +1,42 @@
1
+ from typing import Optional
2
+
3
+ from ...interface import verb_required_block
4
+ from ...interpreter import Context
5
+
6
+
7
+ class RedirectBlock(verb_required_block(True, parameter=True)):
8
+ """
9
+ Redirects the tag response to either the given channel, the author's DMs,
10
+ or uses a reply based on what is passed to the parameter.
11
+
12
+ **Usage:** ``{redirect(<"dm"|"reply"|channel>)}``
13
+
14
+ **Payload:** ``None``
15
+
16
+ **Parameter:** ``"dm", "reply", "channel"``
17
+
18
+ **Examples:**
19
+
20
+ .. tagscript::
21
+
22
+ {redirect(dm)}
23
+ {redirect(reply)}
24
+ {redirect(#general)}
25
+ {redirect(626861902521434160)}
26
+ """
27
+
28
+ ACCEPTED_NAMES = ("redirect",)
29
+
30
+ def process(self, ctx: Context) -> Optional[str]:
31
+ """
32
+ Process the redirect block and params
33
+ """
34
+ param = ctx.verb.parameter.strip()
35
+ if param.lower() == "dm":
36
+ target = "dm"
37
+ elif param.lower() == "reply":
38
+ target = "reply"
39
+ else:
40
+ target = param
41
+ ctx.response.actions["target"] = target
42
+ return ""
@@ -0,0 +1,83 @@
1
+ from typing import Optional
2
+
3
+ from ...interface import verb_required_block
4
+ from ...interpreter import Context
5
+
6
+
7
+ class RequireBlock(verb_required_block(True, parameter=True)):
8
+ """
9
+ The require block will attempt to convert the given parameter into a channel
10
+ role or member, using name or ID. If the user running the tag is not in the
11
+ targeted channel or doesn't have the targeted role, the tag will stop processing
12
+ and it will send the response if one is given. Multiple role or channel
13
+ requirements can be given, and should be split by a ",".
14
+
15
+ **Usage:** ``{require(<role, channel, member>):[response]}``
16
+
17
+ **Aliases:** ``whitelist``
18
+
19
+ **Payload:** ``response``
20
+
21
+ **Parameter:** ``role, channel, member``
22
+
23
+ **Examples:**
24
+
25
+ .. tagscript::
26
+
27
+ {require(Moderator)}
28
+ {require(#general, #bot-cmds):This tag can only be run in #general and #bot-cmds.}
29
+ {require(757425366209134764, 668713062186090506, 737961895356792882):You aren't allowed to use this tag.}
30
+ """
31
+
32
+ ACCEPTED_NAMES = ("require", "whitelist")
33
+
34
+ def process(self, ctx: Context) -> Optional[str]:
35
+ """
36
+ Process the requirements
37
+ """
38
+ actions = ctx.response.actions.get("requires")
39
+ if actions:
40
+ return None
41
+ ctx.response.actions["requires"] = {
42
+ "items": [i.strip() for i in ctx.verb.parameter.split(",")],
43
+ "response": ctx.verb.payload,
44
+ }
45
+ return ""
46
+
47
+
48
+ class BlacklistBlock(verb_required_block(True, parameter=True)):
49
+ """
50
+ The blacklist block will attempt to convert the given parameter into a channel
51
+ or role, using name or ID. If the user running the tag is in the targeted
52
+ channel or has the targeted role, the tag will stop processing and
53
+ it will send the response if one is given. Multiple role or channel
54
+ requirements can be given, and should be split by a ",".
55
+
56
+ **Usage:** ``{blacklist(<role,channel>):[response]}``
57
+
58
+ **Payload:** ``response``
59
+
60
+ **Parameter:** ``role, channel``
61
+
62
+ **Examples:**
63
+
64
+ .. tagscript::
65
+ {blacklist(Muted)}
66
+ {blacklist(#support):This tag is not allowed in #support.}
67
+ {blacklist(Tag Blacklist, 668713062186090506):You are blacklisted from using tags.}
68
+ """
69
+
70
+ ACCEPTED_NAMES = ("blacklist",)
71
+
72
+ def process(self, ctx: Context) -> Optional[str]:
73
+ """
74
+ Process the blacklists
75
+ """
76
+ actions = ctx.response.actions.get("blacklist")
77
+ if actions:
78
+ return None
79
+ ctx.response.actions["blacklist"] = {
80
+ "items": [i.strip() for i in ctx.verb.parameter.split(",")],
81
+ "response": ctx.verb.payload,
82
+ }
83
+ return ""
@@ -0,0 +1,121 @@
1
+ """
2
+ Helpers for blocks, mainly for parsing expressions
3
+ """
4
+
5
+ import re
6
+ from typing import List, Optional
7
+
8
+ __all__ = ("implicit_bool", "helper_parse_if", "helper_split", "helper_parse_list_if")
9
+
10
+ SPLIT_REGEX = re.compile(r"(?<!\\)\|")
11
+ BOOL_LOOKUP = {"true": True, "false": False} # potentially add more bool values
12
+
13
+
14
+ def implicit_bool(string: str) -> Optional[bool]:
15
+ """
16
+ Parse a string to a boolean.
17
+
18
+ >>> implicit_bool("true")
19
+ True
20
+ >>> implicit_bool("FALSE")
21
+ False
22
+ >>> implicit_bool("abc")
23
+ None
24
+
25
+ Parameters
26
+ ----------
27
+ string: str
28
+ The string to convert.
29
+
30
+ Returns
31
+ -------
32
+ bool
33
+ The boolean value of the string.
34
+ None
35
+ The string failed to parse.
36
+ """
37
+ return BOOL_LOOKUP.get(string.lower())
38
+
39
+
40
+ def helper_parse_if(string: str) -> Optional[bool]:
41
+ """
42
+ Parse an expression string to a boolean.
43
+
44
+ >>> helper_parse_if("this == this")
45
+ True
46
+ >>> helper_parse_if("2>3")
47
+ False
48
+ >>> helper_parse_if("40 >= 40")
49
+ True
50
+ >>> helper_parse_if("False")
51
+ False
52
+ >>> helper_parse_if("1")
53
+ None
54
+
55
+ Parameters
56
+ ----------
57
+ string: str
58
+ The string to convert.
59
+
60
+ Returns
61
+ -------
62
+ bool
63
+ The boolean value of the expression.
64
+ None
65
+ The expression failed to parse.
66
+ """
67
+ value = implicit_bool(string)
68
+ if value is not None:
69
+ return value
70
+ try:
71
+ if "!=" in string:
72
+ spl = string.split("!=")
73
+ return spl[0].strip() != spl[1].strip()
74
+ if "==" in string:
75
+ spl = string.split("==")
76
+ return spl[0].strip() == spl[1].strip()
77
+ if ">=" in string:
78
+ spl = string.split(">=")
79
+ return float(spl[0].strip()) >= float(spl[1].strip())
80
+ if "<=" in string:
81
+ spl = string.split("<=")
82
+ return float(spl[0].strip()) <= float(spl[1].strip())
83
+ if ">" in string:
84
+ spl = string.split(">")
85
+ return float(spl[0].strip()) > float(spl[1].strip())
86
+ if "<" in string:
87
+ spl = string.split("<")
88
+ return float(spl[0].strip()) < float(spl[1].strip())
89
+ except: # pylint: disable=bare-except
90
+ pass
91
+
92
+
93
+ def helper_split(
94
+ split_string: str, easy: bool = True, *, maxsplit: int = None
95
+ ) -> Optional[List[str]]:
96
+ """
97
+ A helper method to universalize the splitting logic used in multiple
98
+ blocks and adapters. Please use this wherever a verb needs content to
99
+ be chopped at | , or ~!
100
+
101
+ >>> helper_split("this, should|work")
102
+ ["this, should", "work"]
103
+ """
104
+ args = (maxsplit,) if maxsplit is not None else ()
105
+ if "|" in split_string:
106
+ return SPLIT_REGEX.split(split_string, *args)
107
+ if easy and "~" in split_string:
108
+ return split_string.split("~", *args)
109
+ if easy and "," in split_string:
110
+ return split_string.split(",", *args)
111
+ return
112
+
113
+
114
+ def helper_parse_list_if(if_string: str) -> Optional[List[str]]:
115
+ """
116
+ Returns a list of bool strings from a string.
117
+ """
118
+ split = helper_split(if_string, False)
119
+ if split is None:
120
+ return [helper_parse_if(if_string)]
121
+ return [helper_parse_if(item) for item in split]