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.
- {mmrelay-1.0.3/src/mmrelay.egg-info → mmrelay-1.0.4}/PKG-INFO +1 -1
- {mmrelay-1.0.3 → mmrelay-1.0.4}/setup.cfg +1 -1
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/config.py +26 -0
- mmrelay-1.0.4/src/mmrelay/plugin_loader.py +642 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/base_plugin.py +29 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4/src/mmrelay.egg-info}/PKG-INFO +1 -1
- mmrelay-1.0.3/src/mmrelay/plugin_loader.py +0 -336
- {mmrelay-1.0.3 → mmrelay-1.0.4}/LICENSE +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/MANIFEST.in +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/README.md +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/pyproject.toml +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/requirements.txt +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/sample_config.yaml +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/__init__.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/cli.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/config_checker.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/db_utils.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/log_utils.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/main.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/matrix_utils.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/meshtastic_utils.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/__init__.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/debug_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/drop_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/health_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/help_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/map_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/mesh_relay_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/nodes_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/ping_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/telemetry_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/plugins/weather_plugin.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay/setup_utils.py +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/SOURCES.txt +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/dependency_links.txt +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/entry_points.txt +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/requires.txt +0 -0
- {mmrelay-1.0.3 → mmrelay-1.0.4}/src/mmrelay.egg-info/top_level.txt +0 -0
- {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
|
+
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
|
|
@@ -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
|
+
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|