
洪 民憙 (Hong Minhee)
@hongminhee@hollo.social
I updated iOS/iPadOS/macOS to version 26, but Liquid Glass is still a bit hard on the eyes. Will I ever get used to this new look and feel…?
@hongminhee@hollo.social · 955 following · 1269 followers
An intersectionalist, feminist, and socialist guy living in Seoul (UTC+09:00). @tokolovesme's spouse. Who's behind @fedify, @hollo, and @botkit. Write some free software in #TypeScript, #Haskell, #Rust, & #Python. They/them.
서울에 사는 交叉女性主義者이자 社會主義者. 金剛兔(@tokolovesme)의 配偶者. @fedify, @hollo, @botkit 메인테이너. #TypeScript, #Haskell, #Rust, #Python 等으로 自由 소프트웨어 만듦.
Website | GitHub | Blog | Hackers' Pub |
---|---|---|---|
@hongminhee@hollo.social
I updated iOS/iPadOS/macOS to version 26, but Liquid Glass is still a bit hard on the eyes. Will I ever get used to this new look and feel…?
@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post
Upyo 0.3.0をリリースしました。複数のメールプロバイダー間で自動フェイルオーバーができるプールトランスポートと、ResendとPlunkのサポートを追加。メール配信の信頼性が大幅に向上します。
@hongminhee@hackers.pub
Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.
The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.
The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.
import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 50 },
{ transport: emergencyProvider, priority: 10 },
],
maxRetries: 3,
});
const receipt = await pool.send(message);
This transport proves particularly valuable for high-availability systems that
cannot tolerate email delivery failures. It also enables cost optimization by
routing bulk emails to more economical providers while sending transactional
emails through premium services. Organizations migrating between email providers
can use weighted distribution to gradually shift traffic from one provider to
another. The pool transport handles resource cleanup properly through
AsyncDisposable
support and provides comprehensive error reporting that
aggregates failures from all attempted providers.
For detailed configuration options and usage patterns, refer to the pool transport documentation.
npm add @upyo/pool
pnpm add @upyo/pool
yarn add @upyo/pool
deno add jsr:@upyo/pool
bun add @upyo/pool
The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.
One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});
const receipt = await transport.send(message);
Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.
The Resend transport guide provides comprehensive documentation on configuration and advanced features.
npm add @upyo/resend
pnpm add @upyo/resend
yarn add @upyo/resend
deno add jsr:@upyo/resend
bun add @upyo/resend
The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.
For many teams, the ability to self-host email infrastructure is crucial for
compliance or data sovereignty reasons. Plunk's self-hosted option runs as
a Docker container using the driaug/plunk
image, giving you complete control
over your email infrastructure while maintaining a simple, modern API. The same
codebase works seamlessly with both cloud and self-hosted instances, requiring
only a different base URL configuration.
import { PlunkTransport } from "@upyo/plunk";
// Cloud-hosted
const cloudTransport = new PlunkTransport({
apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});
// Self-hosted
const selfHostedTransport = new PlunkTransport({
apiKey: "your-self-hosted-api-key",
baseUrl: "https://mail.yourcompany.com/api",
});
The Plunk transport includes the production features you'd expect, such as
retry logic with exponential backoff, comprehensive error handling, and support
for attachments (up to 5 per message as per Plunk's API limits). Message
organization through tags helps track different types of emails, while priority
levels ensure urgent messages receive appropriate handling. The transport also
supports request cancellation through AbortSignal
, allowing your application
to gracefully handle timeouts and user-initiated cancellations.
Complete documentation and deployment guidance is available in the Plunk transport documentation.
npm add @upyo/plunk
pnpm add @upyo/plunk
yarn add @upyo/plunk
deno add jsr:@upyo/plunk
bun add @upyo/plunk
All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:
import { createMessage } from "@upyo/core";
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo!",
content: { text: "Works with any transport!" },
});
const receipt = await transport.send(message);
We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.
For the complete changelog and technical details, see CHANGES.md.
For questions or issues, please visit our GitHub repository.
@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post
Upyo 0.3.0을 릴리스했습니다. 이제 여러 이메일 제공 업체 간 자동 페일오버를 가능하게 해주는 풀 트랜스포트와 Resend, Plunk 트랜스포트가 추가되었습니다. 한 이메일 제공 업체가 다운되어도 다른 이메일 제공 업체를 통해 이메일이 계속 전송될 수 있습니다.
@hongminhee@hackers.pub
Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.
The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.
The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.
import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 50 },
{ transport: emergencyProvider, priority: 10 },
],
maxRetries: 3,
});
const receipt = await pool.send(message);
This transport proves particularly valuable for high-availability systems that
cannot tolerate email delivery failures. It also enables cost optimization by
routing bulk emails to more economical providers while sending transactional
emails through premium services. Organizations migrating between email providers
can use weighted distribution to gradually shift traffic from one provider to
another. The pool transport handles resource cleanup properly through
AsyncDisposable
support and provides comprehensive error reporting that
aggregates failures from all attempted providers.
For detailed configuration options and usage patterns, refer to the pool transport documentation.
npm add @upyo/pool
pnpm add @upyo/pool
yarn add @upyo/pool
deno add jsr:@upyo/pool
bun add @upyo/pool
The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.
One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});
const receipt = await transport.send(message);
Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.
The Resend transport guide provides comprehensive documentation on configuration and advanced features.
npm add @upyo/resend
pnpm add @upyo/resend
yarn add @upyo/resend
deno add jsr:@upyo/resend
bun add @upyo/resend
The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.
For many teams, the ability to self-host email infrastructure is crucial for
compliance or data sovereignty reasons. Plunk's self-hosted option runs as
a Docker container using the driaug/plunk
image, giving you complete control
over your email infrastructure while maintaining a simple, modern API. The same
codebase works seamlessly with both cloud and self-hosted instances, requiring
only a different base URL configuration.
import { PlunkTransport } from "@upyo/plunk";
// Cloud-hosted
const cloudTransport = new PlunkTransport({
apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});
// Self-hosted
const selfHostedTransport = new PlunkTransport({
apiKey: "your-self-hosted-api-key",
baseUrl: "https://mail.yourcompany.com/api",
});
The Plunk transport includes the production features you'd expect, such as
retry logic with exponential backoff, comprehensive error handling, and support
for attachments (up to 5 per message as per Plunk's API limits). Message
organization through tags helps track different types of emails, while priority
levels ensure urgent messages receive appropriate handling. The transport also
supports request cancellation through AbortSignal
, allowing your application
to gracefully handle timeouts and user-initiated cancellations.
Complete documentation and deployment guidance is available in the Plunk transport documentation.
npm add @upyo/plunk
pnpm add @upyo/plunk
yarn add @upyo/plunk
deno add jsr:@upyo/plunk
bun add @upyo/plunk
All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:
import { createMessage } from "@upyo/core";
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo!",
content: { text: "Works with any transport!" },
});
const receipt = await transport.send(message);
We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.
For the complete changelog and technical details, see CHANGES.md.
For questions or issues, please visit our GitHub repository.
@hongminhee@hollo.social
Just released Upyo 0.3.0! Now with pool transport for multi-provider failover, plus Resend and Plunk support. Your emails can now automatically fail over between providers when one goes down.
@hongminhee@hackers.pub
Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.
The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.
The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.
import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 50 },
{ transport: emergencyProvider, priority: 10 },
],
maxRetries: 3,
});
const receipt = await pool.send(message);
This transport proves particularly valuable for high-availability systems that
cannot tolerate email delivery failures. It also enables cost optimization by
routing bulk emails to more economical providers while sending transactional
emails through premium services. Organizations migrating between email providers
can use weighted distribution to gradually shift traffic from one provider to
another. The pool transport handles resource cleanup properly through
AsyncDisposable
support and provides comprehensive error reporting that
aggregates failures from all attempted providers.
For detailed configuration options and usage patterns, refer to the pool transport documentation.
npm add @upyo/pool
pnpm add @upyo/pool
yarn add @upyo/pool
deno add jsr:@upyo/pool
bun add @upyo/pool
The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.
One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});
const receipt = await transport.send(message);
Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.
The Resend transport guide provides comprehensive documentation on configuration and advanced features.
npm add @upyo/resend
pnpm add @upyo/resend
yarn add @upyo/resend
deno add jsr:@upyo/resend
bun add @upyo/resend
The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.
For many teams, the ability to self-host email infrastructure is crucial for
compliance or data sovereignty reasons. Plunk's self-hosted option runs as
a Docker container using the driaug/plunk
image, giving you complete control
over your email infrastructure while maintaining a simple, modern API. The same
codebase works seamlessly with both cloud and self-hosted instances, requiring
only a different base URL configuration.
import { PlunkTransport } from "@upyo/plunk";
// Cloud-hosted
const cloudTransport = new PlunkTransport({
apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});
// Self-hosted
const selfHostedTransport = new PlunkTransport({
apiKey: "your-self-hosted-api-key",
baseUrl: "https://mail.yourcompany.com/api",
});
The Plunk transport includes the production features you'd expect, such as
retry logic with exponential backoff, comprehensive error handling, and support
for attachments (up to 5 per message as per Plunk's API limits). Message
organization through tags helps track different types of emails, while priority
levels ensure urgent messages receive appropriate handling. The transport also
supports request cancellation through AbortSignal
, allowing your application
to gracefully handle timeouts and user-initiated cancellations.
Complete documentation and deployment guidance is available in the Plunk transport documentation.
npm add @upyo/plunk
pnpm add @upyo/plunk
yarn add @upyo/plunk
deno add jsr:@upyo/plunk
bun add @upyo/plunk
All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:
import { createMessage } from "@upyo/core";
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo!",
content: { text: "Works with any transport!" },
});
const receipt = await transport.send(message);
We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.
For the complete changelog and technical details, see CHANGES.md.
For questions or issues, please visit our GitHub repository.
@hongminhee@hackers.pub
Upyo 0.3.0 introduces three new transports that expand the library's email delivery capabilities. This release focuses on improving reliability through multi-provider support and offering more deployment options to suit different organizational needs.
The new @upyo/pool package introduces a pool transport that combines multiple email providers into a single transport. This allows applications to distribute email traffic across different providers and automatically fail over when one provider experiences issues.
The pool transport supports several routing strategies. Round-robin distribution cycles through providers evenly, while weighted distribution allows you to send more traffic through preferred providers. Priority-based routing always attempts the highest priority transport first, falling back to others only when needed. For more complex scenarios, you can implement custom routing based on message content, recipient domains, or any other criteria.
import { PoolTransport } from "@upyo/pool";
import { SmtpTransport } from "@upyo/smtp";
import { MailgunTransport } from "@upyo/mailgun";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 50 },
{ transport: emergencyProvider, priority: 10 },
],
maxRetries: 3,
});
const receipt = await pool.send(message);
This transport proves particularly valuable for high-availability systems that
cannot tolerate email delivery failures. It also enables cost optimization by
routing bulk emails to more economical providers while sending transactional
emails through premium services. Organizations migrating between email providers
can use weighted distribution to gradually shift traffic from one provider to
another. The pool transport handles resource cleanup properly through
AsyncDisposable
support and provides comprehensive error reporting that
aggregates failures from all attempted providers.
For detailed configuration options and usage patterns, refer to the pool transport documentation.
npm add @upyo/pool
pnpm add @upyo/pool
yarn add @upyo/pool
deno add jsr:@upyo/pool
bun add @upyo/pool
The @upyo/resend package adds support for Resend, a modern email service provider designed with developer experience in mind. Resend focuses on simplicity without sacrificing the features needed for production applications.
One of Resend's strengths is its intelligent batch optimization. When sending multiple emails, the transport automatically determines the most efficient sending method based on message characteristics. Messages without attachments are sent using Resend's batch API for optimal performance, while the transport seamlessly falls back to individual requests when needed. This optimization happens transparently, requiring no additional configuration.
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: "re_1234567890abcdef_1234567890abcdef1234567890",
});
const receipt = await transport.send(message);
Resend also provides built-in idempotency to prevent duplicate sends during network issues or application retries. The transport automatically generates idempotency keys, though you can provide custom ones when needed. Combined with comprehensive retry logic using exponential backoff, this ensures reliable delivery even during temporary service interruptions. Message tagging support helps organize emails and track performance across different types of communications through Resend's analytics dashboard.
The Resend transport guide provides comprehensive documentation on configuration and advanced features.
npm add @upyo/resend
pnpm add @upyo/resend
yarn add @upyo/resend
deno add jsr:@upyo/resend
bun add @upyo/resend
The @upyo/plunk package brings support for Plunk, an email service that offers both cloud-hosted and self-hosted deployment options. This flexibility makes Plunk an interesting choice for organizations with specific infrastructure requirements.
For many teams, the ability to self-host email infrastructure is crucial for
compliance or data sovereignty reasons. Plunk's self-hosted option runs as
a Docker container using the driaug/plunk
image, giving you complete control
over your email infrastructure while maintaining a simple, modern API. The same
codebase works seamlessly with both cloud and self-hosted instances, requiring
only a different base URL configuration.
import { PlunkTransport } from "@upyo/plunk";
// Cloud-hosted
const cloudTransport = new PlunkTransport({
apiKey: "sk_1234567890abcdef1234567890abcdef1234567890abcdef",
});
// Self-hosted
const selfHostedTransport = new PlunkTransport({
apiKey: "your-self-hosted-api-key",
baseUrl: "https://mail.yourcompany.com/api",
});
The Plunk transport includes the production features you'd expect, such as
retry logic with exponential backoff, comprehensive error handling, and support
for attachments (up to 5 per message as per Plunk's API limits). Message
organization through tags helps track different types of emails, while priority
levels ensure urgent messages receive appropriate handling. The transport also
supports request cancellation through AbortSignal
, allowing your application
to gracefully handle timeouts and user-initiated cancellations.
Complete documentation and deployment guidance is available in the Plunk transport documentation.
npm add @upyo/plunk
pnpm add @upyo/plunk
yarn add @upyo/plunk
deno add jsr:@upyo/plunk
bun add @upyo/plunk
All new transports maintain Upyo's consistent API design, making them drop-in replacements for existing transports. The same message creation and sending code works with any transport:
import { createMessage } from "@upyo/core";
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo!",
content: { text: "Works with any transport!" },
});
const receipt = await transport.send(message);
We continue to work on expanding Upyo's transport options while maintaining the library's focus on simplicity, type safety, and cross-runtime compatibility. Your feedback and contributions help shape the project's direction.
For the complete changelog and technical details, see CHANGES.md.
For questions or issues, please visit our GitHub repository.
@cheeaun@mastodon.social
Kinda disappointed that the resize cursor doesn't smoothly rotate around the curves of the window edges.
@campanula@qdon.space
내년에 한국에서 마츠다 세이코 라이브하는구나
진짜 일본 아티스트들 내한 공연은 시대정신인건가
@cocoa@hackers.pub
This tutorial will guide you through building a simple ActivityPub bot using Python. The bot will listen for mentions and, when it receives a message in a specific format, it will schedule and send a reminder back to the user after a specified delay.
For example, if a user mentions the bot with a message like "@reminder@your.host.com 10m check the oven
", the bot will reply 10 minutes later with a message like "🔔 Reminder for @user: check the oven
".
To follow this tutorial, you will need Python 3.10+ and the following libraries:
server
extra, which includes FastAPI-based components.You can install these dependencies using uv
or pip
.
# Initialize a new project with uv
uv init
# Install dependencies
uv add "apkit[server]" uvicorn cryptography
The project structure is minimal, consisting of a single Python file for our bot's logic.
.
├── main.py
└── private_key.pem
main.py
: Contains all the code for the bot.private_key.pem
: The private key for the bot's Actor. This will be generated automatically on the first run.Our application logic can be broken down into the following steps:
apkit
ActivityPub server./actor
, /inbox
, etc.).Let's dive into each section of the main.py
file.
First, we import the necessary modules and define the basic configuration for our bot.
# main.py
import asyncio
import logging
import re
import uuid
import os
from datetime import timedelta, datetime
# Imports from FastAPI, cryptography, and apkit
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization as crypto_serialization
from apkit.config import AppConfig
from apkit.server import ActivityPubServer
from apkit.server.types import Context, ActorKey
from apkit.server.responses import ActivityResponse
from apkit.models import (
Actor, Application, CryptographicKey, Follow, Create, Note, Mention, Actor as APKitActor, OrderedCollection,
)
from apkit.client import WebfingerResource, WebfingerResult, WebfingerLink
from apkit.client.asyncio.client import ActivityPubClient
# --- Logging Setup ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Basic Configuration ---
HOST = "your.host.com" # Replace with your domain
USER_ID = "reminder" # The bot's username
Make sure to replace your.host.com
with the actual domain where your bot will be hosted. These values determine your bot's unique identifier (e.g., @reminder@your.host.com
).
ActivityPub uses HTTP Signatures to secure communication between servers. This requires each actor to have a public/private key pair. The following code generates a private key and saves it to a file if one doesn't already exist.
# main.py (continued)
# --- Key Persistence ---
KEY_FILE = "private_key.pem"
# Load the private key if it exists, otherwise generate a new one
if os.path.exists(KEY_FILE):
logger.info(f"Loading existing private key from {KEY_FILE}.")
with open(KEY_FILE, "rb") as f:
private_key = crypto_serialization.load_pem_private_key(f.read(), password=None)
else:
logger.info(f"No key file found. Generating new private key and saving to {KEY_FILE}.")
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(KEY_FILE, "wb") as f:
f.write(private_key.private_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PrivateFormat.PKCS8,
encryption_algorithm=crypto_serialization.NoEncryption()
))
# Generate the public key from the private key
public_key_pem = private_key.public_key().public_bytes(
encoding=crypto_serialization.Encoding.PEM,
format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
Next, we define the bot's Actor. The Actor is the bot's identity in the ActivityPub network. We use the Application
type, as this entity is automated.
# main.py (continued)
# --- Actor Definition ---
actor = Application(
id=f"https://{HOST}/actor",
name="Reminder Bot",
preferredUsername=USER_ID,
summary="A bot that sends you reminders. Mention me like: @reminder 5m Check the oven",
inbox=f"https://{HOST}/inbox", # Endpoint for receiving activities
outbox=f"https://{HOST}/outbox", # Endpoint for sending activities
publicKey=CryptographicKey(
id=f"https://{HOST}/actor#main-key",
owner=f"https://{HOST}/actor",
publicKeyPem=public_key_pem
)
)
We initialize the ActivityPubServer
from apkit
, providing it with a function to retrieve our Actor's keys for signing outgoing activities.
# main.py (continued)
# --- Key Retrieval Function ---
async def get_keys_for_actor(identifier: str) -> list[ActorKey]:
"""Returns the key for a given Actor ID."""
if identifier == actor.id:
return [ActorKey(key_id=actor.publicKey.id, private_key=private_key)]
return []
# --- Server Initialization ---
app = ActivityPubServer(apkit_config=AppConfig(
actor_keys=get_keys_for_actor # Register the key retrieval function
))
To serve created activities, we need to store them somewhere. For simplicity, this example uses a basic in-memory dictionary as a store and a cache. In a production application, you would replace this with a persistent database (like SQLite or PostgreSQL) and a proper cache (like Redis).
# main.py (continued)
# --- In-memory Store and Cache ---
ACTIVITY_STORE = {} # A simple dict to store created activities
CACHE = {} # A cache for recently accessed activities
CACHE_TTL = timedelta(minutes=5) # Cache expiration time (5 minutes)
This is the core logic of our bot. The parse_reminder
function uses a regular expression to extract the delay and message from a mention, and send_reminder
schedules the notification.
# main.py (continued)
# --- Reminder Parsing Logic ---
def parse_reminder(text: str) -> tuple[timedelta | None, str | None, str | None]:
"""Parses reminder text like '5m do something'."""
# ... (implementation omitted for brevity)
# --- Reminder Sending Function ---
async def send_reminder(ctx: Context, delay: timedelta, message: str, target_actor: APKitActor, original_note: Note):
"""Waits for a specified delay and then sends a reminder."""
logger.info(f"Scheduling reminder for {target_actor.id} in {delay}: '{message}'")
await asyncio.sleep(delay.total_seconds()) # Asynchronously wait
logger.info(f"Sending reminder to {target_actor.id}")
# Create the reminder Note
reminder_note = Note(...)
# Wrap it in a Create activity
reminder_create = Create(...)
# Store the created activities
ACTIVITY_STORE[reminder_note.id] = reminder_note
ACTIVITY_STORE[reminder_create.id] = reminder_create
# Send the activity to the target actor's inbox
keys = await get_keys_for_actor(f"https://{HOST}/actor")
await ctx.send(keys, target_actor, reminder_create)
logger.info(f"Reminder sent to {target_actor.id}")
We define the required ActivityPub endpoints. Since apkit
is built on FastAPI, we can use standard FastAPI decorators. The main endpoints are:
@user@host
. This is a crucial first step for federation.apkit
handles this route automatically, directing activities to the handlers we'll define in the next step.Here is the code for defining these endpoints:
# main.py (continued)
# The inbox endpoint is handled by apkit automatically.
app.inbox("/inbox")
@app.webfinger()
async def webfinger_endpoint(request: Request, acct: WebfingerResource) -> Response:
"""Handles Webfinger requests to make the bot discoverable."""
if not acct.url:
# Handle resource queries like acct:user@host
if acct.username == USER_ID and acct.host == HOST:
link = WebfingerLink(rel="self", type="application/activity+json", href=actor.id)
wf_result = WebfingerResult(subject=acct, links=[link])
return JSONResponse(wf_result.to_json(), media_type="application/jrd+json")
else:
# Handle resource queries using a URL
if acct.url == f"https://{HOST}/actor":
link = WebfingerLink(rel="self", type="application/activity+json", href=actor.id)
wf_result = WebfingerResult(subject=acct, links=[link])
return JSONResponse(wf_result.to_json(), media_type="application/jrd+json")
return JSONResponse({"message": "Not Found"}, status_code=404)
@app.get("/actor")
async def get_actor_endpoint():
"""Serves the bot's Actor object."""
return ActivityResponse(actor)
@app.get("/outbox")
async def get_outbox_endpoint():
"""Serves a collection of the bot's sent activities."""
items = sorted(ACTIVITY_STORE.values(), key=lambda x: x.id, reverse=True)
outbox_collection = OrderedCollection(
id=actor.outbox,
totalItems=len(items),
orderedItems=items
)
return ActivityResponse(outbox_collection)
@app.get("/notes/{note_id}")
async def get_note_endpoint(note_id: uuid.UUID):
"""Serves a specific Note object, with caching."""
note_uri = f"https://{HOST}/notes/{note_id}"
# Check cache first
if note_uri in CACHE and (datetime.now() - CACHE[note_uri]["timestamp"]) < CACHE_TTL:
return ActivityResponse(CACHE[note_uri]["activity"])
# If not in cache, get from store
if note_uri in ACTIVITY_STORE:
activity = ACTIVITY_STORE[note_uri]
# Add to cache before returning
CACHE[note_uri] = {"activity": activity, "timestamp": datetime.now()}
return ActivityResponse(activity)
return Response(status_code=404) # Not Found
@app.get("/creates/{create_id}")
async def get_create_endpoint(create_id: uuid.UUID):
"""Serves a specific Create activity, with caching."""
create_uri = f"https://{HOST}/creates/{create_id}"
if create_uri in CACHE and (datetime.now() - CACHE[create_uri]["timestamp"]) < CACHE_TTL:
return ActivityResponse(CACHE[create_uri]["activity"])
if create_uri in ACTIVITY_STORE:
activity = ACTIVITY_STORE[create_uri]
CACHE[create_uri] = {"activity": activity, "timestamp": datetime.now()}
return ActivityResponse(activity)
return Response(status_code=404)
We use the @app.on()
decorator to define handlers for specific activity types posted to our inbox.
Follow
requests.Create
activities (specifically for Note
objects) to schedule reminders.# main.py (continued)
# Handler for Follow activities
@app.on(Follow)
async def on_follow_activity(ctx: Context):
"""Automatically accepts follow requests."""
# ... (implementation omitted for brevity)
# Handler for Create activities
@app.on(Create)
async def on_create_activity(ctx: Context):
"""Parses mentions to schedule reminders."""
activity = ctx.activity
# Ignore if it's not a Note
if not (isinstance(activity, Create) and isinstance(activity.object, Note)):
return Response(status_code=202)
note = activity.object
# Check if the bot was mentioned
is_mentioned = any(
isinstance(tag, Mention) and tag.href == actor.id for tag in (note.tag or [])
)
if not is_mentioned:
return Response(status_code=202)
# ... (Parse reminder text)
delay, message, time_str = parse_reminder(command_text)
# If parsing is successful, schedule the reminder as a background task
if delay and message and sender_actor:
asyncio.create_task(send_reminder(ctx, delay, message, sender_actor, note))
reply_content = f"<p>✅ OK! I will remind you in {time_str}.</p>"
else:
# If parsing fails, send usage instructions
reply_content = "<p>🤔 Sorry, I didn\'t understand. Please use the format: `@reminder [time] [message]`.</p><p>Example: `@reminder 10m Check the oven`</p>"
# ... (Create and send the reply Note)
Finally, we run the application using uvicorn
.
# main.py (continued)
if __name__ == "__main__":
import uvicorn
logger.info("Starting uvicorn server...")
uvicorn.run(app, host="0.0.0.0", port=8000)
Set the HOST
and USER_ID
variables in main.py
to match your environment.
Run the server from your terminal:
uvicorn main:app --host 0.0.0.0 --port 8000
Your bot will be running at http://0.0.0.0:8000
.
Now you can mention your bot from anywhere in the Fediverse (e.g., @reminder@your.host.com
) to set a reminder.
This tutorial covers the basics of creating a simple ActivityPub bot. Since it only uses in-memory storage, all reminders will be lost on server restart. Here are some potential improvements:
ACTIVITY_STORE
with a database like SQLite or PostgreSQL.We hope this guide serves as a good starting point for building your own ActivityPub applications!
https://fedi-libs.github.io/apkit/
@me@social.perlmint.app · Reply to perlmint's post
여행 정산 하는 중에 만든 정산 시트 템플릿 대충 정산할 항목 환율이 여럿인 경우와 건별 결제자, 균등 분할 금액, 개별 금액 처리등을 커버합니다.
@yijuckhangwe@qdon.space
마쓰시타 류이치 저자의 『동아시아반일무장전선』
추천
@hannal@hackers.pub
이미지로만 보던 그 유명한 파도 그림을 보고 왔습니다. 실제로 보니 질감과 세밀함이 정말 감동스럽더라고요. 작품 교체를 하는데, 후지산이나 파도 그림은 전반기에 전시하는 것 같아요. 전반기는 11월 2일까지.
"국립 "청주" 박물관에서 전시합니나. "충주" 아니에요. 이상 충주에 가서 밥 먹고 청주에 가서 관람한 1인이었슴미다.
@kodingwarrior@hackers.pub
@z9mb1 @catamorphic @msy @joonnot @d01c2 @icecream_mable @minju2996 @2chanhaeng @lionhairdino @akastoot @jihyeok @linea @woaol @theeluwin @rangho @robin @1ho @nebuleto @moreal @harei106 @realfishbread @jcha0713 @hyunjoon @krepe90 @cetaceanightsky @aioo @ink_punctuation @yg1ee
오늘 Hackers' Public 행사 첫번째 모임에 와주셔서 정말 감사합니다! 정말 즐거웠습니다! 잘 들어가세요!
RE: https://hackers.pub/@kodingwarrior/0198e9db-763b-7135-aa89-1808e9e99227
@kodingwarrior@hackers.pub
서울에서 열리는 Hackers' Pub 오프라인 밋업, "Hackers' Public @ Seoul"이 2025월 9월 14일(일) 처음으로 개최됩니다. 처음 열리는 밋업인 만큼, 참여하는 많은 분들이 재밌게 느낄 수 있는 소재 위주로 연사자 분들을 섭외했습니다.
강연이 끝나고 난 뒤에 자유롭게 네트워킹하는 시간을 가질 예정입니다. 각자 얘기하고 싶은 주제를 들고 오시면 좋습니다.
오프라인 밋업은 여기서 참여신청이 가능합니다. https://event-us.kr/hackerspubseoul/event/110961
@hongminhee@hackers.pub
오늘은 Hackers' Public 첫 번째 모임! 다들 튜링의 사과(@TuringAppleDev)에서 오후 3시에 모여요!
@yichm@g0v.social
滅人器
@hongminhee@hollo.social
By the way, if anyone wants to create an #ActivityPub #bot server, there's also @botkit as an option.
@evan@cosocial.ca · Reply to Evan Prodromou's post
The update was hard; the jsonld.js library was choking on the FEP's context document because Codeberg Pages, where FEP context documents are hosted, is unreliable.
I ended up adding a feature to the `activitystrea.ms` NodeJS library to let you pre-cache a context document, so it didn't have to go fetch it at run time. While I was under the hood, I also fixed a couple of bugs that made it hard to work with extra context documents.
The bot server is now working; I'm going to add more contexts.
@evan@cosocial.ca
I updated the #ActivityPubBot server software to use FEP-5711, the inverse properties FEP. It is a way to declare two-way relations; if X has an `inbox` property with value Y, then Y has an `inboxOf` property with value X. It helps with navigation when you're using the ActivityPub API, and it also helps with spoofing (since both objects agree on the relationship).
@hongminhee@hackers.pub
@robin@hackers.pub
밋업때 뭔가 보여줘야 할 거 같아서 열심히 달리는중
@robin@hackers.pub
밋업때 뭔가 보여줘야 할 거 같아서 열심히 달리는중
@fedify@hollo.social
We've just published an experimental pre-release version 1.9.0-pr.431.1597 that adds CommonJS support to all npm packages in the Fedify ecosystem! 🧪
This experimental build addresses one of the most requested features—better compatibility with CommonJS-based Node.js applications, especially NestJS projects. The pre-release eliminates the need for Node.js's --experimental-require-module
flag and resolves dual package hazard issues.
Note: While we now support CommonJS for legacy project compatibility, we still recommend using ESM or migrating to Deno for the best experience with Fedify.
Install the experimental pre-release version:
npm install @fedify/fedify@1.9.0-pr.431.1597
# or for specific integrations
npm install @fedify/nestjs@1.9.0-pr.431.1597
You should now be able to use standard require()
syntax without any experimental flags.
--experimental-require-module
flag?Your feedback on this experimental build is invaluable for ensuring this major compatibility improvement works smoothly before the official 1.9.0 release!
@erincandescent@erincandescent.net
Whoa, as of 10 days ago we have functions to go between a Uint8Array
and a Base64 Normal/URL Padded/Unpadded string in JavaScript without having to go via a String of “bytes” and manually juggle the padding in every major browser.
What a time to be alive.
@2chanhaeng@hackers.pub
페디파이 빡시게 기여해서 2.0 뜨면 블로그 만들어서 블스 떠야지
@robin@hackers.pub
무슨 연합우주 스캐너 같은 게 있나...?
@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post
久しぶりにブログに記事を書きました。「オープンソース開発の近況」
@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'s post
오랜만에 블로그에 글을 썼습니다: 〈오픈 소스 開發 近況〉.
@hongminhee@hollo.social
I wrote a post on my blog after a long time: Recent open source development updates.
@trwnh@mastodon.social · Reply to Emelia 👸🏻's post
@thisismissem @mariusor @oranadoz @hongminhee +1, an image and a descriptor are different things and should be treated as different things. content negotiation is not a solution here -- the same information should be returned for the same resource (modulo whichever representation you ask for or receive).
@trwnh@mastodon.social · Reply to marius's post
@mariusor @hongminhee > the same URL can represent both
bad idea. an identifier should unambiguously refer to exactly 1 thing
@trwnh@mastodon.social · Reply to Evan Prodromou's post
@evan @hongminhee more and more i am thinking that Link was a bad idea from a data modeling perspective. "assume bare href instead of bare id" is something that can never make sense. if we really want to maintain validity of Link then it should *always* be embedded as an anonymous object:
icon: {
type: Image
url:
{
type: Link
href: foo
height: 400
width: 400
mediaType: image/png
}
}
here, Image.url means "representation of the Image"
@zenn_dev@rss-mstdn.studiofreesia.com
TypeScriptの型推論でCLIバリデーションをなくせた話
https://zenn.dev/hongminhee/articles/27bce6eb777dea