Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better error messages #3

Merged
merged 5 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ Use this library to get a summary of results of internet games (like Wordle) in
![Tests](https://github.com/samirg1/ALTER-SMX-Tool/actions/workflows/tests.yml/badge.svg)

### Requirements
- Only works on MacOS iMessages
- Must have messages stored on iCloud (or on Mac)
- Only works on MacOS with iMessages
- Must have messages stored in iCloud (or simply on Mac)
- Only works for group chats
- Having contacts synced to your Mac is not essential but allows for better viewage of results
- Having contacts synced to your Mac as well is not essential but allows for better viewage of results

### Installation
```python
Expand All @@ -21,15 +21,15 @@ chat-summary user chat_name [options]
```

#### Options
- user (required): The name of the user ('your user' from above)
- user (required): The name of the user with the messages
- chat_name (required): The name given to the messages group chat
- --silence-contacts: Silecne the "unable to find contacts" error
- '-C', '--Connections': Include the 'Connections' game in the results
- '-N', '--Nerdle': Include the 'Nerdle' game in the results
- '-W', '--Wordle': Include the 'Wordle' game in the results

### Output
If all goes well, for each game you opt-in to you will be shown an output like this:
For each game you opt-in to you will be shown an output like this:
```
🟨🟨🟨 {game} 🟨🟨🟨

Expand Down Expand Up @@ -63,16 +63,16 @@ Where {game} is replaced by the game that there were no messages for.

### Errors

- ```"invalid user, user not found"```
- ```"iuser not found, user should be one of: ..."```
- user parameter was invalid, check that you've spelt it correctly and that you are a valid user in the ```\Users``` directory
- ```"could not connect to messages database, ensure you have the right permissions to access file"```
- Ensure there is a `chat.db` file in `\Users\{your user}\Library\Messages`, if not your messages are not stored locally on your Mac, try logging in to iMessage on your Mac
- Ensure you have the right permissions to view the `chat.db` by going System Preferences > Securiy & Privacy > Full Disk Access and ensure that Terminal (or whatever you are using to run the script) is ticked
- ```"chat name not found"```
- ```"could not find stored messages, ensure you have signed in and uploaded iMessages to iCloud"```
- Ensure there is a `chat.db` file in `\Users\{your user}\Library\Messages`, if not your messages are not stored locally on your Mac, try logging in to iMessage on your Mac and uploading the messages to iCloud
- ```"chat name not found, should be one of: ..."```
- you have entered an invalid chat_name argument that doesn't match up with any group chats you are currently in on iMessage
- ensure you've spelt this parameter correctly and that you can verify on your Mac's iMessage app that you can see this group chat
- ```"unable to find contacts"```
- not a destructive error, means that the program could not find contacts that were stored on your computer, so instead of displaying people's names it will instead display their phone numbers in the summary, silence this warning with the `--silence-contacts option`
- not a destructive error, means that the program could not find contacts that were stored on your computer, so instead of displaying people's names it will instead display their phone numbers in the summary, silence this warning with the `--silence-contacts` option
- the program will look for a `'AddressBook-v22.abcddb'` file somewhere in `'/Users/{your user}/Library/Application Support/AddressBook/Sources'`

### Add your own!
Expand Down
33 changes: 20 additions & 13 deletions chat_summary/messages.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import sqlite3
import sys
from typing import Any, Generator, cast
from typing import Generator, cast

import pandas
from pandas import DataFrame, Series
Expand All @@ -16,36 +16,43 @@ def __init__(self, user: str, chat_name: str, silence_contact_error: bool) -> No
self._display_name = user
self.silence_contact_error = silence_contact_error

if user not in os.listdir("/Users"):
print("invalid user, user not found", file=sys.stderr)
users = os.listdir("/Users")
if user not in users:
potential_users = ", ".join(f"'{user}'" for user in users if not user.startswith("."))
print(f"user not found, user should be one of: {potential_users}", file=sys.stderr)
exit(1)

try:
self._connection = sqlite3.connect(f"/Users/{user}/Library/Messages/chat.db")
except (sqlite3.OperationalError, FileNotFoundError):
except sqlite3.OperationalError:
print("could not connect to messages database, ensure you have the right permissions to access file", file=sys.stderr)
exit(1)
except FileNotFoundError:
print("could not find stored messages, ensure you have signed in and uploaded iMessages to iCloud", file=sys.stderr)
exit(1)

def _select_chat_id(self) -> int:
chat_id = pandas.read_sql_query( # pyright: ignore[reportUnknownMemberType]
chats = pandas.read_sql_query( # pyright: ignore[reportUnknownMemberType]
f"""
SELECT
ROWID
ROWID, display_name
FROM
chat
WHERE
display_name == "{self._chat_name}"
display_name != ""
""",
self._connection,
)

if chat_id.empty:
print("chat name not found", file=sys.stderr)
rows: Series[int] = chats["ROWID"]
chat_names: Series[str] = chats["display_name"]
try:
return next(row_id for row_id, name in zip(rows, chat_names) if name == self._chat_name) # type: ignore[no-any-return]
except StopIteration:
potential_chats = ", ".join(set(f"'{name}'" for name in chat_names))
print(f"chat name not found, should be one of: {potential_chats}", file=sys.stderr)
exit(1)

items = cast(dict[Any, list[int]], chat_id)
return items["ROWID"][0]

def _get_addressbook_db_path(self) -> str:
address_source_path = f"/Users/{self._user}/Library/Application Support/AddressBook/Sources" # base path
for dir in os.listdir(address_source_path):
Expand Down Expand Up @@ -118,7 +125,7 @@ def _get_chat_members(self, chat_id: int) -> list[ChatMember]:
break

else: # if we did not find a contact, set the contact name to just the number
chat_members.append(ChatMember(number, number)) # pragma: no cover
chat_members.append(ChatMember(number, number))

chat_members.append(ChatMember(self._display_name, ""))
return chat_members
Expand Down
30 changes: 17 additions & 13 deletions testing/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ def close(self) -> None:

return f()

funcs = {0: f0, 1: f1, 2: f2}
def f3():
raise FileNotFoundError

funcs = {0: f0, 1: f1, 2: f2, 3: f3}

info = [funcs[n] for n in reversed(request.param)]

Expand Down Expand Up @@ -80,16 +83,11 @@ def __getitem__(self, v: tuple[int, slice]):
return f"{v[0]}", f"{v[0]}", "0123"
return f"{v[0]}", "0123"

def __init__(self, empty: bool = False, getitem: dict[str, Any] | None = None, len: int = 3) -> None:
self.is_empty = empty
def __init__(self, *, getitem: dict[str, Any] | None = None, len: int = 3) -> None:
self.getitem = getitem
self.len = len
self.iloc = self._iloc(len)

@property
def empty(self) -> bool:
return self.is_empty

def __getitem__(self, value: str): # type: ignore
return self.getitem[value] # type: ignore

Expand All @@ -102,7 +100,7 @@ def test_no_user_found(mock_listdir: None, capture_std_err: dict[str, str]):
with pytest.raises(SystemExit) as sys_exit:
MessagesDB("user", "", False)
assert sys_exit.value.code == 1
assert capture_std_err["err"] == "invalid user, user not found\n"
assert capture_std_err["err"].startswith("user not found, user should be one of: ")


@pytest.mark.parametrize(("mock_listdir", "mock_sql_connect"), [([["user"]], [0])], indirect=True)
Expand All @@ -112,11 +110,17 @@ def test_operational_error(mock_listdir: None, mock_sql_connect: None, capture_s
assert sys_exit.value.code == 1
assert capture_std_err["err"] == "could not connect to messages database, ensure you have the right permissions to access file\n"

@pytest.mark.parametrize(("mock_listdir", "mock_sql_connect"), [([["user"]], [3])], indirect=True)
def test_filenotfound_error(mock_listdir: None, mock_sql_connect: None, capture_std_err: dict[str, str]):
with pytest.raises(SystemExit) as sys_exit:
MessagesDB("user", "", False)
assert sys_exit.value.code == 1
assert capture_std_err["err"] == "could not find stored messages, ensure you have signed in and uploaded iMessages to iCloud\n"

@pytest.mark.parametrize(
("mock_listdir", "mock_read_sql_query", "mock_sql_connect"),
[
([["user"]], [MockObj(True)], [1]),
([["user"]], [MockObj(getitem={"ROWID": [], "display_name": []})], [1]),
],
indirect=True,
)
Expand All @@ -125,7 +129,7 @@ def test_failed_chat_name(mock_listdir: None, mock_read_sql_query: None, mock_sq
with pytest.raises(SystemExit) as sys_exit:
messagedb.get_messages_members_from_chat()
assert sys_exit.value.code == 1
assert capture_std_err["err"] == "chat name not found\n"
assert capture_std_err["err"].startswith("chat name not found, should be one of: ")


@pytest.mark.parametrize(
Expand Down Expand Up @@ -156,7 +160,7 @@ def test_addressbook_path(mock_listdir: None, mock_sql_connect: None):
@pytest.mark.parametrize(
("mock_listdir", "mock_read_sql_query", "mock_sql_connect"),
[
([["user"], []], [MockObj(getitem={"ROWID": [0]}), MockObj(getitem={"id": []}), SystemExit()], [1]),
([["user"], []], [MockObj(getitem={"ROWID": [0], "display_name": ["1"]}), MockObj(getitem={"id": []}), SystemExit()], [1]),
],
indirect=True,
)
Expand All @@ -170,7 +174,7 @@ def test_addressbook_fail(mock_listdir: None, mock_read_sql_query: None, mock_sq
@pytest.mark.parametrize(
("mock_listdir", "mock_read_sql_query", "mock_sql_connect"),
[
([["user"], []], [MockObj(getitem={"ROWID": [0]}), MockObj(getitem={"id": []}), SystemExit()], [1]),
([["user"], []], [MockObj(getitem={"ROWID": [0], "display_name": ["1"]}), MockObj(getitem={"id": []}), SystemExit()], [1]),
],
indirect=True,
)
Expand Down Expand Up @@ -199,7 +203,7 @@ def test_chat_members(mock_listdir: None, mock_read_sql_query: None, mock_sql_co
@pytest.mark.parametrize(
("mock_listdir", "mock_read_sql_query", "mock_sql_connect"),
[
([["user"], []], [MockObj(getitem={"ROWID": [0]}), MockObj(getitem={"id": []}), MockObj(len=2)], [1]),
([["user"], []], [MockObj(getitem={"ROWID": [0], "display_name": ["1"]}), MockObj(getitem={"id": []}), MockObj(len=2)], [1]),
],
indirect=True,
)
Expand Down