comprobot 2.3.0__tar.gz → 2.3.2__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 (27) hide show
  1. {comprobot-2.3.0 → comprobot-2.3.2}/PKG-INFO +1 -1
  2. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/PKG-INFO +1 -1
  3. {comprobot-2.3.0 → comprobot-2.3.2}/pyproject.toml +1 -1
  4. {comprobot-2.3.0 → comprobot-2.3.2}/src/__main__.py +3 -2
  5. {comprobot-2.3.0 → comprobot-2.3.2}/src/api.py +3 -3
  6. {comprobot-2.3.0 → comprobot-2.3.2}/src/commands.py +6 -11
  7. comprobot-2.3.2/src/data.py +159 -0
  8. {comprobot-2.3.0 → comprobot-2.3.2}/src/process.py +3 -13
  9. {comprobot-2.3.0 → comprobot-2.3.2}/src/start.py +23 -1
  10. {comprobot-2.3.0 → comprobot-2.3.2}/src/templates.py +0 -3
  11. comprobot-2.3.0/src/data.py +0 -87
  12. {comprobot-2.3.0 → comprobot-2.3.2}/LICENSE +0 -0
  13. {comprobot-2.3.0 → comprobot-2.3.2}/README.md +0 -0
  14. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/SOURCES.txt +0 -0
  15. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/dependency_links.txt +0 -0
  16. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/entry_points.txt +0 -0
  17. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/requires.txt +0 -0
  18. {comprobot-2.3.0 → comprobot-2.3.2}/comprobot.egg-info/top_level.txt +0 -0
  19. {comprobot-2.3.0 → comprobot-2.3.2}/setup.cfg +0 -0
  20. {comprobot-2.3.0 → comprobot-2.3.2}/src/__init__.py +0 -0
  21. {comprobot-2.3.0 → comprobot-2.3.2}/src/bot.py +0 -0
  22. {comprobot-2.3.0 → comprobot-2.3.2}/src/config.py +0 -0
  23. {comprobot-2.3.0 → comprobot-2.3.2}/src/functions.py +0 -0
  24. {comprobot-2.3.0 → comprobot-2.3.2}/src/moderation.py +0 -0
  25. {comprobot-2.3.0 → comprobot-2.3.2}/src/money_system.py +0 -0
  26. {comprobot-2.3.0 → comprobot-2.3.2}/src/onboarding.py +0 -0
  27. {comprobot-2.3.0 → comprobot-2.3.2}/src/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comprobot
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A self-hostable Discord bot built for maximum customization.
5
5
  Author: badluma
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comprobot
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A self-hostable Discord bot built for maximum customization.
5
5
  Author: badluma
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "comprobot"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  authors = [{name = "badluma"}]
9
9
  description = "A self-hostable Discord bot built for maximum customization."
10
10
  readme = "README.md"
@@ -14,7 +14,8 @@ def main():
14
14
  description="A self-hostable open-source Discord bot built for maximum customization.",
15
15
  )
16
16
  subparsers = parser.add_subparsers(dest="command", metavar="")
17
- subparsers.add_parser("start", help="Start the bot.")
17
+ start_parser = subparsers.add_parser("start", help="Start the bot.")
18
+ start_parser.add_argument("--daemon", action="store_true", help="Daemonize the process.")
18
19
  subparsers.add_parser("onboard", help="Set up Comprobot for the first time.")
19
20
  config_parser = subparsers.add_parser("config", help="Configure the bot's settings.")
20
21
  config_parser.add_argument("config_args", nargs=argparse.REMAINDER)
@@ -25,7 +26,7 @@ def main():
25
26
 
26
27
  match args.command:
27
28
  case "start":
28
- start()
29
+ start(daemon=getattr(args, "daemon", False))
29
30
  case "onboard":
30
31
  settings = onboarding()
31
32
 
@@ -29,7 +29,7 @@ def access_api(url, parameter, error_message, headers=None):
29
29
  # ---------- Commands ----------
30
30
  def quote():
31
31
  try:
