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.
Files changed (41) hide show
  1. {hungerlib-4.23.dev8/src/hungerlib.egg-info → hungerlib-4.24}/PKG-INFO +9 -6
  2. {hungerlib-4.23.dev8 → hungerlib-4.24}/pyproject.toml +17 -6
  3. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/__init__.py +29 -0
  4. hungerlib-4.24/src/hungerlib/email/__init__.py +9 -0
  5. hungerlib-4.24/src/hungerlib/email/client.py +35 -0
  6. hungerlib-4.24/src/hungerlib/email/email_templates/restart.html +87 -0
  7. hungerlib-4.24/src/hungerlib/email/manager.py +55 -0
  8. hungerlib-4.24/src/hungerlib/email/message.py +33 -0
  9. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/messagerouter.py +79 -59
  10. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/generic.py +3 -2
  11. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/minecraft.py +0 -4
  12. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/__init__.py +17 -1
  13. hungerlib-4.24/src/hungerlib/utils/buffer.py +20 -0
  14. hungerlib-4.24/src/hungerlib/utils/convert.py +61 -0
  15. hungerlib-4.24/src/hungerlib/webhook/__init__.py +4 -0
  16. hungerlib-4.24/src/hungerlib/webhook/client.py +20 -0
  17. hungerlib-4.24/src/hungerlib/webhook/server.py +56 -0
  18. {hungerlib-4.23.dev8 → hungerlib-4.24/src/hungerlib.egg-info}/PKG-INFO +9 -6
  19. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib.egg-info/SOURCES.txt +11 -1
  20. hungerlib-4.24/src/hungerlib.egg-info/requires.txt +7 -0
  21. hungerlib-4.23.dev8/src/hungerlib.egg-info/requires.txt +0 -4
  22. {hungerlib-4.23.dev8 → hungerlib-4.24}/LICENSE +0 -0
  23. {hungerlib-4.23.dev8 → hungerlib-4.24}/README.md +0 -0
  24. {hungerlib-4.23.dev8 → hungerlib-4.24}/setup.cfg +0 -0
  25. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/__init__.py +0 -0
  26. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/backups.py +0 -0
  27. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/command.py +0 -0
  28. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/databases.py +0 -0
  29. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/filemanager.py +0 -0
  30. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/schedule.py +0 -0
  31. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/api/startup.py +0 -0
  32. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/bridgeclient.py +0 -0
  33. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/configloader.py +0 -0
  34. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/panel.py +0 -0
  35. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/servers/__init__.py +0 -0
  36. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/exceptions.py +0 -0
  37. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/time.py +0 -0
  38. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/utils/utils.py +0 -0
  39. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib/validator.py +0 -0
  40. {hungerlib-4.23.dev8 → hungerlib-4.24}/src/hungerlib.egg-info/dependency_links.txt +0 -0
  41. {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.23.dev8
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/iFamishedX/HungerLib
8
- Project-URL: Documentation, https://github.com/iFamishedX/HungerLib
9
- Project-URL: Source, https://github.com/iFamishedX/HungerLib
10
- Project-URL: Issues, https://github.com/iFamishedX/HungerLib/issues
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.23.dev8"
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/iFamishedX/HungerLib"
50
- Documentation = "https://github.com/iFamishedX/HungerLib"
51
- Source = "https://github.com/iFamishedX/HungerLib"
52
- Issues = "https://github.com/iFamishedX/HungerLib/issues"
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,9 @@
1
+ from .message import Email
2
+ from .manager import EmailManager
3
+ from .client import EmailClient
4
+
5
+ __all__ = [
6
+ 'Email',
7
+ 'EmailManager',
8
+ 'EmailClient',
9
+ ]
@@ -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 make_method(level_name):
63
- def _method(text, **ctx):
64
- cfg = self.custom_levels[level_name]
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
- print(msg)
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
- msg = self.origin(text, 'info', extra_maps, override_maps, **ctx)
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
- msg = self.origin(text, 'warn', extra_maps, override_maps, **ctx)
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
- msg = self.origin(text, 'error', extra_maps, override_maps, **ctx)
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,4 @@
1
+ from .server import WebhookServer
2
+ from .client import WebhookClient
3
+
4
+ __all__ = ["WebhookServer", "WebhookClient"]
@@ -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.23.dev8
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/iFamishedX/HungerLib
8
- Project-URL: Documentation, https://github.com/iFamishedX/HungerLib
9
- Project-URL: Source, https://github.com/iFamishedX/HungerLib
10
- Project-URL: Issues, https://github.com/iFamishedX/HungerLib/issues
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
@@ -0,0 +1,7 @@
1
+ requests
2
+ PyYaml
3
+ mapres
4
+ jinja2
5
+ cloudflare
6
+ uvicorn
7
+ fastapi
@@ -1,4 +0,0 @@
1
- requests
2
- mcrcon
3
- PyYaml
4
- mapres
File without changes
File without changes
File without changes