Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
with:
fetch-depth: 1

- name: Set up Python 3.10
uses: actions/setup-python@v4
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: 3.10-dev
python-version: "3.14"

- name: Install Poetry
uses: snok/install-poetry@v1
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,31 @@ If you encounter login issues, you can instead extract your login token from the

You may also set these variables in a `.env` file in the directory from which you are running Truthbrush.

### Public mode (no credentials)

Some Truth Social endpoints are readable without authentication. To run Truthbrush against only those endpoints, pass `--no-auth` on the CLI or construct the client with `require_auth=False`:

```sh
truthbrush --no-auth trends
truthbrush --no-auth user realDonaldTrump
```

```py
from truthbrush import Api

api = Api(require_auth=False)
print(api.trending())
```

Endpoints that require authentication will return an API error (typically HTTP 401) when called in public mode. Which endpoints are publicly accessible is determined by Truth Social and may change without notice.

## CLI Usage

```text
Usage: truthbrush [OPTIONS] COMMAND [ARGS]...

Options:
--no-auth Run without authentication. Only public endpoints will succeed.
--help Show this message and exit.


Expand Down Expand Up @@ -164,8 +183,10 @@ pytest
pytest --log-cli-level=DEBUG -s
```

Please format your code with `black`:
Please format and lint your code with `ruff`, and run `ty` to check types:

```sh
black .
ruff format .
ruff check .
ty check truthbrush/
```
100 changes: 4 additions & 96 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
name = "truthbrush"
version = "0.3.0"
description = "API client for Truth Social"
authors = ["R. Miles McCain <github@sendmiles.email>"]
authors = ["R. Miles McCain <github@sendmiles.email>", "David Thiel"]
license = "Apache 2.0"
readme = "README.md"

[tool.poetry.scripts]
truthbrush = "truthbrush.cli:cli"

[tool.poetry.dependencies]
python = "^3.10"
python = "^3.14"
click = "^8.3.0"
loguru = "^0.7.3"
python-dotenv = "^1.2.0"
Expand All @@ -28,7 +28,7 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.ruff]
target-version = "py310"
target-version = "py314"
line-length = 100

[tool.ruff.lint]
Expand Down
49 changes: 28 additions & 21 deletions test/test_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from dateutil import parser as date_parse
from datetime import UTC

import pytest
from dateutil import parser as date_parse

from truthbrush.api import Api, LoginErrorException

Expand All @@ -13,7 +13,7 @@ def api():

def as_datetime(date_str):
"""Datetime formatter function. Ensures timezone is UTC. Consider moving to Api class."""
return date_parse.parse(date_str).replace(tzinfo=timezone.utc)
return date_parse.parse(date_str).replace(tzinfo=UTC)


def test_lookup(api):
Expand Down Expand Up @@ -58,33 +58,25 @@ def test_pull_statuses(api):
# COMPLETE PULLS

# it fetches a timeline of the user's posts:
full_timeline = list(
api.pull_statuses(username=username, replies=False, verbose=True)
)
full_timeline = list(api.pull_statuses(username=username, replies=False, verbose=True))
assert len(full_timeline) > 25 # more than one page

# the posts are in reverse chronological order:
latest, earliest = full_timeline[0], full_timeline[-1]
latest_at, earliest_at = as_datetime(latest["created_at"]), as_datetime(
earliest["created_at"]
)
latest_at, earliest_at = as_datetime(latest["created_at"]), as_datetime(earliest["created_at"])
assert earliest_at < latest_at

# EMPTY PULLS

# can use created_after param for filtering out posts:
next_pull = list(
api.pull_statuses(
username=username, replies=False, created_after=latest_at, verbose=True
)
api.pull_statuses(username=username, replies=False, created_after=latest_at, verbose=True)
)
assert not any(next_pull)

# can use since_id param for filtering out posts:
next_pull = list(
api.pull_statuses(
username=username, replies=False, since_id=latest["id"], verbose=True
)
api.pull_statuses(username=username, replies=False, since_id=latest["id"], verbose=True)
)
assert not any(next_pull)

Expand All @@ -96,18 +88,14 @@ def test_pull_statuses(api):

# can use created_after param for filtering out posts:
partial_pull = list(
api.pull_statuses(
username=username, replies=False, created_after=recent_at, verbose=True
)
api.pull_statuses(username=username, replies=False, created_after=recent_at, verbose=True)
)
assert len(partial_pull) == n_posts
assert recent["id"] not in [post["id"] for post in partial_pull]

# can use since_id param for filtering out posts:
partial_pull = list(
api.pull_statuses(
username=username, replies=False, since_id=recent["id"], verbose=True
)
api.pull_statuses(username=username, replies=False, since_id=recent["id"], verbose=True)
)
assert len(partial_pull) == n_posts
assert recent["id"] not in [post["id"] for post in partial_pull]
Expand Down Expand Up @@ -155,3 +143,22 @@ def test_pull_statuses(api):
def test_get_auth_id_raises_login_error_exception(api):
with pytest.raises(LoginErrorException):
api.get_auth_id("invalid_username", "invalid_password")


def test_public_mode_does_not_require_credentials(monkeypatch):
monkeypatch.delenv("TRUTHSOCIAL_USERNAME", raising=False)
monkeypatch.delenv("TRUTHSOCIAL_PASSWORD", raising=False)
monkeypatch.delenv("TRUTHSOCIAL_TOKEN", raising=False)
public_api = Api(username=None, password=None, token=None, require_auth=False)
assert public_api.auth_id is None
# user_likes calls __check_login then short-circuits on top_num < 1 before any HTTP.
assert list(public_api.user_likes("abc", top_num=0)) == []


def test_strict_mode_still_raises_without_credentials(monkeypatch):
monkeypatch.delenv("TRUTHSOCIAL_USERNAME", raising=False)
monkeypatch.delenv("TRUTHSOCIAL_PASSWORD", raising=False)
monkeypatch.delenv("TRUTHSOCIAL_TOKEN", raising=False)
strict_api = Api(username=None, password=None, token=None)
with pytest.raises(LoginErrorException):
strict_api.lookup(user_handle="realDonaldTrump")
Loading