32
- quote_response = requests.get("https://zenquotes.io/api/random")
32
+ quote_response = requests.get("https://zenquotes.io/api/random", timeout=10)
33
33
  except requests.exceptions.RequestException as e:
34
34
  return f"{error_messages['quote']} ({e})"
35
35
  if quote_response.status_code != 200:
@@ -143,8 +143,8 @@ def bible(
143
143
  choice(output["general"]["bible"])
144
144
  .replace(r"{{PASSAGE}}", verse["text"].strip())
145
145
  .replace(r"{{BOOK}}", verse["book"].strip())
146
- .replace(r"{{CHAPTER}}", str(verse["chapter"].strip()))
147
- .replace(r"{{VERSE}}", str(verse["verse"].strip()))
146
+ .replace(r"{{CHAPTER}}", str(verse["chapter"]).strip())
147
+ .replace(r"{{VERSE}}", str(verse["verse"]).strip())
148
148
  )
149
149
  elif "text" in data and "reference" in data:
150
150
  parts = data["reference"].split()
@@ -23,15 +23,10 @@ def calculate(calculation):
23
23
 
24
24
  def help(category=None):
25
25
  if not category:
26
- message = "# Commands"
26
+ message = "# Categories"
27
+
27
28
  for category in list(keywords.keys()):
28
- message += f"\n## {category.title()}\n"
29
- for command in list(keywords[category].keys()):
30
- if active.get(command, True):
31
- message += f"\n**{config["prefix"]}{keywords[category][command][0]}** - {descriptions[category][command]}"
32
- if len(keywords[category][command]) > 1:
33
- message += f"\n-# Aliases: {config["prefix"]}{', '.join(keywords[category][command][1:])}"
34
- message += "\n-# \u200b"
29
+ message += f"\n**{category.title()}**\n!help {category}\n-# \u200b"
35
30
  return message
36
31
  else:
37
32
  if not category.lower() in list(keywords.keys()):
@@ -39,10 +34,10 @@ def help(category=None):
39
34
  message = f"## {category.title()}\n"
40
35
  for command in list(keywords[category.lower()].keys()):
41
36
  if active.get(command, True):
42
- message += f"\n**{config["prefix"]}{keywords[category.lower()][command][0]}** - {descriptions[category.lower()][command]}"
37
+ message += f"\n**{config['prefix']}{keywords[category.lower()][command][0]}** - {descriptions[category.lower()][command]}"
43
38
  if len(keywords[category.lower()][command]) > 1:
44
- message += f"\n-# Aliases: {config["prefix"]}{', '.join(keywords[category][command][1:])}"
45
- message += "\n-# "
39
+ message += f"\n-# Aliases: {', '.join(config['prefix'] + alias for alias in keywords[category.lower()][command][1:])}"
40
+ message += "\n-# \u200b"
46
41
  return message
47
42
 
48
43
 
