chatwire 0.2.2__tar.gz → 0.3.0__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 (45) hide show
  1. {chatwire-0.2.2/chatwire.egg-info → chatwire-0.3.0}/PKG-INFO +17 -1
  2. {chatwire-0.2.2 → chatwire-0.3.0}/README.md +16 -0
  3. {chatwire-0.2.2 → chatwire-0.3.0}/_version.py +1 -1
  4. {chatwire-0.2.2 → chatwire-0.3.0}/bridge.py +1 -1
  5. {chatwire-0.2.2 → chatwire-0.3.0/chatwire.egg-info}/PKG-INFO +17 -1
  6. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire.egg-info/SOURCES.txt +1 -0
  7. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire_cli.py +12 -2
  8. {chatwire-0.2.2 → chatwire-0.3.0}/config.py +55 -1
  9. {chatwire-0.2.2 → chatwire-0.3.0}/echo_log.py +4 -3
  10. chatwire-0.3.0/migrations/0003_state_dir_paths.py +30 -0
  11. {chatwire-0.2.2 → chatwire-0.3.0}/web/main.py +5 -4
  12. {chatwire-0.2.2 → chatwire-0.3.0}/web/static/update-check.js +6 -1
  13. {chatwire-0.2.2 → chatwire-0.3.0}/web/templates/index.html +1 -1
  14. {chatwire-0.2.2 → chatwire-0.3.0}/whitelist.py +3 -2
  15. {chatwire-0.2.2 → chatwire-0.3.0}/LICENSE +0 -0
  16. {chatwire-0.2.2 → chatwire-0.3.0}/chat_db.py +0 -0
  17. {chatwire-0.2.2 → chatwire-0.3.0}/chat_send.py +0 -0
  18. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire.egg-info/dependency_links.txt +0 -0
  19. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire.egg-info/entry_points.txt +0 -0
  20. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire.egg-info/requires.txt +0 -0
  21. {chatwire-0.2.2 → chatwire-0.3.0}/chatwire.egg-info/top_level.txt +0 -0
  22. {chatwire-0.2.2 → chatwire-0.3.0}/contacts.py +0 -0
  23. {chatwire-0.2.2 → chatwire-0.3.0}/integrations/__init__.py +0 -0
  24. {chatwire-0.2.2 → chatwire-0.3.0}/integrations/base.py +0 -0
  25. {chatwire-0.2.2 → chatwire-0.3.0}/integrations/telegram/__init__.py +0 -0
  26. {chatwire-0.2.2 → chatwire-0.3.0}/integrations/web/__init__.py +0 -0
  27. {chatwire-0.2.2 → chatwire-0.3.0}/integrations/webhook/__init__.py +0 -0
  28. {chatwire-0.2.2 → chatwire-0.3.0}/migrations/0001_initial.py +0 -0
  29. {chatwire-0.2.2 → chatwire-0.3.0}/migrations/0002_integration_split.py +0 -0
  30. {chatwire-0.2.2 → chatwire-0.3.0}/migrations/__init__.py +0 -0
  31. {chatwire-0.2.2 → chatwire-0.3.0}/prefix.py +0 -0
  32. {chatwire-0.2.2 → chatwire-0.3.0}/pyproject.toml +0 -0
  33. {chatwire-0.2.2 → chatwire-0.3.0}/setup.cfg +0 -0
  34. {chatwire-0.2.2 → chatwire-0.3.0}/templates/__init__.py +0 -0
  35. {chatwire-0.2.2 → chatwire-0.3.0}/templates/launchd/bridge.plist.template +0 -0
  36. {chatwire-0.2.2 → chatwire-0.3.0}/templates/launchd/keepawake.plist.template +0 -0
  37. {chatwire-0.2.2 → chatwire-0.3.0}/templates/launchd/web.plist.template +0 -0
  38. {chatwire-0.2.2 → chatwire-0.3.0}/web/setup_wizard.py +0 -0
  39. {chatwire-0.2.2 → chatwire-0.3.0}/web/static/favicon.svg +0 -0
  40. {chatwire-0.2.2 → chatwire-0.3.0}/web/static/style.css +0 -0
  41. {chatwire-0.2.2 → chatwire-0.3.0}/web/static/sw.js +0 -0
  42. {chatwire-0.2.2 → chatwire-0.3.0}/web/templates/_conversation.html +0 -0
  43. {chatwire-0.2.2 → chatwire-0.3.0}/web/templates/_conversations.html +0 -0
  44. {chatwire-0.2.2 → chatwire-0.3.0}/web/templates/_settings.html +0 -0
  45. {chatwire-0.2.2 → chatwire-0.3.0}/web/templates/_whitelist_rows.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatwire
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: macOS chat-relay bridge: iMessage <-> Telegram, web UI, and pluggable integrations
5
5
  Author: Allen Bina
