modifyself 0.3.7__tar.gz → 0.3.8__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.
- {modifyself-0.3.7 → modifyself-0.3.8}/PKG-INFO +1 -2
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/__init__.py +1 -1
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/headers.py +14 -12
- modifyself-0.3.8/modifyself/utils.py +137 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself.egg-info/PKG-INFO +1 -2
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself.egg-info/requires.txt +0 -1
- {modifyself-0.3.7 → modifyself-0.3.8}/pyproject.toml +1 -2
- modifyself-0.3.7/modifyself/utils.py +0 -177
- {modifyself-0.3.7 → modifyself-0.3.8}/LICENSE +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/README.md +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/activity.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/client.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/__init__.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/checks.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/cog.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/context.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/converters.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/commands/core.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/components.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/core/bitfield.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/core/enums.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/core/mixins.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/core/snowflake.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/errors.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/gateway/__init__.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/gateway/dispatcher.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/gateway/heartbeat.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/gateway/websocket.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/http/__init__.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/http/client.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/http/ratelimit.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/http/route.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/interactions.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/__init__.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/base.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/billing.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/channel.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/guild.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/member.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/message.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/relationship.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/settings.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/user.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/models/webhook.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/state.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/types.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself/voice.py +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself.egg-info/SOURCES.txt +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself.egg-info/dependency_links.txt +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/modifyself.egg-info/top_level.txt +0 -0
- {modifyself-0.3.7 → modifyself-0.3.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modifyself
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: A clean, pythonic Discord self-bot library
|
|
5
5
|
Author-email: modifyself <modifyself@example.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,7 +15,6 @@ Requires-Dist: wreq>=0.1.0
|
|
|
15
15
|
Requires-Dist: aiohttp>=3.8.0
|
|
16
16
|
Requires-Dist: plyer>=0.9
|
|
17
17
|
Requires-Dist: pillow>=10.0
|
|
18
|
-
Requires-Dist: requests>=2.28
|
|
19
18
|
Requires-Dist: PyNaCl>=1.5.0
|
|
20
19
|
Requires-Dist: opuslib>=3.0.0
|
|
21
20
|
Dynamic: license-file
|
|
@@ -9,8 +9,9 @@ import hashlib
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import Dict, Optional, Tuple, List, Any, Union
|
|
11
11
|
|
|
12
|
+
import urllib.request as _urllib_req
|
|
13
|
+
|
|
12
14
|
from wreq import Emulation
|
|
13
|
-
import requests as _requests
|
|
14
15
|
|
|
15
16
|
_GREASE_BRANDS = [
|
|
16
17
|
('"Not A(Brand"', "99"),
|
|
@@ -72,8 +73,6 @@ class HeaderSpoofer:
|
|
|
72
73
|
self._rng = random.Random(self._seed)
|
|
73
74
|
self._emulation = emulation
|
|
74
75
|
self.profile = self._make_profile()
|
|
75
|
-
self.session = _requests.Session()
|
|
76
|
-
self.session.headers.update({"User-Agent": self.profile.user_agent})
|
|
77
76
|
self._async_client: Optional[Any] = None
|
|
78
77
|
self.build_number = self._get_build_number()
|
|
79
78
|
self._launch_id = str(uuid.uuid4())
|
|
@@ -105,11 +104,12 @@ class HeaderSpoofer:
|
|
|
105
104
|
|
|
106
105
|
cache_file = os.path.join(os.path.dirname(__file__), ".chrome_version.json")
|
|
107
106
|
try:
|
|
108
|
-
|
|
107
|
+
_req = _urllib_req.Request(
|
|
109
108
|
"https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions/all/releases?filter=endtime=none",
|
|
110
|
-
|
|
109
|
+
headers={"User-Agent": "Mozilla/5.0"},
|
|
111
110
|
)
|
|
112
|
-
|
|
111
|
+
with _urllib_req.urlopen(_req, timeout=5) as _resp:
|
|
112
|
+
data = json.loads(_resp.read())
|
|
113
113
|
latest = max(data["releases"], key=lambda x: [int(p) for p in x["version"].split(".")])
|
|
114
114
|
major = latest["version"].split(".")[0]
|
|
115
115
|
HeaderSpoofer._chrome_cache = {"major": major}
|
|
@@ -162,18 +162,20 @@ class HeaderSpoofer:
|
|
|
162
162
|
build_cache_file = os.path.join(os.path.dirname(__file__), ".build_cache.json")
|
|
163
163
|
|
|
164
164
|
try:
|
|
165
|
-
|
|
165
|
+
_req1 = _urllib_req.Request(
|
|
166
166
|
"https://discord.com/login",
|
|
167
167
|
headers={"User-Agent": self.profile.user_agent},
|
|
168
|
-
timeout=30,
|
|
169
168
|
)
|
|
170
|
-
|
|
169
|
+
with _urllib_req.urlopen(_req1, timeout=30) as _resp1:
|
|
170
|
+
_html = _resp1.read().decode("utf-8", errors="replace")
|
|
171
|
+
scripts = re.findall(r'<script[^>]+src="(/assets/[^"]+\.js)"', _html)
|
|
171
172
|
for path in reversed(scripts):
|
|
172
|
-
|
|
173
|
+
_req2 = _urllib_req.Request(
|
|
173
174
|
f"https://discord.com{path}",
|
|
174
175
|
headers={"User-Agent": self.profile.user_agent},
|
|
175
|
-
timeout=30,
|
|
176
176
|
)
|
|
177
|
+
with _urllib_req.urlopen(_req2, timeout=30) as _resp2:
|
|
178
|
+
_js_text = _resp2.read().decode("utf-8", errors="replace")
|
|
177
179
|
for pat in [
|
|
178
180
|
r'buildNumber\s*[=:]\s*"?(\d{5,7})"?',
|
|
179
181
|
r'"buildNumber","(\d{5,7})"',
|
|
@@ -181,7 +183,7 @@ class HeaderSpoofer:
|
|
|
181
183
|
r'build_number["\s:=]+(\d{5,7})',
|
|
182
184
|
r'"(\d{6,7})"\s*,\s*"stable"',
|
|
183
185
|
]:
|
|
184
|
-
m = re.search(pat,
|
|
186
|
+
m = re.search(pat, _js_text)
|
|
185
187
|
if m:
|
|
186
188
|
build = int(m.group(1))
|
|
187
189
|
HeaderSpoofer._build_cache = build
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pure utility functions. No Discord-specific state here.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MARKDOWN_ESCAPE_RE = re.compile(r"([*_{\[\]()~`>\#+\-=|.!])")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def escape_markdown(text: str) -> str:
|
|
18
|
+
"""Escape Discord markdown characters."""
|
|
19
|
+
return MARKDOWN_ESCAPE_RE.sub(r"\\\1", text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_time(timestamp: str) -> datetime:
|
|
23
|
+
"""Parse an ISO 8601 timestamp from Discord."""
|
|
24
|
+
if timestamp.endswith("Z"):
|
|
25
|
+
timestamp = timestamp[:-1] + "+00:00"
|
|
26
|
+
return datetime.fromisoformat(timestamp)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def snowflake_time(snowflake: int) -> datetime:
|
|
30
|
+
"""Extract the creation time from a snowflake ID."""
|
|
31
|
+
from .core.snowflake import Snowflake
|
|
32
|
+
return datetime.fromtimestamp(
|
|
33
|
+
((snowflake >> 22) + Snowflake.EPOCH) / 1000,
|
|
34
|
+
tz=timezone.utc,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def chunk_list(lst: list, size: int):
|
|
39
|
+
"""Yield successive chunks of a list."""
|
|
40
|
+
for i in range(0, len(lst), size):
|
|
41
|
+
yield lst[i : i + size]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find(predicate, iterable):
|
|
45
|
+
"""Return the first item in iterable matching predicate."""
|
|
46
|
+
for item in iterable:
|
|
47
|
+
if predicate(item):
|
|
48
|
+
return item
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get(iterable, **attrs):
|
|
53
|
+
"""Return the first item in iterable with matching attributes."""
|
|
54
|
+
for item in iterable:
|
|
55
|
+
if all(getattr(item, k, None) == v for k, v in attrs.items()):
|
|
56
|
+
return item
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def oauth_url(client_id: int, *, permissions: int = 0, guild_id: int | None = None):
|
|
61
|
+
"""Generate an OAuth2 authorization URL."""
|
|
62
|
+
url = f"https://discord.com/oauth2/authorize?client_id={client_id}&scope=bot"
|
|
63
|
+
if permissions:
|
|
64
|
+
url += f"&permissions={permissions}"
|
|
65
|
+
if guild_id:
|
|
66
|
+
url += f"&guild_id={guild_id}"
|
|
67
|
+
return url
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def send_notification(
|
|
71
|
+
title: str,
|
|
72
|
+
message: str,
|
|
73
|
+
image_url: Optional[str] = None,
|
|
74
|
+
timeout: int = 5,
|
|
75
|
+
app_name: str = "modifyself"
|
|
76
|
+
) -> bool:
|
|
77
|
+
"""Send a system desktop notification with an optional image icon."""
|
|
78
|
+
try:
|
|
79
|
+
from plyer import notification as _notify
|
|
80
|
+
except ImportError:
|
|
81
|
+
print("[modifyself] plyer not installed. Install with: pip install plyer pillow")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
icon_path = None
|
|
86
|
+
|
|
87
|
+
if image_url:
|
|
88
|
+
try:
|
|
89
|
+
import urllib.request as _ureq
|
|
90
|
+
with _ureq.urlopen(image_url, timeout=10) as _resp:
|
|
91
|
+
img_data = _resp.read()
|
|
92
|
+
is_windows = sys.platform.startswith("win")
|
|
93
|
+
suffix = ".ico" if is_windows else ".png"
|
|
94
|
+
icon_temp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
|
95
|
+
icon_temp.write(img_data)
|
|
96
|
+
icon_temp.close()
|
|
97
|
+
icon_path = icon_temp.name
|
|
98
|
+
if is_windows:
|
|
99
|
+
try:
|
|
100
|
+
from PIL import Image
|
|
101
|
+
with Image.open(icon_path) as img:
|
|
102
|
+
if img.format != "ICO":
|
|
103
|
+
ico_path = icon_path.replace(".ico", "_converted.ico")
|
|
104
|
+
img.save(ico_path, format="ICO", sizes=[(32, 32), (48, 48), (64, 64)])
|
|
105
|
+
try:
|
|
106
|
+
os.unlink(icon_path)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
icon_path = ico_path
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"[modifyself] ICO conversion warning: {e}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"[modifyself] Error downloading notification image: {e}")
|
|
114
|
+
|
|
115
|
+
_notify.notify(
|
|
116
|
+
title=title,
|
|
117
|
+
message=message,
|
|
118
|
+
app_name=app_name,
|
|
119
|
+
app_icon=icon_path,
|
|
120
|
+
timeout=timeout,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if icon_path and os.path.exists(icon_path):
|
|
124
|
+
try:
|
|
125
|
+
os.unlink(icon_path)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"[modifyself] Failed to send notification: {e}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Default notification images - using GitHub raw URL
|
|
136
|
+
START_IMAGE = None
|
|
137
|
+
ERROR_IMAGE = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modifyself
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: A clean, pythonic Discord self-bot library
|
|
5
5
|
Author-email: modifyself <modifyself@example.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,7 +15,6 @@ Requires-Dist: wreq>=0.1.0
|
|
|
15
15
|
Requires-Dist: aiohttp>=3.8.0
|
|
16
16
|
Requires-Dist: plyer>=0.9
|
|
17
17
|
Requires-Dist: pillow>=10.0
|
|
18
|
-
Requires-Dist: requests>=2.28
|
|
19
18
|
Requires-Dist: PyNaCl>=1.5.0
|
|
20
19
|
Requires-Dist: opuslib>=3.0.0
|
|
21
20
|
Dynamic: license-file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modifyself"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.8"
|
|
8
8
|
description = "A clean, pythonic Discord self-bot library"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -18,7 +18,6 @@ dependencies = [
|
|
|
18
18
|
"aiohttp>=3.8.0",
|
|
19
19
|
"plyer>=0.9",
|
|
20
20
|
"pillow>=10.0",
|
|
21
|
-
"requests>=2.28",
|
|
22
21
|
"PyNaCl>=1.5.0",
|
|
23
22
|
"opuslib>=3.0.0",
|
|
24
23
|
]
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Pure utility functions. No Discord-specific state here.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
import tempfile
|
|
9
|
-
from datetime import datetime, timezone
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
# Notification imports
|
|
13
|
-
try:
|
|
14
|
-
from plyer import notification
|
|
15
|
-
import requests
|
|
16
|
-
from PIL import Image
|
|
17
|
-
NOTIFICATION_AVAILABLE = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
NOTIFICATION_AVAILABLE = False
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
MARKDOWN_ESCAPE_RE = re.compile(r"([*_{\[\]()~`>\#+\-=|.!])")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def escape_markdown(text: str) -> str:
|
|
26
|
-
"""Escape Discord markdown characters."""
|
|
27
|
-
return MARKDOWN_ESCAPE_RE.sub(r"\\\1", text)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def parse_time(timestamp: str) -> datetime:
|
|
31
|
-
"""Parse an ISO 8601 timestamp from Discord."""
|
|
32
|
-
if timestamp.endswith("Z"):
|
|
33
|
-
timestamp = timestamp[:-1] + "+00:00"
|
|
34
|
-
return datetime.fromisoformat(timestamp)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def snowflake_time(snowflake: int) -> datetime:
|
|
38
|
-
"""Extract the creation time from a snowflake ID."""
|
|
39
|
-
from .core.snowflake import Snowflake
|
|
40
|
-
return datetime.fromtimestamp(
|
|
41
|
-
((snowflake >> 22) + Snowflake.EPOCH) / 1000,
|
|
42
|
-
tz=timezone.utc,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def chunk_list(lst: list, size: int):
|
|
47
|
-
"""Yield successive chunks of a list."""
|
|
48
|
-
for i in range(0, len(lst), size):
|
|
49
|
-
yield lst[i : i + size]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def find(predicate, iterable):
|
|
53
|
-
"""Return the first item in iterable matching predicate."""
|
|
54
|
-
for item in iterable:
|
|
55
|
-
if predicate(item):
|
|
56
|
-
return item
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get(iterable, **attrs):
|
|
61
|
-
"""Return the first item in iterable with matching attributes."""
|
|
62
|
-
for item in iterable:
|
|
63
|
-
if all(getattr(item, k, None) == v for k, v in attrs.items()):
|
|
64
|
-
return item
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def oauth_url(client_id: int, *, permissions: int = 0, guild_id: int | None = None):
|
|
69
|
-
"""Generate an OAuth2 authorization URL."""
|
|
70
|
-
url = f"https://discord.com/oauth2/authorize?client_id={client_id}&scope=bot"
|
|
71
|
-
if permissions:
|
|
72
|
-
url += f"&permissions={permissions}"
|
|
73
|
-
if guild_id:
|
|
74
|
-
url += f"&guild_id={guild_id}"
|
|
75
|
-
return url
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def send_notification(
|
|
79
|
-
title: str,
|
|
80
|
-
message: str,
|
|
81
|
-
image_url: Optional[str] = None,
|
|
82
|
-
timeout: int = 5,
|
|
83
|
-
app_name: str = "modifyself"
|
|
84
|
-
) -> bool:
|
|
85
|
-
"""
|
|
86
|
-
Send a system notification with an optional image.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
title: Notification title
|
|
90
|
-
message: Notification message
|
|
91
|
-
image_url: Optional URL to an image to show as the app icon
|
|
92
|
-
timeout: How long to show the notification (seconds)
|
|
93
|
-
app_name: Name of the app
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
True if notification was sent, False otherwise
|
|
97
|
-
"""
|
|
98
|
-
if not NOTIFICATION_AVAILABLE:
|
|
99
|
-
try:
|
|
100
|
-
import plyer
|
|
101
|
-
except ImportError:
|
|
102
|
-
print("[modifyself] plyer not installed. Install with: pip install plyer pillow requests")
|
|
103
|
-
return False
|
|
104
|
-
except Exception as e:
|
|
105
|
-
print(f"[modifyself] Could not initialize notifications: {e}")
|
|
106
|
-
return False
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
icon_path = None
|
|
110
|
-
|
|
111
|
-
# Download image if provided
|
|
112
|
-
if image_url:
|
|
113
|
-
try:
|
|
114
|
-
response = requests.get(image_url, timeout=10, stream=True)
|
|
115
|
-
if response.status_code == 200:
|
|
116
|
-
# Always use .ico extension for Windows
|
|
117
|
-
is_windows = sys.platform.startswith('win')
|
|
118
|
-
suffix = '.ico' if is_windows else '.png'
|
|
119
|
-
|
|
120
|
-
# Create temp file with correct extension
|
|
121
|
-
icon_temp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
|
122
|
-
icon_temp.write(response.content)
|
|
123
|
-
icon_temp.close() # CRITICAL: Close the file before using it
|
|
124
|
-
icon_path = icon_temp.name
|
|
125
|
-
|
|
126
|
-
# On Windows, ensure it's a valid ICO
|
|
127
|
-
if is_windows:
|
|
128
|
-
try:
|
|
129
|
-
# Open the file to verify it's an image
|
|
130
|
-
with Image.open(icon_path) as img:
|
|
131
|
-
# If it's not already ICO, convert it
|
|
132
|
-
if img.format != 'ICO':
|
|
133
|
-
ico_path = icon_path.replace('.ico', '_converted.ico')
|
|
134
|
-
# Save as ICO with common sizes
|
|
135
|
-
img.save(ico_path, format='ICO', sizes=[(32, 32), (48, 48), (64, 64)])
|
|
136
|
-
# Close the image
|
|
137
|
-
img.close()
|
|
138
|
-
# Delete original
|
|
139
|
-
try:
|
|
140
|
-
os.unlink(icon_path)
|
|
141
|
-
except:
|
|
142
|
-
pass
|
|
143
|
-
icon_path = ico_path
|
|
144
|
-
except Exception as e:
|
|
145
|
-
print(f"[modifyself] ICO conversion warning: {e}")
|
|
146
|
-
# Keep the original if conversion fails
|
|
147
|
-
pass
|
|
148
|
-
else:
|
|
149
|
-
print(f"[modifyself] Could not download notification image (status {response.status_code})")
|
|
150
|
-
except Exception as e:
|
|
151
|
-
print(f"[modifyself] Error downloading notification image: {e}")
|
|
152
|
-
|
|
153
|
-
# Send the notification
|
|
154
|
-
notification.notify(
|
|
155
|
-
title=title,
|
|
156
|
-
message=message,
|
|
157
|
-
app_name=app_name,
|
|
158
|
-
app_icon=icon_path,
|
|
159
|
-
timeout=timeout
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Clean up temp file
|
|
163
|
-
if icon_path and os.path.exists(icon_path):
|
|
164
|
-
try:
|
|
165
|
-
os.unlink(icon_path)
|
|
166
|
-
except Exception:
|
|
167
|
-
pass
|
|
168
|
-
|
|
169
|
-
return True
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
print(f"[modifyself] Failed to send notification: {e}")
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
# Default notification images - using GitHub raw URL
|
|
176
|
-
START_IMAGE = None
|
|
177
|
-
ERROR_IMAGE = None
|
|
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
|
|
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
|