hungerlib 4.23.dev8__tar.gz → 4.24__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.
- {hungerlib-4.23.dev8/src/hungerlib.egg-info → hungerlib-4.24}/PKG-INFO +9 -6
- {hungerlib-4.23.dev8 → hungerlib-4.24}/pyproject.toml +17 -6
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/__init__.py +29 -0
- hungerlib-4.24/src/hungerlib/email/__init__.py +9 -0
- hungerlib-4.24/src/hungerlib/email/client.py +35 -0
- hungerlib-4.24/src/hungerlib/email/email_templates/restart.html +87 -0
- hungerlib-4.24/src/hungerlib/email/manager.py +55 -0
- hungerlib-4.24/src/hungerlib/email/message.py +33 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/messagerouter.py +79 -59
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/generic.py +3 -2
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/minecraft.py +0 -4
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/__init__.py +17 -1
- hungerlib-4.24/src/hungerlib/utils/buffer.py +20 -0
- hungerlib-4.24/src/hungerlib/utils/convert.py +61 -0
- hungerlib-4.24/src/hungerlib/webhook/__init__.py +4 -0
- hungerlib-4.24/src/hungerlib/webhook/client.py +20 -0
- hungerlib-4.24/src/hungerlib/webhook/server.py +56 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24/src/hungerlib.egg-info}/PKG-INFO +9 -6
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib.egg-info/SOURCES.txt +11 -1
- hungerlib-4.24/src/hungerlib.egg-info/requires.txt +7 -0
- hungerlib-4.23.dev8/src/hungerlib.egg-info/requires.txt +0 -4
- {hungerlib-4.23.dev8 → hungerlib-4.24}/LICENSE +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/README.md +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/setup.cfg +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/__init__.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/backups.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/command.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/databases.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/filemanager.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/schedule.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/startup.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/bridgeclient.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/configloader.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/panel.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/__init__.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/exceptions.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/time.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/utils.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/validator.py +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib.egg-info/dependency_links.txt +0 -0
- {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hungerlib
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.24
|
|
4
4
|
Summary: Powerful automation library for Pterodactyl.
|
|
5
5
|
Author: iFamished
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Documentation, https://github.com/
|
|
9
|
-
Project-URL: Source, https://github.com/
|
|
10
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/iFamished/HungerLib
|
|
8
|
+
Project-URL: Documentation, https://github.com/iFamished/HungerLib
|
|
9
|
+
Project-URL: Source, https://github.com/iFamished/HungerLib
|
|
10
|
+
Project-URL: Issues, https://github.com/iFamished/HungerLib/issues
|
|
11
11
|
Keywords: pterodactyl,minecraft,automation,rcon,server-management,hungerlib,hungernet
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -23,9 +23,12 @@ Requires-Python: >=3.9
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: requests
|
|
26
|
-
Requires-Dist: mcrcon
|
|
27
26
|
Requires-Dist: PyYaml
|
|
28
27
|
Requires-Dist: mapres
|
|
28
|
+
Requires-Dist: jinja2
|
|
29
|
+
Requires-Dist: cloudflare
|
|
30
|
+
Requires-Dist: uvicorn
|
|
31
|
+
Requires-Dist: fastapi
|
|
29
32
|
Dynamic: license-file
|
|
30
33
|
|
|
31
34
|
# HungerLib
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "hungerlib"
|
|
10
|
-
version = "4.
|
|
10
|
+
version = "4.24"
|
|
11
11
|
description = "Powerful automation library for Pterodactyl."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
@@ -25,9 +25,12 @@ keywords = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"requests",
|
|
28
|
-
"mcrcon",
|
|
29
28
|
"PyYaml",
|
|
30
29
|
"mapres",
|
|
30
|
+
"jinja2",
|
|
31
|
+
"cloudflare",
|
|
32
|
+
"uvicorn",
|
|
33
|
+
"fastapi",
|
|
31
34
|
]
|
|
32
35
|
classifiers = [
|
|
33
36
|
"Programming Language :: Python :: 3",
|
|
@@ -46,12 +49,20 @@ classifiers = [
|
|
|
46
49
|
text = "MIT"
|
|
47
50
|
|
|
48
51
|
[project.urls]
|
|
49
|
-
Homepage = "https://github.com/
|
|
50
|
-
Documentation = "https://github.com/
|
|
51
|
-
Source = "https://github.com/
|
|
52
|
-
Issues = "https://github.com/
|
|
52
|
+
Homepage = "https://github.com/iFamished/HungerLib"
|
|
53
|
+
Documentation = "https://github.com/iFamished/HungerLib"
|
|
54
|
+
Source = "https://github.com/iFamished/HungerLib"
|
|
55
|
+
Issues = "https://github.com/iFamished/HungerLib/issues"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools]
|
|
58
|
+
include-package-data = true
|
|
53
59
|
|
|
54
60
|
[tool.setuptools.packages.find]
|
|
55
61
|
where = [
|
|
56
62
|
"src",
|
|
57
63
|
]
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.package-data]
|
|
66
|
+
"hungerlib.email" = [
|
|
67
|
+
"email_templates/*.html",
|
|
68
|
+
]
|
|
@@ -13,6 +13,8 @@ from .messagerouter import MessageRouter
|
|
|
13
13
|
from .panel import Panel
|
|
14
14
|
from .servers import GenericServer, MinecraftServer
|
|
15
15
|
from .bridgeclient import BridgeClient
|
|
16
|
+
from .email import Email, EmailManager, EmailClient
|
|
17
|
+
from .webhook import WebhookClient, WebhookServer
|
|
16
18
|
from .validator import (
|
|
17
19
|
Validator,
|
|
18
20
|
ValidationError,
|
|
@@ -32,6 +34,14 @@ from .utils import (
|
|
|
32
34
|
Snapshot,
|
|
33
35
|
clearTerminal,
|
|
34
36
|
validateAll,
|
|
37
|
+
Buffer,
|
|
38
|
+
convert,
|
|
39
|
+
|
|
40
|
+
# exceptions
|
|
41
|
+
HungerLibError,
|
|
42
|
+
InvalidLevelError,
|
|
43
|
+
InvalidModeError,
|
|
44
|
+
HungerBridgeError
|
|
35
45
|
)
|
|
36
46
|
|
|
37
47
|
# namespaces
|
|
@@ -45,6 +55,8 @@ utils = SimpleNamespace(
|
|
|
45
55
|
Snapshot = Snapshot,
|
|
46
56
|
clearTerminal = clearTerminal,
|
|
47
57
|
validateAll = validateAll,
|
|
58
|
+
Buffer = Buffer,
|
|
59
|
+
convert = convert,
|
|
48
60
|
)
|
|
49
61
|
|
|
50
62
|
servers = SimpleNamespace(
|
|
@@ -53,6 +65,10 @@ servers = SimpleNamespace(
|
|
|
53
65
|
Minecraft = MinecraftServer,
|
|
54
66
|
)
|
|
55
67
|
|
|
68
|
+
webhook = SimpleNamespace(
|
|
69
|
+
Client = WebhookClient,
|
|
70
|
+
Server = WebhookServer,
|
|
71
|
+
)
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
__all__ = [
|
|
@@ -66,6 +82,9 @@ __all__ = [
|
|
|
66
82
|
'MinecraftServer',
|
|
67
83
|
'BridgeClient',
|
|
68
84
|
'Validator',
|
|
85
|
+
'Email',
|
|
86
|
+
'EmailManager',
|
|
87
|
+
'EmailClient',
|
|
69
88
|
|
|
70
89
|
# sub-modules
|
|
71
90
|
'snapSchedule',
|
|
@@ -77,6 +96,10 @@ __all__ = [
|
|
|
77
96
|
'Snapshot',
|
|
78
97
|
'clearTerminal',
|
|
79
98
|
'validateAll',
|
|
99
|
+
'WebhookServer',
|
|
100
|
+
'WebhookClient',
|
|
101
|
+
'Buffer',
|
|
102
|
+
'convert',
|
|
80
103
|
|
|
81
104
|
# errors
|
|
82
105
|
'ValidationError',
|
|
@@ -85,7 +108,13 @@ __all__ = [
|
|
|
85
108
|
'FallbackError',
|
|
86
109
|
'RecommendedError',
|
|
87
110
|
|
|
111
|
+
'HungerLibError',
|
|
112
|
+
'InvalidLevelError',
|
|
113
|
+
'InvalidModeError',
|
|
114
|
+
'HungerBridgeError',
|
|
115
|
+
|
|
88
116
|
# namespaces
|
|
89
117
|
'utils',
|
|
90
118
|
'servers',
|
|
119
|
+
'webhook',
|
|
91
120
|
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from .message import Email
|
|
3
|
+
|
|
4
|
+
class EmailClient:
|
|
5
|
+
'''
|
|
6
|
+
Represents a sender identity.
|
|
7
|
+
The frontend API for sending emails.
|
|
8
|
+
'''
|
|
9
|
+
def __init__(self, name: str, address: str, manager):
|
|
10
|
+
self.name = name
|
|
11
|
+
self.address = address
|
|
12
|
+
self.manager = manager # backend EmailManager
|
|
13
|
+
|
|
14
|
+
def send(
|
|
15
|
+
self,
|
|
16
|
+
subject: str,
|
|
17
|
+
text: str | None = None,
|
|
18
|
+
html: str | None = None,
|
|
19
|
+
to: list | str | None = None,
|
|
20
|
+
cc: list | str | None = None,
|
|
21
|
+
bcc: list | str | None = None,
|
|
22
|
+
template: str | None = None,
|
|
23
|
+
template_ctx: dict | None = None
|
|
24
|
+
):
|
|
25
|
+
email = Email(
|
|
26
|
+
subject=subject,
|
|
27
|
+
text=text,
|
|
28
|
+
html=html,
|
|
29
|
+
to=to,
|
|
30
|
+
cc=cc,
|
|
31
|
+
bcc=bcc,
|
|
32
|
+
template=template,
|
|
33
|
+
template_ctx=template_ctx
|
|
34
|
+
)
|
|
35
|
+
return self.manager.send_email(self, email)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>{{ title }}</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
background: #eef1f5;
|
|
11
|
+
font-family: Arial, sans-serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.wrapper {
|
|
15
|
+
width: 100%;
|
|
16
|
+
text-align: center;
|
|
17
|
+
padding: 40px 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.box {
|
|
21
|
+
background: #ffffff;
|
|
22
|
+
width: 90%;
|
|
23
|
+
max-width: 600px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
padding: 30px;
|
|
26
|
+
border-radius: 14px;
|
|
27
|
+
border: 1px solid #d9dce3;
|
|
28
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1 {
|
|
33
|
+
color: #4a6cf7;
|
|
34
|
+
margin-bottom: 20px;
|
|
35
|
+
font-size: 28px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
p {
|
|
39
|
+
color: #333;
|
|
40
|
+
font-size: 16px;
|
|
41
|
+
line-height: 1.6;
|
|
42
|
+
margin-bottom: 20px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.button {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
padding: 12px 24px;
|
|
48
|
+
background: #4a6cf7;
|
|
49
|
+
color: #ffffff !important;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
font-size: 16px;
|
|
53
|
+
margin-top: 20px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.footer {
|
|
57
|
+
margin-top: 30px;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
color: #777;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="wrapper">
|
|
65
|
+
<div class="box">
|
|
66
|
+
<h1>{{ heading }}</h1>
|
|
67
|
+
|
|
68
|
+
<p>
|
|
69
|
+
{{ message }}
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<p>
|
|
73
|
+
• Server status updates<br>
|
|
74
|
+
• Crash reports<br>
|
|
75
|
+
• Notifications<br>
|
|
76
|
+
• Alerts<br>
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
<a class="button" href="{{ button_url }}">{{ button_text }}</a>
|
|
80
|
+
|
|
81
|
+
<p class="footer">
|
|
82
|
+
{{ footer }}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from jinja2 import Environment, FileSystemLoader
|
|
2
|
+
import pathlib
|
|
3
|
+
from cloudflare import Cloudflare
|
|
4
|
+
from .message import Email
|
|
5
|
+
|
|
6
|
+
TEMPLATE_DIR = pathlib.Path(__file__).resolve().parent / "email_templates"
|
|
7
|
+
|
|
8
|
+
class EmailManager:
|
|
9
|
+
'''
|
|
10
|
+
Backend email system.
|
|
11
|
+
Holds API token and account ID.
|
|
12
|
+
Sends emails created by EmailClient.
|
|
13
|
+
'''
|
|
14
|
+
def __init__(self, api_token: str, account_id: str):
|
|
15
|
+
self.client = Cloudflare(api_token=api_token)
|
|
16
|
+
self.account_id = account_id
|
|
17
|
+
|
|
18
|
+
# Jinja2 configured to NOT alter whitespace or escape HTML
|
|
19
|
+
self.jinja = Environment(
|
|
20
|
+
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
|
21
|
+
autoescape=False,
|
|
22
|
+
trim_blocks=False,
|
|
23
|
+
lstrip_blocks=False
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def render_template(self, template: str, ctx: dict) -> str:
|
|
27
|
+
tmpl = self.jinja.get_template(f"{template}.html")
|
|
28
|
+
return tmpl.render(**ctx)
|
|
29
|
+
|
|
30
|
+
def send_email(self, user, email: Email) -> bool:
|
|
31
|
+
# Render template if needed
|
|
32
|
+
if email.template and not email.html:
|
|
33
|
+
email.html = self.render_template(email.template, email.template_ctx)
|
|
34
|
+
|
|
35
|
+
payload = {
|
|
36
|
+
"account_id": self.account_id,
|
|
37
|
+
"from_": f"{user.name} <{user.address}>",
|
|
38
|
+
"to": email.to,
|
|
39
|
+
"subject": email.subject
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if email.text:
|
|
43
|
+
payload["text"] = email.text
|
|
44
|
+
|
|
45
|
+
if email.html:
|
|
46
|
+
payload["html"] = email.html
|
|
47
|
+
|
|
48
|
+
if email.cc:
|
|
49
|
+
payload["cc"] = email.cc
|
|
50
|
+
|
|
51
|
+
if email.bcc:
|
|
52
|
+
payload["bcc"] = email.bcc
|
|
53
|
+
|
|
54
|
+
resp = self.client.email_sending.send(**payload)
|
|
55
|
+
return bool(getattr(resp, "delivered", None))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
class Email:
|
|
4
|
+
'''
|
|
5
|
+
Simple email data container.
|
|
6
|
+
'''
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
subject: str,
|
|
10
|
+
text: str | None = None,
|
|
11
|
+
html: str | None = None,
|
|
12
|
+
to: list | str | None = None,
|
|
13
|
+
cc: list | str | None = None,
|
|
14
|
+
bcc: list | str | None = None,
|
|
15
|
+
template: str | None = None,
|
|
16
|
+
template_ctx: dict | None = None
|
|
17
|
+
):
|
|
18
|
+
self.subject = subject
|
|
19
|
+
self.text = text or ''
|
|
20
|
+
self.html = html or ''
|
|
21
|
+
self.to = self._normalize(to)
|
|
22
|
+
self.cc = self._normalize(cc)
|
|
23
|
+
self.bcc = self._normalize(bcc)
|
|
24
|
+
self.template = template
|
|
25
|
+
self.template_ctx = template_ctx or {}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _normalize(value) -> list[str]:
|
|
29
|
+
if value is None:
|
|
30
|
+
return []
|
|
31
|
+
if isinstance(value, str):
|
|
32
|
+
return [s.strip() for s in value.split(',') if s.strip()]
|
|
33
|
+
return [str(x).strip() for x in value]
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from .utils.exceptions import InvalidLevelError
|
|
6
|
+
from .utils.buffer import Buffer
|
|
6
7
|
|
|
7
8
|
class MessageRouter:
|
|
8
9
|
def __init__(
|
|
@@ -20,6 +21,9 @@ class MessageRouter:
|
|
|
20
21
|
info_prefix = '<white>[%hh%:%mm%:%ss%] [INFO]: ',
|
|
21
22
|
warn_prefix = '<yellow>[%hh%:%mm%:%ss%] [WARN]: ',
|
|
22
23
|
error_prefix = '<red>[%hh%:%mm%:%ss%] [ERROR]: ',
|
|
24
|
+
|
|
25
|
+
buffer_enabled = False,
|
|
26
|
+
origin_output = True
|
|
23
27
|
):
|
|
24
28
|
self.name = name
|
|
25
29
|
self.Servers = Servers
|
|
@@ -51,6 +55,22 @@ class MessageRouter:
|
|
|
51
55
|
# custom level registry
|
|
52
56
|
self.custom_levels = {}
|
|
53
57
|
|
|
58
|
+
# routing table
|
|
59
|
+
self.ROUTES = {
|
|
60
|
+
'origin': self.origin,
|
|
61
|
+
'destination': self.destination,
|
|
62
|
+
'broadcast': self.broadcast,
|
|
63
|
+
'filelog': self.filelog
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# buffering
|
|
67
|
+
self.buffer = Buffer(enabled=buffer_enabled)
|
|
68
|
+
self.origin_output = origin_output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def enableOriginOutput(self): self.origin_output = True
|
|
72
|
+
def disableOriginOutput(self): self.origin_output = False
|
|
73
|
+
|
|
54
74
|
def registerLevel(self, name, prefix, file_method=None, routes=None):
|
|
55
75
|
lvl = name.lower()
|
|
56
76
|
self.custom_levels[lvl] = {
|
|
@@ -59,21 +79,9 @@ class MessageRouter:
|
|
|
59
79
|
'routes': routes or ['origin', 'filelog']
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rts = cfg['routes']
|
|
66
|
-
if 'origin' in rts:
|
|
67
|
-
self.origin(text, level=level_name, **ctx)
|
|
68
|
-
if 'destination' in rts:
|
|
69
|
-
self.destination(text, level=level_name, **ctx)
|
|
70
|
-
if 'broadcast' in rts:
|
|
71
|
-
self.broadcast(text, level=level_name, **ctx)
|
|
72
|
-
if 'filelog' in rts:
|
|
73
|
-
self.filelog(text, level=level_name, **ctx)
|
|
74
|
-
return _method
|
|
75
|
-
|
|
76
|
-
setattr(self, lvl, make_method(lvl))
|
|
82
|
+
def _method(text, **ctx):
|
|
83
|
+
self._route(lvl, text, None, None, ctx)
|
|
84
|
+
setattr(self, lvl, _method)
|
|
77
85
|
|
|
78
86
|
def _init_file_logger(self):
|
|
79
87
|
log_file = self.log_path / f'{self.name}_{datetime.now().strftime("%Y-%m-%d")}.log'
|
|
@@ -95,37 +103,68 @@ class MessageRouter:
|
|
|
95
103
|
maps.extend(extra)
|
|
96
104
|
return maps
|
|
97
105
|
|
|
106
|
+
# prefix resolver
|
|
107
|
+
def _resolve_prefix(self, level, ctx):
|
|
108
|
+
lvl = level.lower() if isinstance(level, str) else None
|
|
109
|
+
|
|
110
|
+
if lvl == 'info':
|
|
111
|
+
return self.res(self.info_prefix, override_maps=self.prefix_maps)
|
|
112
|
+
if lvl == 'warn':
|
|
113
|
+
return self.res(self.warn_prefix, override_maps=self.prefix_maps)
|
|
114
|
+
if lvl == 'error':
|
|
115
|
+
return self.res(self.error_prefix, override_maps=self.prefix_maps)
|
|
116
|
+
if lvl in self.custom_levels:
|
|
117
|
+
return self.res(self.custom_levels[lvl]['prefix'], override_maps=self.prefix_maps)
|
|
118
|
+
if level is None:
|
|
119
|
+
return ''
|
|
120
|
+
if lvl == 'custom':
|
|
121
|
+
return ctx.get('prefix', '')
|
|
122
|
+
|
|
123
|
+
return self.res(level, override_maps=self.prefix_maps)
|
|
124
|
+
|
|
125
|
+
# routing engine
|
|
126
|
+
def _route(self, level, text, extra_maps, override_maps, ctx):
|
|
127
|
+
cfg = self.custom_levels.get(level.lower())
|
|
128
|
+
routes = cfg['routes'] if cfg else ['origin', 'filelog']
|
|
129
|
+
|
|
130
|
+
for r in routes:
|
|
131
|
+
self.ROUTES[r](text, level=level, extra_maps=extra_maps, override_maps=override_maps, **ctx)
|
|
132
|
+
|
|
133
|
+
# file logging
|
|
134
|
+
def _log_file(self, level, msg):
|
|
135
|
+
lvl = level.lower()
|
|
136
|
+
if lvl in self.custom_levels:
|
|
137
|
+
method = self.custom_levels[lvl]['file_method']
|
|
138
|
+
if method:
|
|
139
|
+
getattr(self.logger, method)(msg)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
'info': self.logger.info,
|
|
144
|
+
'warn': self.logger.warning,
|
|
145
|
+
'error': self.logger.error
|
|
146
|
+
}[lvl](msg)
|
|
147
|
+
|
|
98
148
|
# routing primitives
|
|
99
149
|
def origin(self, text, level='info', extra_maps=None, override_maps=None, **ctx):
|
|
100
|
-
level_norm = level.lower() if isinstance(level, str) else None
|
|
101
|
-
|
|
102
150
|
maps = self._merge_maps(override_maps or self.origin_maps, extra_maps)
|
|
103
151
|
mapped = self._format(text, maps, **ctx)
|
|
104
|
-
|
|
105
|
-
# Prefix resolution
|
|
106
|
-
if level_norm == 'info':
|
|
107
|
-
prefix = self.res(self.info_prefix, override_maps=self.prefix_maps)
|
|
108
|
-
elif level_norm == 'warn':
|
|
109
|
-
prefix = self.res(self.warn_prefix, override_maps=self.prefix_maps)
|
|
110
|
-
elif level_norm == 'error':
|
|
111
|
-
prefix = self.res(self.error_prefix, override_maps=self.prefix_maps)
|
|
112
|
-
elif level_norm in self.custom_levels:
|
|
113
|
-
prefix = self.res(self.custom_levels[level_norm]['prefix'], override_maps=self.prefix_maps)
|
|
114
|
-
elif level is None:
|
|
115
|
-
prefix = ''
|
|
116
|
-
elif level_norm == 'custom':
|
|
117
|
-
prefix = ctx.get('prefix', '')
|
|
118
|
-
else:
|
|
119
|
-
prefix = self.res(level, override_maps=self.prefix_maps)
|
|
120
|
-
|
|
152
|
+
prefix = self._resolve_prefix(level, ctx)
|
|
121
153
|
msg = prefix + mapped
|
|
122
|
-
|
|
154
|
+
|
|
155
|
+
# buffer logic
|
|
156
|
+
if self.buffer.enabled:
|
|
157
|
+
self.buffer.captured.append(msg)
|
|
158
|
+
|
|
159
|
+
# origin output check
|
|
160
|
+
if self.origin_output:
|
|
161
|
+
print(msg)
|
|
162
|
+
|
|
123
163
|
return msg
|
|
124
164
|
|
|
125
165
|
def destination(self, text, level='info', extra_maps=None, override_maps=None, **ctx):
|
|
126
166
|
maps = self._merge_maps(override_maps or self.destination_maps, extra_maps)
|
|
127
167
|
msg = self._format(text, maps, **ctx)
|
|
128
|
-
|
|
129
168
|
for server in self.Servers:
|
|
130
169
|
if hasattr(server, 'bridge'):
|
|
131
170
|
server.bridge.log(msg, level)
|
|
@@ -134,7 +173,6 @@ class MessageRouter:
|
|
|
134
173
|
def broadcast(self, text, extra_maps=None, override_maps=None, **ctx):
|
|
135
174
|
maps = self._merge_maps(override_maps or self.broadcast_maps, extra_maps)
|
|
136
175
|
msg = self._format(text, maps, **ctx)
|
|
137
|
-
|
|
138
176
|
for server in self.Servers:
|
|
139
177
|
if hasattr(server, 'sendBroadcast'):
|
|
140
178
|
server.sendBroadcast(msg)
|
|
@@ -143,33 +181,15 @@ class MessageRouter:
|
|
|
143
181
|
def filelog(self, text, level='info', extra_maps=None, override_maps=None, **ctx):
|
|
144
182
|
maps = self._merge_maps(override_maps or self.file_maps, extra_maps)
|
|
145
183
|
msg = self._format(text, maps, **ctx)
|
|
146
|
-
|
|
147
|
-
# custom file logging
|
|
148
|
-
if level in self.custom_levels:
|
|
149
|
-
method = self.custom_levels[level]['file_method']
|
|
150
|
-
if method:
|
|
151
|
-
getattr(self.logger, method)(msg)
|
|
152
|
-
return msg
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
'info': self.logger.info,
|
|
156
|
-
'warn': self.logger.warning,
|
|
157
|
-
'error': self.logger.error
|
|
158
|
-
}[level](msg)
|
|
184
|
+
self._log_file(level, msg)
|
|
159
185
|
return msg
|
|
160
186
|
|
|
161
187
|
# passthrough helpers
|
|
162
188
|
def info(self, text, extra_maps=None, override_maps=None, **ctx):
|
|
163
|
-
|
|
164
|
-
self.filelog(text, 'info', extra_maps, override_maps, **ctx)
|
|
165
|
-
return msg
|
|
189
|
+
self._route('info', text, extra_maps, override_maps, ctx)
|
|
166
190
|
|
|
167
191
|
def warn(self, text, extra_maps=None, override_maps=None, **ctx):
|
|
168
|
-
|
|
169
|
-
self.filelog(text, 'warn', extra_maps, override_maps, **ctx)
|
|
170
|
-
return msg
|
|
192
|
+
self._route('warn', text, extra_maps, override_maps, ctx)
|
|
171
193
|
|
|
172
194
|
def error(self, text, extra_maps=None, override_maps=None, **ctx):
|
|
173
|
-
|
|
174
|
-
self.filelog(text, 'error', extra_maps, override_maps, **ctx)
|
|
175
|
-
return msg
|
|
195
|
+
self._route('error', text, extra_maps, override_maps, ctx)
|
|
@@ -69,9 +69,10 @@ class GenericServer:
|
|
|
69
69
|
uptime = self.resources().get('uptime')
|
|
70
70
|
if uptime is None:
|
|
71
71
|
return None
|
|
72
|
-
seconds = uptime // 1000
|
|
73
72
|
if not formatted:
|
|
74
|
-
return seconds
|
|
73
|
+
# return seconds # returns seconds
|
|
74
|
+
return uptime # returns miliseconds
|
|
75
|
+
seconds = uptime // 1000
|
|
75
76
|
hours = seconds // 3600
|
|
76
77
|
minutes = (seconds % 3600) // 60
|
|
77
78
|
secs = seconds % 60
|
|
@@ -69,11 +69,7 @@ class MinecraftServer(GenericServer):
|
|
|
69
69
|
There are 0 of a max of 20 players online:
|
|
70
70
|
'''
|
|
71
71
|
try:
|
|
72
|
-
raw = self.bridge.runCommand('list', show_console=True, silent=False, normalize=False)
|
|
73
|
-
print("RAW OUTPUT:", repr(raw))
|
|
74
|
-
|
|
75
72
|
output = self.bridge.runCommand('list', show_console=False, silent=False, normalize=True)
|
|
76
|
-
print("NORMALIZED OUTPUT:", repr(output))
|
|
77
73
|
except Exception:
|
|
78
74
|
return 0
|
|
79
75
|
if not output:
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
from .buffer import Buffer
|
|
2
|
+
from .utils import Snapshot, clearTerminal, validateAll
|
|
3
|
+
from .convert import convert
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
HungerLibError,
|
|
6
|
+
InvalidLevelError,
|
|
7
|
+
InvalidModeError,
|
|
8
|
+
HungerBridgeError
|
|
9
|
+
)
|
|
1
10
|
from .time import (
|
|
2
11
|
snapSchedule,
|
|
3
12
|
runCountdownEvents,
|
|
@@ -6,7 +15,6 @@ from .time import (
|
|
|
6
15
|
secsUntil,
|
|
7
16
|
minsUntil
|
|
8
17
|
)
|
|
9
|
-
from .utils import Snapshot, clearTerminal, validateAll
|
|
10
18
|
|
|
11
19
|
__all__ = [
|
|
12
20
|
'snapSchedule',
|
|
@@ -18,4 +26,12 @@ __all__ = [
|
|
|
18
26
|
'Snapshot',
|
|
19
27
|
'clearTerminal',
|
|
20
28
|
'validateAll',
|
|
29
|
+
'Buffer',
|
|
30
|
+
'convert',
|
|
31
|
+
|
|
32
|
+
# exceptions
|
|
33
|
+
'HungerLibError',
|
|
34
|
+
'InvalidLevelError',
|
|
35
|
+
'InvalidModeError',
|
|
36
|
+
'HungerBridgeError',
|
|
21
37
|
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Buffer:
|
|
2
|
+
def __init__(self, enabled=True):
|
|
3
|
+
self.enabled = enabled
|
|
4
|
+
self.captured = []
|
|
5
|
+
|
|
6
|
+
def enable(self): self.enabled = True
|
|
7
|
+
|
|
8
|
+
def disable(self): self.enabled = False
|
|
9
|
+
|
|
10
|
+
def toggle(self): self.enabled = not self.enabled
|
|
11
|
+
|
|
12
|
+
def clear(self): self.captured = []
|
|
13
|
+
|
|
14
|
+
def printCaptured(self):
|
|
15
|
+
for msg in self.captured:
|
|
16
|
+
print(msg)
|
|
17
|
+
|
|
18
|
+
def flush(self):
|
|
19
|
+
self.printCaptured()
|
|
20
|
+
self.clear()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
from mapres import datamap, res
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@datamap.pipes
|
|
6
|
+
class TimeUnits:
|
|
7
|
+
ns: float = 1e-9
|
|
8
|
+
us: float = 1e-6
|
|
9
|
+
ms: float = 1e-3
|
|
10
|
+
s: float = 1
|
|
11
|
+
min: float = 60
|
|
12
|
+
h: float = 3600
|
|
13
|
+
day: float = 86400
|
|
14
|
+
time_units = TimeUnits()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@datamap.pipes
|
|
18
|
+
class ByteUnits:
|
|
19
|
+
# decimal
|
|
20
|
+
b: int = 1
|
|
21
|
+
kb: int = 1_000
|
|
22
|
+
mb: int = 1_000_000
|
|
23
|
+
gb: int = 1_000_000_000
|
|
24
|
+
tb: int = 1_000_000_000_000
|
|
25
|
+
|
|
26
|
+
# binary
|
|
27
|
+
kib: int = 1024
|
|
28
|
+
mib: int = 1024**2
|
|
29
|
+
gib: int = 1024**3
|
|
30
|
+
tib: int = 1024**4
|
|
31
|
+
byte_units = ByteUnits()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _time(value, src, dst, rounding=None):
|
|
35
|
+
value = float(value)
|
|
36
|
+
|
|
37
|
+
src_factor = float(getattr(time_units, src))
|
|
38
|
+
dst_factor = float(getattr(time_units, dst))
|
|
39
|
+
|
|
40
|
+
base = value * src_factor
|
|
41
|
+
result = base / dst_factor
|
|
42
|
+
|
|
43
|
+
return round(result, rounding) if rounding is not None else result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _byte(value, src, dst, rounding=None):
|
|
47
|
+
value = float(value)
|
|
48
|
+
|
|
49
|
+
src_factor = float(getattr(byte_units, src))
|
|
50
|
+
dst_factor = float(getattr(byte_units, dst))
|
|
51
|
+
|
|
52
|
+
base = value * src_factor
|
|
53
|
+
result = base / dst_factor
|
|
54
|
+
|
|
55
|
+
return round(result, rounding) if rounding is not None else result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
convert = SimpleNamespace(
|
|
59
|
+
time=_time,
|
|
60
|
+
byte=_byte,
|
|
61
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
class WebhookClient:
|
|
4
|
+
def __init__(self, url: str, token: str, timeout: int = 5):
|
|
5
|
+
self.url = url
|
|
6
|
+
self.token = token
|
|
7
|
+
self.timeout = timeout
|
|
8
|
+
|
|
9
|
+
def send(self, event: str, **ctx):
|
|
10
|
+
payload = {'event': event, **ctx}
|
|
11
|
+
|
|
12
|
+
resp = requests.post(
|
|
13
|
+
self.url,
|
|
14
|
+
json=payload,
|
|
15
|
+
headers={'x-webhook-token': self.token},
|
|
16
|
+
timeout=self.timeout
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
resp.raise_for_status()
|
|
20
|
+
return resp.json()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import uvicorn
|
|
2
|
+
from fastapi import FastAPI, Request, Header, HTTPException
|
|
3
|
+
|
|
4
|
+
class WebhookServer:
|
|
5
|
+
def __init__(self, port: int, token: str, host: str = "0.0.0.0", logLevel: str = "info"):
|
|
6
|
+
self.port = port
|
|
7
|
+
self.token = token
|
|
8
|
+
self.host = host
|
|
9
|
+
self.logLevel = logLevel
|
|
10
|
+
|
|
11
|
+
self.app = FastAPI()
|
|
12
|
+
self._server = None
|
|
13
|
+
|
|
14
|
+
# routing tables
|
|
15
|
+
self._endpointHandlers: dict[str, callable] = {}
|
|
16
|
+
self._eventHandlers: dict[str, callable] = {}
|
|
17
|
+
|
|
18
|
+
def addEndpoint(self, path: str, handler: callable):
|
|
19
|
+
# register endpoint
|
|
20
|
+
self._endpointHandlers[path] = handler
|
|
21
|
+
|
|
22
|
+
@self.app.post(path)
|
|
23
|
+
async def _endpoint(request: Request, x_webhook_token: str | None = Header(None)):
|
|
24
|
+
if x_webhook_token != self.token:
|
|
25
|
+
raise HTTPException(status_code=401, detail="invalid token")
|
|
26
|
+
|
|
27
|
+
data = await request.json()
|
|
28
|
+
event = (data.get("event") or "").lower()
|
|
29
|
+
|
|
30
|
+
# endpoint handler
|
|
31
|
+
await handler(event, data)
|
|
32
|
+
|
|
33
|
+
# event handler
|
|
34
|
+
if event in self._eventHandlers:
|
|
35
|
+
await self._eventHandlers[event](data)
|
|
36
|
+
|
|
37
|
+
return {"status": "ok"}
|
|
38
|
+
|
|
39
|
+
def addHandler(self, event: str, handler: callable):
|
|
40
|
+
# register event handler
|
|
41
|
+
self._eventHandlers[event.lower()] = handler
|
|
42
|
+
|
|
43
|
+
async def start(self):
|
|
44
|
+
config = uvicorn.Config(
|
|
45
|
+
self.app,
|
|
46
|
+
host=self.host,
|
|
47
|
+
port=self.port,
|
|
48
|
+
log_level=self.logLevel
|
|
49
|
+
)
|
|
50
|
+
self._server = uvicorn.Server(config)
|
|
51
|
+
await self._server.serve()
|
|
52
|
+
|
|
53
|
+
async def stop(self):
|
|
54
|
+
# stop server
|
|
55
|
+
if self._server:
|
|
56
|
+
self._server.should_exit = True
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hungerlib
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.24
|
|
4
4
|
Summary: Powerful automation library for Pterodactyl.
|
|
5
5
|
Author: iFamished
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Documentation, https://github.com/
|
|
9
|
-
Project-URL: Source, https://github.com/
|
|
10
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/iFamished/HungerLib
|
|
8
|
+
Project-URL: Documentation, https://github.com/iFamished/HungerLib
|
|
9
|
+
Project-URL: Source, https://github.com/iFamished/HungerLib
|
|
10
|
+
Project-URL: Issues, https://github.com/iFamished/HungerLib/issues
|
|
11
11
|
Keywords: pterodactyl,minecraft,automation,rcon,server-management,hungerlib,hungernet
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -23,9 +23,12 @@ Requires-Python: >=3.9
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: requests
|
|
26
|
-
Requires-Dist: mcrcon
|
|
27
26
|
Requires-Dist: PyYaml
|
|
28
27
|
Requires-Dist: mapres
|
|
28
|
+
Requires-Dist: jinja2
|
|
29
|
+
Requires-Dist: cloudflare
|
|
30
|
+
Requires-Dist: uvicorn
|
|
31
|
+
Requires-Dist: fastapi
|
|
29
32
|
Dynamic: license-file
|
|
30
33
|
|
|
31
34
|
# HungerLib
|
|
@@ -19,10 +19,20 @@ src/hungerlib/api/databases.py
|
|
|
19
19
|
src/hungerlib/api/filemanager.py
|
|
20
20
|
src/hungerlib/api/schedule.py
|
|
21
21
|
src/hungerlib/api/startup.py
|
|
22
|
+
src/hungerlib/email/__init__.py
|
|
23
|
+
src/hungerlib/email/client.py
|
|
24
|
+
src/hungerlib/email/manager.py
|
|
25
|
+
src/hungerlib/email/message.py
|
|
26
|
+
src/hungerlib/email/email_templates/restart.html
|
|
22
27
|
src/hungerlib/servers/__init__.py
|
|
23
28
|
src/hungerlib/servers/generic.py
|
|
24
29
|
src/hungerlib/servers/minecraft.py
|
|
25
30
|
src/hungerlib/utils/__init__.py
|
|
31
|
+
src/hungerlib/utils/buffer.py
|
|
32
|
+
src/hungerlib/utils/convert.py
|
|
26
33
|
src/hungerlib/utils/exceptions.py
|
|
27
34
|
src/hungerlib/utils/time.py
|
|
28
|
-
src/hungerlib/utils/utils.py
|
|
35
|
+
src/hungerlib/utils/utils.py
|
|
36
|
+
src/hungerlib/webhook/__init__.py
|
|
37
|
+
src/hungerlib/webhook/client.py
|
|
38
|
+
src/hungerlib/webhook/server.py
|
|
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
|