6
6
  License: MIT License
@@ -184,6 +184,22 @@ the first install, and it's the same on every Mac.
184
184
  `scripts/check-permissions.sh` (or `chatwire doctor`) will tell you
185
185
  which prompts you still need to click.
186
186
 
187
+ ## Privacy
188
+
189
+ **No telemetry.** chatwire collects no analytics, sends no usage data, and
190
+ includes no third-party SDKs that report back. Your messages, contacts,
191
+ and `chat.db` stay on your Mac; outbound traffic only goes to integrations
192
+ you configure (e.g. Telegram via your own bot token).
193
+
194
+ Two narrow third-party requests the web UI does make, neither carrying any
195
+ of your data:
196
+
197
+ - The update-check banner fetches `api.github.com/repos/<repo>/releases/latest`
198
+ once a day to surface new-version notices. Disable by setting
199
+ `UPDATE_CHECK_REPO=""` in the launchd agent's environment.
200
+ - Static assets (htmx, emoji-picker-element) load from `unpkg.com` and
201
+ `cdn.jsdelivr.net`.
202
+
187
203
  ## Repo layout
188
204
 
189
205
  ```
@@ -128,6 +128,22 @@ the first install, and it's the same on every Mac.
128
128
  `scripts/check-permissions.sh` (or `chatwire doctor`) will tell you
129
129
  which prompts you still need to click.
130
130
 
131
+ ## Privacy
132
+
133
+ **No telemetry.** chatwire collects no analytics, sends no usage data, and
134
+ includes no third-party SDKs that report back. Your messages, contacts,
135
+ and `chat.db` stay on your Mac; outbound traffic only goes to integrations
136
+ you configure (e.g. Telegram via your own bot token).
137
+
138
+ Two narrow third-party requests the web UI does make, neither carrying any
139
+ of your data:
140
+
141
+ - The update-check banner fetches `api.github.com/repos/<repo>/releases/latest`
142
+ once a day to surface new-version notices. Disable by setting
143
+ `UPDATE_CHECK_REPO=""` in the launchd agent's environment.
144
+ - Static assets (htmx, emoji-picker-element) load from `unpkg.com` and
145
+ `cdn.jsdelivr.net`.
146
+
131
147
  ## Repo layout
132
148
 
133
149
  ```
@@ -9,4 +9,4 @@ Format: PEP 440-flavored semver. Pre-1.0 dev builds carry a `-dev` suffix
9
9
  which the update-check JS treats as "skip the check" (no point pinging
10
10
  GitHub when you cloned from main).