@@ -0,0 +1,159 @@
1
+ import os
2
+ from collections.abc import Mapping
3
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
4
+
5
+ import appdirs
6
+ import tomlkit
7
+ import tomlkit.exceptions
8
+
9
+ from . import templates
10
+
11
+ Migration = Union[Tuple[str, str], Callable]
12
+
13
+ RENAME_COMMANDS: List[Migration] = [("commands", "general")]
14
+
15
+
16
+ def get_data_path(filename: str) -> str:
17
+ base_dir = appdirs.user_data_dir(appname="Comprobot", appauthor=False)
18
+ return os.path.join(base_dir, filename)
19
+
20
+
21
+ def _write(path: str, data) -> None:
22
+ os.makedirs(os.path.dirname(path), exist_ok=True)
23
+ with open(path, "w", encoding="utf-8") as f:
24
+ tomlkit.dump(data, f)
25
+
26
+
27
+ def _apply_migrations(data, migrations: List[Migration]) -> bool:
28
+ changed = False
29
+ for migration in migrations:
30
+ if callable(migration):
31
+ changed |= bool(migration(data))
32
+ else:
33
+ old_key, new_key = migration
34
+ if old_key in data:
35
+ if new_key not in data:
36
+ data[new_key] = data[old_key]
37
+ del data[old_key]
38
+ changed = True
39
+ return changed
40
+
41
+
42
+ def _merge_defaults(data, defaults) -> bool:
43
+ changed = False
44
+ for key, value in defaults.items():
45
+ if key not in data:
46
+ data[key] = value
47
+ changed = True
48
+ elif isinstance(value, Mapping) and isinstance(data.get(key), Mapping):
49
+ changed |= _merge_defaults(data[key], value)
50
+ return changed
51
+
52
+
53
+ def _reorder(data, defaults) -> bool:
54
+ in_data = set(data.keys())
55
+ target = [k for k in defaults if k in in_data] + [k for k in in_data if k not in defaults]
56
+
57
+ if list(data.keys()) == target:
58
+ changed = False
59
+ for key in defaults:
60
+ if isinstance(defaults[key], Mapping) and isinstance(data.get(key), Mapping):
61
+ changed |= _reorder(data[key], defaults[key])
62
+ return changed
63
+
64
+ snapshot = {k: data[k] for k in in_data}
65
+ for key in list(in_data):
66
+ del data[key]
67
+ for key in target:
68
+ data[key] = snapshot[key]
69
+
70
+ for key in defaults:
71
+ if isinstance(defaults[key], Mapping) and isinstance(data.get(key), Mapping):
72
+ _reorder(data[key], defaults[key])
73
+
74
+ return True
75
+
76
+
77
+ def _prune(data, defaults) -> bool:
78
+ changed = False
79
+ for key in [k for k in data if k not in defaults]:
80
+ del data[key]
81
+ changed = True
82
+ for key, value in defaults.items():
83
+ if isinstance(value, Mapping) and isinstance(data.get(key), Mapping):
84
+ changed |= _prune(data[key], value)
85
+ return changed
86
+
87
+
88
+ def load_or_create(
89
+ path: str,
90
+ template_content: str,
91
+ migrations: Optional[List[Migration]] = None,
92
+ prune_obsolete: bool = True,
93
+ ) -> dict:
94
+ defaults = tomlkit.loads(template_content)
95
+
96
+ try:
97
+ with open(path, "r", encoding="utf-8") as f:
98
+ data = tomlkit.loads(f.read())
99
+ except FileNotFoundError:
100
+ _write(path, defaults)
101
+ return defaults
102
+ except tomlkit.exceptions.TOMLKitError:
103
+ backup = path + ".bak"
104
+ os.replace(path, backup)
105
+ print(f"[Comprobot] Corrupt TOML at {path!r} — backed up to {backup!r}, resetting to defaults.")
106
+ _write(path, defaults)
107
+ return defaults
108
+
109
+ changed = _apply_migrations(data, migrations or [])
110
+ changed |= _merge_defaults(data, defaults)
111
+ if prune_obsolete:
112
+ changed |= _prune(data, defaults)
113
+ changed |= _reorder(data, defaults)
114
+
115
+ if changed:
116
+ _write(path, data)
117
+
118
+ return data
119
+
120
+
121
+ def save_toml(data, path: str) -> None:
122
+ with open(path, "w", encoding="utf-8") as f:
123
+ tomlkit.dump(data, f)
124
+
125
+
126
+ error_messages: Dict[str, str] = load_or_create(
127
+ get_data_path("error-messages.toml"), templates.error_messages
128
+ )
129
+ config: Dict[str, Any] = load_or_create(
130
+ get_data_path("config.toml"), templates.config
131
+ )
132
+ keywords: Dict[str, Dict[str, List[str]]] = load_or_create(
133
+ get_data_path("keywords.toml"), templates.keywords, migrations=RENAME_COMMANDS
134
+ )
135
+ ai: Dict[str, Any] = load_or_create(
136
+ get_data_path("ai.toml"), templates.ai
137
+ )
138
+ system_prompt_text: str = str(ai["system_prompt"])
139
+ money: Dict[str, Dict[str, int]] = load_or_create(
140
+ get_data_path("money.toml"), "[members]\n", prune_obsolete=False
141
+ )
142
+ active: Dict[str, bool] = load_or_create(
143
+ get_data_path("active.toml"), templates.active
144
+ )
145
+ output: Dict[str, Dict[str, List[str]]] = load_or_create(
146
+ get_data_path("output.toml"), templates.output, migrations=RENAME_COMMANDS
147
+ )
148
+ moderation: Dict[Any, Any] = load_or_create(
149
+ get_data_path("moderation.toml"), templates.moderation
150
+ )
151
+ descriptions: Dict[str, Dict[str, str]] = load_or_create(
152
+ get_data_path("descriptions.toml"), templates.descriptions, migrations=RENAME_COMMANDS
153
+ )
154
+
155
+ env_path = get_data_path(".env")
156
+ if not os.path.isfile(env_path):
157
+ os.makedirs(os.path.dirname(env_path), exist_ok=True)
158
+ with open(env_path, "w", encoding="utf-8") as f:
159
+ f.write(templates.env)
@@ -61,17 +61,7 @@ class Comprobot(ext_commands.Cog):
61
61
  @ext_commands.check(lambda ctx: active["waifu"])
