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

Make comments an option #28

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
44d80b7
Bump fastapi from 0.88.0 to 0.89.0
dependabot[bot] Jan 9, 2023
add1537
Merge pull request #7 from Deuchnord/dependabot/pip/main/fastapi-0.89.0
Deuchnord Jan 9, 2023
8703382
Fix compatibility with fastapi 0.89
Jan 10, 2023
45ab987
Simplify method to get message, fix note ID
Jan 10, 2023
ef8f185
Add compatibility for Lemmy (#8)
Deuchnord Jan 11, 2023
81a4c2c
Remove the avatar and header endpoints, use URLs instead in configura…
Deuchnord Jan 11, 2023
7678a46
Bump fastapi from 0.89.0 to 0.89.1 (#10)
dependabot[bot] Jan 11, 2023
42ee933
Update docs: remove avatar and header reference
Jan 11, 2023
da2f587
Add pycln to check imports
Jan 12, 2023
f5b3e7b
Fix imports
Jan 12, 2023
47a5764
Refactor inbox handling (#11)
Deuchnord Jan 12, 2023
84fb800
Fix missing field in database, update the feed correctly after #8 (#14)
Deuchnord Jan 12, 2023
5311f24
Bump requests from 2.28.1 to 2.28.2 (#16)
dependabot[bot] Jan 13, 2023
0da3af5
Comments: save responses to the database (#15)
Deuchnord Jan 23, 2023
ca27587
Bump pycln from 2.1.2 to 2.1.3 (#17)
dependabot[bot] Jan 23, 2023
45b84b9
Bump requests from 2.28.1 to 2.28.2 (#21)
dependabot[bot] Jan 23, 2023
ff7f5e8
Bump pytest from 7.2.0 to 7.2.1 (#20)
dependabot[bot] Jan 23, 2023
49c73dc
Database: test init and upgrade (#22)
Deuchnord Jan 24, 2023
046d510
Add Markdown tests (#23)
Deuchnord Jan 24, 2023
f8f5423
Add webserver tests (#25)
Deuchnord Jan 25, 2023
3dbadef
Comments: support delete event (#24)
Deuchnord Jan 25, 2023
7f5261d
Make comments an option
Jan 26, 2023
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
19 changes: 0 additions & 19 deletions .github/workflows/black.yml

This file was deleted.

34 changes: 34 additions & 0 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Code quality

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
black:
name: Check code style
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- uses: psf/[email protected]

pycln:
name: Check imports
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- run: |
pip install poetry
poetry install
- run: |
poetry run pycln --check f2ap tests
74 changes: 74 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
pytest:
name: Unit tests
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
python_version:
- '3.9'
- '3.10'
- '3.11'

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x'
- run: |
pip install poetry
poetry install
- run: |
poetry run pytest --cov=f2ap tests/*.py

- name: Push code coverage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_PARALLEL: true
COVERALLS_FLAG_NAME: "Py${{ matrix.python_version }}_${{ matrix.os }}"
run: |
python3 -m poetry run coveralls --service=github

# Upload generated artifacts only if tests don't pass, to help debugging.
- name: Upload artifacts
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-files
path: tests/files/

coverage:
name: Push coverage report
needs: pytest
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Prepare Python
uses: actions/setup-python@v4
with:
python-version: "3.x"

- name: Install dependencies
run: |
pip install poetry
poetry install

- name: Upload coverage report
run: |
poetry run coveralls --finish --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.toml
!*.dist.toml
*.db
*.bak

!/tests/files/database/upgrade/database-v1.db
.coverage
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ![f2ap](logo.svg)

[![Coverage Status](https://coveralls.io/repos/github/Deuchnord/f2ap/badge.svg?branch=main)](https://coveralls.io/github/Deuchnord/f2ap?branch=main)

f2ap (_Feed to ActivityPub_) is a web application that uses the RSS/Atom feed of your website to expose it on the Fediverse
through ActivityPub.

Expand Down Expand Up @@ -95,12 +97,6 @@ server {
}
}

# Exposes the avatar and the header of the profile
# Change the <username> here with the username of the actor you expose (for instance: blog)
location ~ /actors/<username>/(avatar|header) {
proxy_pass http://127.0.0.1:8000;
}

## ...
}
```
Expand Down
14 changes: 12 additions & 2 deletions config.dist.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ update_freq = 5
[actor]
username = "blog"
display_name = "The most perfect blog of the Web"
avatar = "/path/to/avatar.png"
header = "/path/to/header.jpg"
avatar = "https://example.com/images/avatar.png"
header = "https://example.com/images/header.jpg"
summary = "Why make threads when you can have a blog? 👀"

# A list of people you want the actor to follow, in `@[email protected]` format.
Expand Down Expand Up @@ -60,3 +60,13 @@ format = "[{title}]({url})\n{summary}\n{tags}"
# Available formats: camelCase, CamelCase, snake_case
# Default: camelCase
tag_format = "camelCase"

# A list of groups you want to send the message to, additionally to the followers.
# This is required to get the messages discoverable by some social applications like Lemmy.
groups = []

# Set this to true to enable the comments feature.
# When disabled (default behavior):
# - the comments are silently rejected (social applications still receive an Accepted response to prevent them sending the responses again and again)
# - the API and JS widget are not available
accept_responses = false
144 changes: 138 additions & 6 deletions f2ap/activitypub.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import requests
import logging

from typing import Union
from dateutil import parser as dateparser
from typing import Union, Optional, Callable
from uuid import uuid4

from . import postie, model
from . import postie, model, signature, html
from .config import Configuration
from .markdown import parse_markdown, find_hashtags

W3_PUBLIC_STREAM = "https://www.w3.org/ns/activitystreams#Public"
from .data import Database
from .enum import Visibility
from .exceptions import UnauthorizedHttpError
from .markdown import parse_markdown

MIME_JSON_ACTIVITY = "application/activity+json"

Expand Down Expand Up @@ -37,7 +39,7 @@ def search_actor(domain: str, username: str) -> Union[None, dict]:
return None


def get_actor(href: str):
def get_actor(href: str) -> dict:
try:
actor = requests.get(href, headers={"Accept": "application/activity+json"})
actor.raise_for_status()
Expand Down Expand Up @@ -139,3 +141,133 @@ def propagate_messages(
message.object.content = parse_markdown(message.object.content)
for inbox in inboxes:
postie.deliver(config, inbox, message.dict())


def handle_inbox(
config: Configuration,
db: Database,
headers: dict,
inbox: dict,
on_following_accepted: Callable,
) -> Union[None, tuple[dict, dict]]:
actor = get_actor_from_inbox(db, inbox)
if actor is None:
return

check_message_signature(config, actor, headers, inbox)

return actor, handle_inbox_message(db, inbox, on_following_accepted, config.message.accept_responses)


def get_actor_from_inbox(db: Database, inbox: dict) -> dict:
try:
actor = requests.get(inbox.get("actor"), headers={"Accept": MIME_JSON_ACTIVITY})

actor.raise_for_status()

actor = actor.json()
return actor

except requests.exceptions.HTTPError:
# If the message says the actor has been deleted, delete it from the followers (if they were following)
if inbox.get("type") == "Delete" and inbox.get("actor") == inbox.get("object"):
db.delete_follower(inbox.get("object"))

return None


def check_message_signature(
config: Configuration, actor: dict, headers: dict, inbox: dict
):
public_key_pem = actor.get("publicKey", {}).get("publicKeyPem")

try:
if public_key_pem is None:
raise ValueError("Missing public key on actor.")

signature.validate_headers(
public_key_pem, headers, f"/actors/{config.actor.preferred_username}/inbox"
)

return

except ValueError as e:
logging.debug(f"Could not validate signature: {e.args[0]}. Request rejected.")
logging.debug(f"Headers: {headers}")
logging.debug(f"Public key: {public_key_pem}")
logging.debug(inbox)

raise UnauthorizedHttpError(str(e))


def handle_inbox_message(
db: Database, inbox: dict, on_following_accepted: Callable, accept_responses: bool
) -> Optional[dict]:
if (
inbox.get("type") == "Accept"
and inbox.get("object", {}).get("type") == "Follow"
):
on_following_accepted(inbox.get("object").get("id"), inbox.get("actor"))
logging.debug(f"Following {inbox.get('actor')} successful.")
return

if inbox.get("type") == "Follow":
db.insert_follower(inbox.get("actor"))
return {"type": "Accept", "object": inbox}

if inbox.get("type") == "Undo" and inbox.get("object", {}).get("type") == "Follow":
db.delete_follower(inbox.get("actor"))
return

if accept_responses and inbox.get("type") == "Create" and inbox.get("object", {}).get("type") == "Note":
# Save comments to a note
note = inbox.get("object")
in_reply_to = note.get("inReplyTo")
if in_reply_to is None:
return

replying_to = db.get_note(in_reply_to)
if replying_to is None:
return

content = html.sanitize(note["content"])
published_at = dateparser.isoparse(note["published"])
db.insert_comment(
replying_to,
note["id"],
published_at,
note["attributedTo"],
content,
get_note_visibility(note),
note["tag"],
)

return

if inbox.get("type") == "Delete":
o = inbox.get("object", {})
if not isinstance(o, dict):
logging.debug(f"Tried to delete unsupported object: {inbox}")
return

# Tombstone might be a Note, try to delete it.
# Note: this is always done, even when accept_responses is False, just in case it has been disabled lately.
db.delete_comment(o.get("id"))

return

logging.debug(f"Unsupported message received in the inbox: {inbox}")


def get_note_visibility(note: dict) -> Visibility:
author = get_actor(note.get("attributedTo"))
if author is None:
return Visibility.MENTIONED_ONLY

if model.W3C_ACTIVITYSTREAMS_PUBLIC not in [*note.get("to"), *note.get("cc")]:
if author.get("followers") in [*note.get("to", []), *note.get("cc", [])]:
return Visibility.FOLLOWERS_ONLY

return Visibility.MENTIONED_ONLY

return Visibility.PUBLIC
Loading