11
11
  """
12
- __version__ = "0.2.2"
12
+ __version__ = "0.3.0"
@@ -30,6 +30,7 @@ import inspect
30
30
 
31
31
  import config as _bridge_config # noqa: E402 — must run before env-consuming imports
32
32
  CFG = _bridge_config.apply_to_environ()
33
+ from config import STATE_DIR # noqa: E402
33
34
 
34
35
  from chat_db import ChatDBReader, InboundMessage
35
36
  from contacts import load_lookup as load_contacts
@@ -66,7 +67,6 @@ POLL_INTERVAL_S = float(os.environ.get("POLL_INTERVAL_S", "2"))
66
67
  # normal operation; safe to `tail -f` from a separate SSH session.
67
68
  DEBUG_MIRROR_FILE = os.environ.get("DEBUG_MIRROR_FILE", "").strip() or None
68
69
 
69
- STATE_DIR = Path.home() / ".imessage-tg"
70
70
  STATE_DIR.mkdir(parents=True, exist_ok=True)
71
71
  STATE_PATH = STATE_DIR / "state.json"
72
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatwire
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: macOS chat-relay bridge: iMessage <-> Telegram, web UI, and pluggable integrations
5
5
  Author: Allen Bina
6
6
  License: MIT License
@@ -184,6 +184,22 @@ the first install, and it's the same on every Mac.
184
184
  `scripts/check-permissions.sh` (or `chatwire doctor`) will tell you
185
185
  which prompts you still need to click.
186
186
 
187
+ ## Privacy
188
+
189
+ **No telemetry.** chatwire collects no analytics, sends no usage data, and
190
+ includes no third-party SDKs that report back. Your messages, contacts,
191
+ and `chat.db` stay on your Mac; outbound traffic only goes to integrations
192
+ you configure (e.g. Telegram via your own bot token).
193
+
194
+ Two narrow third-party requests the web UI does make, neither carrying any
195
+ of your data:
196
+
197
+ - The update-check banner fetches `api.github.com/repos/<repo>/releases/latest`
198
+ once a day to surface new-version notices. Disable by setting
199
+ `UPDATE_CHECK_REPO=""` in the launchd agent's environment.
200
+ - Static assets (htmx, emoji-picker-element) load from `unpkg.com` and
201
+ `cdn.jsdelivr.net`.
202
+
187
203
  ## Repo layout
188
204
 
189
205
  ```
@@ -24,6 +24,7 @@ integrations/web/__init__.py
24
24
  integrations/webhook/__init__.py
25
25
  migrations/0001_initial.py
26
26
  migrations/0002_integration_split.py
27
+ migrations/0003_state_dir_paths.py
27
28
  migrations/__init__.py
28
29
  templates/__init__.py
29
30
  templates/launchd/bridge.plist.template
@@ -276,10 +276,20 @@ def cmd_doctor(args: argparse.Namespace) -> int:
276
276
 
277
277
 
278
278
  def cmd_migrate(args: argparse.Namespace) -> int:
279
- ran = config.migrate_legacy_env()
280
- if ran:
279
+ env_ran = config.migrate_legacy_env()
280
+ if env_ran:
281
281
  print(f"migrated {config.LEGACY_ENV_PATH} → {config.CONFIG_PATH}")
282
282
  print(f"chmod 600 enforced. legacy file left in place — delete after verification.")
283
+
284
+ state_copied = config.migrate_state_dir()
285
+ if state_copied:
286
+ print(f"copied {len(state_copied)} state file(s) "
287
+ f"{config.LEGACY_STATE_DIR} → {config.STATE_DIR}: "
288
+ f"{', '.join(state_copied)}")
289
+ print(f"legacy dir left in place — `rm -rf {config.LEGACY_STATE_DIR}` "
290
+ f"after verifying agents are healthy.")
291
+
292
+ if env_ran or state_copied:
283
293
  return 0
284
294
  if config.CONFIG_PATH.exists():
285
295
  print(f"already migrated: {config.CONFIG_PATH} exists. nothing to do.")
@@ -22,6 +22,7 @@ from __future__ import annotations
22
22
  import json
23
23
  import logging
24
24
  import os
25
+ import shutil
25
26
  import stat
26
27
  from pathlib import Path
27
28
 
@@ -30,6 +31,13 @@ log = logging.getLogger("chatwire.config")
30
31
  CONFIG_DIR = Path.home() / ".chatwire"
31
32
  CONFIG_PATH = CONFIG_DIR / "config.json"
32
33
 
34
+ # Runtime state files (state.json, whitelist.json, echo_log.jsonl, mirror.jsonl,
35
+ # push_subs.json, thumb_cache/) live alongside config under the same dir.
36
+ # Same path as CONFIG_DIR by design — exposing it as its own name lets state
37
+ # consumers (bridge.py, whitelist.py, echo_log.py, web/main.py) import the
38
+ # concept without pretending they're reading config.
39
+ STATE_DIR = CONFIG_DIR
40
+
33
41
  # Read-only fallbacks for prior config locations. First save under the new
34
42
  # CONFIG_PATH migrates a user off the legacy path. Order matters: most
35
43
  # recent first, so a user mid-rename gets the freshest snapshot.
@@ -41,7 +49,19 @@ LEGACY_CONFIG_PATHS: list[Path] = [
41
49
  LEGACY_ENV_DIR = Path.home() / ".imessage-tg"
42
50
  LEGACY_ENV_PATH = LEGACY_ENV_DIR / ".env"
43
51
 
44
- CURRENT_VERSION = 2
52
+ # Pre-0.3.0 the runtime state lived alongside the legacy .env in this dir.
53
+ # `migrate_state_dir()` copies these into STATE_DIR on first 0.3.0 boot.
54
+ LEGACY_STATE_DIR = LEGACY_ENV_DIR
55
+ STATE_FILES = (
56
+ "state.json",
57
+ "whitelist.json",
58
+ "echo_log.jsonl",
59
+ "mirror.jsonl",
60
+ "push_subs.json",
61
+ "thumb_cache", # directory
62
+ )
63
+
64
+ CURRENT_VERSION = 3
45
65
 
46
66
 
47
67
  def _read_legacy_env() -> dict[str, str]:
@@ -122,6 +142,7 @@ def apply_to_environ() -> dict:
122
142
  if path.exists():
123
143
  _ensure_secure(path)
124
144
  break
145
+ migrate_state_dir()
125
146
  cfg = load_config()
126
147
  cfg = _run_migrations(cfg)
127
148
  for k, v in _flatten_v2_to_env(cfg).items():
@@ -217,6 +238,39 @@ def save_config(cfg: dict) -> None:
217
238
  tmp.replace(CONFIG_PATH)
218
239
 
219
240
 
241
+ def migrate_state_dir() -> list[str]:
242
+ """Copy any pre-0.3.0 state files from LEGACY_STATE_DIR into STATE_DIR.
243
+
244
+ Idempotent by file-existence check: a file is only copied when the
245
+ destination doesn't already exist. The legacy dir is left in place so
246
+ the operator can verify and remove it manually.
247
+
248
+ Returns the list of file/dir names that were copied (empty list = no-op).
249
+ """
250
+ if not LEGACY_STATE_DIR.exists() or LEGACY_STATE_DIR == STATE_DIR:
251
+ return []
252
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
253
+ copied: list[str] = []
254
+ for name in STATE_FILES:
255
+ src = LEGACY_STATE_DIR / name
256
+ dst = STATE_DIR / name
257
+ if not src.exists() or dst.exists():
258
+ continue
259
+ try:
260
+ if src.is_dir():
261
+ shutil.copytree(src, dst)
262
+ else:
263
+ shutil.copy2(src, dst)
264
+ except OSError:
265
+ log.exception("failed to copy %s → %s", src, dst)
266
+ continue
267
+ copied.append(name)
268
+ if copied:
269
+ log.info("migrated %d state file(s) from %s → %s: %s",
270
+ len(copied), LEGACY_STATE_DIR, STATE_DIR, ", ".join(copied))
271
+ return copied
272
+
273
+
220
274
  def migrate_legacy_env() -> bool:
221
275
  """If config.json is absent and a legacy .env exists, write config.json