62
62
  async def waifu_cmd(self, ctx):
63
63
  await ctx.send(api.waifu())
64
-
65
- @ext_commands.group(
66
- name=keywords["general"]["image"][0],
67
- aliases=keywords["general"]["image"][1:],
68
- invoke_without_command=True,
69
- )
70
- @ext_commands.check(lambda ctx: active["image"])
71
- async def image_cmd(self, ctx):
72
- await ctx.send(error_messages["missing_argument"])
73
-
74
- @image_cmd.command(
64
+ @ext_commands.command(
75
65
  name=keywords["general"]["duck"][0],
76
66
  aliases=keywords["general"]["duck"][1:],
77
67
  )
@@ -79,7 +69,7 @@ class Comprobot(ext_commands.Cog):
79
69
  async def duck_cmd(self, ctx):
80
70
  await ctx.send(api.duck())
81
71
 
82
- @image_cmd.command(
72
+ @ext_commands.command(
83
73
  name=keywords["general"]["dog"][0],
84
74
  aliases=keywords["general"]["dog"][1:],
85
75
  )
@@ -87,7 +77,7 @@ class Comprobot(ext_commands.Cog):
87
77
  async def dog_cmd(self, ctx):
88
78
  await ctx.send(api.dog())
89
79
 
90
- @image_cmd.command(
80
+ @ext_commands.command(
91
81
  name=keywords["general"]["cat"][0],
92
82
  aliases=keywords["general"]["cat"][1:],
93
83
  )
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import platform
2
3
  import sys
3
4
  from os import getenv as os_getenv
@@ -33,6 +34,14 @@ async def _setup_hook():
33
34
  client.setup_hook = _setup_hook
34
35
 
35
36
 
37
+ @client.event
38
+ async def on_message(message):
39
+ if message.author == client.user:
40
+ return
41
+ ctx = await client.get_context(message)
42
+ await client.invoke(ctx)
43
+
44
+
36
45
  @client.event
37
46
  async def on_ready():
38
47
  user_name = client.user.name if client.user else "Unknown"
@@ -40,7 +49,20 @@ async def on_ready():
40
49
  para()
41
50
 
42
51
 
43
- def start():
52
+ def _daemonize():
53
+ if os.fork() != 0:
54
+ os._exit(0)
55
+ os.setsid()
56
+ if os.fork() != 0:
57
+ os._exit(0)
58
+ devnull = os.open(os.devnull, os.O_RDONLY)
59
+ os.dup2(devnull, 0)
60
+ os.close(devnull)
61
+
62
+
63
+ def start(daemon=False):
64
+ if daemon:
65
+ _daemonize()
44
66
  print(
45
67
  f"Configuration directory: {appdirs.user_data_dir(appname='Comprobot', appauthor=False)}"
46
68
  )
@@ -80,7 +80,6 @@ quote = ["quote"]
80
80
  joke = ["joke"]
81
81
  meme = ["meme"]
82
82
  waifu = ["waifu"]
83
- image = ["image", "picture"]
84
83
  duck = ["duck"]
85
84
  dog = ["dog"]
86
85
  cat = ["cat"]
@@ -128,7 +127,6 @@ active = r"""quote = true
128
127
  joke = true
129
128
  meme = true
130
129
  waifu = true
131
- image = true
132
130
  duck = true
133
131
  dog = true
134
132
  cat = true
@@ -157,7 +155,6 @@ quote = "Get a random inspirational quote."
157
155
  joke = "Get a random two-part joke"
158
156
  meme = "Get a random meme image"
159
157
  waifu = "Compare two random waifu images"
160
- image = "Get a random animal image (duck/dog/cat)"
161
158
  duck = "Get a random duck image"
162
159
  dog = "Get a random dog image"
163
160
  cat = "Get a random cat image"
@@ -1,87 +0,0 @@
1
- import os
2
- from typing import Any, Dict, List
3
-
4
- import appdirs
5
- import tomlkit
6
-
7
- from . import templates
8
-
9
-
10
- def get_data_path(filename):
11
-
12
- base_dir = appdirs.user_data_dir(appname="Comprobot", appauthor=False)
13
- return os.path.join(base_dir, filename)
14
-
15
-
16
- def ensure_file(path, content):
17
-
18
- os.makedirs(os.path.dirname(path), exist_ok=True)
19
- if not os.path.isfile(path):
20
- with open(path, "w", encoding="utf-8") as f:
21
- f.write(content)
22
-
23
-
24
- def merge_defaults(data, defaults):
25
- for key, value in defaults.items():
26
- if key not in data:
27
- data[key] = value
28
- elif isinstance(value, dict) and isinstance(data.get(key), dict):
29
- merge_defaults(data[key], value)
30
-
31
-
32
- def migrate_section(data, old_key, new_key):
33
- if old_key in data and new_key not in data:
34
- data[new_key] = data[old_key]
35
- del data[old_key]
36
-
37
-
38
- def load_or_create(path, template_content):
39
- try:
40
- with open(path, "r", encoding="utf-8") as f:
41
- data = tomlkit.loads(f.read())
42
- except FileNotFoundError:
43
- ensure_file(path, template_content)
44
- with open(path, "r", encoding="utf-8") as f:
45
- data = tomlkit.loads(f.read())
46
-
47
- migrate_section(data, "commands", "general")
48
-
49
- defaults = tomlkit.loads(template_content)
50
- merge_defaults(data, defaults)
51
-
52
- if data != defaults:
53
- with open(path, "w", encoding="utf-8") as f:
54
- tomlkit.dump(data, f)
55
-
56
- return data
57
-
58
-
59
- error_messages: Dict[str, str] = load_or_create(
60
- get_data_path("error-messages.toml"), templates.error_messages
61
- )
62
- config: Dict[str, Any] = load_or_create(get_data_path("config.toml"), templates.config)
63
- keywords: Dict[str, Dict[str, List[str]]] = load_or_create(
64
- get_data_path("keywords.toml"), templates.keywords
65
- )
66
- ai: Dict[str, Any] = load_or_create(get_data_path("ai.toml"), templates.ai)
67
- system_prompt_text: str = str(ai["system_prompt"])
68
- money: Dict[str, Dict[str, int]] = load_or_create(
69
- get_data_path("money.toml"), r"""members = {}"""
70
- )
71
- active: Dict[str, bool] = load_or_create(get_data_path("active.toml"), templates.active)
72
- output: Dict[str, Dict[str, List[str]]] = load_or_create(
73
- get_data_path("output.toml"), templates.output
74
- )
75
- moderation: Dict[Any, Any] = load_or_create(
76
- get_data_path("moderation.toml"), templates.moderation
77
- )
78
- descriptions: Dict[str, Dict[str, str]] = load_or_create(
79
- get_data_path("descriptions.toml"), templates.descriptions
80
- )
81
-
82
- ensure_file(get_data_path(".env"), templates.env)
83
-
84
-
85
- def save_toml(data, path):
86
- with open(path, "w", encoding="utf-8") as f:
87
- tomlkit.dump(data, f)
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