mmrelay 1.0.3__tar.gz → 1.0.4__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.

Potentially problematic release.


This version of mmrelay might be problematic. Click here for more details.

Files changed (39) hide show
  1. {mmrelay-1.0.3/src/mmrelay.egg-info → mmrelay-1.0.4}/PKG-INFO +1 -1
  2. {mmrelay-1.0.3 → mmrelay-1.0.4}/setup.cfg +1 -1
  3. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/config.py +26 -0
  4. mmrelay-1.0.4/src/mmrelay/plugin_loader.py +642 -0
  5. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/base_plugin.py +29 -0
  6. {mmrelay-1.0.3 → mmrelay-1.0.4/src/mmrelay.egg-info}/PKG-INFO +1 -1
  7. mmrelay-1.0.3/src/mmrelay/plugin_loader.py +0 -336
  8. {mmrelay-1.0.3 → mmrelay-1.0.4}/LICENSE +0 -0
  9. {mmrelay-1.0.3 → mmrelay-1.0.4}/MANIFEST.in +0 -0
  10. {mmrelay-1.0.3 → mmrelay-1.0.4}/README.md +0 -0
  11. {mmrelay-1.0.3 → mmrelay-1.0.4}/pyproject.toml +0 -0
  12. {mmrelay-1.0.3 → mmrelay-1.0.4}/requirements.txt +0 -0
  13. {mmrelay-1.0.3 → mmrelay-1.0.4}/sample_config.yaml +0 -0
  14. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/__init__.py +0 -0
  15. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/cli.py +0 -0
  16. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/config_checker.py +0 -0
  17. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/db_utils.py +0 -0
  18. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/log_utils.py +0 -0
  19. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/main.py +0 -0
  20. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/matrix_utils.py +0 -0
  21. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/meshtastic_utils.py +0 -0
  22. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/__init__.py +0 -0
  23. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/debug_plugin.py +0 -0
  24. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/drop_plugin.py +0 -0
  25. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/health_plugin.py +0 -0
  26. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/help_plugin.py +0 -0
  27. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/map_plugin.py +0 -0
  28. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/mesh_relay_plugin.py +0 -0
  29. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/nodes_plugin.py +0 -0
  30. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/ping_plugin.py +0 -0
  31. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/telemetry_plugin.py +0 -0
  32. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/weather_plugin.py +0 -0
  33. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/setup_utils.py +0 -0
  34. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/SOURCES.txt +0 -0
  35. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/dependency_links.txt +0 -0
  36. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/entry_points.txt +0 -0
  37. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/requires.txt +0 -0
  38. {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/top_level.txt +0 -0
  39. {mmrelay-1.0.3 → mmrelay-1.0.4}/tools/mmrelay.service +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.0.3
3
+ Version: 1.0.4
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,6 +1,6 @@
1
1
  [metadata]
2
2
  name = mmrelay
3
- version = 1.0.3
3
+ version = 1.0.4
4
4
  author = Geoff Whittington, Jeremiah K., and contributors
5
5
  author_email = jeremiahk@gmx.com
6
6
  description = Bridge between Meshtastic mesh networks and Matrix chat rooms
@@ -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.
@@ -0,0 +1,642 @@
1
+ # trunk-ignore-all(bandit)
2
+ import hashlib
3
+ import importlib.util
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ from mmrelay.config import get_app_path, get_base_dir
9
+ from mmrelay.log_utils import get_logger
10
+
11
+ # Global config variable that will be set from main.py
12
+ config = None
13
+
14
+ logger = get_logger(name="Plugins")
15
+ sorted_active_plugins = []
16
+ plugins_loaded = False
17
+
18
+
19
+ def get_custom_plugin_dirs():
20
+ """
21
+ Returns a list of directories to check for custom plugins in order of priority:
22
+ 1. User directory (~/.mmrelay/plugins/custom)
23
+ 2. Local directory (plugins/custom) for backward compatibility
24
+ """
25
+ dirs = []
26
+
27
+ # Check user directory first (preferred location)
28
+ user_dir = os.path.join(get_base_dir(), "plugins", "custom")
29
+ os.makedirs(user_dir, exist_ok=True)
30
+ dirs.append(user_dir)
31
+
32
+ # Check local directory (backward compatibility)
33
+ local_dir = os.path.join(get_app_path(), "plugins", "custom")
34
+ dirs.append(local_dir)
35
+
36
+ return dirs
37
+
38
+
39
+ def get_community_plugin_dirs():
40
+ """
41
+ Returns a list of directories to check for community plugins in order of priority:
42
+ 1. User directory (~/.mmrelay/plugins/community)
43
+ 2. Local directory (plugins/community) for backward compatibility
44
+ """
45
+ dirs = []
46
+
47
+ # Check user directory first (preferred location)
48
+ user_dir = os.path.join(get_base_dir(), "plugins", "community")
49
+ os.makedirs(user_dir, exist_ok=True)
50
+ dirs.append(user_dir)
51
+
52
+ # Check local directory (backward compatibility)
53
+ local_dir = os.path.join(get_app_path(), "plugins", "community")
54
+ dirs.append(local_dir)
55
+
56
+ return dirs
57
+
58
+
59
+ def clone_or_update_repo(repo_url, tag, plugins_dir):
60
+ # Extract the repository name from the URL
61
+ repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
62
+ repo_path = os.path.join(plugins_dir, repo_name)
63
+
64
+ # Default branch names to try if tag is not specified
65
+ default_branches = ["main", "master"]
66
+
67
+ # If tag is one of the default branches, we'll handle it as a branch
68
+ is_default_branch = tag in default_branches
69
+
70
+ if os.path.isdir(repo_path):
71
+ try:
72
+ # Fetch all branches but don't fetch tags to avoid conflicts
73
+ try:
74
+ subprocess.check_call(["git", "-C", repo_path, "fetch", "origin"])
75
+ except subprocess.CalledProcessError as e:
76
+ logger.warning(f"Error fetching from remote: {e}")
77
+ # Continue anyway, we'll try to use what we have
78
+
79
+ # If it's a default branch, handle it differently
80
+ if is_default_branch:
81
+ try:
82
+ # Check if we're already on the right branch
83
+ current_branch = subprocess.check_output(
84
+ ["git", "-C", repo_path, "rev-parse", "--abbrev-ref", "HEAD"],
85
+ universal_newlines=True,
86
+ ).strip()
87
+
88
+ if current_branch == tag:
89
+ # We're on the right branch, just pull
90
+ try:
91
+ subprocess.check_call(
92
+ ["git", "-C", repo_path, "pull", "origin", tag]
93
+ )
94
+ logger.info(f"Updated repository {repo_name} branch {tag}")
95
+ return True
96
+ except subprocess.CalledProcessError as e:
97
+ logger.warning(f"Error pulling branch {tag}: {e}")
98
+ # Continue anyway, we'll use what we have
99
+ return True
100
+ else:
101
+ # Switch to the right branch
102
+ subprocess.check_call(["git", "-C", repo_path, "checkout", tag])
103
+ subprocess.check_call(
104
+ ["git", "-C", repo_path, "pull", "origin", tag]
105
+ )
106
+ logger.info(f"Switched to and updated branch {tag}")
107
+ return True
108
+ except subprocess.CalledProcessError:
109
+ # If we can't checkout the specified branch, try the other default branch
110
+ other_default = "main" if tag == "master" else "master"
111
+ try:
112
+ logger.warning(
113
+ f"Branch {tag} not found, trying {other_default}"
114
+ )
115
+ subprocess.check_call(
116
+ ["git", "-C", repo_path, "checkout", other_default]
117
+ )
118
+ subprocess.check_call(
119
+ ["git", "-C", repo_path, "pull", "origin", other_default]
120
+ )
121
+ logger.info(f"Using {other_default} branch instead of {tag}")
122
+ return True
123
+ except subprocess.CalledProcessError:
124
+ # If that fails too, just use whatever branch we're on
125
+ logger.warning(
126
+ "Could not checkout any default branch, using current branch"
127
+ )
128
+ return True
129
+ else:
130
+ # Handle tag checkout
131
+ # Check if we're already on the correct tag/commit
132
+ try:
133
+ # Get the current commit hash
134
+ current_commit = subprocess.check_output(
135
+ ["git", "-C", repo_path, "rev-parse", "HEAD"],
136
+ universal_newlines=True,
137
+ ).strip()
138
+
139
+ # Get the commit hash for the tag
140
+ tag_commit = None
141
+ try:
142
+ tag_commit = subprocess.check_output(
143
+ ["git", "-C", repo_path, "rev-parse", tag],
144
+ universal_newlines=True,
145
+ ).strip()
146
+ except subprocess.CalledProcessError:
147
+ # Tag doesn't exist locally, we'll need to fetch it
148
+ pass
149
+
150
+ # If we're already at the tag's commit, we're done
151
+ if tag_commit and current_commit == tag_commit:
152
+ logger.info(f"Repository {repo_name} is already at tag {tag}")
153
+ return True
154
+
155
+ # Otherwise, try to checkout the tag
156
+ subprocess.check_call(["git", "-C", repo_path, "checkout", tag])
157
+ logger.info(f"Updated repository {repo_name} to tag {tag}")
158
+ return True
159
+ except subprocess.CalledProcessError:
160
+ # If tag checkout fails, try to fetch it specifically
161
+ logger.warning(
162
+ f"Tag {tag} not found locally, trying to fetch it specifically"
163
+ )
164
+ try:
165
+ # Try to fetch the specific tag, but first remove any existing tag with the same name
166
+ try:
167
+ # Delete the local tag if it exists to avoid conflicts
168
+ subprocess.check_call(
169
+ ["git", "-C", repo_path, "tag", "-d", tag]
170
+ )
171
+ except subprocess.CalledProcessError:
172
+ # Tag doesn't exist locally, which is fine
173
+ pass
174
+
175
+ # Now fetch the tag from remote
176
+ try:
177
+ # Try to fetch the tag
178
+ subprocess.check_call(
179
+ [
180
+ "git",
181
+ "-C",
182
+ repo_path,
183
+ "fetch",
184
+ "origin",
185
+ f"refs/tags/{tag}",
186
+ ]
187
+ )
188
+ except subprocess.CalledProcessError:
189
+ # If that fails, try to fetch the tag without the refs/tags/ prefix
190
+ subprocess.check_call(
191
+ [
192
+ "git",
193
+ "-C",
194
+ repo_path,
195
+ "fetch",
196
+ "origin",
197
+ f"refs/tags/{tag}:refs/tags/{tag}",
198
+ ]
199
+ )
200
+
201
+ subprocess.check_call(["git", "-C", repo_path, "checkout", tag])
202
+ logger.info(f"Successfully fetched and checked out tag {tag}")
203
+ return True
204
+ except subprocess.CalledProcessError:
205
+ # If that fails too, try as a branch
206
+ logger.warning(f"Could not fetch tag {tag}, trying as a branch")
207
+ try:
208
+ subprocess.check_call(
209
+ ["git", "-C", repo_path, "fetch", "origin", tag]
210
+ )
211
+ subprocess.check_call(
212
+ ["git", "-C", repo_path, "checkout", tag]
213
+ )
214
+ subprocess.check_call(
215
+ ["git", "-C", repo_path, "pull", "origin", tag]
216
+ )
217
+ logger.info(
218
+ f"Updated repository {repo_name} to branch {tag}"
219
+ )
220
+ return True
221
+ except subprocess.CalledProcessError:
222
+ # If all else fails, just use a default branch
223
+ logger.warning(
224
+ f"Could not checkout {tag} as tag or branch, trying default branches"
225
+ )
226
+ for default_branch in default_branches:
227
+ try:
228
+ subprocess.check_call(
229
+ [
230
+ "git",
231
+ "-C",
232
+ repo_path,
233
+ "checkout",
234
+ default_branch,
235
+ ]
236
+ )
237
+ subprocess.check_call(
238
+ [
239
+ "git",
240
+ "-C",
241
+ repo_path,
242
+ "pull",
243
+ "origin",
244
+ default_branch,
245
+ ]
246
+ )
247
+ logger.info(
248
+ f"Using {default_branch} instead of {tag}"
249
+ )
250
+ return True
251
+ except subprocess.CalledProcessError:
252
+ continue
253
+
254
+ # If we get here, we couldn't checkout any branch
255
+ logger.warning(
256
+ "Could not checkout any branch, using current state"
257
+ )
258
+ return True
259
+ except subprocess.CalledProcessError as e:
260
+ logger.error(f"Error updating repository {repo_name}: {e}")
261
+ logger.error(
262
+ f"Please manually git clone the repository {repo_url} into {repo_path}"
263
+ )
264
+ return False
265
+ else:
266
+ # Repository doesn't exist yet, clone it
267
+ try:
268
+ os.makedirs(plugins_dir, exist_ok=True)
269
+
270
+ # If it's a default branch, just clone it directly
271
+ if is_default_branch:
272
+ try:
273
+ # Try to clone with the specified branch
274
+ subprocess.check_call(
275
+ ["git", "clone", "--branch", tag, repo_url], cwd=plugins_dir
276
+ )
277
+ logger.info(
278
+ f"Cloned repository {repo_name} from {repo_url} at branch {tag}"
279
+ )
280
+ return True
281
+ except subprocess.CalledProcessError:
282
+ # If that fails, try the other default branch
283
+ other_default = "main" if tag == "master" else "master"
284
+ try:
285
+ logger.warning(
286
+ f"Could not clone with branch {tag}, trying {other_default}"
287
+ )
288
+ subprocess.check_call(
289
+ ["git", "clone", "--branch", other_default, repo_url],
290
+ cwd=plugins_dir,
291
+ )
292
+ logger.info(
293
+ f"Cloned repository {repo_name} from {repo_url} at branch {other_default}"
294
+ )
295
+ return True
296
+ except subprocess.CalledProcessError:
297
+ # If that fails too, clone without specifying a branch
298
+ logger.warning(
299
+ f"Could not clone with branch {other_default}, cloning default branch"
300
+ )
301
+ subprocess.check_call(
302
+ ["git", "clone", repo_url], cwd=plugins_dir
303
+ )
304
+ logger.info(
305
+ f"Cloned repository {repo_name} from {repo_url} (default branch)"
306
+ )
307
+ return True
308
+ else:
309
+ # It's a tag, try to clone with the tag
310
+ try:
311
+ # Try to clone with the specified tag
312
+ subprocess.check_call(
313
+ ["git", "clone", "--branch", tag, repo_url], cwd=plugins_dir
314
+ )
315
+ logger.info(
316
+ f"Cloned repository {repo_name} from {repo_url} at tag {tag}"
317
+ )
318
+ return True
319
+ except subprocess.CalledProcessError:
320
+ # If that fails, clone without specifying a tag
321
+ logger.warning(
322
+ f"Could not clone with tag {tag}, cloning default branch"
323
+ )
324
+ subprocess.check_call(["git", "clone", repo_url], cwd=plugins_dir)
325
+
326
+ # Then try to fetch and checkout the tag
327
+ try:
328
+ # Try to fetch the tag
329
+ try:
330
+ subprocess.check_call(
331
+ [
332
+ "git",
333
+ "-C",
334
+ repo_path,
335
+ "fetch",
336
+ "origin",
337
+ f"refs/tags/{tag}",
338
+ ]
339
+ )
340
+ except subprocess.CalledProcessError:
341
+ # If that fails, try to fetch the tag without the refs/tags/ prefix
342
+ subprocess.check_call(
343
+ [
344
+ "git",
345
+ "-C",
346
+ repo_path,
347
+ "fetch",
348
+ "origin",
349
+ f"refs/tags/{tag}:refs/tags/{tag}",
350
+ ]
351
+ )
352
+
353
+ # Now checkout the tag
354
+ subprocess.check_call(["git", "-C", repo_path, "checkout", tag])
355
+ logger.info(
356
+ f"Cloned repository {repo_name} and checked out tag {tag}"
357
+ )
358
+ return True
359
+ except subprocess.CalledProcessError:
360
+ # If that fails, try as a branch
361
+ try:
362
+ logger.warning(
363
+ f"Could not checkout {tag} as a tag, trying as a branch"
364
+ )
365
+ subprocess.check_call(
366
+ ["git", "-C", repo_path, "fetch", "origin", tag]
367
+ )
368
+ subprocess.check_call(
369
+ ["git", "-C", repo_path, "checkout", tag]
370
+ )
371
+ logger.info(
372
+ f"Cloned repository {repo_name} and checked out branch {tag}"
373
+ )
374
+ return True
375
+ except subprocess.CalledProcessError:
376
+ logger.warning(
377
+ f"Could not checkout {tag}, using default branch"
378
+ )
379
+ logger.info(
380
+ f"Cloned repository {repo_name} from {repo_url} (default branch)"
381
+ )
382
+ return True
383
+ except subprocess.CalledProcessError as e:
384
+ logger.error(f"Error cloning repository {repo_name}: {e}")
385
+ logger.error(
386
+ f"Please manually git clone the repository {repo_url} into {repo_path}"
387
+ )
388
+ return False
389
+ # Install requirements if requirements.txt exists
390
+ requirements_path = os.path.join(repo_path, "requirements.txt")
391
+ if os.path.isfile(requirements_path):
392
+ try:
393
+ # Use pip to install the requirements.txt
394
+ subprocess.check_call(
395
+ [sys.executable, "-m", "pip", "install", "-r", requirements_path]
396
+ )
397
+ logger.info(f"Installed requirements for plugin {repo_name}")
398
+ except subprocess.CalledProcessError as e:
399
+ logger.error(f"Error installing requirements for plugin {repo_name}: {e}")
400
+ logger.error(
401
+ f"Please manually install the requirements from {requirements_path}"
402
+ )
403
+ sys.exit(1)
404
+
405
+
406
+ def load_plugins_from_directory(directory, recursive=False):
407
+ plugins = []
408
+ if os.path.isdir(directory):
409
+ for root, _dirs, files in os.walk(directory):
410
+ for filename in files:
411
+ if filename.endswith(".py"):
412
+ plugin_path = os.path.join(root, filename)
413
+ module_name = (
414
+ "plugin_"
415
+ + hashlib.sha256(plugin_path.encode("utf-8")).hexdigest()
416
+ )
417
+ spec = importlib.util.spec_from_file_location(
418
+ module_name, plugin_path
419
+ )
420
+ plugin_module = importlib.util.module_from_spec(spec)
421
+ try:
422
+ spec.loader.exec_module(plugin_module)
423
+ if hasattr(plugin_module, "Plugin"):
424
+ plugins.append(plugin_module.Plugin())
425
+ else:
426
+ logger.warning(
427
+ f"{plugin_path} does not define a Plugin class."
428
+ )
429
+ except Exception as e:
430
+ logger.error(f"Error loading plugin {plugin_path}: {e}")
431
+ if not recursive:
432
+ break
433
+ else:
434
+ if not plugins_loaded: # Only log the missing directory once
435
+ logger.debug(f"Directory {directory} does not exist.")
436
+ return plugins
437
+
438
+
439
+ def load_plugins(passed_config=None):
440
+ global sorted_active_plugins
441
+ global plugins_loaded
442
+ global config
443
+
444
+ if plugins_loaded:
445
+ return sorted_active_plugins
446
+
447
+ logger.info("Checking plugin config...")
448
+
449
+ # Update the global config if a config is passed
450
+ if passed_config is not None:
451
+ config = passed_config
452
+
453
+ # Check if config is available
454
+ if config is None:
455
+ logger.error("No configuration available. Cannot load plugins.")
456
+ return []
457
+
458
+ # Import core plugins
459
+ from mmrelay.plugins.debug_plugin import Plugin as DebugPlugin
460
+ from mmrelay.plugins.drop_plugin import Plugin as DropPlugin
461
+ from mmrelay.plugins.health_plugin import Plugin as HealthPlugin
462
+ from mmrelay.plugins.help_plugin import Plugin as HelpPlugin
463
+ from mmrelay.plugins.map_plugin import Plugin as MapPlugin
464
+ from mmrelay.plugins.mesh_relay_plugin import Plugin as MeshRelayPlugin
465
+ from mmrelay.plugins.nodes_plugin import Plugin as NodesPlugin
466
+ from mmrelay.plugins.ping_plugin import Plugin as PingPlugin
467
+ from mmrelay.plugins.telemetry_plugin import Plugin as TelemetryPlugin
468
+ from mmrelay.plugins.weather_plugin import Plugin as WeatherPlugin
469
+
470
+ # Initial list of core plugins
471
+ core_plugins = [
472
+ HealthPlugin(),
473
+ MapPlugin(),
474
+ MeshRelayPlugin(),
475
+ PingPlugin(),
476
+ TelemetryPlugin(),
477
+ WeatherPlugin(),
478
+ HelpPlugin(),
479
+ NodesPlugin(),
480
+ DropPlugin(),
481
+ DebugPlugin(),
482
+ ]
483
+
484
+ plugins = core_plugins.copy()
485
+
486
+ # Process and load custom plugins
487
+ custom_plugins_config = config.get("custom-plugins", {})
488
+ custom_plugin_dirs = get_custom_plugin_dirs()
489
+
490
+ active_custom_plugins = [
491
+ plugin_name
492
+ for plugin_name, plugin_info in custom_plugins_config.items()
493
+ if plugin_info.get("active", False)
494
+ ]
495
+
496
+ if active_custom_plugins:
497
+ logger.debug(
498
+ f"Loading active custom plugins: {', '.join(active_custom_plugins)}"
499
+ )
500
+
501
+ # Only load custom plugins that are explicitly enabled
502
+ for plugin_name in active_custom_plugins:
503
+ plugin_found = False
504
+
505
+ # Try each directory in order
506
+ for custom_dir in custom_plugin_dirs:
507
+ plugin_path = os.path.join(custom_dir, plugin_name)
508
+ if os.path.exists(plugin_path):
509
+ logger.debug(f"Loading custom plugin from: {plugin_path}")
510
+ plugins.extend(
511
+ load_plugins_from_directory(plugin_path, recursive=False)
512
+ )
513
+ plugin_found = True
514
+ break
515
+
516
+ if not plugin_found:
517
+ logger.warning(
518
+ f"Custom plugin '{plugin_name}' not found in any of the plugin directories"
519
+ )
520
+
521
+ # Process and download community plugins
522
+ community_plugins_config = config.get("community-plugins", {})
523
+ community_plugin_dirs = get_community_plugin_dirs()
524
+
525
+ # Get the first directory for cloning (prefer user directory)
526
+ community_plugins_dir = community_plugin_dirs[
527
+ -1
528
+ ] # Use the user directory for new clones
529
+
530
+ # Create community plugins directory if needed
531
+ active_community_plugins = [
532
+ plugin_name
533
+ for plugin_name, plugin_info in community_plugins_config.items()
534
+ if plugin_info.get("active", False)
535
+ ]
536
+
537
+ if active_community_plugins:
538
+ # Ensure all community plugin directories exist
539
+ for dir_path in community_plugin_dirs:
540
+ os.makedirs(dir_path, exist_ok=True)
541
+
542
+ logger.debug(
543
+ f"Loading active community plugins: {', '.join(active_community_plugins)}"
544
+ )
545
+
546
+ # Only process community plugins if config section exists and is a dictionary
547
+ if isinstance(community_plugins_config, dict):
548
+ for plugin_name, plugin_info in community_plugins_config.items():
549
+ if not plugin_info.get("active", False):
550
+ logger.debug(
551
+ f"Skipping community plugin {plugin_name} - not active in config"
552
+ )
553
+ continue
554
+
555
+ repo_url = plugin_info.get("repository")
556
+ tag = plugin_info.get("tag", "master")
557
+ if repo_url:
558
+ # Clone to the user directory by default
559
+ success = clone_or_update_repo(repo_url, tag, community_plugins_dir)
560
+ if not success:
561
+ logger.warning(
562
+ f"Failed to clone/update plugin {plugin_name}, skipping"
563
+ )
564
+ continue
565
+ else:
566
+ logger.error("Repository URL not specified for a community plugin")
567
+ logger.error("Please specify the repository URL in config.yaml")
568
+ continue
569
+
570
+ # Only load community plugins that are explicitly enabled
571
+ for plugin_name in active_community_plugins:
572
+ plugin_info = community_plugins_config[plugin_name]
573
+ repo_url = plugin_info.get("repository")
574
+ if repo_url:
575
+ # Extract repository name from URL
576
+ repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
577
+
578
+ # Try each directory in order
579
+ plugin_found = False
580
+ for dir_path in community_plugin_dirs:
581
+ plugin_path = os.path.join(dir_path, repo_name)
582
+ if os.path.exists(plugin_path):
583
+ logger.debug(f"Loading community plugin from: {plugin_path}")
584
+ plugins.extend(
585
+ load_plugins_from_directory(plugin_path, recursive=True)
586
+ )
587
+ plugin_found = True
588
+ break
589
+
590
+ if not plugin_found:
591
+ logger.warning(
592
+ f"Community plugin '{repo_name}' not found in any of the plugin directories"
593
+ )
594
+ else:
595
+ logger.error(
596
+ f"Repository URL not specified for community plugin: {plugin_name}"
597
+ )
598
+
599
+ # Filter and sort active plugins by priority
600
+ active_plugins = []
601
+ for plugin in plugins:
602
+ plugin_name = getattr(plugin, "plugin_name", plugin.__class__.__name__)
603
+
604
+ # Determine if the plugin is active based on the configuration
605
+ if plugin in core_plugins:
606
+ # Core plugins: default to inactive unless specified otherwise
607
+ plugin_config = config.get("plugins", {}).get(plugin_name, {})
608
+ is_active = plugin_config.get("active", False)
609
+ else:
610
+ # Custom and community plugins: default to inactive unless specified
611
+ if plugin_name in config.get("custom-plugins", {}):
612
+ plugin_config = config.get("custom-plugins", {}).get(plugin_name, {})
613
+ elif plugin_name in community_plugins_config:
614
+ plugin_config = community_plugins_config.get(plugin_name, {})
615
+ else:
616
+ plugin_config = {}
617
+
618
+ is_active = plugin_config.get("active", False)
619
+
620
+ if is_active:
621
+ plugin.priority = plugin_config.get(
622
+ "priority", getattr(plugin, "priority", 100)
623
+ )
624
+ active_plugins.append(plugin)
625
+ try:
626
+ plugin.start()
627
+ except Exception as e:
628
+ logger.error(f"Error starting plugin {plugin_name}: {e}")
629
+
630
+ sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority)
631
+
632
+ # Log all loaded plugins
633
+ if sorted_active_plugins:
634
+ plugin_names = [
635
+ getattr(plugin, "plugin_name", plugin.__class__.__name__)
636
+ for plugin in sorted_active_plugins
637
+ ]
638
+ logger.info(f"Plugins loaded: {', '.join(plugin_names)}")
639
+ else:
640
+ logger.info("Plugins loaded: none")
641
+
642
+ plugins_loaded = True # Set the flag to indicate that plugins have been load
@@ -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
3
+ Version: 1.0.4
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,336 +0,0 @@
1
- # trunk-ignore-all(bandit)
2
- import hashlib
3
- import importlib.util
4
- import os
5
- import subprocess
6
- import sys
7
-
8
- from mmrelay.config import get_app_path, get_base_dir
9
- from mmrelay.log_utils import get_logger
10
-
11
- # Global config variable that will be set from main.py
12
- config = None
13
-
14
- logger = get_logger(name="Plugins")
15
- sorted_active_plugins = []
16
- plugins_loaded = False
17
-
18
-
19
- def get_custom_plugin_dirs():
20
- """
21
- Returns a list of directories to check for custom plugins in order of priority:
22
- 1. User directory (~/.mmrelay/plugins/custom)
23
- 2. Local directory (plugins/custom) for backward compatibility
24
- """
25
- dirs = []
26
-
27
- # Check user directory first (preferred location)
28
- user_dir = os.path.join(get_base_dir(), "plugins", "custom")
29
- os.makedirs(user_dir, exist_ok=True)
30
- dirs.append(user_dir)
31
-
32
- # Check local directory (backward compatibility)
33
- local_dir = os.path.join(get_app_path(), "plugins", "custom")
34
- dirs.append(local_dir)
35
-
36
- return dirs
37
-
38
-
39
- def get_community_plugin_dirs():
40
- """
41
- Returns a list of directories to check for community plugins in order of priority:
42
- 1. User directory (~/.mmrelay/plugins/community)
43
- 2. Local directory (plugins/community) for backward compatibility
44
- """
45
- dirs = []
46
-
47
- # Check user directory first (preferred location)
48
- user_dir = os.path.join(get_base_dir(), "plugins", "community")
49
- os.makedirs(user_dir, exist_ok=True)
50
- dirs.append(user_dir)
51
-
52
- # Check local directory (backward compatibility)
53
- local_dir = os.path.join(get_app_path(), "plugins", "community")
54
- dirs.append(local_dir)
55
-
56
- return dirs
57
-
58
-
59
- def clone_or_update_repo(repo_url, tag, plugins_dir):
60
- # Extract the repository name from the URL
61
- repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
62
- repo_path = os.path.join(plugins_dir, repo_name)
63
- if os.path.isdir(repo_path):
64
- try:
65
- subprocess.check_call(["git", "-C", repo_path, "fetch"])
66
- subprocess.check_call(["git", "-C", repo_path, "checkout", tag])
67
- subprocess.check_call(["git", "-C", repo_path, "pull", "origin", tag])
68
- logger.info(f"Updated repository {repo_name} to {tag}")
69
- except subprocess.CalledProcessError as e:
70
- logger.error(f"Error updating repository {repo_name}: {e}")
71
- logger.error(
72
- f"Please manually git clone the repository {repo_url} into {repo_path}"
73
- )
74
- sys.exit(1)
75
- else:
76
- try:
77
- os.makedirs(plugins_dir, exist_ok=True)
78
- subprocess.check_call(
79
- ["git", "clone", "--branch", tag, repo_url], cwd=plugins_dir
80
- )
81
- logger.info(f"Cloned repository {repo_name} from {repo_url} at {tag}")
82
- except subprocess.CalledProcessError as e:
83
- logger.error(f"Error cloning repository {repo_name}: {e}")
84
- logger.error(
85
- f"Please manually git clone the repository {repo_url} into {repo_path}"
86
- )
87
- sys.exit(1)
88
- # Install requirements if requirements.txt exists
89
- requirements_path = os.path.join(repo_path, "requirements.txt")
90
- if os.path.isfile(requirements_path):
91
- try:
92
- # Use pip to install the requirements.txt
93
- subprocess.check_call(
94
- [sys.executable, "-m", "pip", "install", "-r", requirements_path]
95
- )
96
- logger.info(f"Installed requirements for plugin {repo_name}")
97
- except subprocess.CalledProcessError as e:
98
- logger.error(f"Error installing requirements for plugin {repo_name}: {e}")
99
- logger.error(
100
- f"Please manually install the requirements from {requirements_path}"
101
- )
102
- sys.exit(1)
103
-
104
-
105
- def load_plugins_from_directory(directory, recursive=False):
106
- plugins = []
107
- if os.path.isdir(directory):
108
- for root, _dirs, files in os.walk(directory):
109
- for filename in files:
110
- if filename.endswith(".py"):
111
- plugin_path = os.path.join(root, filename)
112
- module_name = (
113
- "plugin_"
114
- + hashlib.sha256(plugin_path.encode("utf-8")).hexdigest()
115
- )
116
- spec = importlib.util.spec_from_file_location(
117
- module_name, plugin_path
118
- )
119
- plugin_module = importlib.util.module_from_spec(spec)
120
- try:
121
- spec.loader.exec_module(plugin_module)
122
- if hasattr(plugin_module, "Plugin"):
123
- plugins.append(plugin_module.Plugin())
124
- else:
125
- logger.warning(
126
- f"{plugin_path} does not define a Plugin class."
127
- )
128
- except Exception as e:
129
- logger.error(f"Error loading plugin {plugin_path}: {e}")
130
- if not recursive:
131
- break
132
- else:
133
- if not plugins_loaded: # Only log the missing directory once
134
- logger.debug(f"Directory {directory} does not exist.")
135
- return plugins
136
-
137
-
138
- def load_plugins(passed_config=None):
139
- global sorted_active_plugins
140
- global plugins_loaded
141
- global config
142
-
143
- if plugins_loaded:
144
- return sorted_active_plugins
145
-
146
- logger.info("Checking plugin config...")
147
-
148
- # Update the global config if a config is passed
149
- if passed_config is not None:
150
- config = passed_config
151
-
152
- # Check if config is available
153
- if config is None:
154
- logger.error("No configuration available. Cannot load plugins.")
155
- return []
156
-
157
- # Import core plugins
158
- from mmrelay.plugins.debug_plugin import Plugin as DebugPlugin
159
- from mmrelay.plugins.drop_plugin import Plugin as DropPlugin
160
- from mmrelay.plugins.health_plugin import Plugin as HealthPlugin
161
- from mmrelay.plugins.help_plugin import Plugin as HelpPlugin
162
- from mmrelay.plugins.map_plugin import Plugin as MapPlugin
163
- from mmrelay.plugins.mesh_relay_plugin import Plugin as MeshRelayPlugin
164
- from mmrelay.plugins.nodes_plugin import Plugin as NodesPlugin
165
- from mmrelay.plugins.ping_plugin import Plugin as PingPlugin
166
- from mmrelay.plugins.telemetry_plugin import Plugin as TelemetryPlugin
167
- from mmrelay.plugins.weather_plugin import Plugin as WeatherPlugin
168
-
169
- # Initial list of core plugins
170
- core_plugins = [
171
- HealthPlugin(),
172
- MapPlugin(),
173
- MeshRelayPlugin(),
174
- PingPlugin(),
175
- TelemetryPlugin(),
176
- WeatherPlugin(),
177
- HelpPlugin(),
178
- NodesPlugin(),
179
- DropPlugin(),
180
- DebugPlugin(),
181
- ]
182
-
183
- plugins = core_plugins.copy()
184
-
185
- # Process and load custom plugins
186
- custom_plugins_config = config.get("custom-plugins", {})
187
- custom_plugin_dirs = get_custom_plugin_dirs()
188
-
189
- active_custom_plugins = [
190
- plugin_name
191
- for plugin_name, plugin_info in custom_plugins_config.items()
192
- if plugin_info.get("active", False)
193
- ]
194
-
195
- if active_custom_plugins:
196
- logger.debug(
197
- f"Loading active custom plugins: {', '.join(active_custom_plugins)}"
198
- )
199
-
200
- # Only load custom plugins that are explicitly enabled
201
- for plugin_name in active_custom_plugins:
202
- plugin_found = False
203
-
204
- # Try each directory in order
205
- for custom_dir in custom_plugin_dirs:
206
- plugin_path = os.path.join(custom_dir, plugin_name)
207
- if os.path.exists(plugin_path):
208
- logger.debug(f"Loading custom plugin from: {plugin_path}")
209
- plugins.extend(
210
- load_plugins_from_directory(plugin_path, recursive=False)
211
- )
212
- plugin_found = True
213
- break
214
-
215
- if not plugin_found:
216
- logger.warning(
217
- f"Custom plugin '{plugin_name}' not found in any of the plugin directories"
218
- )
219
-
220
- # Process and download community plugins
221
- community_plugins_config = config.get("community-plugins", {})
222
- community_plugin_dirs = get_community_plugin_dirs()
223
-
224
- # Get the first directory for cloning (prefer user directory)
225
- community_plugins_dir = community_plugin_dirs[
226
- -1
227
- ] # Use the user directory for new clones
228
-
229
- # Create community plugins directory if needed
230
- active_community_plugins = [
231
- plugin_name
232
- for plugin_name, plugin_info in community_plugins_config.items()
233
- if plugin_info.get("active", False)
234
- ]
235
-
236
- if active_community_plugins:
237
- # Ensure all community plugin directories exist
238
- for dir_path in community_plugin_dirs:
239
- os.makedirs(dir_path, exist_ok=True)
240
-
241
- logger.debug(
242
- f"Loading active community plugins: {', '.join(active_community_plugins)}"
243
- )
244
-
245
- # Only process community plugins if config section exists and is a dictionary
246
- if isinstance(community_plugins_config, dict):
247
- for plugin_name, plugin_info in community_plugins_config.items():
248
- if not plugin_info.get("active", False):
249
- logger.debug(
250
- f"Skipping community plugin {plugin_name} - not active in config"
251
- )
252
- continue
253
-
254
- repo_url = plugin_info.get("repository")
255
- tag = plugin_info.get("tag", "master")
256
- if repo_url:
257
- # Clone to the user directory by default
258
- clone_or_update_repo(repo_url, tag, community_plugins_dir)
259
- else:
260
- logger.error("Repository URL not specified for a community plugin")
261
- logger.error("Please specify the repository URL in config.yaml")
262
- sys.exit(1)
263
-
264
- # Only load community plugins that are explicitly enabled
265
- for plugin_name in active_community_plugins:
266
- plugin_info = community_plugins_config[plugin_name]
267
- repo_url = plugin_info.get("repository")
268
- if repo_url:
269
- # Extract repository name from URL
270
- repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
271
-
272
- # Try each directory in order
273
- plugin_found = False
274
- for dir_path in community_plugin_dirs:
275
- plugin_path = os.path.join(dir_path, repo_name)
276
- if os.path.exists(plugin_path):
277
- logger.debug(f"Loading community plugin from: {plugin_path}")
278
- plugins.extend(
279
- load_plugins_from_directory(plugin_path, recursive=True)
280
- )
281
- plugin_found = True
282
- break
283
-
284
- if not plugin_found:
285
- logger.warning(
286
- f"Community plugin '{repo_name}' not found in any of the plugin directories"
287
- )
288
- else:
289
- logger.error(
290
- f"Repository URL not specified for community plugin: {plugin_name}"
291
- )
292
-
293
- # Filter and sort active plugins by priority
294
- active_plugins = []
295
- for plugin in plugins:
296
- plugin_name = getattr(plugin, "plugin_name", plugin.__class__.__name__)
297
-
298
- # Determine if the plugin is active based on the configuration
299
- if plugin in core_plugins:
300
- # Core plugins: default to inactive unless specified otherwise
301
- plugin_config = config.get("plugins", {}).get(plugin_name, {})
302
- is_active = plugin_config.get("active", False)
303
- else:
304
- # Custom and community plugins: default to inactive unless specified
305
- if plugin_name in config.get("custom-plugins", {}):
306
- plugin_config = config.get("custom-plugins", {}).get(plugin_name, {})
307
- elif plugin_name in community_plugins_config:
308
- plugin_config = community_plugins_config.get(plugin_name, {})
309
- else:
310
- plugin_config = {}
311
-
312
- is_active = plugin_config.get("active", False)
313
-
314
- if is_active:
315
- plugin.priority = plugin_config.get(
316
- "priority", getattr(plugin, "priority", 100)
317
- )
318
- active_plugins.append(plugin)
319
- try:
320
- plugin.start()
321
- except Exception as e:
322
- logger.error(f"Error starting plugin {plugin_name}: {e}")
323
-
324
- sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority)
325
-
326
- # Log all loaded plugins
327
- if sorted_active_plugins:
328
- plugin_names = [
329
- getattr(plugin, "plugin_name", plugin.__class__.__name__)
330
- for plugin in sorted_active_plugins
331
- ]
332
- logger.info(f"Plugins loaded: {', '.join(plugin_names)}")
333
- else:
334
- logger.info("Plugins loaded: none")
335
-
336
- plugins_loaded = True # Set the flag to indicate that plugins have been load
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