222
276
  from it. Returns True if a migration ran. Idempotent: a no-op once
@@ -2,7 +2,7 @@
2
2
  agree on "what we just sent" — used to suppress chat.db echoes of bridge-
3
3
  originated outbound. Both processes append; the bridge consults on each poll.
4
4
 
5
- Format: JSONL at ~/.imessage-tg/echo_log.jsonl
5
+ Format: JSONL at ~/.chatwire/echo_log.jsonl
6
6
  {"t": <epoch>, "h": <handle_lc>, "k": "text"|"photo", "b": <body or null>}
7
7
 
8
8
  Tail-only consumer: `seen_recently` reads the last ~200 lines and checks
@@ -12,9 +12,10 @@ from __future__ import annotations
12
12
 
13
13
  import json
14
14
  import time
15
- from pathlib import Path
16
15
 
17
- LOG = Path.home() / ".imessage-tg" / "echo_log.jsonl"
16
+ from config import STATE_DIR
17
+
18
+ LOG = STATE_DIR / "echo_log.jsonl"
18
19
  LOG.parent.mkdir(parents=True, exist_ok=True)
19
20
  TAIL_LINES = 200
20
21
 
@@ -0,0 +1,30 @@
1
+ """Repoint `debug.mirror_file` from the legacy state dir to the new one.
2
+
3
+ 0.3.0 moves runtime state from `~/.imessage-tg/` to `~/.chatwire/` (see
4
+ `config.migrate_state_dir`). The mirror file is the one piece of state
5
+ whose location the user can override via config, so an explicit user
6
+ value at the legacy *default* gets bumped here. Anything pointed
7
+ elsewhere on purpose is left alone.
8
+
9
+ Pairs with the filesystem-level copy in `migrate_state_dir()` — that
10
+ moves the file's bytes; this updates the pointer in config.json.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ target_version = 3
17
+
18
+
19
+ def migrate(cfg: dict) -> dict:
20
+ debug = cfg.get("debug")
21
+ if not isinstance(debug, dict):
22
+ return cfg
23
+ current = debug.get("mirror_file")
24
+ if not isinstance(current, str):
25
+ return cfg
26
+ legacy_default = str(Path.home() / ".imessage-tg" / "mirror.jsonl")
27
+ new_default = str(Path.home() / ".chatwire" / "mirror.jsonl")
28
+ if current == legacy_default:
29
+ debug["mirror_file"] = new_default
30
+ return cfg
@@ -26,7 +26,7 @@ import logging
26
26
  from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
27
27
 
28
28
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
29
- log = logging.getLogger("imessage-tg-web")
29
+ log = logging.getLogger("chatwire.web")
30
30
  from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
31
31
  from fastapi.staticfiles import StaticFiles
32
32
  from fastapi.templating import Jinja2Templates
@@ -49,6 +49,7 @@ from whitelist import ( # noqa: E402
49
49
  # Load config from ~/.chatwire/config.json (or legacy fallbacks).
50
50
  import config as _bridge_config # noqa: E402
51
51
  _bridge_config.apply_to_environ()
52
+ from config import STATE_DIR # noqa: E402
52
53
 
53
54
  SELF_HANDLES = {h.strip().lower() for h in os.environ.get("SELF_HANDLES", "").split(",") if h.strip()}
54
55
 
@@ -57,7 +58,7 @@ def relay_handles() -> set[str]:
57
58
  return SELF_HANDLES | wl_all()
58
59
 
59
60
 
60
- MIRROR_FILE = Path(os.environ.get("DEBUG_MIRROR_FILE", str(Path.home() / ".imessage-tg/mirror.jsonl")))
61
+ MIRROR_FILE = Path(os.environ.get("DEBUG_MIRROR_FILE", str(STATE_DIR / "mirror.jsonl")))
61
62
 
62
63
  WEB_PORT = int(os.environ.get("WEB_PORT", "8723"))
63
64
  HISTORY_LIMIT = 100
@@ -66,7 +67,7 @@ CONVO_LIMIT = 50
66
67
  VAPID_PRIVATE_KEY = os.environ.get("VAPID_PRIVATE_KEY", "")
67
68
  VAPID_PUBLIC_KEY = os.environ.get("VAPID_PUBLIC_KEY", "")
68
69
  VAPID_CONTACT = os.environ.get("VAPID_CONTACT", "mailto:admin@example.com")
69
- PUSH_SUBS_FILE = Path.home() / ".imessage-tg" / "push_subs.json"
70
+ PUSH_SUBS_FILE = STATE_DIR / "push_subs.json"
70
71
 
71
72
  CONTACTS = load_contacts()
72
73
  IMAGE_INDEX = load_image_index()
@@ -232,7 +233,7 @@ ATTACHMENTS_BASE = (Path.home() / "Library" / "Messages" / "Attachments").resolv
232
233
  # On-disk thumb cache. Lives outside the Messages.app attachments dir so we
233
234
  # never risk Messages noticing extra files alongside the originals. Keyed by
234
235
  # (path, mtime) so renamed/replaced originals invalidate cleanly.
235
- THUMB_CACHE_DIR = (Path.home() / ".imessage-tg" / "thumb_cache").resolve()
236
+ THUMB_CACHE_DIR = (STATE_DIR / "thumb_cache").resolve()
236
237
  THUMB_MAX_EDGE = 720 # px; covers retina at the chat's ~280–360 displayed size
237
238
  THUMB_TTL_DAYS = 180
238
239
 
@@ -58,12 +58,17 @@
58
58
  }
59
59
 
60
60
  function showBanner(latestTag, releaseUrl) {
61
+ if (!latestTag) return;
61
62
  const banner = document.getElementById('update-banner');
62
63
  if (!banner) return;
63
64
  const v = banner.querySelector('.update-banner-version');
64
65
  const a = banner.querySelector('.update-banner-link');
65
66
  if (v) v.textContent = latestTag;
66
- if (a && releaseUrl) a.setAttribute('href', releaseUrl);
67
+ if (a) {
68
+ const repo = meta('update-check-repo');
69
+ const fallback = repo ? 'https://github.com/' + repo + '/releases' : '#';
70
+ a.setAttribute('href', releaseUrl || fallback);
71
+ }
67
72
  banner.hidden = false;
68
73
  document.body.classList.add('has-update-banner');
69
74
 
@@ -20,7 +20,7 @@
20
20
  <strong>Update available:</strong>
21
21
  <span class="update-banner-version"></span>
22
22
  &mdash;
23
- <a class="update-banner-link" href="" target="_blank" rel="noopener">release notes</a>
23
+ <a class="update-banner-link" href="#" target="_blank" rel="noopener">release notes</a>
24
24
  </span>
25
25
  <button type="button" class="update-banner-dismiss" aria-label="Dismiss">&times;</button>
26
26
  </div>
@@ -20,11 +20,12 @@ import json
20
20
  import logging
21
21
  import os
22
22
  import threading
23
- from pathlib import Path
23
+
24
+ from config import STATE_DIR
24
25
 
25
26
  log = logging.getLogger("whitelist")
26
27
 
27
- WHITELIST_FILE = Path.home() / ".imessage-tg" / "whitelist.json"
28
+ WHITELIST_FILE = STATE_DIR / "whitelist.json"
28
29
 
29
30
  _lock = threading.Lock()
30
31
  _cached_handles: set[str] = set()
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