AdvancedTagscript 3.2.3__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.
- TagScriptEngine/__init__.py +227 -0
- TagScriptEngine/_warnings.py +88 -0
- TagScriptEngine/adapter/__init__.py +50 -0
- TagScriptEngine/adapter/discordadapters.py +596 -0
- TagScriptEngine/adapter/functionadapter.py +23 -0
- TagScriptEngine/adapter/intadapter.py +22 -0
- TagScriptEngine/adapter/objectadapter.py +35 -0
- TagScriptEngine/adapter/redbotadapters.py +161 -0
- TagScriptEngine/adapter/stringadapter.py +47 -0
- TagScriptEngine/block/__init__.py +130 -0
- TagScriptEngine/block/allowedmentions.py +60 -0
- TagScriptEngine/block/assign.py +43 -0
- TagScriptEngine/block/breakblock.py +41 -0
- TagScriptEngine/block/case.py +63 -0
- TagScriptEngine/block/command.py +141 -0
- TagScriptEngine/block/comment.py +29 -0
- TagScriptEngine/block/control.py +149 -0
- TagScriptEngine/block/cooldown.py +95 -0
- TagScriptEngine/block/count.py +68 -0
- TagScriptEngine/block/embedblock.py +306 -0
- TagScriptEngine/block/fiftyfifty.py +34 -0
- TagScriptEngine/block/helpers.py +164 -0
- TagScriptEngine/block/loosevariablegetter.py +40 -0
- TagScriptEngine/block/mathblock.py +164 -0
- TagScriptEngine/block/randomblock.py +51 -0
- TagScriptEngine/block/range.py +56 -0
- TagScriptEngine/block/redirect.py +42 -0
- TagScriptEngine/block/replaceblock.py +110 -0
- TagScriptEngine/block/require_blacklist.py +79 -0
- TagScriptEngine/block/shortcutredirect.py +23 -0
- TagScriptEngine/block/stopblock.py +38 -0
- TagScriptEngine/block/strf.py +70 -0
- TagScriptEngine/block/strictvariablegetter.py +38 -0
- TagScriptEngine/block/substr.py +25 -0
- TagScriptEngine/block/urlencodeblock.py +41 -0
- TagScriptEngine/exceptions.py +105 -0
- TagScriptEngine/interface/__init__.py +14 -0
- TagScriptEngine/interface/adapter.py +75 -0
- TagScriptEngine/interface/block.py +124 -0
- TagScriptEngine/interpreter.py +502 -0
- TagScriptEngine/py.typed +0 -0
- TagScriptEngine/utils.py +71 -0
- TagScriptEngine/verb.py +160 -0
- advancedtagscript-3.2.3.dist-info/METADATA +99 -0
- advancedtagscript-3.2.3.dist-info/RECORD +48 -0
- advancedtagscript-3.2.3.dist-info/WHEEL +5 -0
- advancedtagscript-3.2.3.dist-info/licenses/LICENSE +1 -0
- advancedtagscript-3.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
from ..interface import Block
|
|
3
|
+
from ..interpreter import Context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommentBlock(Block):
|
|
7
|
+
"""
|
|
8
|
+
The comment block is just for comments, it will not be parsed,
|
|
9
|
+
however it will be removed from your tag's output.
|
|
10
|
+
|
|
11
|
+
**Usage:** ``{comment([other]):[text]}``
|
|
12
|
+
|
|
13
|
+
**Aliases:** /, Comment, comment, //, #
|
|
14
|
+
|
|
15
|
+
**Payload:** ``text``
|
|
16
|
+
|
|
17
|
+
**Parameter:** ``other``
|
|
18
|
+
|
|
19
|
+
.. tagscript::
|
|
20
|
+
|
|
21
|
+
{#:Comment!}
|
|
22
|
+
|
|
23
|
+
{Comment(Something):Comment!}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("/", "Comment", "comment", "//", "#")
|
|
27
|
+
|
|
28
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
29
|
+
return ""
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple, cast
|
|
4
|
+
|
|
5
|
+
from ..interface import verb_required_block, Block
|
|
6
|
+
from ..interpreter import Context
|
|
7
|
+
from . import helper_parse_if, helper_parse_list_if, helper_split
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__: Tuple[str, ...] = ("AnyBlock", "AllBlock", "IfBlock")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_into_output(payload: str, result: Optional[bool]) -> Optional[str]:
|
|
14
|
+
if result is None:
|
|
15
|
+
return
|
|
16
|
+
try:
|
|
17
|
+
output = helper_split(payload, False)
|
|
18
|
+
if output is not None and len(output) == 2:
|
|
19
|
+
if result:
|
|
20
|
+
return output[0]
|
|
21
|
+
else:
|
|
22
|
+
return output[1]
|
|
23
|
+
elif result:
|
|
24
|
+
return payload
|
|
25
|
+
else:
|
|
26
|
+
return ""
|
|
27
|
+
except: # noqa: E722
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ImplicitPPRBlock: Block = verb_required_block(True, payload=True, parameter=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AnyBlock(ImplicitPPRBlock): # type: ignore
|
|
35
|
+
"""
|
|
36
|
+
The any block checks that any of the passed expressions are true.
|
|
37
|
+
Multiple expressions can be passed to the parameter by splitting them with ``|``.
|
|
38
|
+
|
|
39
|
+
The payload is a required message that must be split by ``|``.
|
|
40
|
+
If the expression evaluates true, then the message before the ``|`` is returned, else the message after is returned.
|
|
41
|
+
|
|
42
|
+
**Usage:** ``{any(<expression|expression|...>):<message>}``
|
|
43
|
+
|
|
44
|
+
**Aliases:** ``or``
|
|
45
|
+
|
|
46
|
+
**Payload:** message
|
|
47
|
+
|
|
48
|
+
**Parameter:** expression
|
|
49
|
+
|
|
50
|
+
**Examples:** ::
|
|
51
|
+
|
|
52
|
+
{any({args}==hi|{args}==hello|{args}==heyy):Hello {user}!|How rude.}
|
|
53
|
+
# if {args} is hi
|
|
54
|
+
Hello sravan#0001!
|
|
55
|
+
|
|
56
|
+
# if {args} is what's up!
|
|
57
|
+
How rude.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("any", "or")
|
|
61
|
+
|
|
62
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
63
|
+
result = any(helper_parse_list_if(ctx.verb.parameter) or [])
|
|
64
|
+
return parse_into_output(cast(str, ctx.verb.payload), result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AllBlock(ImplicitPPRBlock): # type: ignore
|
|
68
|
+
"""
|
|
69
|
+
The all block checks that all of the passed expressions are true.
|
|
70
|
+
Multiple expressions can be passed to the parameter by splitting them with ``|``.
|
|
71
|
+
|
|
72
|
+
The payload is a required message that must be split by ``|``.
|
|
73
|
+
If the expression evaluates true, then the message before the ``|`` is returned, else the message after is returned.
|
|
74
|
+
|
|
75
|
+
**Usage:** ``{all(<expression|expression|...>):<message>}``
|
|
76
|
+
|
|
77
|
+
**Aliases:** ``and``
|
|
78
|
+
|
|
79
|
+
**Payload:** message
|
|
80
|
+
|
|
81
|
+
**Parameter:** expression
|
|
82
|
+
|
|
83
|
+
**Examples:** ::
|
|
84
|
+
|
|
85
|
+
{all({args}>=100|{args}<=1000):You picked {args}.|You must provide a number between 100 and 1000.}
|
|
86
|
+
# if {args} is 52
|
|
87
|
+
You must provide a number between 100 and 1000.
|
|
88
|
+
|
|
89
|
+
# if {args} is 282
|
|
90
|
+
You picked 282.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("all", "and")
|
|
94
|
+
|
|
95
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
96
|
+
result = all(helper_parse_list_if(ctx.verb.parameter) or [])
|
|
97
|
+
return parse_into_output(cast(str, ctx.verb.payload), result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class IfBlock(ImplicitPPRBlock): # type: ignore
|
|
101
|
+
"""
|
|
102
|
+
The if block returns a message based on the passed expression to the parameter.
|
|
103
|
+
An expression is represented by two values compared with an operator.
|
|
104
|
+
|
|
105
|
+
The payload is a required message that must be split by ``|``.
|
|
106
|
+
If the expression evaluates true, then the message before the ``|`` is returned, else the message after is returned.
|
|
107
|
+
|
|
108
|
+
**Expression Operators:**
|
|
109
|
+
|
|
110
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
111
|
+
| Operator | Check | Example | Description |
|
|
112
|
+
+==========+==========================+=========+=============================================+
|
|
113
|
+
| ``==`` | equality | a==a | value 1 is equal to value 2 |
|
|
114
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
115
|
+
| ``!=`` | inequality | a!=b | value 1 is not equal to value 2 |
|
|
116
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
117
|
+
| ``>`` | greater than | 5>3 | value 1 is greater than value 2 |
|
|
118
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
119
|
+
| ``<`` | less than | 4<8 | value 1 is less than value 2 |
|
|
120
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
121
|
+
| ``>=`` | greater than or equality | 10>=10 | value 1 is greater than or equal to value 2 |
|
|
122
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
123
|
+
| ``<=`` | less than or equality | 5<=6 | value 1 is less than or equal to value 2 |
|
|
124
|
+
+----------+--------------------------+---------+---------------------------------------------+
|
|
125
|
+
|
|
126
|
+
**Usage:** ``{if(<expression>):<message>]}``
|
|
127
|
+
|
|
128
|
+
**Payload:** message
|
|
129
|
+
|
|
130
|
+
**Parameter:** expression
|
|
131
|
+
|
|
132
|
+
**Examples:** ::
|
|
133
|
+
|
|
134
|
+
{if({args}==63):You guessed it! The number I was thinking of was 63!|Too {if({args}<63):low|high}, try again.}
|
|
135
|
+
# if args is 63
|
|
136
|
+
# You guessed it! The number I was thinking of was 63!
|
|
137
|
+
|
|
138
|
+
# if args is 73
|
|
139
|
+
# Too low, try again.
|
|
140
|
+
|
|
141
|
+
# if args is 14
|
|
142
|
+
# Too high, try again.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("if",)
|
|
146
|
+
|
|
147
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
148
|
+
result = helper_parse_if(cast(str, ctx.verb.parameter))
|
|
149
|
+
return parse_into_output(cast(str, ctx.verb.payload), result)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, cast
|
|
5
|
+
|
|
6
|
+
from discord.ext.commands import Cooldown, CooldownMapping
|
|
7
|
+
|
|
8
|
+
from ..exceptions import CooldownExceeded
|
|
9
|
+
from ..interface import verb_required_block
|
|
10
|
+
from ..interpreter import Context
|
|
11
|
+
from .helpers import helper_split
|
|
12
|
+
|
|
13
|
+
__all__: Tuple[str, ...] = ("CooldownBlock",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CooldownBlock(verb_required_block(True, payload=True, parameter=True)): # type: ignore
|
|
17
|
+
"""
|
|
18
|
+
The cooldown block implements cooldowns when running a tag.
|
|
19
|
+
The parameter requires 2 values to be passed: ``rate`` and ``per`` integers.
|
|
20
|
+
The ``rate`` is the number of times the tag can be used every ``per`` seconds.
|
|
21
|
+
|
|
22
|
+
The payload requires a ``key`` value, which is the key used to store the cooldown.
|
|
23
|
+
A key should be any string that is unique. If a channel's ID is passed as a key,
|
|
24
|
+
the tag's cooldown will be enforced on that channel. Running the tag in a separate channel
|
|
25
|
+
would have a different cooldown with the same ``rate`` and ``per`` values.
|
|
26
|
+
|
|
27
|
+
The payload also has an optional ``message`` value, which is the message to be sent when the
|
|
28
|
+
cooldown is exceeded. If no message is passed, the default message will be sent instead.
|
|
29
|
+
The cooldown message supports 2 blocks: ``key`` and ``retry_after``.
|
|
30
|
+
|
|
31
|
+
**Usage:** ``{cooldown(<rate>|<per>):<key>|[message]}``
|
|
32
|
+
|
|
33
|
+
**Payload:** key, message
|
|
34
|
+
|
|
35
|
+
**Parameter:** rate, per
|
|
36
|
+
|
|
37
|
+
**Examples:** ::
|
|
38
|
+
|
|
39
|
+
{cooldown(1|10):{author(id)}}
|
|
40
|
+
# the tag author used the tag more than once in 10 seconds
|
|
41
|
+
# The bucket for 741074175875088424 has reached its cooldown. Retry in 3.25 seconds."
|
|
42
|
+
|
|
43
|
+
{cooldown(3|3):{channel(id)}|Slow down! This tag can only be used 3 times per 3 seconds per channel. Try again in **{retry_after}** seconds."}
|
|
44
|
+
# the tag was used more than 3 times in 3 seconds in a channel
|
|
45
|
+
# Slow down! This tag can only be used 3 times per 3 seconds per channel. Try again in **0.74** seconds.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("cooldown",)
|
|
49
|
+
COOLDOWNS: Dict[Any, CooldownMapping] = {}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def create_cooldown(cls, key: Any, rate: float, per: int) -> CooldownMapping:
|
|
53
|
+
cooldown = CooldownMapping.from_cooldown(rate, per, lambda x: x)
|
|
54
|
+
cls.COOLDOWNS[key] = cooldown
|
|
55
|
+
return cooldown
|
|
56
|
+
|
|
57
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
58
|
+
verb = ctx.verb
|
|
59
|
+
try:
|
|
60
|
+
rate, per = cast(List[str], helper_split(cast(str, verb.parameter), maxsplit=1))
|
|
61
|
+
per = int(per)
|
|
62
|
+
rate = float(rate)
|
|
63
|
+
except (ValueError, TypeError):
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if split := helper_split(cast(str, verb.payload), False, maxsplit=1):
|
|
67
|
+
key, message = split
|
|
68
|
+
else:
|
|
69
|
+
key = verb.payload
|
|
70
|
+
message = None
|
|
71
|
+
|
|
72
|
+
cooldown_key = ctx.response.extra_kwargs.get("cooldown_key")
|
|
73
|
+
if cooldown_key is None:
|
|
74
|
+
cooldown_key = ctx.original_message
|
|
75
|
+
try:
|
|
76
|
+
cooldown = self.COOLDOWNS[cooldown_key]
|
|
77
|
+
base = cast(Cooldown, cooldown._cooldown)
|
|
78
|
+
if (rate, per) != (base.rate, base.per):
|
|
79
|
+
cooldown = self.create_cooldown(cooldown_key, rate, per)
|
|
80
|
+
except KeyError:
|
|
81
|
+
cooldown = self.create_cooldown(cooldown_key, rate, per)
|
|
82
|
+
|
|
83
|
+
current = time.time()
|
|
84
|
+
bucket = cast(Cooldown, cooldown.get_bucket(key, current))
|
|
85
|
+
retry_after = bucket.update_rate_limit(current)
|
|
86
|
+
if retry_after:
|
|
87
|
+
retry_after = round(retry_after, 2)
|
|
88
|
+
if message:
|
|
89
|
+
message = message.replace("{key}", str(key)).replace(
|
|
90
|
+
"{retry_after}", str(retry_after)
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
message = f"The bucket for {key} has reached its cooldown. Retry in {retry_after} seconds."
|
|
94
|
+
raise CooldownExceeded(message, bucket, cast(str, key), retry_after)
|
|
95
|
+
return ""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple, cast
|
|
4
|
+
|
|
5
|
+
from ..interface import verb_required_block
|
|
6
|
+
from ..interpreter import Context
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__: Tuple[str, ...] = ("CountBlock", "LengthBlock")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CountBlock(verb_required_block(True, payload=True)): # type: ignore
|
|
13
|
+
"""
|
|
14
|
+
The count block will count how much of text is in message.
|
|
15
|
+
This is case sensitive and will include substrings, if you
|
|
16
|
+
don't provide a parameter, it will count the spaces in the
|
|
17
|
+
message.
|
|
18
|
+
|
|
19
|
+
**Usage:** ``{count([text]):<message>}``
|
|
20
|
+
|
|
21
|
+
**Aliases:** ``None``
|
|
22
|
+
|
|
23
|
+
**Payload:** ``message``
|
|
24
|
+
|
|
25
|
+
**Parameter:** text
|
|
26
|
+
|
|
27
|
+
.. tagscript::
|
|
28
|
+
{count(Tag):TagScriptEngine}
|
|
29
|
+
# 1
|
|
30
|
+
|
|
31
|
+
{count(Tag): Tag Script Engine TagScriptEngine}
|
|
32
|
+
# 2
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("count",)
|
|
36
|
+
|
|
37
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
38
|
+
if ctx.verb.parameter:
|
|
39
|
+
payload: str = cast(str, ctx.verb.payload)
|
|
40
|
+
return str(payload.count(ctx.verb.parameter))
|
|
41
|
+
return str(len(cast(str, ctx.verb.payload)) + 1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LengthBlock(verb_required_block(True, payload=True)): # type: ignore
|
|
45
|
+
"""
|
|
46
|
+
The length block will check the length of the given String.
|
|
47
|
+
If a parameter is passed in, the block will check the length
|
|
48
|
+
based on what you passed in, w for word, s for spaces.
|
|
49
|
+
If you provide an invalid parameter, the block will return -1.
|
|
50
|
+
|
|
51
|
+
**Usage:** ``{length(<text>)}``
|
|
52
|
+
|
|
53
|
+
**Aliases:** ``len``
|
|
54
|
+
|
|
55
|
+
**Payload:** None
|
|
56
|
+
|
|
57
|
+
**Parameter:** ``text``
|
|
58
|
+
|
|
59
|
+
.. tagscript::
|
|
60
|
+
|
|
61
|
+
{len("TagScriptEngine")}
|
|
62
|
+
15
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("length", "len")
|
|
66
|
+
|
|
67
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
68
|
+
return str(len(ctx.verb.parameter)) if ctx.verb.parameter else "-1"
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from inspect import ismethod
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
|
6
|
+
|
|
7
|
+
from discord import Colour, Embed
|
|
8
|
+
|
|
9
|
+
from ..interface import Block
|
|
10
|
+
from ..interpreter import Context
|
|
11
|
+
from .helpers import helper_split, easier_helper_split, implicit_bool
|
|
12
|
+
from ..utils import truncate
|
|
13
|
+
from ..exceptions import BadColourArgument, EmbedParseError
|
|
14
|
+
from .._warnings import removal
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import orjson # noqa: F401
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
_has_orjson: bool = False
|
|
20
|
+
else:
|
|
21
|
+
_has_orjson: bool = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__: Tuple[str, ...] = ("EmbedBlock",)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if _has_orjson:
|
|
28
|
+
_from_json = orjson.loads # type: ignore
|
|
29
|
+
else:
|
|
30
|
+
_from_json = json.loads
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def string_to_color(argument: str) -> Colour:
|
|
34
|
+
arg = argument.replace("0x", "").lower()
|
|
35
|
+
|
|
36
|
+
if arg[0] == "#":
|
|
37
|
+
arg = arg[1:]
|
|
38
|
+
try:
|
|
39
|
+
value = int(arg, base=16)
|
|
40
|
+
if not (0 <= value <= 0xFFFFFF):
|
|
41
|
+
raise BadColourArgument(arg)
|
|
42
|
+
return Colour(value=value)
|
|
43
|
+
except ValueError:
|
|
44
|
+
arg = arg.replace(" ", "_")
|
|
45
|
+
method = getattr(Colour, arg, None)
|
|
46
|
+
if arg.startswith("from_") or method is None or not ismethod(method):
|
|
47
|
+
raise BadColourArgument(arg)
|
|
48
|
+
return method()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_color(embed: Embed, attribute: str, value: str) -> None:
|
|
52
|
+
value = string_to_color(value) # type: ignore
|
|
53
|
+
setattr(embed, attribute, value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_dynamic_url(embed: Embed, attribute: str, value: str) -> None:
|
|
57
|
+
method = getattr(embed, f"set_{attribute}")
|
|
58
|
+
method(url=value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_field(embed: Embed, _: str, payload: str) -> None:
|
|
62
|
+
if (data := easier_helper_split(payload, maxsplit=3)) is None: # type: ignore
|
|
63
|
+
raise EmbedParseError("`add_field` payload was not split by |")
|
|
64
|
+
try:
|
|
65
|
+
name, value, _inline = data
|
|
66
|
+
inline = implicit_bool(_inline)
|
|
67
|
+
if inline is None:
|
|
68
|
+
raise EmbedParseError(
|
|
69
|
+
"`inline` argument for `add_field` is not a boolean value (_inline)"
|
|
70
|
+
)
|
|
71
|
+
except ValueError:
|
|
72
|
+
name, value = cast(List[str], helper_split(payload, 2)) # type: ignore
|
|
73
|
+
inline = False
|
|
74
|
+
name = truncate(name, max=FIELD_LIMITS["field.name"])
|
|
75
|
+
value = truncate(value, max=FIELD_LIMITS["field.value"])
|
|
76
|
+
embed.add_field(name=name, value=value, inline=inline)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_footer(embed: Embed, _: str, payload: str) -> None:
|
|
80
|
+
data = easier_helper_split(payload, maxsplit=2) # type: ignore
|
|
81
|
+
if data is None:
|
|
82
|
+
embed.set_footer(text=truncate(payload, max=FIELD_LIMITS["footer.text"]))
|
|
83
|
+
else:
|
|
84
|
+
text, icon_url = data
|
|
85
|
+
embed.set_footer(text=truncate(text, max=FIELD_LIMITS["footer.text"]), icon_url=icon_url)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Discord embed field character limits
|
|
89
|
+
# https://docs.discord.com/developers/resources/message#embed-object-embed-limits
|
|
90
|
+
FIELD_LIMITS: Dict[str, int] = {
|
|
91
|
+
"title": 256,
|
|
92
|
+
"description": 4096,
|
|
93
|
+
"footer.text": 2048,
|
|
94
|
+
"author.name": 256,
|
|
95
|
+
"field.name": 256,
|
|
96
|
+
"field.value": 1024,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EmbedBlock(Block):
|
|
101
|
+
"""
|
|
102
|
+
An embed block will send an embed in the tag response.
|
|
103
|
+
There are two ways to use the embed block, either by using properly
|
|
104
|
+
formatted embed JSON from an embed generator or manually inputting
|
|
105
|
+
the accepted embed attributes.
|
|
106
|
+
|
|
107
|
+
**JSON**
|
|
108
|
+
|
|
109
|
+
Using JSON to create an embed offers complete embed customization.
|
|
110
|
+
Multiple embed generators are available online to visualize and generate
|
|
111
|
+
embed JSON.
|
|
112
|
+
|
|
113
|
+
**Usage:** ``{embed(<json>)}``
|
|
114
|
+
|
|
115
|
+
**Payload:** None
|
|
116
|
+
|
|
117
|
+
**Parameter:** json
|
|
118
|
+
|
|
119
|
+
**Examples:** ::
|
|
120
|
+
|
|
121
|
+
{embed({"title":"Hello!", "description":"This is a test embed."})}
|
|
122
|
+
{embed({
|
|
123
|
+
"title":"Here's a random duck!",
|
|
124
|
+
"image":{"url":"https://random-d.uk/api/randomimg"},
|
|
125
|
+
"color":15194415
|
|
126
|
+
})}
|
|
127
|
+
|
|
128
|
+
**Manual**
|
|
129
|
+
|
|
130
|
+
The following embed attributes can be set manually:
|
|
131
|
+
|
|
132
|
+
* ``title``
|
|
133
|
+
* ``description``
|
|
134
|
+
* ``color``
|
|
135
|
+
* ``url``
|
|
136
|
+
* ``thumbnail``
|
|
137
|
+
* ``image``
|
|
138
|
+
* ``footer``
|
|
139
|
+
* ``field`` - (See below)
|
|
140
|
+
|
|
141
|
+
Adding a field to an embed requires the payload to be split by ``|``,
|
|
142
|
+
``;`` or ``,`` into either 2 or 3 parts. The first part is the name
|
|
143
|
+
of the field, the second is the text of the field, and the third
|
|
144
|
+
optionally specifies whether the field should be inline.
|
|
145
|
+
|
|
146
|
+
**Usage:** ``{embed(<attribute>):<value>}``
|
|
147
|
+
|
|
148
|
+
**Payload:** value
|
|
149
|
+
|
|
150
|
+
**Parameter:** attribute
|
|
151
|
+
|
|
152
|
+
**Examples:** ::
|
|
153
|
+
|
|
154
|
+
{embed(color):#37b2cb}
|
|
155
|
+
{embed(title):Rules}
|
|
156
|
+
{embed(description):Follow these rules to ensure a good experience in our server!}
|
|
157
|
+
{embed(field):Rule 1|Respect everyone you speak to.|false}
|
|
158
|
+
{embed(footer):Thanks for reading!|{guild(icon)}}
|
|
159
|
+
|
|
160
|
+
Both methods can be combined to create an embed in a tag.
|
|
161
|
+
The following tagscript uses JSON to create an embed with fields and later
|
|
162
|
+
set the embed title.
|
|
163
|
+
|
|
164
|
+
::
|
|
165
|
+
|
|
166
|
+
{embed(title):my embed title}
|
|
167
|
+
{embed({{
|
|
168
|
+
"fields": [
|
|
169
|
+
{
|
|
170
|
+
"name": "Field 1",
|
|
171
|
+
"value": "field description",
|
|
172
|
+
"inline": false
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
})}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("embed",)
|
|
179
|
+
|
|
180
|
+
ATTRIBUTE_HANDLERS: Dict[str, Any] = {
|
|
181
|
+
"description": setattr,
|
|
182
|
+
"title": setattr,
|
|
183
|
+
"color": set_color,
|
|
184
|
+
"colour": set_color,
|
|
185
|
+
"url": setattr,
|
|
186
|
+
"thumbnail": set_dynamic_url,
|
|
187
|
+
"image": set_dynamic_url,
|
|
188
|
+
"field": add_field,
|
|
189
|
+
"footer": set_footer,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@removal(
|
|
193
|
+
name="EmbedBlock",
|
|
194
|
+
reason=(
|
|
195
|
+
"One of EmbedBlock's trait is scheduled to be removed in the next minor release, "
|
|
196
|
+
"A minor exception handling which would restrict the embed from getting sent and "
|
|
197
|
+
"would raise TagScriptEngine.exceptions.EmbedParseError incase it had more than "
|
|
198
|
+
"6000 characters, to know more about the limitations of discord embeds refer to the "
|
|
199
|
+
"[Official Discord API Docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-limits)."
|
|
200
|
+
),
|
|
201
|
+
version="3.2.0",
|
|
202
|
+
)
|
|
203
|
+
def __init__(self) -> None:
|
|
204
|
+
super().__init__()
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def get_embed(ctx: Context) -> Embed:
|
|
208
|
+
return ctx.response.actions.get("embed", Embed())
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def value_to_color(value: Optional[Union[int, str]]) -> Colour:
|
|
212
|
+
if value is None or isinstance(value, Colour):
|
|
213
|
+
return value # type: ignore
|
|
214
|
+
if isinstance(value, int):
|
|
215
|
+
return Colour(value)
|
|
216
|
+
elif isinstance(value, str):
|
|
217
|
+
return string_to_color(value)
|
|
218
|
+
else:
|
|
219
|
+
raise EmbedParseError("Received invalid type for color key (expected string or int)")
|
|
220
|
+
|
|
221
|
+
def text_to_embed(self, text: str) -> Embed:
|
|
222
|
+
try:
|
|
223
|
+
data = _from_json(text)
|
|
224
|
+
except (json.decoder.JSONDecodeError, ValueError) as error:
|
|
225
|
+
raise EmbedParseError(error) from error
|
|
226
|
+
|
|
227
|
+
if data.get("embed"):
|
|
228
|
+
data = data["embed"]
|
|
229
|
+
if data.get("timestamp"):
|
|
230
|
+
data["timestamp"] = data["timestamp"].strip("Z")
|
|
231
|
+
|
|
232
|
+
color = data.pop("color", data.pop("colour", None))
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
embed = Embed.from_dict(data)
|
|
236
|
+
except Exception as error:
|
|
237
|
+
raise EmbedParseError(error) from error
|
|
238
|
+
else:
|
|
239
|
+
if color := self.value_to_color(color):
|
|
240
|
+
embed.color = color
|
|
241
|
+
self._truncate_embed_fields(embed)
|
|
242
|
+
return embed
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def update_embed(cls, embed: Embed, attribute: str, value: str) -> Embed:
|
|
246
|
+
# Truncate value to Discord's per-field limit before setting
|
|
247
|
+
if attribute in FIELD_LIMITS:
|
|
248
|
+
value = truncate(value, max=FIELD_LIMITS[attribute])
|
|
249
|
+
handler = cls.ATTRIBUTE_HANDLERS[attribute]
|
|
250
|
+
try:
|
|
251
|
+
handler(embed, attribute, value)
|
|
252
|
+
except Exception as error:
|
|
253
|
+
raise EmbedParseError(error) from error
|
|
254
|
+
return embed
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _truncate_embed_fields(embed: Embed) -> None:
|
|
258
|
+
"""Truncate embed fields to Discord's per-field character limits."""
|
|
259
|
+
if embed.title and len(embed.title) > FIELD_LIMITS["title"]:
|
|
260
|
+
embed.title = truncate(embed.title, max=FIELD_LIMITS["title"])
|
|
261
|
+
if embed.description and len(embed.description) > FIELD_LIMITS["description"]:
|
|
262
|
+
embed.description = truncate(embed.description, max=FIELD_LIMITS["description"])
|
|
263
|
+
if embed.footer and embed.footer.text and len(embed.footer.text) > FIELD_LIMITS["footer.text"]:
|
|
264
|
+
embed.set_footer(text=truncate(embed.footer.text, max=FIELD_LIMITS["footer.text"]), icon_url=embed.footer.icon_url)
|
|
265
|
+
if embed.author and embed.author.name and len(embed.author.name) > FIELD_LIMITS["author.name"]:
|
|
266
|
+
embed.set_author(name=truncate(embed.author.name, max=FIELD_LIMITS["author.name"]), url=embed.author.url, icon_url=embed.author.icon_url)
|
|
267
|
+
for field in embed.fields:
|
|
268
|
+
if field.name and len(field.name) > FIELD_LIMITS["field.name"]:
|
|
269
|
+
idx = embed.fields.index(field)
|
|
270
|
+
embed.set_field_at(idx, name=truncate(field.name, max=FIELD_LIMITS["field.name"]), value=field.value, inline=field.inline)
|
|
271
|
+
if field.value and len(field.value) > FIELD_LIMITS["field.value"]:
|
|
272
|
+
idx = embed.fields.index(field)
|
|
273
|
+
embed.set_field_at(idx, name=field.name, value=truncate(field.value, max=FIELD_LIMITS["field.value"]), inline=field.inline)
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def return_error(error: Exception) -> str:
|
|
277
|
+
return f"Embed Parse Error: {error}"
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def return_embed(ctx: Context, embed: Embed) -> str:
|
|
281
|
+
try:
|
|
282
|
+
length = len(embed)
|
|
283
|
+
except Exception as error:
|
|
284
|
+
return str(error)
|
|
285
|
+
if length > 6000:
|
|
286
|
+
return f"`MAX EMBED LENGTH REACHED ({length}/6000)`"
|
|
287
|
+
ctx.response.actions["embed"] = embed
|
|
288
|
+
return ""
|
|
289
|
+
|
|
290
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
291
|
+
if not ctx.verb.parameter:
|
|
292
|
+
return self.return_embed(ctx, self.get_embed(ctx))
|
|
293
|
+
|
|
294
|
+
lowered = ctx.verb.parameter.lower()
|
|
295
|
+
try:
|
|
296
|
+
if ctx.verb.parameter.startswith("{") and ctx.verb.parameter.endswith("}"):
|
|
297
|
+
embed = self.text_to_embed(ctx.verb.parameter)
|
|
298
|
+
elif lowered in self.ATTRIBUTE_HANDLERS and ctx.verb.payload:
|
|
299
|
+
embed = self.get_embed(ctx)
|
|
300
|
+
embed = self.update_embed(embed, lowered, ctx.verb.payload)
|
|
301
|
+
else:
|
|
302
|
+
return
|
|
303
|
+
except EmbedParseError as error:
|
|
304
|
+
return self.return_error(error)
|
|
305
|
+
|
|
306
|
+
return self.return_embed(ctx, embed)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ..interface import verb_required_block
|
|
7
|
+
from ..interpreter import Context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__: Tuple[str, ...] = ("FiftyFiftyBlock",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FiftyFiftyBlock(verb_required_block(True, payload=True)): # type: ignore
|
|
14
|
+
"""
|
|
15
|
+
The fifty-fifty block has a 50% change of returning the payload, and 50% chance of returning null.
|
|
16
|
+
|
|
17
|
+
**Usage:** ``{50:<message>}``
|
|
18
|
+
|
|
19
|
+
**Aliases:** ``5050, ?``
|
|
20
|
+
|
|
21
|
+
**Payload:** message
|
|
22
|
+
|
|
23
|
+
**Parameter:** None
|
|
24
|
+
|
|
25
|
+
**Examples:** ::
|
|
26
|
+
|
|
27
|
+
I pick {if({5050:.}!=):heads|tails}
|
|
28
|
+
# I pick heads
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
ACCEPTED_NAMES: Tuple[str, ...] = ("5050", "50", "?")
|
|
32
|
+
|
|
33
|
+
def process(self, ctx: Context) -> Optional[str]:
|
|
34
|
+
return random.choice(["", ctx.verb.payload])
|