tflows 0.0.8__tar.gz → 0.0.9.dev0__tar.gz
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.
- {tflows-0.0.8/tflows.egg-info → tflows-0.0.9.dev0}/PKG-INFO +1 -1
- {tflows-0.0.8 → tflows-0.0.9.dev0}/pyproject.toml +1 -1
- tflows-0.0.9.dev0/tflows/engine.py +220 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0/tflows.egg-info}/PKG-INFO +1 -1
- tflows-0.0.8/tflows/engine.py +0 -172
- {tflows-0.0.8 → tflows-0.0.9.dev0}/License +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/NOTICE +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/README.md +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/setup.cfg +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/__init__.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/bot.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/builtins.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/loader.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/registry.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows/utils.py +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows.egg-info/SOURCES.txt +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows.egg-info/dependency_links.txt +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows.egg-info/requires.txt +0 -0
- {tflows-0.0.8 → tflows-0.0.9.dev0}/tflows.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tflows"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.9.dev0"
|
|
8
8
|
description = "A lightweight automation and Discord bot framework that lets you build bots using a simple scripting system."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
# -----------------------
|
|
5
|
+
# LOG PATTERN (SAFE)
|
|
6
|
+
# -----------------------
|
|
7
|
+
LOG_PATTERN = re.compile(r"\$log\((\d+)\)\{([\s\S]*?)\}")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Engine:
|
|
11
|
+
def __init__(self, registry):
|
|
12
|
+
self.registry = registry
|
|
13
|
+
|
|
14
|
+
# -----------------------
|
|
15
|
+
# VARIABLES
|
|
16
|
+
# -----------------------
|
|
17
|
+
async def replace_vars(self, ctx, text: str):
|
|
18
|
+
|
|
19
|
+
if "$ping" in text:
|
|
20
|
+
try:
|
|
21
|
+
latency = ctx._state._get_client().latency * 1000
|
|
22
|
+
text = text.replace("$ping", f"{latency:.2f}ms")
|
|
23
|
+
except:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
async def var_replacer(match):
|
|
27
|
+
name = match.group(1)
|
|
28
|
+
args = match.group(2) or ""
|
|
29
|
+
|
|
30
|
+
handler = self.registry.get_var(name)
|
|
31
|
+
if handler:
|
|
32
|
+
result = handler(ctx, args)
|
|
33
|
+
|
|
34
|
+
if asyncio.iscoroutine(result):
|
|
35
|
+
result = await result
|
|
36
|
+
|
|
37
|
+
return str(result)
|
|
38
|
+
|
|
39
|
+
return match.group(0)
|
|
40
|
+
|
|
41
|
+
pattern = r"\$(\w+)(?:\((.*?)\))?"
|
|
42
|
+
matches = list(re.finditer(pattern, text, re.DOTALL))
|
|
43
|
+
|
|
44
|
+
for match in reversed(matches):
|
|
45
|
+
try:
|
|
46
|
+
replacement = await var_replacer(match)
|
|
47
|
+
start, end = match.span()
|
|
48
|
+
text = text[:start] + replacement + text[end:]
|
|
49
|
+
except:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
# -----------------------
|
|
55
|
+
# SAFE LOG SYSTEM (QUEUE STYLE, NON-BLOCKING)
|
|
56
|
+
# -----------------------
|
|
57
|
+
async def handle_log(self, ctx, text: str):
|
|
58
|
+
matches = list(LOG_PATTERN.finditer(text))
|
|
59
|
+
if not matches:
|
|
60
|
+
return text
|
|
61
|
+
|
|
62
|
+
bot = getattr(ctx, "bot", None) or getattr(self.registry, "bot", None)
|
|
63
|
+
|
|
64
|
+
if bot is None:
|
|
65
|
+
print("[tflow] ERROR: bot not found")
|
|
66
|
+
return text
|
|
67
|
+
|
|
68
|
+
for match in matches:
|
|
69
|
+
channel_id = match.group(1)
|
|
70
|
+
message = match.group(2)
|
|
71
|
+
|
|
72
|
+
channel = bot.get_channel(int(channel_id))
|
|
73
|
+
|
|
74
|
+
if channel is None:
|
|
75
|
+
try:
|
|
76
|
+
channel = await bot.fetch_channel(int(channel_id))
|
|
77
|
+
except:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if channel is None:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
message = await self.replace_vars(ctx, message)
|
|
85
|
+
except:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
await channel.send(message)
|
|
90
|
+
print(f"[tflow] log sent -> {channel_id}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print("[tflow] send failed:", e)
|
|
93
|
+
|
|
94
|
+
text = text.replace(match.group(0), "")
|
|
95
|
+
|
|
96
|
+
return text.strip()
|
|
97
|
+
|
|
98
|
+
# -----------------------
|
|
99
|
+
# EMBED PARSER
|
|
100
|
+
# -----------------------
|
|
101
|
+
async def parse_embed(self, ctx, block: str):
|
|
102
|
+
|
|
103
|
+
import discord
|
|
104
|
+
|
|
105
|
+
e = discord.Embed()
|
|
106
|
+
block = block.replace("\r\n", "\n")
|
|
107
|
+
|
|
108
|
+
def grab(key):
|
|
109
|
+
m = re.search(rf"\${key}\[(.*?)\]", block, re.DOTALL)
|
|
110
|
+
return m.group(1).strip() if m else None
|
|
111
|
+
|
|
112
|
+
title = grab("title")
|
|
113
|
+
desc = grab("desc")
|
|
114
|
+
footer = grab("footer")
|
|
115
|
+
color = grab("color")
|
|
116
|
+
|
|
117
|
+
clean = re.sub(
|
|
118
|
+
r"\$(title|desc|footer|color)\[.*?\]",
|
|
119
|
+
"",
|
|
120
|
+
block,
|
|
121
|
+
flags=re.DOTALL
|
|
122
|
+
).strip()
|
|
123
|
+
|
|
124
|
+
async def apply(v):
|
|
125
|
+
if not v:
|
|
126
|
+
return None
|
|
127
|
+
return await self.replace_vars(ctx, v)
|
|
128
|
+
|
|
129
|
+
if title:
|
|
130
|
+
e.title = await apply(title)
|
|
131
|
+
|
|
132
|
+
full_desc = desc if desc else clean
|
|
133
|
+
e.description = await self.replace_vars(ctx, full_desc or "No content")
|
|
134
|
+
|
|
135
|
+
if footer:
|
|
136
|
+
e.set_footer(text=await apply(footer))
|
|
137
|
+
|
|
138
|
+
if color:
|
|
139
|
+
try:
|
|
140
|
+
c = color.replace("#", "").lower()
|
|
141
|
+
e.color = discord.Color.white() if c == "white" else discord.Color(int(c, 16))
|
|
142
|
+
except:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
await ctx.channel.send(embed=e)
|
|
146
|
+
|
|
147
|
+
# -----------------------
|
|
148
|
+
# MAIN ENGINE (FULLY STABLE FLOW)
|
|
149
|
+
# -----------------------
|
|
150
|
+
async def run(self, ctx, code: str):
|
|
151
|
+
|
|
152
|
+
lines = code.split("\n")
|
|
153
|
+
i = 0
|
|
154
|
+
|
|
155
|
+
while i < len(lines):
|
|
156
|
+
line = lines[i].strip()
|
|
157
|
+
|
|
158
|
+
i += 1 # prevent infinite loop edge cases early
|
|
159
|
+
|
|
160
|
+
if not line:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# -----------------------
|
|
164
|
+
# EMBED BLOCK
|
|
165
|
+
# -----------------------
|
|
166
|
+
if line == "embed":
|
|
167
|
+
block = []
|
|
168
|
+
|
|
169
|
+
while i < len(lines):
|
|
170
|
+
if lines[i].strip() == "endembed":
|
|
171
|
+
i += 1
|
|
172
|
+
break
|
|
173
|
+
block.append(lines[i])
|
|
174
|
+
i += 1
|
|
175
|
+
|
|
176
|
+
await self.parse_embed(ctx, "\n".join(block))
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# -----------------------
|
|
180
|
+
# STEP 1: VARIABLES
|
|
181
|
+
# -----------------------
|
|
182
|
+
try:
|
|
183
|
+
line = await self.replace_vars(ctx, line)
|
|
184
|
+
except:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# -----------------------
|
|
188
|
+
# STEP 2: LOG SYSTEM (SAFE SIDE EFFECT)
|
|
189
|
+
# -----------------------
|
|
190
|
+
try:
|
|
191
|
+
line = await self.handle_log(ctx, line)
|
|
192
|
+
except:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
# IMPORTANT: skip empty results (THIS fixes your crash)
|
|
196
|
+
if not line or not line.strip():
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# -----------------------
|
|
200
|
+
# STEP 3: REGISTRY FUNCTIONS
|
|
201
|
+
# -----------------------
|
|
202
|
+
parts = line.split(" ", 1)
|
|
203
|
+
name = parts[0].strip()
|
|
204
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
205
|
+
|
|
206
|
+
if not name:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
func = self.registry.get(name)
|
|
210
|
+
|
|
211
|
+
if func:
|
|
212
|
+
try:
|
|
213
|
+
result = func(ctx, args)
|
|
214
|
+
|
|
215
|
+
if asyncio.iscoroutine(result):
|
|
216
|
+
await result
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"[tflow] function error: {name} -> {e}")
|
|
219
|
+
else:
|
|
220
|
+
print(f"[tflow] Unknown function: {name}")
|
tflows-0.0.8/tflows/engine.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import asyncio
|
|
3
|
-
|
|
4
|
-
class Engine:
|
|
5
|
-
def __init__(self, registry):
|
|
6
|
-
self.registry = registry
|
|
7
|
-
|
|
8
|
-
# -----------------------
|
|
9
|
-
# VARIABLES (FIXED)
|
|
10
|
-
# -----------------------
|
|
11
|
-
async def replace_vars(self, ctx, text: str):
|
|
12
|
-
|
|
13
|
-
# $ping (keep simple)
|
|
14
|
-
if "$ping" in text:
|
|
15
|
-
latency = ctx._state._get_client().latency * 1000
|
|
16
|
-
text = text.replace("$ping", f"{latency:.2f}ms")
|
|
17
|
-
|
|
18
|
-
async def var_replacer(match):
|
|
19
|
-
name = match.group(1)
|
|
20
|
-
args = match.group(2) or ""
|
|
21
|
-
|
|
22
|
-
handler = self.registry.get_var(name)
|
|
23
|
-
if handler:
|
|
24
|
-
result = handler(ctx, args)
|
|
25
|
-
|
|
26
|
-
# ✅ SUPPORT ASYNC NOW
|
|
27
|
-
if asyncio.iscoroutine(result):
|
|
28
|
-
result = await result
|
|
29
|
-
|
|
30
|
-
return str(result)
|
|
31
|
-
|
|
32
|
-
return match.group(0)
|
|
33
|
-
|
|
34
|
-
# IMPORTANT: async replace loop
|
|
35
|
-
pattern = r"\$(\w+)(?:\((.*?)\))?"
|
|
36
|
-
matches = list(re.finditer(pattern, text, re.DOTALL))
|
|
37
|
-
|
|
38
|
-
for match in reversed(matches):
|
|
39
|
-
replacement = await var_replacer(match)
|
|
40
|
-
start, end = match.span()
|
|
41
|
-
text = text[:start] + replacement + text[end:]
|
|
42
|
-
|
|
43
|
-
return text
|
|
44
|
-
|
|
45
|
-
# -----------------------
|
|
46
|
-
# EMBED PARSER
|
|
47
|
-
# -----------------------
|
|
48
|
-
async def parse_embed(self, ctx, block: str):
|
|
49
|
-
|
|
50
|
-
import discord
|
|
51
|
-
import re
|
|
52
|
-
|
|
53
|
-
e = discord.Embed()
|
|
54
|
-
block = block.replace("\r\n", "\n")
|
|
55
|
-
|
|
56
|
-
# -----------------------
|
|
57
|
-
# EXTRACT HELPERS
|
|
58
|
-
# -----------------------
|
|
59
|
-
def grab(key):
|
|
60
|
-
m = re.search(rf"\${key}\[(.*?)\]", block, re.DOTALL)
|
|
61
|
-
return m.group(1).strip() if m else None
|
|
62
|
-
|
|
63
|
-
title = grab("title")
|
|
64
|
-
desc = grab("desc")
|
|
65
|
-
footer = grab("footer")
|
|
66
|
-
color = grab("color")
|
|
67
|
-
|
|
68
|
-
clean = re.sub(
|
|
69
|
-
r"\$(title|desc|footer|color)\[.*?\]",
|
|
70
|
-
"",
|
|
71
|
-
block,
|
|
72
|
-
flags=re.DOTALL
|
|
73
|
-
).strip()
|
|
74
|
-
|
|
75
|
-
# -----------------------
|
|
76
|
-
# SAFE APPLY (ALWAYS AWAIT VAR ENGINE)
|
|
77
|
-
# -----------------------
|
|
78
|
-
async def apply(v):
|
|
79
|
-
if not v:
|
|
80
|
-
return None
|
|
81
|
-
return await self.replace_vars(ctx, v)
|
|
82
|
-
|
|
83
|
-
# -----------------------
|
|
84
|
-
# TITLE
|
|
85
|
-
# -----------------------
|
|
86
|
-
if title:
|
|
87
|
-
e.title = await apply(title)
|
|
88
|
-
|
|
89
|
-
# -----------------------
|
|
90
|
-
# DESCRIPTION
|
|
91
|
-
# -----------------------
|
|
92
|
-
full_desc = desc if desc else clean
|
|
93
|
-
e.description = await self.replace_vars(ctx, full_desc or "No content")
|
|
94
|
-
|
|
95
|
-
# -----------------------
|
|
96
|
-
# FOOTER
|
|
97
|
-
# -----------------------
|
|
98
|
-
if footer:
|
|
99
|
-
e.set_footer(text=await apply(footer))
|
|
100
|
-
|
|
101
|
-
# -----------------------
|
|
102
|
-
# COLOR
|
|
103
|
-
# -----------------------
|
|
104
|
-
if color:
|
|
105
|
-
try:
|
|
106
|
-
c = color.replace("#", "").lower()
|
|
107
|
-
|
|
108
|
-
if c == "white":
|
|
109
|
-
e.color = discord.Color.white()
|
|
110
|
-
else:
|
|
111
|
-
e.color = discord.Color(int(c, 16))
|
|
112
|
-
|
|
113
|
-
except Exception:
|
|
114
|
-
pass
|
|
115
|
-
|
|
116
|
-
# -----------------------
|
|
117
|
-
# SEND
|
|
118
|
-
# -----------------------
|
|
119
|
-
await ctx.channel.send(embed=e)
|
|
120
|
-
|
|
121
|
-
# -----------------------
|
|
122
|
-
# MAIN ENGINE
|
|
123
|
-
# -----------------------
|
|
124
|
-
async def run(self, ctx, code: str):
|
|
125
|
-
|
|
126
|
-
lines = code.split("\n")
|
|
127
|
-
i = 0
|
|
128
|
-
|
|
129
|
-
while i < len(lines):
|
|
130
|
-
line = lines[i].strip()
|
|
131
|
-
|
|
132
|
-
if not line:
|
|
133
|
-
i += 1
|
|
134
|
-
continue
|
|
135
|
-
|
|
136
|
-
# -----------------------
|
|
137
|
-
# EMBED BLOCK
|
|
138
|
-
# -----------------------
|
|
139
|
-
if line == "embed":
|
|
140
|
-
i += 1
|
|
141
|
-
block = []
|
|
142
|
-
|
|
143
|
-
while i < len(lines):
|
|
144
|
-
if lines[i].strip() == "endembed":
|
|
145
|
-
break
|
|
146
|
-
block.append(lines[i])
|
|
147
|
-
i += 1
|
|
148
|
-
|
|
149
|
-
await self.parse_embed(ctx, "\n".join(block))
|
|
150
|
-
i += 1
|
|
151
|
-
continue
|
|
152
|
-
|
|
153
|
-
# -----------------------
|
|
154
|
-
# NORMAL FUNCTIONS
|
|
155
|
-
# -----------------------
|
|
156
|
-
line = await self.replace_vars(ctx, line)
|
|
157
|
-
|
|
158
|
-
parts = line.split(" ", 1)
|
|
159
|
-
name = parts[0]
|
|
160
|
-
args = parts[1] if len(parts) > 1 else ""
|
|
161
|
-
|
|
162
|
-
func = self.registry.get(name)
|
|
163
|
-
|
|
164
|
-
if func:
|
|
165
|
-
result = func(ctx, args)
|
|
166
|
-
|
|
167
|
-
if asyncio.iscoroutine(result):
|
|
168
|
-
await result
|
|
169
|
-
else:
|
|
170
|
-
print(f"[tflow] Unknown function: {name}")
|
|
171
|
-
|
|
172
|
-
i += 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|