mmrelay 1.0.3__py3-none-any.whl → 1.0.5__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.
Potentially problematic release.
This version of mmrelay might be problematic. Click here for more details.
- mmrelay/config.py +26 -0
- mmrelay/plugin_loader.py +526 -22
- mmrelay/plugins/base_plugin.py +29 -0
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/METADATA +1 -1
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/RECORD +9 -9
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/WHEEL +0 -0
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.3.dist-info → mmrelay-1.0.5.dist-info}/top_level.txt +0 -0
mmrelay/config.py
CHANGED
|
@@ -103,6 +103,32 @@ def get_data_dir():
|
|
|
103
103
|
return data_dir
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def get_plugin_data_dir(plugin_name=None):
|
|
107
|
+
"""
|
|
108
|
+
Returns the directory for storing plugin-specific data files.
|
|
109
|
+
If plugin_name is provided, returns a plugin-specific subdirectory.
|
|
110
|
+
Creates the directory if it doesn't exist.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
- get_plugin_data_dir() returns ~/.mmrelay/data/plugins/
|
|
114
|
+
- get_plugin_data_dir("my_plugin") returns ~/.mmrelay/data/plugins/my_plugin/
|
|
115
|
+
"""
|
|
116
|
+
# Get the base data directory
|
|
117
|
+
base_data_dir = get_data_dir()
|
|
118
|
+
|
|
119
|
+
# Create the plugins directory
|
|
120
|
+
plugins_data_dir = os.path.join(base_data_dir, "plugins")
|
|
121
|
+
os.makedirs(plugins_data_dir, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
# If a plugin name is provided, create and return a plugin-specific directory
|
|
124
|
+
if plugin_name:
|
|
125
|
+
plugin_data_dir = os.path.join(plugins_data_dir, plugin_name)
|
|
126
|
+
os.makedirs(plugin_data_dir, exist_ok=True)
|
|
127
|
+
return plugin_data_dir
|
|
128
|
+
|
|
129
|
+
return plugins_data_dir
|
|
130
|
+
|
|
131
|
+
|
|
106
132
|
def get_log_dir():
|
|
107
133
|
"""
|
|
108
134
|
Returns the directory for storing log files.
|
mmrelay/plugin_loader.py
CHANGED
|
@@ -56,50 +56,444 @@ def get_community_plugin_dirs():
|
|
|
56
56
|
return dirs
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def clone_or_update_repo(repo_url,
|
|
59
|
+
def clone_or_update_repo(repo_url, ref, plugins_dir):
|
|
60
60
|
# Extract the repository name from the URL
|
|
61
61
|
repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
|
|
62
62
|
repo_path = os.path.join(plugins_dir, repo_name)
|
|
63
|
+
|
|
64
|
+
# Default branch names to try if ref is not specified
|
|
65
|
+
default_branches = ["main", "master"]
|
|
66
|
+
|
|
67
|
+
# Get the ref type and value
|
|
68
|
+
ref_type = ref["type"] # "tag" or "branch"
|
|
69
|
+
ref_value = ref["value"]
|
|
70
|
+
|
|
71
|
+
# Log what we're trying to do
|
|
72
|
+
logger.info(f"Using {ref_type} '{ref_value}' for repository {repo_name}")
|
|
73
|
+
|
|
74
|
+
# If it's a branch and one of the default branches, we'll handle it specially
|
|
75
|
+
is_default_branch = ref_type == "branch" and ref_value in default_branches
|
|
76
|
+
|
|
63
77
|
if os.path.isdir(repo_path):
|
|
64
78
|
try:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
# Fetch all branches but don't fetch tags to avoid conflicts
|
|
80
|
+
try:
|
|
81
|
+
subprocess.check_call(["git", "-C", repo_path, "fetch", "origin"])
|
|
82
|
+
except subprocess.CalledProcessError as e:
|
|
83
|
+
logger.warning(f"Error fetching from remote: {e}")
|
|
84
|
+
# Continue anyway, we'll try to use what we have
|
|
85
|
+
|
|
86
|
+
# If it's a default branch, handle it differently
|
|
87
|
+
if is_default_branch:
|
|
88
|
+
try:
|
|
89
|
+
# Check if we're already on the right branch
|
|
90
|
+
current_branch = subprocess.check_output(
|
|
91
|
+
["git", "-C", repo_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
92
|
+
universal_newlines=True,
|
|
93
|
+
).strip()
|
|
94
|
+
|
|
95
|
+
if current_branch == ref_value:
|
|
96
|
+
# We're on the right branch, just pull
|
|
97
|
+
try:
|
|
98
|
+
subprocess.check_call(
|
|
99
|
+
["git", "-C", repo_path, "pull", "origin", ref_value]
|
|
100
|
+
)
|
|
101
|
+
logger.info(
|
|
102
|
+
f"Updated repository {repo_name} branch {ref_value}"
|
|
103
|
+
)
|
|
104
|
+
return True
|
|
105
|
+
except subprocess.CalledProcessError as e:
|
|
106
|
+
logger.warning(f"Error pulling branch {ref_value}: {e}")
|
|
107
|
+
# Continue anyway, we'll use what we have
|
|
108
|
+
return True
|
|
109
|
+
else:
|
|
110
|
+
# Switch to the right branch
|
|
111
|
+
subprocess.check_call(
|
|
112
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
113
|
+
)
|
|
114
|
+
subprocess.check_call(
|
|
115
|
+
["git", "-C", repo_path, "pull", "origin", ref_value]
|
|
116
|
+
)
|
|
117
|
+
if ref_type == "branch":
|
|
118
|
+
logger.info(f"Switched to and updated branch {ref_value}")
|
|
119
|
+
else:
|
|
120
|
+
logger.info(f"Switched to and updated tag {ref_value}")
|
|
121
|
+
return True
|
|
122
|
+
except subprocess.CalledProcessError:
|
|
123
|
+
# If we can't checkout the specified branch, try the other default branch
|
|
124
|
+
other_default = "main" if ref_value == "master" else "master"
|
|
125
|
+
try:
|
|
126
|
+
logger.warning(
|
|
127
|
+
f"Branch {ref_value} not found, trying {other_default}"
|
|
128
|
+
)
|
|
129
|
+
subprocess.check_call(
|
|
130
|
+
["git", "-C", repo_path, "checkout", other_default]
|
|
131
|
+
)
|
|
132
|
+
subprocess.check_call(
|
|
133
|
+
["git", "-C", repo_path, "pull", "origin", other_default]
|
|
134
|
+
)
|
|
135
|
+
logger.info(
|
|
136
|
+
f"Using {other_default} branch instead of {ref_value}"
|
|
137
|
+
)
|
|
138
|
+
return True
|
|
139
|
+
except subprocess.CalledProcessError:
|
|
140
|
+
# If that fails too, just use whatever branch we're on
|
|
141
|
+
logger.warning(
|
|
142
|
+
"Could not checkout any default branch, using current branch"
|
|
143
|
+
)
|
|
144
|
+
return True
|
|
145
|
+
else:
|
|
146
|
+
# Handle tag checkout
|
|
147
|
+
# Check if we're already on the correct tag/commit
|
|
148
|
+
try:
|
|
149
|
+
# Get the current commit hash
|
|
150
|
+
current_commit = subprocess.check_output(
|
|
151
|
+
["git", "-C", repo_path, "rev-parse", "HEAD"],
|
|
152
|
+
universal_newlines=True,
|
|
153
|
+
).strip()
|
|
154
|
+
|
|
155
|
+
# Get the commit hash for the tag
|
|
156
|
+
tag_commit = None
|
|
157
|
+
try:
|
|
158
|
+
tag_commit = subprocess.check_output(
|
|
159
|
+
["git", "-C", repo_path, "rev-parse", ref_value],
|
|
160
|
+
universal_newlines=True,
|
|
161
|
+
).strip()
|
|
162
|
+
except subprocess.CalledProcessError:
|
|
163
|
+
# Tag doesn't exist locally, we'll need to fetch it
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
# If we're already at the tag's commit, we're done
|
|
167
|
+
if tag_commit and current_commit == tag_commit:
|
|
168
|
+
logger.info(
|
|
169
|
+
f"Repository {repo_name} is already at tag {ref_value}"
|
|
170
|
+
)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
# Otherwise, try to checkout the tag
|
|
174
|
+
subprocess.check_call(
|
|
175
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
176
|
+
)
|
|
177
|
+
if ref_type == "branch":
|
|
178
|
+
logger.info(
|
|
179
|
+
f"Updated repository {repo_name} to branch {ref_value}"
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
logger.info(
|
|
183
|
+
f"Updated repository {repo_name} to tag {ref_value}"
|
|
184
|
+
)
|
|
185
|
+
return True
|
|
186
|
+
except subprocess.CalledProcessError:
|
|
187
|
+
# If tag checkout fails, try to fetch it specifically
|
|
188
|
+
logger.warning(
|
|
189
|
+
f"Tag {ref_value} not found locally, trying to fetch it specifically"
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
# Try to fetch the specific tag, but first remove any existing tag with the same name
|
|
193
|
+
try:
|
|
194
|
+
# Delete the local tag if it exists to avoid conflicts
|
|
195
|
+
subprocess.check_call(
|
|
196
|
+
["git", "-C", repo_path, "tag", "-d", ref_value]
|
|
197
|
+
)
|
|
198
|
+
except subprocess.CalledProcessError:
|
|
199
|
+
# Tag doesn't exist locally, which is fine
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# Now fetch the tag from remote
|
|
203
|
+
try:
|
|
204
|
+
# Try to fetch the tag
|
|
205
|
+
subprocess.check_call(
|
|
206
|
+
[
|
|
207
|
+
"git",
|
|
208
|
+
"-C",
|
|
209
|
+
repo_path,
|
|
210
|
+
"fetch",
|
|
211
|
+
"origin",
|
|
212
|
+
f"refs/tags/{ref_value}",
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
except subprocess.CalledProcessError:
|
|
216
|
+
# If that fails, try to fetch the tag without the refs/tags/ prefix
|
|
217
|
+
subprocess.check_call(
|
|
218
|
+
[
|
|
219
|
+
"git",
|
|
220
|
+
"-C",
|
|
221
|
+
repo_path,
|
|
222
|
+
"fetch",
|
|
223
|
+
"origin",
|
|
224
|
+
f"refs/tags/{ref_value}:refs/tags/{ref_value}",
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
subprocess.check_call(
|
|
229
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
230
|
+
)
|
|
231
|
+
if ref_type == "branch":
|
|
232
|
+
logger.info(
|
|
233
|
+
f"Successfully fetched and checked out branch {ref_value}"
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
logger.info(
|
|
237
|
+
f"Successfully fetched and checked out tag {ref_value}"
|
|
238
|
+
)
|
|
239
|
+
return True
|
|
240
|
+
except subprocess.CalledProcessError:
|
|
241
|
+
# If that fails too, try as a branch
|
|
242
|
+
logger.warning(
|
|
243
|
+
f"Could not fetch tag {ref_value}, trying as a branch"
|
|
244
|
+
)
|
|
245
|
+
try:
|
|
246
|
+
subprocess.check_call(
|
|
247
|
+
["git", "-C", repo_path, "fetch", "origin", ref_value]
|
|
248
|
+
)
|
|
249
|
+
subprocess.check_call(
|
|
250
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
251
|
+
)
|
|
252
|
+
subprocess.check_call(
|
|
253
|
+
["git", "-C", repo_path, "pull", "origin", ref_value]
|
|
254
|
+
)
|
|
255
|
+
logger.info(
|
|
256
|
+
f"Updated repository {repo_name} to branch {ref_value}"
|
|
257
|
+
)
|
|
258
|
+
return True
|
|
259
|
+
except subprocess.CalledProcessError:
|
|
260
|
+
# If all else fails, just use a default branch
|
|
261
|
+
logger.warning(
|
|
262
|
+
f"Could not checkout {ref_value} as tag or branch, trying default branches"
|
|
263
|
+
)
|
|
264
|
+
for default_branch in default_branches:
|
|
265
|
+
try:
|
|
266
|
+
subprocess.check_call(
|
|
267
|
+
[
|
|
268
|
+
"git",
|
|
269
|
+
"-C",
|
|
270
|
+
repo_path,
|
|
271
|
+
"checkout",
|
|
272
|
+
default_branch,
|
|
273
|
+
]
|
|
274
|
+
)
|
|
275
|
+
subprocess.check_call(
|
|
276
|
+
[
|
|
277
|
+
"git",
|
|
278
|
+
"-C",
|
|
279
|
+
repo_path,
|
|
280
|
+
"pull",
|
|
281
|
+
"origin",
|
|
282
|
+
default_branch,
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
logger.info(
|
|
286
|
+
f"Using {default_branch} instead of {ref_value}"
|
|
287
|
+
)
|
|
288
|
+
return True
|
|
289
|
+
except subprocess.CalledProcessError:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# If we get here, we couldn't checkout any branch
|
|
293
|
+
logger.warning(
|
|
294
|
+
"Could not checkout any branch, using current state"
|
|
295
|
+
)
|
|
296
|
+
return True
|
|
69
297
|
except subprocess.CalledProcessError as e:
|
|
70
298
|
logger.error(f"Error updating repository {repo_name}: {e}")
|
|
71
299
|
logger.error(
|
|
72
300
|
f"Please manually git clone the repository {repo_url} into {repo_path}"
|
|
73
301
|
)
|
|
74
|
-
|
|
302
|
+
return False
|
|
75
303
|
else:
|
|
304
|
+
# Repository doesn't exist yet, clone it
|
|
76
305
|
try:
|
|
77
306
|
os.makedirs(plugins_dir, exist_ok=True)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
307
|
+
|
|
308
|
+
# If it's a default branch, just clone it directly
|
|
309
|
+
if is_default_branch:
|
|
310
|
+
try:
|
|
311
|
+
# Try to clone with the specified branch
|
|
312
|
+
subprocess.check_call(
|
|
313
|
+
["git", "clone", "--branch", ref_value, repo_url],
|
|
314
|
+
cwd=plugins_dir,
|
|
315
|
+
)
|
|
316
|
+
if ref_type == "branch":
|
|
317
|
+
logger.info(
|
|
318
|
+
f"Cloned repository {repo_name} from {repo_url} at branch {ref_value}"
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
logger.info(
|
|
322
|
+
f"Cloned repository {repo_name} from {repo_url} at tag {ref_value}"
|
|
323
|
+
)
|
|
324
|
+
return True
|
|
325
|
+
except subprocess.CalledProcessError:
|
|
326
|
+
# If that fails, try the other default branch
|
|
327
|
+
other_default = "main" if ref_value == "master" else "master"
|
|
328
|
+
try:
|
|
329
|
+
logger.warning(
|
|
330
|
+
f"Could not clone with branch {ref_value}, trying {other_default}"
|
|
331
|
+
)
|
|
332
|
+
subprocess.check_call(
|
|
333
|
+
["git", "clone", "--branch", other_default, repo_url],
|
|
334
|
+
cwd=plugins_dir,
|
|
335
|
+
)
|
|
336
|
+
logger.info(
|
|
337
|
+
f"Cloned repository {repo_name} from {repo_url} at branch {other_default}"
|
|
338
|
+
)
|
|
339
|
+
return True
|
|
340
|
+
except subprocess.CalledProcessError:
|
|
341
|
+
# If that fails too, clone without specifying a branch
|
|
342
|
+
logger.warning(
|
|
343
|
+
f"Could not clone with branch {other_default}, cloning default branch"
|
|
344
|
+
)
|
|
345
|
+
subprocess.check_call(
|
|
346
|
+
["git", "clone", repo_url], cwd=plugins_dir
|
|
347
|
+
)
|
|
348
|
+
logger.info(
|
|
349
|
+
f"Cloned repository {repo_name} from {repo_url} (default branch)"
|
|
350
|
+
)
|
|
351
|
+
return True
|
|
352
|
+
else:
|
|
353
|
+
# It's a tag, try to clone with the tag
|
|
354
|
+
try:
|
|
355
|
+
# Try to clone with the specified tag
|
|
356
|
+
subprocess.check_call(
|
|
357
|
+
["git", "clone", "--branch", ref_value, repo_url],
|
|
358
|
+
cwd=plugins_dir,
|
|
359
|
+
)
|
|
360
|
+
if ref_type == "branch":
|
|
361
|
+
logger.info(
|
|
362
|
+
f"Cloned repository {repo_name} from {repo_url} at branch {ref_value}"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
logger.info(
|
|
366
|
+
f"Cloned repository {repo_name} from {repo_url} at tag {ref_value}"
|
|
367
|
+
)
|
|
368
|
+
return True
|
|
369
|
+
except subprocess.CalledProcessError:
|
|
370
|
+
# If that fails, clone without specifying a tag
|
|
371
|
+
logger.warning(
|
|
372
|
+
f"Could not clone with tag {ref_value}, cloning default branch"
|
|
373
|
+
)
|
|
374
|
+
subprocess.check_call(["git", "clone", repo_url], cwd=plugins_dir)
|
|
375
|
+
|
|
376
|
+
# Then try to fetch and checkout the tag
|
|
377
|
+
try:
|
|
378
|
+
# Try to fetch the tag
|
|
379
|
+
try:
|
|
380
|
+
subprocess.check_call(
|
|
381
|
+
[
|
|
382
|
+
"git",
|
|
383
|
+
"-C",
|
|
384
|
+
repo_path,
|
|
385
|
+
"fetch",
|
|
386
|
+
"origin",
|
|
387
|
+
f"refs/tags/{ref_value}",
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
except subprocess.CalledProcessError:
|
|
391
|
+
# If that fails, try to fetch the tag without the refs/tags/ prefix
|
|
392
|
+
subprocess.check_call(
|
|
393
|
+
[
|
|
394
|
+
"git",
|
|
395
|
+
"-C",
|
|
396
|
+
repo_path,
|
|
397
|
+
"fetch",
|
|
398
|
+
"origin",
|
|
399
|
+
f"refs/tags/{ref_value}:refs/tags/{ref_value}",
|
|
400
|
+
]
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Now checkout the tag
|
|
404
|
+
subprocess.check_call(
|
|
405
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
406
|
+
)
|
|
407
|
+
if ref_type == "branch":
|
|
408
|
+
logger.info(
|
|
409
|
+
f"Cloned repository {repo_name} and checked out branch {ref_value}"
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
logger.info(
|
|
413
|
+
f"Cloned repository {repo_name} and checked out tag {ref_value}"
|
|
414
|
+
)
|
|
415
|
+
return True
|
|
416
|
+
except subprocess.CalledProcessError:
|
|
417
|
+
# If that fails, try as a branch
|
|
418
|
+
try:
|
|
419
|
+
logger.warning(
|
|
420
|
+
f"Could not checkout {ref_value} as a tag, trying as a branch"
|
|
421
|
+
)
|
|
422
|
+
subprocess.check_call(
|
|
423
|
+
["git", "-C", repo_path, "fetch", "origin", ref_value]
|
|
424
|
+
)
|
|
425
|
+
subprocess.check_call(
|
|
426
|
+
["git", "-C", repo_path, "checkout", ref_value]
|
|
427
|
+
)
|
|
428
|
+
logger.info(
|
|
429
|
+
f"Cloned repository {repo_name} and checked out branch {ref_value}"
|
|
430
|
+
)
|
|
431
|
+
return True
|
|
432
|
+
except subprocess.CalledProcessError:
|
|
433
|
+
logger.warning(
|
|
434
|
+
f"Could not checkout {ref_value}, using default branch"
|
|
435
|
+
)
|
|
436
|
+
logger.info(
|
|
437
|
+
f"Cloned repository {repo_name} from {repo_url} (default branch)"
|
|
438
|
+
)
|
|
439
|
+
return True
|
|
82
440
|
except subprocess.CalledProcessError as e:
|
|
83
441
|
logger.error(f"Error cloning repository {repo_name}: {e}")
|
|
84
442
|
logger.error(
|
|
85
443
|
f"Please manually git clone the repository {repo_url} into {repo_path}"
|
|
86
444
|
)
|
|
87
|
-
|
|
445
|
+
return False
|
|
88
446
|
# Install requirements if requirements.txt exists
|
|
89
447
|
requirements_path = os.path.join(repo_path, "requirements.txt")
|
|
90
448
|
if os.path.isfile(requirements_path):
|
|
91
449
|
try:
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
450
|
+
# Check if we're running in a pipx environment
|
|
451
|
+
in_pipx = "PIPX_HOME" in os.environ or "PIPX_LOCAL_VENVS" in os.environ
|
|
452
|
+
|
|
453
|
+
# Read requirements from file
|
|
454
|
+
with open(requirements_path, "r") as f:
|
|
455
|
+
requirements = [
|
|
456
|
+
line.strip()
|
|
457
|
+
for line in f
|
|
458
|
+
if line.strip() and not line.startswith("#")
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
if requirements:
|
|
462
|
+
if in_pipx:
|
|
463
|
+
# Use pipx inject for each requirement
|
|
464
|
+
logger.info(
|
|
465
|
+
f"Installing requirements for plugin {repo_name} with pipx inject"
|
|
466
|
+
)
|
|
467
|
+
for req in requirements:
|
|
468
|
+
logger.info(f"Installing {req}")
|
|
469
|
+
subprocess.check_call(["pipx", "inject", "mmrelay", req])
|
|
470
|
+
else:
|
|
471
|
+
# Use pip to install the requirements.txt
|
|
472
|
+
logger.info(
|
|
473
|
+
f"Installing requirements for plugin {repo_name} with pip"
|
|
474
|
+
)
|
|
475
|
+
subprocess.check_call(
|
|
476
|
+
[
|
|
477
|
+
sys.executable,
|
|
478
|
+
"-m",
|
|
479
|
+
"pip",
|
|
480
|
+
"install",
|
|
481
|
+
"-r",
|
|
482
|
+
requirements_path,
|
|
483
|
+
]
|
|
484
|
+
)
|
|
485
|
+
logger.info(
|
|
486
|
+
f"Successfully installed requirements for plugin {repo_name}"
|
|
487
|
+
)
|
|
97
488
|
except subprocess.CalledProcessError as e:
|
|
98
489
|
logger.error(f"Error installing requirements for plugin {repo_name}: {e}")
|
|
99
490
|
logger.error(
|
|
100
491
|
f"Please manually install the requirements from {requirements_path}"
|
|
101
492
|
)
|
|
102
|
-
|
|
493
|
+
# Don't exit, just continue with a warning
|
|
494
|
+
logger.warning(
|
|
495
|
+
f"Plugin {repo_name} may not work correctly without its dependencies"
|
|
496
|
+
)
|
|
103
497
|
|
|
104
498
|
|
|
105
499
|
def load_plugins_from_directory(directory, recursive=False):
|
|
@@ -117,14 +511,101 @@ def load_plugins_from_directory(directory, recursive=False):
|
|
|
117
511
|
module_name, plugin_path
|
|
118
512
|
)
|
|
119
513
|
plugin_module = importlib.util.module_from_spec(spec)
|
|
514
|
+
|
|
515
|
+
# Create a compatibility layer for plugins
|
|
516
|
+
# This allows plugins to import from 'plugins' or 'mmrelay.plugins'
|
|
517
|
+
if "mmrelay.plugins" not in sys.modules:
|
|
518
|
+
import mmrelay.plugins
|
|
519
|
+
|
|
520
|
+
sys.modules["mmrelay.plugins"] = mmrelay.plugins
|
|
521
|
+
|
|
522
|
+
# For backward compatibility with older plugins
|
|
523
|
+
if "plugins" not in sys.modules:
|
|
524
|
+
import mmrelay.plugins
|
|
525
|
+
|
|
526
|
+
sys.modules["plugins"] = mmrelay.plugins
|
|
527
|
+
|
|
120
528
|
try:
|
|
529
|
+
# Add the plugin's directory to sys.path temporarily
|
|
530
|
+
plugin_dir = os.path.dirname(plugin_path)
|
|
531
|
+
sys.path.insert(0, plugin_dir)
|
|
532
|
+
|
|
533
|
+
# Execute the module
|
|
121
534
|
spec.loader.exec_module(plugin_module)
|
|
535
|
+
|
|
536
|
+
# Remove the plugin directory from sys.path
|
|
537
|
+
if plugin_dir in sys.path:
|
|
538
|
+
sys.path.remove(plugin_dir)
|
|
539
|
+
|
|
122
540
|
if hasattr(plugin_module, "Plugin"):
|
|
123
541
|
plugins.append(plugin_module.Plugin())
|
|
124
542
|
else:
|
|
125
543
|
logger.warning(
|
|
126
544
|
f"{plugin_path} does not define a Plugin class."
|
|
127
545
|
)
|
|
546
|
+
except ModuleNotFoundError as e:
|
|
547
|
+
missing_module = str(e).split()[-1].strip("'")
|
|
548
|
+
logger.warning(
|
|
549
|
+
f"Missing dependency for plugin {plugin_path}: {missing_module}"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Try to automatically install the missing dependency
|
|
553
|
+
try:
|
|
554
|
+
# Check if we're running in a pipx environment
|
|
555
|
+
in_pipx = (
|
|
556
|
+
"PIPX_HOME" in os.environ
|
|
557
|
+
or "PIPX_LOCAL_VENVS" in os.environ
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
if in_pipx:
|
|
561
|
+
logger.info(
|
|
562
|
+
f"Attempting to install missing dependency with pipx inject: {missing_module}"
|
|
563
|
+
)
|
|
564
|
+
subprocess.check_call(
|
|
565
|
+
["pipx", "inject", "mmrelay", missing_module]
|
|
566
|
+
)
|
|
567
|
+
else:
|
|
568
|
+
logger.info(
|
|
569
|
+
f"Attempting to install missing dependency with pip: {missing_module}"
|
|
570
|
+
)
|
|
571
|
+
subprocess.check_call(
|
|
572
|
+
[
|
|
573
|
+
sys.executable,
|
|
574
|
+
"-m",
|
|
575
|
+
"pip",
|
|
576
|
+
"install",
|
|
577
|
+
missing_module,
|
|
578
|
+
]
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
logger.info(
|
|
582
|
+
f"Successfully installed {missing_module}, retrying plugin load"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Try to load the module again
|
|
586
|
+
spec.loader.exec_module(plugin_module)
|
|
587
|
+
|
|
588
|
+
if hasattr(plugin_module, "Plugin"):
|
|
589
|
+
plugins.append(plugin_module.Plugin())
|
|
590
|
+
else:
|
|
591
|
+
logger.warning(
|
|
592
|
+
f"{plugin_path} does not define a Plugin class."
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
except subprocess.CalledProcessError:
|
|
596
|
+
logger.error(
|
|
597
|
+
f"Failed to automatically install {missing_module}"
|
|
598
|
+
)
|
|
599
|
+
logger.error("Please install it manually:")
|
|
600
|
+
logger.error(
|
|
601
|
+
f"pipx inject mmrelay {missing_module} # if using pipx"
|
|
602
|
+
)
|
|
603
|
+
logger.error(
|
|
604
|
+
f"pip install {missing_module} # if using pip"
|
|
605
|
+
)
|
|
606
|
+
logger.error(
|
|
607
|
+
f"Plugin directory: {os.path.dirname(plugin_path)}"
|
|
608
|
+
)
|
|
128
609
|
except Exception as e:
|
|
129
610
|
logger.error(f"Error loading plugin {plugin_path}: {e}")
|
|
130
611
|
if not recursive:
|
|
@@ -223,7 +704,7 @@ def load_plugins(passed_config=None):
|
|
|
223
704
|
|
|
224
705
|
# Get the first directory for cloning (prefer user directory)
|
|
225
706
|
community_plugins_dir = community_plugin_dirs[
|
|
226
|
-
|
|
707
|
+
0
|
|
227
708
|
] # Use the user directory for new clones
|
|
228
709
|
|
|
229
710
|
# Create community plugins directory if needed
|
|
@@ -252,14 +733,37 @@ def load_plugins(passed_config=None):
|
|
|
252
733
|
continue
|
|
253
734
|
|
|
254
735
|
repo_url = plugin_info.get("repository")
|
|
255
|
-
|
|
736
|
+
|
|
737
|
+
# Support both tag and branch parameters
|
|
738
|
+
tag = plugin_info.get("tag")
|
|
739
|
+
branch = plugin_info.get("branch")
|
|
740
|
+
|
|
741
|
+
# Determine what to use (tag, branch, or default)
|
|
742
|
+
if tag and branch:
|
|
743
|
+
logger.warning(
|
|
744
|
+
f"Both tag and branch specified for plugin {plugin_name}, using tag"
|
|
745
|
+
)
|
|
746
|
+
ref = {"type": "tag", "value": tag}
|
|
747
|
+
elif tag:
|
|
748
|
+
ref = {"type": "tag", "value": tag}
|
|
749
|
+
elif branch:
|
|
750
|
+
ref = {"type": "branch", "value": branch}
|
|
751
|
+
else:
|
|
752
|
+
# Default to main branch if neither is specified
|
|
753
|
+
ref = {"type": "branch", "value": "main"}
|
|
754
|
+
|
|
256
755
|
if repo_url:
|
|
257
756
|
# Clone to the user directory by default
|
|
258
|
-
clone_or_update_repo(repo_url,
|
|
757
|
+
success = clone_or_update_repo(repo_url, ref, community_plugins_dir)
|
|
758
|
+
if not success:
|
|
759
|
+
logger.warning(
|
|
760
|
+
f"Failed to clone/update plugin {plugin_name}, skipping"
|
|
761
|
+
)
|
|
762
|
+
continue
|
|
259
763
|
else:
|
|
260
764
|
logger.error("Repository URL not specified for a community plugin")
|
|
261
765
|
logger.error("Please specify the repository URL in config.yaml")
|
|
262
|
-
|
|
766
|
+
continue
|
|
263
767
|
|
|
264
768
|
# Only load community plugins that are explicitly enabled
|
|
265
769
|
for plugin_name in active_community_plugins:
|
|
@@ -274,7 +778,7 @@ def load_plugins(passed_config=None):
|
|
|
274
778
|
for dir_path in community_plugin_dirs:
|
|
275
779
|
plugin_path = os.path.join(dir_path, repo_name)
|
|
276
780
|
if os.path.exists(plugin_path):
|
|
277
|
-
logger.
|
|
781
|
+
logger.info(f"Loading community plugin from: {plugin_path}")
|
|
278
782
|
plugins.extend(
|
|
279
783
|
load_plugins_from_directory(plugin_path, recursive=True)
|
|
280
784
|
)
|
mmrelay/plugins/base_plugin.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import threading
|
|
2
3
|
import time
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
@@ -5,6 +6,7 @@ from abc import ABC, abstractmethod
|
|
|
5
6
|
import markdown
|
|
6
7
|
import schedule
|
|
7
8
|
|
|
9
|
+
from mmrelay.config import get_plugin_data_dir
|
|
8
10
|
from mmrelay.db_utils import (
|
|
9
11
|
delete_plugin_data,
|
|
10
12
|
get_plugin_data,
|
|
@@ -195,6 +197,33 @@ class BasePlugin(ABC):
|
|
|
195
197
|
def get_data(self):
|
|
196
198
|
return get_plugin_data(self.plugin_name)
|
|
197
199
|
|
|
200
|
+
def get_plugin_data_dir(self, subdir=None):
|
|
201
|
+
"""
|
|
202
|
+
Returns the directory for storing plugin-specific data files.
|
|
203
|
+
Creates the directory if it doesn't exist.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
subdir (str, optional): Optional subdirectory within the plugin's data directory.
|
|
207
|
+
If provided, this subdirectory will be created.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str: Path to the plugin's data directory or subdirectory
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
self.get_plugin_data_dir() returns ~/.mmrelay/data/plugins/your_plugin_name/
|
|
214
|
+
self.get_plugin_data_dir("data_files") returns ~/.mmrelay/data/plugins/your_plugin_name/data_files/
|
|
215
|
+
"""
|
|
216
|
+
# Get the plugin-specific data directory
|
|
217
|
+
plugin_dir = get_plugin_data_dir(self.plugin_name)
|
|
218
|
+
|
|
219
|
+
# If a subdirectory is specified, create and return it
|
|
220
|
+
if subdir:
|
|
221
|
+
subdir_path = os.path.join(plugin_dir, subdir)
|
|
222
|
+
os.makedirs(subdir_path, exist_ok=True)
|
|
223
|
+
return subdir_path
|
|
224
|
+
|
|
225
|
+
return plugin_dir
|
|
226
|
+
|
|
198
227
|
def matches(self, event):
|
|
199
228
|
from mmrelay.matrix_utils import bot_command
|
|
200
229
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
5
|
Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
mmrelay/__init__.py,sha256=8fghAZAeSUb8uIaiit0FGNUWI0ZsBR__QbXNwqNbGtw,588
|
|
2
2
|
mmrelay/cli.py,sha256=yBYOkCwGYDJ2gRKPoPltxB_PydX1CPNUerVJHcNuHTo,12355
|
|
3
|
-
mmrelay/config.py,sha256=
|
|
3
|
+
mmrelay/config.py,sha256=5VZag8iSc5yLQgvwI76bbpizbtqag74cHnfXCrWHNyA,7910
|
|
4
4
|
mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
|
|
5
5
|
mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
|
|
6
6
|
mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
|
|
7
7
|
mmrelay/main.py,sha256=acgBF-DnL0Rs8MmYAfbNHZ9xjqM3Qc0_oKtATN4iOEE,11085
|
|
8
8
|
mmrelay/matrix_utils.py,sha256=GkIVj2bbPHtx1emFMwhEhc1SWHcv4UvkuyZYdb-Wnwo,30511
|
|
9
9
|
mmrelay/meshtastic_utils.py,sha256=Pd0j7mz008ncBDIjRGyHOIb0U3vKq06uXWoJP-csHcQ,23381
|
|
10
|
-
mmrelay/plugin_loader.py,sha256=
|
|
10
|
+
mmrelay/plugin_loader.py,sha256=Gg8E_TKqndMQHiVJwtmlIC-jCgcty-5EsX_5JJ6Onvo,36590
|
|
11
11
|
mmrelay/setup_utils.py,sha256=GHttNLoqPwOpjSly0osaCnotPxCq0hfyopsZ2qq313A,14091
|
|
12
12
|
mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
|
|
13
|
-
mmrelay/plugins/base_plugin.py,sha256=
|
|
13
|
+
mmrelay/plugins/base_plugin.py,sha256=hv21tSEYG-AB36aLAFdW9DDKm0NOTRNPpGIO5F3i1ts,8633
|
|
14
14
|
mmrelay/plugins/debug_plugin.py,sha256=Jziht9Nj_bRO6Rmy7TjfBXaYo5eM3XsenbWFxPpyUs4,443
|
|
15
15
|
mmrelay/plugins/drop_plugin.py,sha256=ACchX6GfEldqTyvsZVg-5jwbe2-CjENQVVZFnw8SaEM,4618
|
|
16
16
|
mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
|
|
@@ -21,9 +21,9 @@ mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A
|
|
|
21
21
|
mmrelay/plugins/ping_plugin.py,sha256=RTRdgDQUSO33lreDTmWsTlI0L1C3FJrXE0KYqfEWYO0,4017
|
|
22
22
|
mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
|
|
23
23
|
mmrelay/plugins/weather_plugin.py,sha256=yoKA_HdFqFEhgYdXqLhvXatLphCyLJFuGUKCR7fILv0,8546
|
|
24
|
-
mmrelay-1.0.
|
|
25
|
-
mmrelay-1.0.
|
|
26
|
-
mmrelay-1.0.
|
|
27
|
-
mmrelay-1.0.
|
|
28
|
-
mmrelay-1.0.
|
|
29
|
-
mmrelay-1.0.
|
|
24
|
+
mmrelay-1.0.5.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
|
|
25
|
+
mmrelay-1.0.5.dist-info/METADATA,sha256=IrC7LB07B_2BqV3oTs-Epo3veb7XYlXE8H1aeD1WMQw,6953
|
|
26
|
+
mmrelay-1.0.5.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
27
|
+
mmrelay-1.0.5.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
28
|
+
mmrelay-1.0.5.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
29
|
+
mmrelay-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|