Skip to content

Firefish

Firefish docker containers are listed on their website. The source code is available here and general information on the project can be found at joinfirefish.org/.

Usage

For Firefish, we need to configure users through the browser. This means for the initial start, one has to run

git clone https://codeberg.org/helge/funfedidev.git
cd funfedidev/fediverse-pasture

to clone the repository and then

docker compose --file firefish.yml --profile nginx up

to start the server. Finally, we add the following to /etc/hosts

/etc/hosts
127.0.0.1         firefish_web

Afterwards one can open http://firefish_web/ and create an admin user.

Patches

Firefish needs to be patched to do webfinger lookups over http.

Building support tables

We use the following class to obtain information from Firefish. Due to the behavior of the endpoints changing recently, this is currently unstable.

fediverse_pasture.runner.application.firefish.FirefishApplication dataclass

Used to query a firefish application

Source code in fediverse_pasture/runner/application/firefish.py
@dataclass
class FirefishApplication:
    """Used to query a firefish application"""

    domain: str
    username: str
    session: aiohttp.ClientSession | None = None
    token: str = "VvGrKxhpIzJ1PomJFTBObYWOELgGniVi"

    firefish_id: str | None = None

    async def determine_actor_uri(self):
        actor_uri, _ = await bovine.clients.lookup_uri_with_webfinger(
            self.session, f"acct:{self.username}@{self.domain}", f"http://{self.domain}"
        )
        return actor_uri

    async def determine_actor_firefish_id(self):
        """Determines the firefish id for the user on the
        one_actor server, i.e. the one with hostname
        `pasture_one_actor"""
        response = await self.session.post(
            f"http://{self.domain}/api/users/search", data={"query": "actor"}
        )
        users = await response.json()

        for user in users:
            if user.get("host") == "pasture_one_actor":
                return user.get("id")

    async def user_post_with_published(self, published: datetime):
        if not self.firefish_id:
            self.firefish_id = await self.determine_actor_firefish_id()

        response = await self.session.post(
            f"http://{self.domain}/api/users/notes",
            data={"userId": self.firefish_id},
            headers={"Authorization": f"Bearer {self.token}"},
        )
        notes = await response.json()

        for data in notes:
            created_at = data.get("createdAt")
            created_at = datetime.fromisoformat(created_at.removesuffix("Z"))
            if created_at == published:
                return data

        return None

    async def top_public(self):
        response = await self.session.post(
            f"http://{self.domain}/api/notes/global-timeline"
        )
        public_timeline = await response.json()
        return public_timeline[0]

    async def top_public_with_published(self, published: datetime) -> dict | None:
        data = await self.top_public()
        created_at = data.get("createdAt")
        created_at = datetime.fromisoformat(created_at.removesuffix("Z"))
        if created_at == published:
            return data
        return None

    async def last_activity(
        self, session: aiohttp.ClientSession
    ) -> ApplicationAdapterForLastActivity:
        self.session = session

        actor_uri = await self.determine_actor_uri()

        return ApplicationAdapterForLastActivity(
            actor_uri=actor_uri,
            fetch_activity=self.user_post_with_published,
            application_name="firefish",
        )

determine_actor_firefish_id() async

Determines the firefish id for the user on the one_actor server, i.e. the one with hostname `pasture_one_actor

Source code in fediverse_pasture/runner/application/firefish.py
async def determine_actor_firefish_id(self):
    """Determines the firefish id for the user on the
    one_actor server, i.e. the one with hostname
    `pasture_one_actor"""
    response = await self.session.post(
        f"http://{self.domain}/api/users/search", data={"query": "actor"}
    )
    users = await response.json()

    for user in users:
        if user.get("host") == "pasture_one_actor":
            return user.get("id")