Hashtag

#페디버스

92 posts tagged with this hashtag.

@botkit@hackers.pub

Note

이 글은 BotKit의 공식 튜토리얼 Building an RSS bot(영문)을 한국어로 옮긴 것입니다. 원문은 계속 업데이트되므로, 최신 내용은 위 링크에서 확인하는 편이 좋습니다.

BotKit의 시작하기 안내서를 따라 하면 몇 분 만에 봇을 띄울 수 있습니다. 다만 그 봇은 인사말에 답할 뿐, 딱히 감시할 대상은 없습니다. 이 튜토리얼은 거기서 한 걸음 더 나아갑니다. RSS나 Atom, RDF 피드를 주시하다가 새 글이 올라오면 페디버스에 게시하는 봇을 만듭니다. 그 과정에서 예약 작업, 상태 저장, 멘션에 답장하기를 다루고, 나중에는 피드가 하나에서 여럿으로 늘어났을 때 필요해지는 createInstance()와 동적 봇 그룹까지 짚어 봅니다.

1부에서는 피드 하나를 감시하는 봇 하나를 만듭니다. 2부에서는 그 봇을 인스턴스로 바꿔서, 인스턴스를 멘션하며 피드 URL을 알려주는 것만으로 피드별 봇을 하나씩 등록할 수 있게 합니다. 각 부의 마지막에는 봇을 공개 인터넷에 노출시키고, ActivityPub.Academy에서 실제 Mastodon 계정으로 팔로우하고 멘션하고 지켜보며 테스트합니다.

이 튜토리얼은 시작하기를 이미 읽었다는 전제로 진행하므로, createBot()이나 Text를 처음부터 다시 설명하지는 않습니다. 완성된 프로젝트는 BotKit 저장소의 examples/rss-bot/에서도 볼 수 있습니다.

봇 하나로 시작하기

프로젝트 준비하기

새 디렉터리를 만들고, BotKit과 함께 피드의 원본 XML을 평범한 객체로 바꿔 줄 @rowanmanning/feed-parser를 설치합니다.

Deno

mkdir rss-bot && cd rss-bot
deno add jsr:@fedify/botkit npm:@rowanmanning/feed-parser

npm

mkdir rss-bot && cd rss-bot
npm init -y
npm add @fedify/botkit @rowanmanning/feed-parser

pnpm

mkdir rss-bot && cd rss-bot
pnpm init
pnpm add @fedify/botkit @rowanmanning/feed-parser

Yarn

mkdir rss-bot && cd rss-bot
yarn init -y
yarn add @fedify/botkit @rowanmanning/feed-parser

피드 가져오고 파싱하기

@rowanmanning/feed-parser는 RSS 0.9x, RSS 2.0, RDF Site Summary 1.0(RSS 1.0), Atom 0.3/1.0 피드를 모두 같은 parseFeed() 함수, 같은 Feed/FeedItem 형태로 파싱해 줍니다. 덕분에 봇 코드 쪽에서는 지금 이 피드가 어떤 포맷인지 굳이 분기할 필요가 없습니다.

parseFeed()가 하지 않는 일도 있습니다. 바로 가져오기(fetch)입니다. 이 함수는 이미 손에 쥔 XML 문자열을 객체로 바꿔 줄 뿐, 그 문자열을 어떻게 구할지는 호출하는 쪽의 몫입니다. 봇 코드도 이 구분을 그대로 따라서, 가져오기를 전담하는 feed.ts 모듈을 두고, 이를 어떤 주기로 폴링할지는 별도로 다룹니다.

feed.ts

// 피드(RSS, Atom, RDF)를 가져오고 파싱한다. 가져오기는 이 모듈의 일이고,
// 파싱은 전적으로 @rowanmanning/feed-parser의 일이다. 이 라이브러리는 피드를
// 그저 XML 문자열로만 볼 뿐, 그 문자열이 어떻게 조달됐는지는 신경 쓰지 않는다.

import { parseFeed } from "@rowanmanning/feed-parser";

// @rowanmanning/feed-parser의 공개 엔트리포인트는 parseFeed 함수만 내보낼 뿐
// 반환값인 Feed/FeedItem 타입은 내보내지 않는다. 그래서 패키지 내부 경로를
// 직접 뒤지는 대신, 함수의 반환 타입에서 역으로 타입을 끌어온다.
export type Feed = ReturnType<typeof parseFeed>;
export type FeedItem = Feed["items"][number];

export async function fetchFeed(
  url: string | URL,
  signal?: AbortSignal,
): Promise<Feed> {
  const response = await fetch(url, { signal });
  if (!response.ok) {
    throw new Error(
      `Failed to fetch feed: ${response.status} ${response.statusText}`,
    );
  }
  return parseFeed(await response.text());
}

맨 위의 타입 유도 코드는 자잘한 우회로입니다. @rowanmanning/feed-parser 패키지는 parseFeed 함수만 내보낼 뿐 그 함수가 반환하는 Feed, FeedItem 클래스는 내보내지 않으므로, type { Feed }를 패키지에서 직접 가져오려 하면 실패합니다. ReturnType<typeof parseFeed>는 타입을 직접 이름 붙여 가져오는 대신, 함수 시그니처로부터 TypeScript가 스스로 타입을 유추하게 만들어 이 문제를 피해 갑니다.

폴링하고 게시하기

봇 자체는 단순하게 시작합니다. 봇을 만들고, 일정 주기로 피드를 폴링하고, 새로 올라온 글을 게시하는 것뿐입니다. 식별자를 비롯한 나머지 설정은 시작하기에서 본 패턴을 그대로 따르는데, 다만 여기서 쓰는 kv 스토어와 queue는 로컬 개발용 인메모리 구현입니다. 재시작이 문제가 되는 시점에 다른 것으로 교체할 예정입니다.

bot.ts

// RSS/Atom/RDF 피드를 일정 주기로 폴링하다가 새 글이 올라오면 페디버스에
// 게시하는 봇.
//
// 실행:  deno serve --allow-net --allow-env --watch bot.ts
//   또는: npx srvx serve --port 8000 --entry ./bot.ts
// 설정:  ORIGIN=https://your-domain
//        FEED_URL=https://example.com/feed.xml (기본값은 Hacker News)
//        POLL_INTERVAL_MS=600000 (기본값은 10분)

import {
  createBot,
  InProcessMessageQueue,
  link,
  MemoryKvStore,
  text,
} from "@fedify/botkit";
import { fetchFeed } from "./feed.ts";
import type { FeedItem } from "./feed.ts";

const FEED_URL = process.env.FEED_URL ?? "https://news.ycombinator.com/rss";
const ORIGIN = process.env.ORIGIN ?? "http://localhost:8000";

const rawPollIntervalMs = process.env.POLL_INTERVAL_MS;
const POLL_INTERVAL_MS = rawPollIntervalMs == null || rawPollIntervalMs === ""
  ? 1000 * 60 * 10
  : Number(rawPollIntervalMs);
if (!Number.isFinite(POLL_INTERVAL_MS) || POLL_INTERVAL_MS <= 0) {
  throw new RangeError(
    `POLL_INTERVAL_MS must be a positive number of milliseconds: ${rawPollIntervalMs}`,
  );
}

const bot = createBot<void>({
  username: "rssbot",
  name: "RSS Bot",
  summary: text`I watch ${link(FEED_URL)} and post new entries here.`,
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
});

function itemKey(item: FeedItem): string | null {
  return item.id ?? item.url;
}

const posted = new Set<string>();
let firstPoll = true;

async function poll(): Promise<void> {
  const feed = await fetchFeed(FEED_URL);
  const items = [...feed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  if (firstPoll) {
    // 봇을 맨 처음 띄운 순간 피드에 이미 올라와 있던 글 전부를 팔로워에게
    // 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만 "새 글"로 친다.
    firstPoll = false;
    for (const item of items) {
      const key = itemKey(item);
      if (key != null) posted.add(key);
    }
    console.log(`Baseline: ${posted.size} existing item(s) from ${FEED_URL}.`);
    return;
  }

  const session = bot.getSession(ORIGIN);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || posted.has(key)) continue;
    await session.publish(
      text`${item.title ?? "(untitled)"}

${link(item.url ?? FEED_URL)}`,
    );
    posted.add(key);
    publishedCount++;
  }
  console.log(`Posted ${publishedCount} new item(s) from ${FEED_URL}.`);
}

let polling = false;

async function pollOnce(): Promise<void> {
  if (polling) return; // 이전 폴링이 아직 진행 중이면 건너뛴다
  polling = true;
  try {
    await poll();
  } catch (error) {
    console.error("Failed to poll feed:", error);
  } finally {
    polling = false;
  }
}

pollOnce();
setInterval(pollOnce, POLL_INTERVAL_MS);

export default bot;

맨 처음 폴링에서는 피드에 이미 올라와 있던 글을 기록만 할 뿐, 게시하지는 않습니다. 이 처리가 없다면 봇을 처음 실행하는 순간 피드의 현재 첫 페이지 전체가 팔로워들에게 쏟아지는데, 이는 “새 글”이 뜻해야 할 바가 전혀 아닙니다. 다음 폴링에서 새로 나타나는 글만 새 글로 칩니다.

pollOnce()는 실제 폴링 로직을 polling 플래그로 감쌉니다. 느린 요청이 다음 예약 틱과 겹치지 않도록 하기 위해서입니다. setInterval()은 콜백이 끝나기를 기다리지 않고 다음 호출을 예약하므로, 이 방어 장치가 없으면 POLL_INTERVAL_MS보다 오래 걸리는 피드 요청 때문에 폴링 두 개가 동시에 돌면서 같은 글을 중복 게시할 수 있습니다.

POLL_INTERVAL_MS는 시작 시점에 한 번 파싱하고, 유한한 양수인지 검사합니다. 값이 비어 있거나 잘못돼서 그대로 setInterval(fn, NaN)으로 흘러 들어가면 에러도 나지 않고, 그냥 이벤트 루프의 거의 매 틱마다 실행되며 피드를 두들겨 댑니다. 실행 시점에 요란하게 실패하는 편이, 실행 중에 조용히 잘못되는 것보다 낫습니다.

FEED_URL의 기본값은 Hacker News의 첫 페이지 피드입니다. RSS든 Atom이든 RDF든 어떤 피드를 써도 되지만, 테스트하는 동안에는 자주 갱신되는 피드를 쓰는 편이 좋습니다. 봇이 실제로 뭔가 게시하는 모습을 보려고 오래 기다릴 필요가 없기 때문입니다.

봇 실행하기

시작하기에서와 마찬가지로 봇을 기본 내보내기(default export)로 내보내고 실행합니다.

Deno

deno serve --allow-net --allow-env --watch bot.ts

Node.js

npx srvx serve --port 8000 --entry ./bot.ts

콘솔에는 몇 초 안에 베이스라인 개수가 찍히고, 이후 POLL_INTERVAL_MS가 지나면 Posted 0 new item(s)가 뜨거나, 그사이 피드에 뭔가 새로 올라왔다면 실제로 글이 하나 게시됩니다. 10분을 기다리지 않고 바로 게시되는 모습을 보고 싶다면 지금은 주기를 짧게 잡아 봅니다.

POLL_INTERVAL_MS=60000 deno serve --allow-net --allow-env --watch bot.ts

Tip

fedify lookup은 URL이나 핸들로 임의의 액터나 오브젝트를 조회해서 ActivityPub 표현을 출력해 줍니다. 여기서 fedify lookup http://localhost:8000/ap/actor/bot을 실행해 보면, 더 나아가기 전에 봇의 액터 문서가 제대로 나오는지 빠르게 확인할 수 있습니다.

멘션에 답장하기

게시만 하고 응답은 전혀 하지 않는 봇은, 잘 돌아가고 있어도 죽은 것처럼 느껴집니다. onMention은 봇에게 할 말을 만들어 줍니다. 지금 어떤 피드를 보고 있는지, 얼마나 자주 확인하는지 같은 것들 말이죠.

bot.ts

function formatInterval(ms: number): string {
  if (ms < 60_000) {
    const seconds = Math.round(ms / 1000);
    return `${seconds} second${seconds === 1 ? "" : "s"}`;
  }
  const minutes = Math.round(ms / 60_000);
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
}

let feedTitle: string | null = null;

bot.onMention = async (_session, message) => {
  await message.reply(
    text`I'm watching ${
      link(feedTitle ?? FEED_URL, FEED_URL)
    } and check for new posts every ${formatInterval(POLL_INTERVAL_MS)}.`,
  );
};

feedTitle은 poll()이 실행될 때마다 피드 자체의 <title>에서 값을 받아 옵니다(fetchFeed()가 반환한 직후에 feedTitle = feed.title;을 추가하면 됩니다). 그래서 답장은 언제나 봇이 방금 실제로 확인한 내용을 반영할 뿐, 한 번 타이핑해 놓고 갱신되지 않는 이름을 말하지 않습니다. 첫 폴링이 끝나기 전에는 피드의 URL을 대신 출력합니다.

formatInterval()이 존재하는 이유는, POLL_INTERVAL_MS가 테스트 중에는 60000처럼 작을 수도 있고 운영 환경에서는 기본값인 10분일 수도 있어서인데, “매 600000밀리초마다”는 봇이 사람에게 할 만한 말이 아니기 때문입니다.

재시작에도 살아남기

이 버전에는 아직 버그가 하나 남아 있는데, 재시작할 때만 드러납니다. 자주 갱신되는 피드를 감시하던 봇이 피드에 새 글이 올라온 직후, 다음 예약 폴링이 그 글에 닿기 전에 재시작되면, 그 글은 영영 게시되지 않고 실패한 흔적도 전혀 남지 않습니다. firstPoll과 posted 둘 다 프로세스 메모리에만 있던 값이라 재시작하면 초기화되므로, 봇은 바로 다음 폴링을 마치 이전에 한 번도 실행된 적이 없는 것처럼 취급합니다. 그 시점에 피드가 보여 주는 것을 그대로 베이스라인으로 삼아 “이미 본 글” 집합에 편입시켜 버릴 뿐, 그 “새” 글을 게시하는 일은 결코 일어나지 않습니다. 겉으로는 잘 돌아가는 것처럼 보이는 봇이 재시작이나 배포 때마다 조용히 글을 흘리고 있을 수 있는 것입니다.

해법은 봇 자신의 상태와 폴러의 상태를 모두 디스크에 남기는 것입니다. 그것도 하나가 아니라 SQLite 데이터베이스 두 개로 나눠서요.

bot.db는 온전히 @fedify/botkit-sqlite의 몫입니다. 여기에는 BotKit이 액터의 암호화 키, 보낸 액티비티, 그 밖에 Repository가 책임지는 모든 것을 저장합니다. 봇 자신의 코드는 이 안에 무엇이 들었는지 알 필요가 없고, 알려고 해서도 안 됩니다.

app.db는 이 프로젝트 자체의 스키마입니다. 어떤 글을 이미 게시했는지를 담아서, 재시작해도 잊어버리지 않게 합니다.

@fedify/botkit-sqlite를 설치합니다.

Deno

deno add jsr:@fedify/botkit-sqlite

npm

npm add @fedify/botkit-sqlite

pnpm

pnpm add @fedify/botkit-sqlite

Yarn

yarn add @fedify/botkit-sqlite

app.db는 Node.js 내장 모듈인 node:sqlite로 직접 엽니다.

db.ts

// app.db: 피드 폴러 자체의 상태. @fedify/botkit-sqlite의 SqliteRepository가
// 전담하는 bot.db와는 분리되어 있다. 지금은 어떤 피드 항목을 이미 게시했는지만
// 추적해서, 재시작해도 이미 본 것을 잊지 않게 한다.
//
// node:sqlite의 DatabaseSync API는 완전히 동기식이다. 아래 호출 중 어느
// 것도 await가 필요 없고, 붙일 수도 없다.

import { DatabaseSync } from "node:sqlite";

export function openAppDb(path: string): DatabaseSync {
  const db = new DatabaseSync(path);
  db.exec(`
    CREATE TABLE IF NOT EXISTS posted_items (
      item_id TEXT PRIMARY KEY,
      posted_at TEXT NOT NULL
    )
  `);
  // 특정 항목에 관한 것이 아닌 상태를 위한 작은 키-값 테이블. 맨 처음
  // 폴링이 일어났는지 여부는 posted_items에서 추론하는 대신 여기에 둔다.
  // 그렇게 하지 않으면, 첫 폴링 시점에 피드가 마침 비어 있거나 항목들에
  // 쓸 만한 id/url이 없는 경우 베이스라인이 잡혔다는 사실 자체를 영영
  // 기록하지 못하게 된다.
  db.exec(`
    CREATE TABLE IF NOT EXISTS app_meta (
      key TEXT PRIMARY KEY,
      value TEXT NOT NULL
    )
  `);
  return db;
}

export function isBaselined(db: DatabaseSync): boolean {
  return (
    db.prepare("SELECT 1 FROM app_meta WHERE key = 'baselined'").get() != null
  );
}

export function markBaselined(db: DatabaseSync): void {
  db.prepare(
    "INSERT OR IGNORE INTO app_meta (key, value) VALUES ('baselined', '1')",
  ).run();
}

export function isPosted(db: DatabaseSync, itemId: string): boolean {
  return (
    db.prepare("SELECT 1 FROM posted_items WHERE item_id = ?").get(
      itemId,
    ) != null
  );
}

export function markPosted(db: DatabaseSync, itemId: string): void {
  db.prepare(
    "INSERT OR IGNORE INTO posted_items (item_id, posted_at) VALUES (?, ?)",
  ).run(itemId, new Date().toISOString());
}

Note

node:sqlite가 실험적 플래그에서 벗어나기 전 버전의 Node.js(22.13.0과 23.4.0 이전)에서는 시작할 때 ExperimentalWarning: SQLite is an experimental feature라는 줄이 뜹니다. 정상이니 신경 쓰지 않아도 되고, 모듈은 그대로 잘 동작합니다.

isBaselined()를 posted_items와 별도로 두는 데는 이유가 있습니다. “첫 폴링이 일어났는가”라는 질문의 답을 posted_items의 행 개수로 대신하려 하면 거의 항상은 맞아떨어지지만, 딱 하나 예외가 있습니다. 바로 그 첫 폴링에서 피드가 마침 항목을 하나도 반환하지 않거나, 쓸 만한 id/url이 없는 항목만 반환하는 경우입니다. app_meta는 그 시점에 표시할 수 있었던 항목이 몇 개였는지와 무관하게, “이 이정표에 도달했는가” 자체를 독립적으로 추적합니다.

bot.ts는 이제 두 데이터베이스를 모두 만들고, posted/firstPoll을 영속화된 대응물로 바꿉니다.

bot.ts

// RSS/Atom/RDF 피드를 일정 주기로 폴링하다가 새 글이 올라오면 페디버스에
// 게시하는 봇.
//
// 실행:  deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts
//   또는: npx srvx serve --port 8000 --entry ./bot.ts
// 설정:  ORIGIN=https://your-domain
//        FEED_URL=https://example.com/feed.xml (기본값은 Hacker News)
//        POLL_INTERVAL_MS=600000 (기본값은 10분)

import {
  createBot,
  InProcessMessageQueue,
  link,
  MemoryKvStore,
  text,
} from "@fedify/botkit";
import { SqliteRepository } from "@fedify/botkit-sqlite";
import { mkdirSync } from "node:fs";
import { fetchFeed } from "./feed.ts";
import type { FeedItem } from "./feed.ts";
import {
  isBaselined,
  isPosted,
  markBaselined,
  markPosted,
  openAppDb,
} from "./db.ts";

mkdirSync("./data", { recursive: true });

const FEED_URL = process.env.FEED_URL ?? "https://news.ycombinator.com/rss";
const ORIGIN = process.env.ORIGIN ?? "http://localhost:8000";

// POLL_INTERVAL_MS를 환경 변수에서 파싱하고 검증하는 부분은 앞서와 똑같으므로
// 여기서는 생략한다.

const bot = createBot<void>({
  username: "rssbot",
  name: "RSS Bot",
  summary: text`I watch ${link(FEED_URL)} and post new entries here.`,
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
  repository: new SqliteRepository({ path: "./data/bot.db" }),
});

const appDb = openAppDb("./data/app.db");

function itemKey(item: FeedItem): string | null {
  return item.id ?? item.url;
}

let feedTitle: string | null = null;

async function poll(): Promise<void> {
  const feed = await fetchFeed(FEED_URL);
  feedTitle = feed.title;
  const items = [...feed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  // 이 봇이 태어나서 처음 실행되는 순간 피드의 현재 첫 페이지 전체를
  // 팔로워에게 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만
  // "새 글"로 친다. 이 값은 app.db에 남기 때문에, 재시작할 때마다가 아니라
  // 통틀어 딱 한 번만 일어난다.
  const isFirstEverPoll = !isBaselined(appDb);

  const session = bot.getSession(ORIGIN);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || isPosted(appDb, key)) continue;
    if (!isFirstEverPoll) {
      await session.publish(
        text`${item.title ?? "(untitled)"}

${link(item.url ?? FEED_URL)}`,
      );
      publishedCount++;
    }
    markPosted(appDb, key);
  }
  if (isFirstEverPoll) markBaselined(appDb);
  console.log(
    isFirstEverPoll
      ? `Baseline: ${items.length} existing item(s) from ${FEED_URL}.`
      : `Posted ${publishedCount} new item(s) from ${FEED_URL}.`,
  );
}

두 데이터베이스 파일 모두 어딘가 담길 곳이 필요합니다. Deno의 권한 모델을 생각하면 프로젝트 루트에 흩어놓지 않고 전용 디렉터리 하나에 모아 두는 편이 좋습니다. mkdirSync("./data", ...)가 그 디렉터리를 만들어 주므로, 프로세스는 파일시스템 전체가 아니라 이 경로 하나에만 읽기·쓰기 권한을 가지면 됩니다.

이제 더 넓어진 권한으로 봇을 다시 실행합니다.

Deno

deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts

Node.js

npx srvx serve --port 8000 --entry ./bot.ts

새 글이 올라온 직후에 예전과 같은 방식으로 재시작해 봅니다. 이번에는 그 글이 베이스라인 속으로 사라지는 대신, 다음 폴링에 그대로 나타납니다.

실제로 공개하기

지금까지 봇은 localhost에서만 동작하면 됐습니다. 다른 페디버스 서버가 이 봇에 닿으려면 공개 주소가 필요합니다.

터널링 서비스를 쓰면 아직 내 서버가 없어도 “공개 주소” 문제의 절반을 해결할 수 있습니다. 이런 서비스들은 봇 앞에서 L7 리버스 프록시처럼 동작하므로, behindProxy를 켜서 이들이 붙이는 X-Forwarded-* 헤더를 신뢰하게 하고, 로컬 개발과 터널링된 실행이 같은 코드를 쓸 수 있도록 환경 변수로 읽어들입니다.

import { createBot } from "@fedify/botkit";

const BEHIND_PROXY = process.env.BEHIND_PROXY?.trim()?.toLowerCase() ===
  "true";

const bot = createBot<void>({
  // 나머지 옵션은 지면상 생략
  behindProxy: BEHIND_PROXY,
});

그다음 터널을 띄웁니다. 이 튜토리얼에서는 fedify tunnel을 쭉 사용하지만, 이미 다른 서비스를 써 왔다면 아래 중 무엇을 써도 같은 방식으로 동작합니다.

fedify tunnel

fedify tunnel 8000

ngrok

ngrok http 8000

Tailscale Funnel

tailscale funnel 8000

Cloudflare Tunnel

cloudflared tunnel --url http://localhost:8000

fedify tunnel은 준비가 끝나면 공개 호스트명을 출력합니다.

✔ Your local server at 8000 is now publicly accessible:

https://c4d3933be87bc2.lhr.life/

Press ^C to close the tunnel.

이번에는 ORIGIN은 그 주소로, BEHIND_PROXY는 true로 맞춰서 봇을 다시 실행합니다.[1]

Deno

ORIGIN=https://c4d3933be87bc2.lhr.life BEHIND_PROXY=true \
  deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts

Node.js

ORIGIN=https://c4d3933be87bc2.lhr.life BEHIND_PROXY=true \
  npx srvx serve --port 8000 --entry ./bot.ts

브라우저로 그 주소에 들어가 보면 봇 자신의 프로필 페이지가 나타납니다.

봇의 웹 프로필 페이지. 이름과 핸들, 소개, 그리고 팔로워·게시물 수가 0으로 표시되어 있다

ActivityPub.Academy로 테스트하기

ActivityPub.Academy는 몇 초 만에 임시 Mastodon 계정을 하나 내주는데, 바로 이런 용도, 즉 봇을 팔로우하고 멘션하면서 어떤 답이 돌아오는지 지켜보기에 딱 맞습니다. 계정은 하루 뒤에 지워지므로 테스트가 끝난 뒤 따로 정리할 것도 없습니다.

가입한 다음, 봇의 페디버스 핸들로 검색합니다. @rssbot@c4d3933be87bc2.lhr.life이고, 자신의 터널 호스트명으로 바꿔 넣으면 됩니다.

ActivityPub.Academy에서 봇의 핸들을 검색해서 봇이 검색 결과로 나타난 모습

검색 결과를 열고 Follow를 누릅니다.

오른쪽 위에 Follow 버튼이 있는, ActivityPub.Academy에서 본 봇의 프로필

팔로우한 뒤 같은 프로필. “1 Follower”와 Unfollow 버튼이 보인다

그다음 멘션합니다.

Hey @rssbot@c4d3933be87bc2.lhr.life, what are you up to?

답장은 몇 초 안에 도착합니다.

멘션과 봇의 답장을 보여 주는 스레드: “I'm watching Hacker News and check for new posts every 2 minutes.”

다음번에 봇의 폴링이 정말로 새로운 글을 잡아내면, 다른 여느 게시물과 마찬가지로 타임라인에 나타납니다.

ActivityPub.Academy의 홈 타임라인. 앞서의 멘션 답장 위에 봇이 새로 게시한 글이 보인다

봇 여럿, 인스턴스 하나

피드 하나로 아이디어는 확인했습니다. 하지만 FEED_URL 하나를 하드코딩하는 방식으로는 여러 피드를 동시에 돌리거나, 재배포 없이 피드를 추가하거나, 갈수록 시끄러워지는 타임라인 하나를 다 같이 쓰는 대신 피드마다 자기만의 페디버스 정체성을 갖게 하기는 어렵습니다. Instance는 바로 이럴 때를 위해 BotKit이 마련해 둔 조각입니다. 서버 하나가 여러 봇을 호스팅하되, 각 봇은 저마다 액터와 팔로워, 인박스를 가지면서도 같은 키-값 스토어, 큐, 리포지토리를 밑에서 함께 씁니다.

createBot()이 사라지는 것은 아닙니다. 딱 하나의 액터로만 존재하면 되는 봇이라면 굳이 바꿀 이유가 없습니다. 이제부터는 rssbot을, 등록된 피드마다 봇 하나씩을 호스팅하는 인스턴스로 바꾸고, 여기에 누군가 URL과 함께 멘션했을 때 피드를 추가하는 일만 하는 작은 등록 담당 봇을 하나 더합니다.

순진한 식별자, 그리고 그것이 깨지는 이유

피드별 봇은 저마다 자기만의 식별자가 필요합니다. 봇이 페디버스에 연합되는 순간 액터 URI에 새겨지는 내부 이름 말이죠. 가장 먼저 떠오르는 생각은 피드 자체에서, 이를테면 호스트명에서 하나를 뽑아내는 것입니다. https://xkcd.com/rss.xml을 xkcd-com으로 바꾸는 식으로요. 이 문자열은 마침 사용자명으로도 나쁘지 않습니다. 사람이 봇을 찾으려고 실제로 입력하는, @xkcd-com@your-domain의 사람이 읽을 수 있는 절반이니까요.

문제는 이걸 둘 다에 쓰려는 데서 생깁니다. 식별자는 봇이 한 번 연합되고 나면 영원히 고정돼 있어야 합니다. 원격 서버들이 캐시해 둔 액터 URI 안에 그 식별자를 그대로 담고 있어서, 나중에 바꾸면 모든 팔로워 관계와 다른 서버가 들고 있는 모든 참조가 고아가 돼 버립니다. 그런데 피드의 호스트명에는 그런 종류의 영속성이 없습니다. 사이트는 옮겨 다닙니다. 블로그가 도메인을 하나에서 다른 곳으로 옮기고도 여전히 같은 피드, 같은 팔로워를 가진 같은 피드일 수 있는데, 이미 옛 식별자를 갖고 있는 모든 이를 깨뜨리지 않으면서 식별자만 새 이름에 맞게 바꿀 방법은 없습니다. 사용자명으로는 더할 나위 없이 좋은 성질, 즉 기억하기 쉽고 봇의 실체와 맞닿아 있다는 성질이야말로, 식별자로는 나쁜 선택이 되게 만드는 바로 그 성질입니다.

해법은 이 둘을 떼어 놓는 것입니다. 식별자는 등록 시점에 한 번 배정되고 두 번 다시 손대지 않는, 의미 없는 문자열입니다. 슬러그는 피드의 URL에서 뽑아낸 것으로, 언제든 다시 계산해도 되고, “사람이 이 봇을 찾으려고 뭐라고 입력하는가”라는 질문에만 쓰일 뿐 “이 봇의 액터 URI에는 무엇이 들어 있는가”라는 질문에는 결코 쓰이지 않습니다.

feeds 테이블

app.db는 이 구분을 담을 feeds 테이블을 새로 얻고, posted_items는 봇 하나의 이력을 추적하는 대신 피드별로 범위가 좁혀집니다.

db.ts

// app.db: 피드 폴러 자체의 상태. @fedify/botkit-sqlite의 SqliteRepository가
// 전담하는 bot.db와는 분리되어 있다.
//
// node:sqlite의 DatabaseSync API는 완전히 동기식이다. 아래 호출 중 어느
// 것도 await가 필요 없고, 붙일 수도 없다.
//
// 피드의 identifier는 의미 없이 고정된 키다. 한 번 연합되고 나면 액터 URI에
// 새겨지므로, (피드의 URL이나 제목처럼) 바뀔 수 있는 것으로부터는 절대
// 유도해서는 안 된다. slug는 핸들에서 사람이 읽는 부분으로, mapUsername을
// 거쳐 identifier와 서로 오간다.

import { DatabaseSync } from "node:sqlite";

export interface FeedRow {
  readonly identifier: string;
  readonly url: string;
  readonly slug: string;
  readonly title: string | null;
  readonly baselined: boolean;
}

function toFeedRow(row: Record<string, unknown>): FeedRow {
  return {
    identifier: row.identifier as string,
    url: row.url as string,
    slug: row.slug as string,
    title: row.title as string | null,
    baselined: (row.baselined as number) !== 0,
  };
}

export function openAppDb(path: string): DatabaseSync {
  const db = new DatabaseSync(path);
  db.exec(`
    CREATE TABLE IF NOT EXISTS feeds (
      identifier TEXT PRIMARY KEY,
      url TEXT NOT NULL UNIQUE,
      slug TEXT NOT NULL UNIQUE,
      title TEXT,
      baselined INTEGER NOT NULL DEFAULT 0,
      created_at TEXT NOT NULL
    )
  `);
  migrateLegacyPostedItems(db);
  db.exec(`
    CREATE TABLE IF NOT EXISTS posted_items (
      feed_identifier TEXT NOT NULL,
      item_id TEXT NOT NULL,
      posted_at TEXT NOT NULL,
      PRIMARY KEY (feed_identifier, item_id)
    )
  `);
  return db;
}

// 1부의 app.db에는 posted_items(item_id, posted_at)만 있었고, 어느 피드에
// 속한 항목인지 하는 개념 자체가 없었다. 피드가 하나뿐이었으니 당연했다.
// openAppDb()의 CREATE TABLE IF NOT EXISTS가 옛 테이블을 그대로 놔둘 것이므로,
// 그 행들을 (그 글을 게시했을 수 있는 유일한 식별자인) "bot" 식별자 아래로
// 옮겨서 이어받는다.
function migrateLegacyPostedItems(db: DatabaseSync): void {
  const columns = db.prepare("PRAGMA table_info(posted_items)").all() as {
    readonly name: string;
  }[];
  const isLegacy = columns.length > 0 &&
    !columns.some((column) => column.name === "feed_identifier");
  if (!isLegacy) return;
  db.exec("ALTER TABLE posted_items RENAME TO posted_items_legacy");
  db.exec(`
    CREATE TABLE posted_items (
      feed_identifier TEXT NOT NULL,
      item_id TEXT NOT NULL,
      posted_at TEXT NOT NULL,
      PRIMARY KEY (feed_identifier, item_id)
    )
  `);
  db.exec(`
    INSERT INTO posted_items (feed_identifier, item_id, posted_at)
    SELECT 'bot', item_id, posted_at FROM posted_items_legacy
  `);
  db.exec("DROP TABLE posted_items_legacy");
}

function slugify(url: string): string {
  return new URL(url).hostname.toLowerCase().replace(/\./g, "-");
}

export function getFeedByIdentifier(
  db: DatabaseSync,
  identifier: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE identifier = ?").get(
    identifier,
  );
  return row == null ? undefined : toFeedRow(row);
}

export function getFeedBySlug(
  db: DatabaseSync,
  slug: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE slug = ?").get(slug);
  return row == null ? undefined : toFeedRow(row);
}

export function getFeedByUrl(
  db: DatabaseSync,
  url: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE url = ?").get(url);
  return row == null ? undefined : toFeedRow(row);
}

export function listFeeds(db: DatabaseSync): readonly FeedRow[] {
  const rows = db.prepare("SELECT * FROM feeds").all();
  return rows.map(toFeedRow);
}

// 기존 단일 봇 배포의 피드를 시작 시점에 딱 한 번 이 테이블의 행으로
// 이어받는 데 쓴다. identifier가 기본 키이므로 첫 실행 이후로는 매번 아무
// 일도 하지 않는다.
export function seedFeed(
  db: DatabaseSync,
  feed: { identifier: string; url: string; slug: string },
): void {
  db.prepare(
    "INSERT OR IGNORE INTO feeds (identifier, url, slug, created_at) VALUES (?, ?, ?, ?)",
  ).run(feed.identifier, feed.url, feed.slug, new Date().toISOString());
  migrateLegacyBaselined(db, feed.identifier);
}

// 1부의 app.db는 "첫 폴링이 일어났는가"를 별도의 app_meta 테이블에
// 추적했다. 물어볼 대상이 하나뿐이었으니 그럴 만했다. 이 피드의 행이
// (바로 위에서) 생기고 나면 그 플래그를 이 행으로 옮기고 app_meta는
// 지운다. app_meta가 더 이상 확인할 대상이 아니게 되는 첫 실행 이후로는
// 매번 아무 일도 하지 않는다.
function migrateLegacyBaselined(db: DatabaseSync, identifier: string): void {
  const hasAppMeta = db.prepare(
    "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'app_meta'",
  ).get() != null;
  if (!hasAppMeta) return;
  const wasBaselined = db.prepare(
    "SELECT 1 FROM app_meta WHERE key = 'baselined'",
  ).get() != null;
  if (wasBaselined) markFeedBaselined(db, identifier);
  db.exec("DROP TABLE app_meta");
}

// 새로 멘션된 피드 URL을 새로운, 의미 없는 식별자로 등록한다. 식별자는
// URL이나 제목처럼 나중에 바뀔 수 있는 것으로부터는 절대 유도하지 않는다.
// slug(사람이 읽는 핸들)는 유도하며, 이미 쓰이고 있는 것과 충돌하면 숫자
// 접미사를 붙인다.
export function addFeed(db: DatabaseSync, url: string): FeedRow {
  const baseSlug = slugify(url);
  let slug = baseSlug;
  for (let suffix = 2; getFeedBySlug(db, slug) != null; suffix++) {
    slug = `${baseSlug}-${suffix}`;
  }
  const identifier = `feed_${
    crypto.randomUUID().replace(/-/g, "").slice(0, 12)
  }`;
  db.prepare(
    "INSERT INTO feeds (identifier, url, slug, created_at) VALUES (?, ?, ?, ?)",
  ).run(identifier, url, slug, new Date().toISOString());
  return { identifier, url, slug, title: null, baselined: false };
}

export function updateFeedTitle(
  db: DatabaseSync,
  identifier: string,
  title: string,
): void {
  db.prepare("UPDATE feeds SET title = ? WHERE identifier = ?").run(
    title,
    identifier,
  );
}

export function markFeedBaselined(db: DatabaseSync, identifier: string): void {
  db.prepare("UPDATE feeds SET baselined = 1 WHERE identifier = ?").run(
    identifier,
  );
}

export function isPosted(
  db: DatabaseSync,
  feedIdentifier: string,
  itemId: string,
): boolean {
  return (
    db.prepare(
      "SELECT 1 FROM posted_items WHERE feed_identifier = ? AND item_id = ?",
    ).get(feedIdentifier, itemId) != null
  );
}

export function markPosted(
  db: DatabaseSync,
  feedIdentifier: string,
  itemId: string,
): void {
  db.prepare(
    "INSERT OR IGNORE INTO posted_items (feed_identifier, item_id, posted_at) VALUES (?, ?, ?)",
  ).run(feedIdentifier, itemId, new Date().toISOString());
}

identifier와 slug 둘 다 UNIQUE 제약을 갖지만, 충돌 시 다시 계산해서 재시도하는 것은 slug뿐입니다. identifier는 addFeed()가 한 번 생성하면 그대로 놔둡니다. baselined는 1부에서는 별도의 app_meta 테이블에 있으면서 앱 전체가 폴링을 한 번이라도 한 적 있는지를 추적했지만, 이제 첫 폴링 여부가 앱 전체가 아니라 특정 피드 하나에 관한 사실이 되면서 feeds의 평범한 칼럼 하나로 합쳐집니다.

slugify()는 피드 호스트명을 소문자로 바꾸고 점을 하이픈으로 바꿉니다. news.ycombinator.com은 news-ycombinator-com이 됩니다. 그리고 그 슬러그가 이미 쓰이고 있으면 addFeed()가 숫자 접미사(-2, -3, …)를 붙입니다. 반면 identifier는 crypto.randomUUID()에서 생성한, feed_로 시작하는 짧은 무작위 문자열로, 피드에 관한 그 무엇으로부터도 유도되지 않습니다.

위의 두 migrateLegacy* 함수가 존재하는 이유는, 1부에서 쓰던 기존 app.db에는 여전히 옛 posted_items(item_id, posted_at) 테이블과 별도의 app_meta 키-값 테이블이 있고, CREATE TABLE IF NOT EXISTS는 이 둘 중 어느 것도 건드리지 않기 때문입니다. 이미 존재하는 테이블은 그냥 그대로 두므로, 옛 posted_items는 이 파일의 나머지 부분이 기대하는 feed_identifier 칼럼이 없는 채로 남고, isPosted()를 처음 호출하는 순간 no such column: feed_identifier로 실패하고 말 것입니다. migrateLegacyPostedItems()는 openAppDb()가 뭔가를 만들기 전에 그 모양을 확인해서 발견하면 마이그레이션하고, migrateLegacyBaselined()는 app_meta의 baselined 플래그에 대해 같은 일을 하되, 그 플래그가 속할 피드 행이 실제로 존재하게 된 다음 seedFeed()에서 호출됩니다. 둘 다 실행 전에 옛 모양인지부터 확인하므로, 기존 app.db가 아예 없는 새 설치라면 그냥 건너뛰고, 이미 마이그레이션된 것이라면 그 이후 실행마다 건너뜁니다. posted_items에는 이미 feed_identifier가 있고, app_meta는 이미 사라졌으니까요.

봇에서 인스턴스로

bot.ts는 이제 봇 하나가 아니라 여럿을 호스팅하므로 instance.ts가 됩니다.

instance.ts

import {
  createInstance,
  InProcessMessageQueue,
  MemoryKvStore,
} from "@fedify/botkit";
import { SqliteRepository } from "@fedify/botkit-sqlite";
import { mkdirSync } from "node:fs";
import { openAppDb, seedFeed } from "./db.ts";

// FEED_URL, BEHIND_PROXY는 앞서와 같은 방식으로 환경 변수에서 읽어 온다.
// 여기서는 생략한다.

mkdirSync("./data", { recursive: true });

const instance = createInstance<void>({
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
  repository: new SqliteRepository({ path: "./data/bot.db" }),
  behindProxy: BEHIND_PROXY,
  // 1부의 봇은 identifier를 명시적으로 지정한 적이 없으므로, createBot()이
  // 기본값으로 "bot"을 붙였다. "rssbot"은 어디까지나 사용자명이었을 뿐이다.
  // (아래의 legacyObjectUris가 아니라) 바로 이 identifier를 그대로 재사용하는
  // 것이 액터의 URI와 키, 팔로워 관계를 그대로 지켜 주는 열쇠다.
  // legacyObjectUris는 원격 서버가 이 봇이 인스턴스로 옮겨 오기 전부터
  // 캐시해 두고 있을 수 있는, 개별 오브젝트(게시물, 팔로우) URI의 *옛*
  // 형식만 다시 써 준다.
  legacyObjectUris: { identifier: "bot" },
});

const appDb = openAppDb("./data/app.db");

// 원래 피드를 봇의 실제(기본) identifier와 기존 사용자명 그대로 여기 행으로
// 이어받는다. 그래서 이 피드는 이제부터 등록되는 다른 모든 피드와 똑같은
// 동적 봇 그룹이 처리한다. 첫 실행 이후로는 아무 일도 하지 않는다.
seedFeed(appDb, { identifier: "bot", url: FEED_URL, slug: "rssbot" });

createInstance()는 createBot()이 쓰던 것과 같은 인프라 옵션들, 즉 kv, queue, repository, behindProxy를 그대로 받습니다. 새로 등장한 것은 legacyObjectUris입니다. 1부의 rssbot은 identifier 옵션을 명시적으로 지정한 적이 없으므로 createBot()이 기본값으로 "bot"을 붙였습니다. "rssbot"은 어디까지나 사용자명이었을 뿐입니다. legacyObjectUris가 아니라 바로 이 같은 identifier를 재사용하는 것이, 이제 서버에 봇 하나만 있는 게 아니라 여럿 중 하나가 된 지금도 원래 봇의 액터 URI와 암호화 키, 팔로워 관계를 온전히 지켜 주는 열쇠입니다. legacyObjectUris는 좀 더 좁은 범위, 즉 createInstance()로 옮기기 전에 게시된 개별 오브젝트 URI(게시물과 팔로우)의 옛 형식을 다룹니다. 원격 서버가 여전히 그 형식을 캐시하고 있다가 나중에 Accept나 Like로 돌려보낼 수 있기 때문입니다.

seedFeed()는 이 같은 봇을 평범한 행 하나로 이어받습니다. 같은 identifier, 같은 URL, 슬러그로는 같은 사용자명을 씁니다. INSERT OR IGNORE이므로 첫 실행 이후로는 매번 아무 일도 하지 않고, 이제부터 원래 봇은 나중에 등록되는 어떤 피드와도 똑같은 동적 봇 그룹이 처리하며, 다른 어디에도 별도의 특수 처리는 없습니다.

이게 가능하려면 미리 이뤄져야 할 마이그레이션이 있는데, 이 코드 자체가 하는 일은 아닙니다. BotKit은 이미 createBot() 배포의 리포지토리 데이터를 createInstance()가 기대하는 구조로 마이그레이션해 주지만, 그 배포가 createBot() 아래에서 BotKit 0.5.0 이상으로 처음 실행될 때 딱 한 번만 그렇습니다. 여기까지 왔다면 1부의 봇은 그저 실행된 것만으로 이미 그 과정을 마쳤을 것입니다. 만약 0.5.0에서 아예 실행된 적이 없는 배포를 옮기는 중이라면, 먼저 createBot()으로 한 번 실행하거나 리포지토리의 migrate() 메서드를 직접 호출하세요. 자세한 내용은 단일 봇 배포 마이그레이션하기를 참고하세요. createInstance()로 바꾸기 전후로 액터의 URI에 fedify lookup을 직접 돌려 보는 것도 해 볼 만합니다. 공개 키와 WebFinger 핸들이 완전히 똑같이 나와야 합니다.

인스턴스 하나에 피드 여럿

// 동적 봇 그룹: feeds 테이블의 행 하나마다 봇 하나씩을, 필요할 때마다
// 해석한다. 피드가 추가될 때마다 명령형으로 createBot()을 호출하는 대신,
// "피드마다 봇 하나"를 사전에 한 번의 등록으로 바꿔 주는 것이 바로 이
// 부분이다. 아래의 registryBot.onMention을 참고할 것.
const feedBots = instance.createBot(
  (_ctx, identifier) => {
    const feed = getFeedByIdentifier(appDb, identifier);
    if (feed == null) return null;
    return { username: feed.slug, name: feed.title ?? feed.url };
  },
  {
    mapUsername(_ctx, username) {
      const feed = getFeedBySlug(appDb, username);
      return feed?.identifier ?? null;
    },
  },
);

feedBots.onMention = async (session, message) => {
  const feed = getFeedByIdentifier(appDb, session.bot.identifier);
  if (feed == null) return;
  await message.reply(
    text`I'm watching ${
      link(feed.title ?? feed.url, feed.url)
    } and check for new posts every ${formatInterval(POLL_INTERVAL_MS)}.`,
  );
};

instance.createBot()에 고정된 식별자 대신 함수를 넘기면 BotGroup이 만들어집니다. 사전에 선언해 두는 대신 필요할 때마다 해석되는 봇들의 집합인데, BotKit 자체 문서에서 지역별 날씨 봇을 만들 때 쓰는 것과 같은 동적 봇 패턴입니다. 여기 디스패처가 하는 일도 같은 종류의 조회입니다. 식별자가 주어지면 feeds에서 대응하는 행을 찾고, 없으면 null을 반환해서 BotKit에게 이 식별자는 애초에 피드 봇이 아니라고 알려 줍니다. mapUsername은 그 반대 방향으로, identifier가 아니라 slug로 같은 조회를 수행합니다. 그래서 @xkcd-com@your-domain과, 무작위로 생성된 feed_a1b2c3d4e5f6을 식별자로 가진 액터가 양쪽 방향 모두에서 같은 봇으로 해석됩니다.

BotGroup은 자기만의 단일한 식별자를 갖지 않으므로, 이를 통해 게시하려면 어느 봇인지 말해 줘야 합니다. 1부에서 쓰던 bot.getSession(origin) 대신 feedBots.getSession(origin, identifier)를 씁니다. 폴링은 feeds의 모든 행을 돌면서, 피드마다 정확히 그렇게 한 번씩 합니다.

const FETCH_TIMEOUT_MS = 30_000;

async function pollFeed(feed: FeedRow): Promise<void> {
  const parsed = await fetchFeed(
    feed.url,
    AbortSignal.timeout(FETCH_TIMEOUT_MS),
  );
  if (parsed.title != null && parsed.title !== feed.title) {
    updateFeedTitle(appDb, feed.identifier, parsed.title);
  }
  const items = [...parsed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  // 이 피드가 처음 폴링되는 순간 현재 첫 페이지 전체를 팔로워에게
  // 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만 "새 글"로 친다.
  const isFirstEverPoll = !feed.baselined;

  const session = await feedBots.getSession(ORIGIN, feed.identifier);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || isPosted(appDb, feed.identifier, key)) continue;
    if (!isFirstEverPoll) {
      await session.publish(
        text`${item.title ?? "(untitled)"}

${link(item.url ?? feed.url)}`,
      );
      publishedCount++;
    }
    markPosted(appDb, feed.identifier, key);
  }
  if (isFirstEverPoll) markFeedBaselined(appDb, feed.identifier);
  console.log(
    isFirstEverPoll
      ? `Baseline: ${items.length} existing item(s) from ${feed.url}.`
      : `Posted ${publishedCount} new item(s) from ${feed.url}.`,
  );
}

async function pollAll(): Promise<void> {
  for (const feed of listFeeds(appDb)) {
    try {
      await pollFeed(feed);
    } catch (error) {
      console.error(`Failed to poll feed ${feed.url}:`, error);
    }
  }
}

여기서는 식별자와 “이미 게시했음” 상태가 어디서 오는지를 빼면 1부의 폴링 로직과 다를 게 없습니다. 바깥 스코프에서 붙잡아 온 변수 대신 feed.identifier를 쓰고, 하나로 공유하던 Set 대신 그 identifier로 범위를 좁힌 isPosted/markPosted를 쓰고, fetchFeed()에 명시적인 AbortSignal.timeout()을 건다는 점이 다를 뿐입니다.

pollAll()은 피드를 하나씩 순서대로 기다리고, pollAllOnce()의 polling 가드 덕분에 현재 주기가 끝나기 전까지는 어떤 나중 틱도 새 주기를 시작할 수 없습니다.

이 타임아웃이 없다면, 연결은 받아 놓고 응답은 영영 하지 않는 서버 하나 때문에 그 서버가 물려 있는 피드뿐 아니라 인스턴스의 모든 피드가 영원히 멈춰 버릴 것입니다. 타임아웃이 있으면 이는 그저 평범한 피드별 실패, 이를테면 요청 시간 초과나 피드가 갑자기 이상한 XML을 반환하는 것과 다를 바 없어지고, pollAll()의 try/catch가 이를 똑같은 방식으로 처리합니다. 로그를 남기고 다음 피드로 넘어가는 식으로요.

멘션으로 피드 등록하기

지금까지 그룹에 봇을 하나 더한다는 것은 결국 feeds에 행을 하나 넣는다는 뜻이었습니다. feedBots의 디스패처는 그 테이블에 나타나는 식별자라면 무엇이든, 시작할 때부터 있던 것이든 방금 삽입된 것이든 상관없이 이미 알아서 해석해 주므로, 일단 피드가 등록되고 나면 instance.createBot()을 다시 호출할 필요가 전혀 없습니다. 바로 이것이 정적 봇 그룹 대신 동적 봇 그룹을 쓰는 이유입니다. 등록이 배포가 아니라 데이터베이스 쓰기 한 번으로 끝나는 것 말이죠.

작은 정적 봇 registry가 그 쓰기를 위한 대문 역할을 합니다.

function extractUrl(input: string): string | null {
  const match = input.match(/https?:\/\/\S+/)?.[0];
  return match != null && URL.canParse(match) ? match : null;
}

// 정적 봇: 등록 창구. 피드 URL과 함께 이 봇을 멘션하면 feeds 테이블에
// 행이 하나 추가된다. feedBots의 디스패처는 그 새 행을, 다음번에 그
// identifier가 해석될 때 알아서 집어 든다.
//
// 이 코드는 누가 멘션을 보냈는지 확인하지 않고, 아래의 폴링 루프가 가져오기
// 전에 그 URL을 검증하지도 않는다. 실제 배포에 필요한 것은 튜토리얼의
// "더 해볼 만한 것들" 절을 참고할 것.
const registryBot = instance.createBot("registry", {
  username: "registry",
  name: "Feed Registry",
  summary: text`Mention me with a feed URL to register a new feed bot.`,
});

registryBot.onMention = async (_session, message) => {
  const url = extractUrl(message.text);
  if (url == null) {
    await message.reply(text`Please include a feed URL in your mention.`);
    return;
  }
  const existing = getFeedByUrl(appDb, url);
  if (existing != null) {
    await message.reply(
      text`Already watching that feed: ${
        mention(`@${existing.slug}@${new URL(ORIGIN).host}`)
      }.`,
    );
    return;
  }
  const feed = addFeed(appDb, url);
  await message.reply(
    text`Registered! Give it a few minutes, then look for ${
      mention(`@${feed.slug}@${new URL(ORIGIN).host}`)
    }.`,
  );
};

extractUrl()은 멘션 텍스트 안에서 URL처럼 생기고 URL.canParse()를 통과하는 첫 번째 것을 찾습니다. 잘린 http://%처럼 형식이 잘못된 것은 걸러 내는데, 그렇지 않으면 addFeed()의 new URL() 호출까지 그대로 흘러 들어가 예외를 던지게 됩니다. 이미 감시 중인 URL을 등록하려 하면 feeds.url의 UNIQUE 제약에 부딪혀 죽는 대신 답장으로 알려 줍니다.

두 답장 모두 새 봇의 핸들을 mention()으로 만들지, 템플릿에 그대로 끼워 넣은 평범한 @${slug}@${host} 문자열로 만들지 않습니다. 평범한 텍스트로 적은 핸들은 그냥 텍스트일 뿐입니다. 링크로 렌더링되지도 않고, mention()이 붙여 주는 Mention 태그가 없으니 실제 멘션처럼 동작하지도 않습니다. mention()은 사람이 검색하듯 그 핸들을 실제로 찾아보고, 그 조회가 실패할 때만 평범한 텍스트로 물러나므로, 새 봇의 액터를 어쩌다 아직 해석할 수 없는 상황이라도 답장이 깨지는 대신 우아하게 낮은 수준으로 대응합니다.

registryBot.onMention이 하지 않는 일도 있습니다. 누가 묻고 있는지 확인하지 않고, pollFeed()에 곧 넘길 그 URL이 실제로 가져오기에 안전한 곳을 가리키는지도 확인하지 않습니다. 둘 다 의도적으로 남겨 둔 빈틈이고, 둘 다 여기가 아니라 아래 더 해볼 만한 것들에서 다룹니다.

두 런타임에서 인스턴스 검증하기

1부의 첫 실행과 같은 모양입니다. 다만 이제 단일 봇이 아니라 인스턴스를 호스팅하는 파일을 대상으로 합니다.

Deno

deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch instance.ts

Node.js

npx srvx serve --port 8000 --entry ./instance.ts

앞서와 같은 방식으로 터널을 열고, ActivityPub.Academy의 계정에서 피드 URL과 함께 등록 봇을 멘션합니다. 스킴을 포함한 전체 URL을 그대로 입력하세요. extractUrl()은 http://나 https:// 링크만 매칭하는데, Mastodon은 자동 링크된 URL을 보여 줄 때 스킴을 감춰서 표시하므로, 아래에는 스킴이 보이지 않는 것입니다.

Hey @registry, please watch https://xkcd.com/rss.xml

등록 봇에 대한 멘션과 그 답장: “Registered! Give it a few minutes, then look for @xkcd-com@…”

답장의 핸들은 mention()으로 만들어졌으므로, 그 답장을 이루는 실제 Note 객체에는 제대로 된 Mention 태그가 붙어 있고, 새 봇에게 직접 주소가 지정되어 있습니다. 그럴듯하게 보이도록 형식만 갖춘 게 아니라요. 위 스크린샷에서 ActivityPub.Academy는 이를 클릭 가능한 링크로 렌더링하지 않지만, 그 밑에 있는 데이터 자체는 정확합니다. 등록 봇 자신의 웹 페이지는 같은 답장을, 링크가 온전히 살아 있는 채로 보여 줍니다.

등록 봇 자신의 프로필 페이지. 새 봇의 핸들이 동작하는 링크로 렌더링된 같은 “Registered!” 게시물이 보인다

1~2분 뒤, 새 봇이 자기 슬러그로 나타납니다. 이 봇과 이관된 원래 봇을 둘 다 팔로우해 보면, 이 둘이 서로의 별칭이 아니라 진짜로 별개인 액터임을 확인할 수 있습니다.

세 개의 별개 봇을 보여 주는 어느 계정의 팔로잉 목록: rssbot 핸들로 이관된 원래 봇, 새로 등록된 xkcd.com 피드, 그리고 별도의 Node.js 실행에서 나온 세 번째 봇

새 봇을 직접 멘션하면, 등록 봇이 아니라 feedBots.onMention이 답합니다.

새 피드 봇에 대한 멘션과 그 답장: “I'm watching xkcd.com and check for new posts every 2 minutes.”

여기까지의 모든 과정은 Node.js에서도 똑같이 동작합니다. instance.ts를 두 번째 호스트명 뒤에서 터널링하고, 같은 방식으로 피드를 등록하면 같은 핸들이 WebFinger를 통해 그대로 해석됩니다.

서버에 배포하기

터미널이 아니라 프로세스 매니저 아래에서 instance.ts를 실행하는 것은 직접 호스팅하기의 systemd·Caddy 구성을 거의 그대로 따릅니다. 런타임 설치, botkit 사용자 만들기, Caddy 설정, 서비스 활성화까지 전체 과정은 그 문서에 있습니다. 이 봇에 특유한 것 두 가지만 짚고 넘어가면 되는데, 둘 다 examples/rss-bot/deploy/의 systemd 유닛과 Caddyfile에 이미 반영되어 있습니다.

ExecStart는 이 튜토리얼에서 쭉 써 온 것과 같은, 범위를 좁힌 Deno 권한(--allow-net --allow-env --allow-read=./data --allow-write=./data)을 쓰지, 직접 호스팅하기의 범용 -A를 쓰지 않습니다. 그리고 ProtectSystem=strict가 ReadWritePaths에 나열되지 않은 한 작업 디렉터리 전체를 읽기 전용으로 만들어 버리는데, 이 봇은 bot.db와 app.db를 ./data 아래에 쓰므로 유닛에 다음을 추가합니다.

ReadWritePaths=/opt/botkit/data

그 경로는 서비스가 시작되기 전에 botkit 사용자 소유로 이미 존재해야 합니다.

sudo mkdir -p /opt/botkit/data
sudo chown botkit:botkit /opt/botkit/data

직접 호스팅하기 자체의 mkdir -p /opt/botkit 단계 바로 다음에 실행하면 됩니다. 이걸 건너뛰면 유닛은 아예 시작조차 하지 못하고 종료 코드 226/NAMESPACE를 냅니다. systemd는 이미 존재하는 ReadWritePaths 항목만 허용 목록에 올리기 때문입니다.

Node.js에서는 이번에는 srvx가 npx를 통해서만 실행되는 게 아니라, 실제로 봇의 의존성과 함께 배포돼야 합니다. package.json에 일반 의존성으로 들어 있으므로, npm ci(또는 배포를 빌드한 패키지 매니저에 맞는 대응 명령)가 ExecStart가 호출하는 바로 그 바이너리인 /opt/botkit/node_modules/.bin/srvx를 설치해 줍니다.

Caddyfile은 그 밖의 나머지 부분에서 직접 호스팅하기를 그대로 따릅니다. ACME 연락처 이메일을 이용한 자동 HTTPS, localhost:8000으로의 리버스 프록시, 그리고 같은 기본 보안 헤더까지요.

더 해볼 만한 것들

아래 내용은 *examples/rss-bot/*에는 구현되어 있지 않습니다. 실제 배포라면 대체로 아래 순서로 손보게 될 만한 것들입니다.

  • 피드가 알려 주는 갱신 힌트: RSS의 선택 사항인 <ttl> 엘리먼트와 Syndication 모듈의 <sy:updatePeriod>, <sy:updateFrequency>, <sy:updateBase>는 모두 피드가 봇이 모든 피드에 똑같이 POLL_INTERVAL_MS를 추측해 적용하는 대신, 스스로 폴링 주기를 제안할 수 있게 해 줍니다. Atom에는 이에 대응하는 표준이 없습니다.
  • HTTP 조건부 요청: 매 폴링마다 If-Modified-Since나 If-None-Match를 함께 보내고 304 Not Modified 응답을 처리하면, 전혀 바뀌지 않은 피드를 다시 파싱하는 일을 건너뛸 수 있습니다.
  • WebSub: <atom:link rel="hub" href="…">를 광고하는 피드라면, setInterval() 대신 웹훅으로 폴링을 기다리는 게 아니라 갱신을 직접 밀어 넣을 수 있습니다.
  • 피드를 등록할 수 있는 사람의 허용 목록: 지금은 URL이 담긴 멘션이 등록 봇에게 오기만 하면 누구든 그 URL을 등록할 수 있습니다.
  • 멘션된 URL을 가져오기 전에 검증하기: registryBot.onMention은 extractUrl()이 찾은 것을 그대로 pollFeed()의 fetch() 호출로 넘길 뿐, localhost나 내부 주소를 가리키는 것을 막을 장치가 전혀 없습니다. @fedify/vocab-runtime의 validatePublicUrl()이 정확히 이런 용도로 존재하니, 처음부터 새로 짤 필요는 없습니다.

  1. 호스트명은 사용하는 터널링 서비스에 따라 실제로는 다르게 나옵니다. ↩︎

@botkit@hackers.pub

Note

이 글은 BotKit의 공식 튜토리얼 Building an RSS bot(영문)을 한국어로 옮긴 것입니다. 원문은 계속 업데이트되므로, 최신 내용은 위 링크에서 확인하는 편이 좋습니다.

BotKit의 시작하기 안내서를 따라 하면 몇 분 만에 봇을 띄울 수 있습니다. 다만 그 봇은 인사말에 답할 뿐, 딱히 감시할 대상은 없습니다. 이 튜토리얼은 거기서 한 걸음 더 나아갑니다. RSS나 Atom, RDF 피드를 주시하다가 새 글이 올라오면 페디버스에 게시하는 봇을 만듭니다. 그 과정에서 예약 작업, 상태 저장, 멘션에 답장하기를 다루고, 나중에는 피드가 하나에서 여럿으로 늘어났을 때 필요해지는 createInstance()와 동적 봇 그룹까지 짚어 봅니다.

1부에서는 피드 하나를 감시하는 봇 하나를 만듭니다. 2부에서는 그 봇을 인스턴스로 바꿔서, 인스턴스를 멘션하며 피드 URL을 알려주는 것만으로 피드별 봇을 하나씩 등록할 수 있게 합니다. 각 부의 마지막에는 봇을 공개 인터넷에 노출시키고, ActivityPub.Academy에서 실제 Mastodon 계정으로 팔로우하고 멘션하고 지켜보며 테스트합니다.

이 튜토리얼은 시작하기를 이미 읽었다는 전제로 진행하므로, createBot()이나 Text를 처음부터 다시 설명하지는 않습니다. 완성된 프로젝트는 BotKit 저장소의 examples/rss-bot/에서도 볼 수 있습니다.

봇 하나로 시작하기

프로젝트 준비하기

새 디렉터리를 만들고, BotKit과 함께 피드의 원본 XML을 평범한 객체로 바꿔 줄 @rowanmanning/feed-parser를 설치합니다.

Deno

mkdir rss-bot && cd rss-bot
deno add jsr:@fedify/botkit npm:@rowanmanning/feed-parser

npm

mkdir rss-bot && cd rss-bot
npm init -y
npm add @fedify/botkit @rowanmanning/feed-parser

pnpm

mkdir rss-bot && cd rss-bot
pnpm init
pnpm add @fedify/botkit @rowanmanning/feed-parser

Yarn

mkdir rss-bot && cd rss-bot
yarn init -y
yarn add @fedify/botkit @rowanmanning/feed-parser

피드 가져오고 파싱하기

@rowanmanning/feed-parser는 RSS 0.9x, RSS 2.0, RDF Site Summary 1.0(RSS 1.0), Atom 0.3/1.0 피드를 모두 같은 parseFeed() 함수, 같은 Feed/FeedItem 형태로 파싱해 줍니다. 덕분에 봇 코드 쪽에서는 지금 이 피드가 어떤 포맷인지 굳이 분기할 필요가 없습니다.

parseFeed()가 하지 않는 일도 있습니다. 바로 가져오기(fetch)입니다. 이 함수는 이미 손에 쥔 XML 문자열을 객체로 바꿔 줄 뿐, 그 문자열을 어떻게 구할지는 호출하는 쪽의 몫입니다. 봇 코드도 이 구분을 그대로 따라서, 가져오기를 전담하는 feed.ts 모듈을 두고, 이를 어떤 주기로 폴링할지는 별도로 다룹니다.

feed.ts

// 피드(RSS, Atom, RDF)를 가져오고 파싱한다. 가져오기는 이 모듈의 일이고,
// 파싱은 전적으로 @rowanmanning/feed-parser의 일이다. 이 라이브러리는 피드를
// 그저 XML 문자열로만 볼 뿐, 그 문자열이 어떻게 조달됐는지는 신경 쓰지 않는다.

import { parseFeed } from "@rowanmanning/feed-parser";

// @rowanmanning/feed-parser의 공개 엔트리포인트는 parseFeed 함수만 내보낼 뿐
// 반환값인 Feed/FeedItem 타입은 내보내지 않는다. 그래서 패키지 내부 경로를
// 직접 뒤지는 대신, 함수의 반환 타입에서 역으로 타입을 끌어온다.
export type Feed = ReturnType<typeof parseFeed>;
export type FeedItem = Feed["items"][number];

export async function fetchFeed(
  url: string | URL,
  signal?: AbortSignal,
): Promise<Feed> {
  const response = await fetch(url, { signal });
  if (!response.ok) {
    throw new Error(
      `Failed to fetch feed: ${response.status} ${response.statusText}`,
    );
  }
  return parseFeed(await response.text());
}

맨 위의 타입 유도 코드는 자잘한 우회로입니다. @rowanmanning/feed-parser 패키지는 parseFeed 함수만 내보낼 뿐 그 함수가 반환하는 Feed, FeedItem 클래스는 내보내지 않으므로, type { Feed }를 패키지에서 직접 가져오려 하면 실패합니다. ReturnType<typeof parseFeed>는 타입을 직접 이름 붙여 가져오는 대신, 함수 시그니처로부터 TypeScript가 스스로 타입을 유추하게 만들어 이 문제를 피해 갑니다.

폴링하고 게시하기

봇 자체는 단순하게 시작합니다. 봇을 만들고, 일정 주기로 피드를 폴링하고, 새로 올라온 글을 게시하는 것뿐입니다. 식별자를 비롯한 나머지 설정은 시작하기에서 본 패턴을 그대로 따르는데, 다만 여기서 쓰는 kv 스토어와 queue는 로컬 개발용 인메모리 구현입니다. 재시작이 문제가 되는 시점에 다른 것으로 교체할 예정입니다.

bot.ts

// RSS/Atom/RDF 피드를 일정 주기로 폴링하다가 새 글이 올라오면 페디버스에
// 게시하는 봇.
//
// 실행:  deno serve --allow-net --allow-env --watch bot.ts
//   또는: npx srvx serve --port 8000 --entry ./bot.ts
// 설정:  ORIGIN=https://your-domain
//        FEED_URL=https://example.com/feed.xml (기본값은 Hacker News)
//        POLL_INTERVAL_MS=600000 (기본값은 10분)

import {
  createBot,
  InProcessMessageQueue,
  link,
  MemoryKvStore,
  text,
} from "@fedify/botkit";
import { fetchFeed } from "./feed.ts";
import type { FeedItem } from "./feed.ts";

const FEED_URL = process.env.FEED_URL ?? "https://news.ycombinator.com/rss";
const ORIGIN = process.env.ORIGIN ?? "http://localhost:8000";

const rawPollIntervalMs = process.env.POLL_INTERVAL_MS;
const POLL_INTERVAL_MS = rawPollIntervalMs == null || rawPollIntervalMs === ""
  ? 1000 * 60 * 10
  : Number(rawPollIntervalMs);
if (!Number.isFinite(POLL_INTERVAL_MS) || POLL_INTERVAL_MS <= 0) {
  throw new RangeError(
    `POLL_INTERVAL_MS must be a positive number of milliseconds: ${rawPollIntervalMs}`,
  );
}

const bot = createBot<void>({
  username: "rssbot",
  name: "RSS Bot",
  summary: text`I watch ${link(FEED_URL)} and post new entries here.`,
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
});

function itemKey(item: FeedItem): string | null {
  return item.id ?? item.url;
}

const posted = new Set<string>();
let firstPoll = true;

async function poll(): Promise<void> {
  const feed = await fetchFeed(FEED_URL);
  const items = [...feed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  if (firstPoll) {
    // 봇을 맨 처음 띄운 순간 피드에 이미 올라와 있던 글 전부를 팔로워에게
    // 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만 "새 글"로 친다.
    firstPoll = false;
    for (const item of items) {
      const key = itemKey(item);
      if (key != null) posted.add(key);
    }
    console.log(`Baseline: ${posted.size} existing item(s) from ${FEED_URL}.`);
    return;
  }

  const session = bot.getSession(ORIGIN);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || posted.has(key)) continue;
    await session.publish(
      text`${item.title ?? "(untitled)"}

${link(item.url ?? FEED_URL)}`,
    );
    posted.add(key);
    publishedCount++;
  }
  console.log(`Posted ${publishedCount} new item(s) from ${FEED_URL}.`);
}

let polling = false;

async function pollOnce(): Promise<void> {
  if (polling) return; // 이전 폴링이 아직 진행 중이면 건너뛴다
  polling = true;
  try {
    await poll();
  } catch (error) {
    console.error("Failed to poll feed:", error);
  } finally {
    polling = false;
  }
}

pollOnce();
setInterval(pollOnce, POLL_INTERVAL_MS);

export default bot;

맨 처음 폴링에서는 피드에 이미 올라와 있던 글을 기록만 할 뿐, 게시하지는 않습니다. 이 처리가 없다면 봇을 처음 실행하는 순간 피드의 현재 첫 페이지 전체가 팔로워들에게 쏟아지는데, 이는 “새 글”이 뜻해야 할 바가 전혀 아닙니다. 다음 폴링에서 새로 나타나는 글만 새 글로 칩니다.

pollOnce()는 실제 폴링 로직을 polling 플래그로 감쌉니다. 느린 요청이 다음 예약 틱과 겹치지 않도록 하기 위해서입니다. setInterval()은 콜백이 끝나기를 기다리지 않고 다음 호출을 예약하므로, 이 방어 장치가 없으면 POLL_INTERVAL_MS보다 오래 걸리는 피드 요청 때문에 폴링 두 개가 동시에 돌면서 같은 글을 중복 게시할 수 있습니다.

POLL_INTERVAL_MS는 시작 시점에 한 번 파싱하고, 유한한 양수인지 검사합니다. 값이 비어 있거나 잘못돼서 그대로 setInterval(fn, NaN)으로 흘러 들어가면 에러도 나지 않고, 그냥 이벤트 루프의 거의 매 틱마다 실행되며 피드를 두들겨 댑니다. 실행 시점에 요란하게 실패하는 편이, 실행 중에 조용히 잘못되는 것보다 낫습니다.

FEED_URL의 기본값은 Hacker News의 첫 페이지 피드입니다. RSS든 Atom이든 RDF든 어떤 피드를 써도 되지만, 테스트하는 동안에는 자주 갱신되는 피드를 쓰는 편이 좋습니다. 봇이 실제로 뭔가 게시하는 모습을 보려고 오래 기다릴 필요가 없기 때문입니다.

봇 실행하기

시작하기에서와 마찬가지로 봇을 기본 내보내기(default export)로 내보내고 실행합니다.

Deno

deno serve --allow-net --allow-env --watch bot.ts

Node.js

npx srvx serve --port 8000 --entry ./bot.ts

콘솔에는 몇 초 안에 베이스라인 개수가 찍히고, 이후 POLL_INTERVAL_MS가 지나면 Posted 0 new item(s)가 뜨거나, 그사이 피드에 뭔가 새로 올라왔다면 실제로 글이 하나 게시됩니다. 10분을 기다리지 않고 바로 게시되는 모습을 보고 싶다면 지금은 주기를 짧게 잡아 봅니다.

POLL_INTERVAL_MS=60000 deno serve --allow-net --allow-env --watch bot.ts

Tip

fedify lookup은 URL이나 핸들로 임의의 액터나 오브젝트를 조회해서 ActivityPub 표현을 출력해 줍니다. 여기서 fedify lookup http://localhost:8000/ap/actor/bot을 실행해 보면, 더 나아가기 전에 봇의 액터 문서가 제대로 나오는지 빠르게 확인할 수 있습니다.

멘션에 답장하기

게시만 하고 응답은 전혀 하지 않는 봇은, 잘 돌아가고 있어도 죽은 것처럼 느껴집니다. onMention은 봇에게 할 말을 만들어 줍니다. 지금 어떤 피드를 보고 있는지, 얼마나 자주 확인하는지 같은 것들 말이죠.

bot.ts

function formatInterval(ms: number): string {
  if (ms < 60_000) {
    const seconds = Math.round(ms / 1000);
    return `${seconds} second${seconds === 1 ? "" : "s"}`;
  }
  const minutes = Math.round(ms / 60_000);
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
}

let feedTitle: string | null = null;

bot.onMention = async (_session, message) => {
  await message.reply(
    text`I'm watching ${
      link(feedTitle ?? FEED_URL, FEED_URL)
    } and check for new posts every ${formatInterval(POLL_INTERVAL_MS)}.`,
  );
};

feedTitle은 poll()이 실행될 때마다 피드 자체의 <title>에서 값을 받아 옵니다(fetchFeed()가 반환한 직후에 feedTitle = feed.title;을 추가하면 됩니다). 그래서 답장은 언제나 봇이 방금 실제로 확인한 내용을 반영할 뿐, 한 번 타이핑해 놓고 갱신되지 않는 이름을 말하지 않습니다. 첫 폴링이 끝나기 전에는 피드의 URL을 대신 출력합니다.

formatInterval()이 존재하는 이유는, POLL_INTERVAL_MS가 테스트 중에는 60000처럼 작을 수도 있고 운영 환경에서는 기본값인 10분일 수도 있어서인데, “매 600000밀리초마다”는 봇이 사람에게 할 만한 말이 아니기 때문입니다.

재시작에도 살아남기

이 버전에는 아직 버그가 하나 남아 있는데, 재시작할 때만 드러납니다. 자주 갱신되는 피드를 감시하던 봇이 피드에 새 글이 올라온 직후, 다음 예약 폴링이 그 글에 닿기 전에 재시작되면, 그 글은 영영 게시되지 않고 실패한 흔적도 전혀 남지 않습니다. firstPoll과 posted 둘 다 프로세스 메모리에만 있던 값이라 재시작하면 초기화되므로, 봇은 바로 다음 폴링을 마치 이전에 한 번도 실행된 적이 없는 것처럼 취급합니다. 그 시점에 피드가 보여 주는 것을 그대로 베이스라인으로 삼아 “이미 본 글” 집합에 편입시켜 버릴 뿐, 그 “새” 글을 게시하는 일은 결코 일어나지 않습니다. 겉으로는 잘 돌아가는 것처럼 보이는 봇이 재시작이나 배포 때마다 조용히 글을 흘리고 있을 수 있는 것입니다.

해법은 봇 자신의 상태와 폴러의 상태를 모두 디스크에 남기는 것입니다. 그것도 하나가 아니라 SQLite 데이터베이스 두 개로 나눠서요.

bot.db는 온전히 @fedify/botkit-sqlite의 몫입니다. 여기에는 BotKit이 액터의 암호화 키, 보낸 액티비티, 그 밖에 Repository가 책임지는 모든 것을 저장합니다. 봇 자신의 코드는 이 안에 무엇이 들었는지 알 필요가 없고, 알려고 해서도 안 됩니다.

app.db는 이 프로젝트 자체의 스키마입니다. 어떤 글을 이미 게시했는지를 담아서, 재시작해도 잊어버리지 않게 합니다.

@fedify/botkit-sqlite를 설치합니다.

Deno

deno add jsr:@fedify/botkit-sqlite

npm

npm add @fedify/botkit-sqlite

pnpm

pnpm add @fedify/botkit-sqlite

Yarn

yarn add @fedify/botkit-sqlite

app.db는 Node.js 내장 모듈인 node:sqlite로 직접 엽니다.

db.ts

// app.db: 피드 폴러 자체의 상태. @fedify/botkit-sqlite의 SqliteRepository가
// 전담하는 bot.db와는 분리되어 있다. 지금은 어떤 피드 항목을 이미 게시했는지만
// 추적해서, 재시작해도 이미 본 것을 잊지 않게 한다.
//
// node:sqlite의 DatabaseSync API는 완전히 동기식이다. 아래 호출 중 어느
// 것도 await가 필요 없고, 붙일 수도 없다.

import { DatabaseSync } from "node:sqlite";

export function openAppDb(path: string): DatabaseSync {
  const db = new DatabaseSync(path);
  db.exec(`
    CREATE TABLE IF NOT EXISTS posted_items (
      item_id TEXT PRIMARY KEY,
      posted_at TEXT NOT NULL
    )
  `);
  // 특정 항목에 관한 것이 아닌 상태를 위한 작은 키-값 테이블. 맨 처음
  // 폴링이 일어났는지 여부는 posted_items에서 추론하는 대신 여기에 둔다.
  // 그렇게 하지 않으면, 첫 폴링 시점에 피드가 마침 비어 있거나 항목들에
  // 쓸 만한 id/url이 없는 경우 베이스라인이 잡혔다는 사실 자체를 영영
  // 기록하지 못하게 된다.
  db.exec(`
    CREATE TABLE IF NOT EXISTS app_meta (
      key TEXT PRIMARY KEY,
      value TEXT NOT NULL
    )
  `);
  return db;
}

export function isBaselined(db: DatabaseSync): boolean {
  return (
    db.prepare("SELECT 1 FROM app_meta WHERE key = 'baselined'").get() != null
  );
}

export function markBaselined(db: DatabaseSync): void {
  db.prepare(
    "INSERT OR IGNORE INTO app_meta (key, value) VALUES ('baselined', '1')",
  ).run();
}

export function isPosted(db: DatabaseSync, itemId: string): boolean {
  return (
    db.prepare("SELECT 1 FROM posted_items WHERE item_id = ?").get(
      itemId,
    ) != null
  );
}

export function markPosted(db: DatabaseSync, itemId: string): void {
  db.prepare(
    "INSERT OR IGNORE INTO posted_items (item_id, posted_at) VALUES (?, ?)",
  ).run(itemId, new Date().toISOString());
}

Note

node:sqlite가 실험적 플래그에서 벗어나기 전 버전의 Node.js(22.13.0과 23.4.0 이전)에서는 시작할 때 ExperimentalWarning: SQLite is an experimental feature라는 줄이 뜹니다. 정상이니 신경 쓰지 않아도 되고, 모듈은 그대로 잘 동작합니다.

isBaselined()를 posted_items와 별도로 두는 데는 이유가 있습니다. “첫 폴링이 일어났는가”라는 질문의 답을 posted_items의 행 개수로 대신하려 하면 거의 항상은 맞아떨어지지만, 딱 하나 예외가 있습니다. 바로 그 첫 폴링에서 피드가 마침 항목을 하나도 반환하지 않거나, 쓸 만한 id/url이 없는 항목만 반환하는 경우입니다. app_meta는 그 시점에 표시할 수 있었던 항목이 몇 개였는지와 무관하게, “이 이정표에 도달했는가” 자체를 독립적으로 추적합니다.

bot.ts는 이제 두 데이터베이스를 모두 만들고, posted/firstPoll을 영속화된 대응물로 바꿉니다.

bot.ts

// RSS/Atom/RDF 피드를 일정 주기로 폴링하다가 새 글이 올라오면 페디버스에
// 게시하는 봇.
//
// 실행:  deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts
//   또는: npx srvx serve --port 8000 --entry ./bot.ts
// 설정:  ORIGIN=https://your-domain
//        FEED_URL=https://example.com/feed.xml (기본값은 Hacker News)
//        POLL_INTERVAL_MS=600000 (기본값은 10분)

import {
  createBot,
  InProcessMessageQueue,
  link,
  MemoryKvStore,
  text,
} from "@fedify/botkit";
import { SqliteRepository } from "@fedify/botkit-sqlite";
import { mkdirSync } from "node:fs";
import { fetchFeed } from "./feed.ts";
import type { FeedItem } from "./feed.ts";
import {
  isBaselined,
  isPosted,
  markBaselined,
  markPosted,
  openAppDb,
} from "./db.ts";

mkdirSync("./data", { recursive: true });

const FEED_URL = process.env.FEED_URL ?? "https://news.ycombinator.com/rss";
const ORIGIN = process.env.ORIGIN ?? "http://localhost:8000";

// POLL_INTERVAL_MS를 환경 변수에서 파싱하고 검증하는 부분은 앞서와 똑같으므로
// 여기서는 생략한다.

const bot = createBot<void>({
  username: "rssbot",
  name: "RSS Bot",
  summary: text`I watch ${link(FEED_URL)} and post new entries here.`,
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
  repository: new SqliteRepository({ path: "./data/bot.db" }),
});

const appDb = openAppDb("./data/app.db");

function itemKey(item: FeedItem): string | null {
  return item.id ?? item.url;
}

let feedTitle: string | null = null;

async function poll(): Promise<void> {
  const feed = await fetchFeed(FEED_URL);
  feedTitle = feed.title;
  const items = [...feed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  // 이 봇이 태어나서 처음 실행되는 순간 피드의 현재 첫 페이지 전체를
  // 팔로워에게 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만
  // "새 글"로 친다. 이 값은 app.db에 남기 때문에, 재시작할 때마다가 아니라
  // 통틀어 딱 한 번만 일어난다.
  const isFirstEverPoll = !isBaselined(appDb);

  const session = bot.getSession(ORIGIN);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || isPosted(appDb, key)) continue;
    if (!isFirstEverPoll) {
      await session.publish(
        text`${item.title ?? "(untitled)"}

${link(item.url ?? FEED_URL)}`,
      );
      publishedCount++;
    }
    markPosted(appDb, key);
  }
  if (isFirstEverPoll) markBaselined(appDb);
  console.log(
    isFirstEverPoll
      ? `Baseline: ${items.length} existing item(s) from ${FEED_URL}.`
      : `Posted ${publishedCount} new item(s) from ${FEED_URL}.`,
  );
}

두 데이터베이스 파일 모두 어딘가 담길 곳이 필요합니다. Deno의 권한 모델을 생각하면 프로젝트 루트에 흩어놓지 않고 전용 디렉터리 하나에 모아 두는 편이 좋습니다. mkdirSync("./data", ...)가 그 디렉터리를 만들어 주므로, 프로세스는 파일시스템 전체가 아니라 이 경로 하나에만 읽기·쓰기 권한을 가지면 됩니다.

이제 더 넓어진 권한으로 봇을 다시 실행합니다.

Deno

deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts

Node.js

npx srvx serve --port 8000 --entry ./bot.ts

새 글이 올라온 직후에 예전과 같은 방식으로 재시작해 봅니다. 이번에는 그 글이 베이스라인 속으로 사라지는 대신, 다음 폴링에 그대로 나타납니다.

실제로 공개하기

지금까지 봇은 localhost에서만 동작하면 됐습니다. 다른 페디버스 서버가 이 봇에 닿으려면 공개 주소가 필요합니다.

터널링 서비스를 쓰면 아직 내 서버가 없어도 “공개 주소” 문제의 절반을 해결할 수 있습니다. 이런 서비스들은 봇 앞에서 L7 리버스 프록시처럼 동작하므로, behindProxy를 켜서 이들이 붙이는 X-Forwarded-* 헤더를 신뢰하게 하고, 로컬 개발과 터널링된 실행이 같은 코드를 쓸 수 있도록 환경 변수로 읽어들입니다.

import { createBot } from "@fedify/botkit";

const BEHIND_PROXY = process.env.BEHIND_PROXY?.trim()?.toLowerCase() ===
  "true";

const bot = createBot<void>({
  // 나머지 옵션은 지면상 생략
  behindProxy: BEHIND_PROXY,
});

그다음 터널을 띄웁니다. 이 튜토리얼에서는 fedify tunnel을 쭉 사용하지만, 이미 다른 서비스를 써 왔다면 아래 중 무엇을 써도 같은 방식으로 동작합니다.

fedify tunnel

fedify tunnel 8000

ngrok

ngrok http 8000

Tailscale Funnel

tailscale funnel 8000

Cloudflare Tunnel

cloudflared tunnel --url http://localhost:8000

fedify tunnel은 준비가 끝나면 공개 호스트명을 출력합니다.

✔ Your local server at 8000 is now publicly accessible:

https://c4d3933be87bc2.lhr.life/

Press ^C to close the tunnel.

이번에는 ORIGIN은 그 주소로, BEHIND_PROXY는 true로 맞춰서 봇을 다시 실행합니다.[1]

Deno

ORIGIN=https://c4d3933be87bc2.lhr.life BEHIND_PROXY=true \
  deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch bot.ts

Node.js

ORIGIN=https://c4d3933be87bc2.lhr.life BEHIND_PROXY=true \
  npx srvx serve --port 8000 --entry ./bot.ts

브라우저로 그 주소에 들어가 보면 봇 자신의 프로필 페이지가 나타납니다.

봇의 웹 프로필 페이지. 이름과 핸들, 소개, 그리고 팔로워·게시물 수가 0으로 표시되어 있다

ActivityPub.Academy로 테스트하기

ActivityPub.Academy는 몇 초 만에 임시 Mastodon 계정을 하나 내주는데, 바로 이런 용도, 즉 봇을 팔로우하고 멘션하면서 어떤 답이 돌아오는지 지켜보기에 딱 맞습니다. 계정은 하루 뒤에 지워지므로 테스트가 끝난 뒤 따로 정리할 것도 없습니다.

가입한 다음, 봇의 페디버스 핸들로 검색합니다. @rssbot@c4d3933be87bc2.lhr.life이고, 자신의 터널 호스트명으로 바꿔 넣으면 됩니다.

ActivityPub.Academy에서 봇의 핸들을 검색해서 봇이 검색 결과로 나타난 모습

검색 결과를 열고 Follow를 누릅니다.

오른쪽 위에 Follow 버튼이 있는, ActivityPub.Academy에서 본 봇의 프로필

팔로우한 뒤 같은 프로필. “1 Follower”와 Unfollow 버튼이 보인다

그다음 멘션합니다.

Hey @rssbot@c4d3933be87bc2.lhr.life, what are you up to?

답장은 몇 초 안에 도착합니다.

멘션과 봇의 답장을 보여 주는 스레드: “I'm watching Hacker News and check for new posts every 2 minutes.”

다음번에 봇의 폴링이 정말로 새로운 글을 잡아내면, 다른 여느 게시물과 마찬가지로 타임라인에 나타납니다.

ActivityPub.Academy의 홈 타임라인. 앞서의 멘션 답장 위에 봇이 새로 게시한 글이 보인다

봇 여럿, 인스턴스 하나

피드 하나로 아이디어는 확인했습니다. 하지만 FEED_URL 하나를 하드코딩하는 방식으로는 여러 피드를 동시에 돌리거나, 재배포 없이 피드를 추가하거나, 갈수록 시끄러워지는 타임라인 하나를 다 같이 쓰는 대신 피드마다 자기만의 페디버스 정체성을 갖게 하기는 어렵습니다. Instance는 바로 이럴 때를 위해 BotKit이 마련해 둔 조각입니다. 서버 하나가 여러 봇을 호스팅하되, 각 봇은 저마다 액터와 팔로워, 인박스를 가지면서도 같은 키-값 스토어, 큐, 리포지토리를 밑에서 함께 씁니다.

createBot()이 사라지는 것은 아닙니다. 딱 하나의 액터로만 존재하면 되는 봇이라면 굳이 바꿀 이유가 없습니다. 이제부터는 rssbot을, 등록된 피드마다 봇 하나씩을 호스팅하는 인스턴스로 바꾸고, 여기에 누군가 URL과 함께 멘션했을 때 피드를 추가하는 일만 하는 작은 등록 담당 봇을 하나 더합니다.

순진한 식별자, 그리고 그것이 깨지는 이유

피드별 봇은 저마다 자기만의 식별자가 필요합니다. 봇이 페디버스에 연합되는 순간 액터 URI에 새겨지는 내부 이름 말이죠. 가장 먼저 떠오르는 생각은 피드 자체에서, 이를테면 호스트명에서 하나를 뽑아내는 것입니다. https://xkcd.com/rss.xml을 xkcd-com으로 바꾸는 식으로요. 이 문자열은 마침 사용자명으로도 나쁘지 않습니다. 사람이 봇을 찾으려고 실제로 입력하는, @xkcd-com@your-domain의 사람이 읽을 수 있는 절반이니까요.

문제는 이걸 둘 다에 쓰려는 데서 생깁니다. 식별자는 봇이 한 번 연합되고 나면 영원히 고정돼 있어야 합니다. 원격 서버들이 캐시해 둔 액터 URI 안에 그 식별자를 그대로 담고 있어서, 나중에 바꾸면 모든 팔로워 관계와 다른 서버가 들고 있는 모든 참조가 고아가 돼 버립니다. 그런데 피드의 호스트명에는 그런 종류의 영속성이 없습니다. 사이트는 옮겨 다닙니다. 블로그가 도메인을 하나에서 다른 곳으로 옮기고도 여전히 같은 피드, 같은 팔로워를 가진 같은 피드일 수 있는데, 이미 옛 식별자를 갖고 있는 모든 이를 깨뜨리지 않으면서 식별자만 새 이름에 맞게 바꿀 방법은 없습니다. 사용자명으로는 더할 나위 없이 좋은 성질, 즉 기억하기 쉽고 봇의 실체와 맞닿아 있다는 성질이야말로, 식별자로는 나쁜 선택이 되게 만드는 바로 그 성질입니다.

해법은 이 둘을 떼어 놓는 것입니다. 식별자는 등록 시점에 한 번 배정되고 두 번 다시 손대지 않는, 의미 없는 문자열입니다. 슬러그는 피드의 URL에서 뽑아낸 것으로, 언제든 다시 계산해도 되고, “사람이 이 봇을 찾으려고 뭐라고 입력하는가”라는 질문에만 쓰일 뿐 “이 봇의 액터 URI에는 무엇이 들어 있는가”라는 질문에는 결코 쓰이지 않습니다.

feeds 테이블

app.db는 이 구분을 담을 feeds 테이블을 새로 얻고, posted_items는 봇 하나의 이력을 추적하는 대신 피드별로 범위가 좁혀집니다.

db.ts

// app.db: 피드 폴러 자체의 상태. @fedify/botkit-sqlite의 SqliteRepository가
// 전담하는 bot.db와는 분리되어 있다.
//
// node:sqlite의 DatabaseSync API는 완전히 동기식이다. 아래 호출 중 어느
// 것도 await가 필요 없고, 붙일 수도 없다.
//
// 피드의 identifier는 의미 없이 고정된 키다. 한 번 연합되고 나면 액터 URI에
// 새겨지므로, (피드의 URL이나 제목처럼) 바뀔 수 있는 것으로부터는 절대
// 유도해서는 안 된다. slug는 핸들에서 사람이 읽는 부분으로, mapUsername을
// 거쳐 identifier와 서로 오간다.

import { DatabaseSync } from "node:sqlite";

export interface FeedRow {
  readonly identifier: string;
  readonly url: string;
  readonly slug: string;
  readonly title: string | null;
  readonly baselined: boolean;
}

function toFeedRow(row: Record<string, unknown>): FeedRow {
  return {
    identifier: row.identifier as string,
    url: row.url as string,
    slug: row.slug as string,
    title: row.title as string | null,
    baselined: (row.baselined as number) !== 0,
  };
}

export function openAppDb(path: string): DatabaseSync {
  const db = new DatabaseSync(path);
  db.exec(`
    CREATE TABLE IF NOT EXISTS feeds (
      identifier TEXT PRIMARY KEY,
      url TEXT NOT NULL UNIQUE,
      slug TEXT NOT NULL UNIQUE,
      title TEXT,
      baselined INTEGER NOT NULL DEFAULT 0,
      created_at TEXT NOT NULL
    )
  `);
  migrateLegacyPostedItems(db);
  db.exec(`
    CREATE TABLE IF NOT EXISTS posted_items (
      feed_identifier TEXT NOT NULL,
      item_id TEXT NOT NULL,
      posted_at TEXT NOT NULL,
      PRIMARY KEY (feed_identifier, item_id)
    )
  `);
  return db;
}

// 1부의 app.db에는 posted_items(item_id, posted_at)만 있었고, 어느 피드에
// 속한 항목인지 하는 개념 자체가 없었다. 피드가 하나뿐이었으니 당연했다.
// openAppDb()의 CREATE TABLE IF NOT EXISTS가 옛 테이블을 그대로 놔둘 것이므로,
// 그 행들을 (그 글을 게시했을 수 있는 유일한 식별자인) "bot" 식별자 아래로
// 옮겨서 이어받는다.
function migrateLegacyPostedItems(db: DatabaseSync): void {
  const columns = db.prepare("PRAGMA table_info(posted_items)").all() as {
    readonly name: string;
  }[];
  const isLegacy = columns.length > 0 &&
    !columns.some((column) => column.name === "feed_identifier");
  if (!isLegacy) return;
  db.exec("ALTER TABLE posted_items RENAME TO posted_items_legacy");
  db.exec(`
    CREATE TABLE posted_items (
      feed_identifier TEXT NOT NULL,
      item_id TEXT NOT NULL,
      posted_at TEXT NOT NULL,
      PRIMARY KEY (feed_identifier, item_id)
    )
  `);
  db.exec(`
    INSERT INTO posted_items (feed_identifier, item_id, posted_at)
    SELECT 'bot', item_id, posted_at FROM posted_items_legacy
  `);
  db.exec("DROP TABLE posted_items_legacy");
}

function slugify(url: string): string {
  return new URL(url).hostname.toLowerCase().replace(/\./g, "-");
}

export function getFeedByIdentifier(
  db: DatabaseSync,
  identifier: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE identifier = ?").get(
    identifier,
  );
  return row == null ? undefined : toFeedRow(row);
}

export function getFeedBySlug(
  db: DatabaseSync,
  slug: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE slug = ?").get(slug);
  return row == null ? undefined : toFeedRow(row);
}

export function getFeedByUrl(
  db: DatabaseSync,
  url: string,
): FeedRow | undefined {
  const row = db.prepare("SELECT * FROM feeds WHERE url = ?").get(url);
  return row == null ? undefined : toFeedRow(row);
}

export function listFeeds(db: DatabaseSync): readonly FeedRow[] {
  const rows = db.prepare("SELECT * FROM feeds").all();
  return rows.map(toFeedRow);
}

// 기존 단일 봇 배포의 피드를 시작 시점에 딱 한 번 이 테이블의 행으로
// 이어받는 데 쓴다. identifier가 기본 키이므로 첫 실행 이후로는 매번 아무
// 일도 하지 않는다.
export function seedFeed(
  db: DatabaseSync,
  feed: { identifier: string; url: string; slug: string },
): void {
  db.prepare(
    "INSERT OR IGNORE INTO feeds (identifier, url, slug, created_at) VALUES (?, ?, ?, ?)",
  ).run(feed.identifier, feed.url, feed.slug, new Date().toISOString());
  migrateLegacyBaselined(db, feed.identifier);
}

// 1부의 app.db는 "첫 폴링이 일어났는가"를 별도의 app_meta 테이블에
// 추적했다. 물어볼 대상이 하나뿐이었으니 그럴 만했다. 이 피드의 행이
// (바로 위에서) 생기고 나면 그 플래그를 이 행으로 옮기고 app_meta는
// 지운다. app_meta가 더 이상 확인할 대상이 아니게 되는 첫 실행 이후로는
// 매번 아무 일도 하지 않는다.
function migrateLegacyBaselined(db: DatabaseSync, identifier: string): void {
  const hasAppMeta = db.prepare(
    "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'app_meta'",
  ).get() != null;
  if (!hasAppMeta) return;
  const wasBaselined = db.prepare(
    "SELECT 1 FROM app_meta WHERE key = 'baselined'",
  ).get() != null;
  if (wasBaselined) markFeedBaselined(db, identifier);
  db.exec("DROP TABLE app_meta");
}

// 새로 멘션된 피드 URL을 새로운, 의미 없는 식별자로 등록한다. 식별자는
// URL이나 제목처럼 나중에 바뀔 수 있는 것으로부터는 절대 유도하지 않는다.
// slug(사람이 읽는 핸들)는 유도하며, 이미 쓰이고 있는 것과 충돌하면 숫자
// 접미사를 붙인다.
export function addFeed(db: DatabaseSync, url: string): FeedRow {
  const baseSlug = slugify(url);
  let slug = baseSlug;
  for (let suffix = 2; getFeedBySlug(db, slug) != null; suffix++) {
    slug = `${baseSlug}-${suffix}`;
  }
  const identifier = `feed_${
    crypto.randomUUID().replace(/-/g, "").slice(0, 12)
  }`;
  db.prepare(
    "INSERT INTO feeds (identifier, url, slug, created_at) VALUES (?, ?, ?, ?)",
  ).run(identifier, url, slug, new Date().toISOString());
  return { identifier, url, slug, title: null, baselined: false };
}

export function updateFeedTitle(
  db: DatabaseSync,
  identifier: string,
  title: string,
): void {
  db.prepare("UPDATE feeds SET title = ? WHERE identifier = ?").run(
    title,
    identifier,
  );
}

export function markFeedBaselined(db: DatabaseSync, identifier: string): void {
  db.prepare("UPDATE feeds SET baselined = 1 WHERE identifier = ?").run(
    identifier,
  );
}

export function isPosted(
  db: DatabaseSync,
  feedIdentifier: string,
  itemId: string,
): boolean {
  return (
    db.prepare(
      "SELECT 1 FROM posted_items WHERE feed_identifier = ? AND item_id = ?",
    ).get(feedIdentifier, itemId) != null
  );
}

export function markPosted(
  db: DatabaseSync,
  feedIdentifier: string,
  itemId: string,
): void {
  db.prepare(
    "INSERT OR IGNORE INTO posted_items (feed_identifier, item_id, posted_at) VALUES (?, ?, ?)",
  ).run(feedIdentifier, itemId, new Date().toISOString());
}

identifier와 slug 둘 다 UNIQUE 제약을 갖지만, 충돌 시 다시 계산해서 재시도하는 것은 slug뿐입니다. identifier는 addFeed()가 한 번 생성하면 그대로 놔둡니다. baselined는 1부에서는 별도의 app_meta 테이블에 있으면서 앱 전체가 폴링을 한 번이라도 한 적 있는지를 추적했지만, 이제 첫 폴링 여부가 앱 전체가 아니라 특정 피드 하나에 관한 사실이 되면서 feeds의 평범한 칼럼 하나로 합쳐집니다.

slugify()는 피드 호스트명을 소문자로 바꾸고 점을 하이픈으로 바꿉니다. news.ycombinator.com은 news-ycombinator-com이 됩니다. 그리고 그 슬러그가 이미 쓰이고 있으면 addFeed()가 숫자 접미사(-2, -3, …)를 붙입니다. 반면 identifier는 crypto.randomUUID()에서 생성한, feed_로 시작하는 짧은 무작위 문자열로, 피드에 관한 그 무엇으로부터도 유도되지 않습니다.

위의 두 migrateLegacy* 함수가 존재하는 이유는, 1부에서 쓰던 기존 app.db에는 여전히 옛 posted_items(item_id, posted_at) 테이블과 별도의 app_meta 키-값 테이블이 있고, CREATE TABLE IF NOT EXISTS는 이 둘 중 어느 것도 건드리지 않기 때문입니다. 이미 존재하는 테이블은 그냥 그대로 두므로, 옛 posted_items는 이 파일의 나머지 부분이 기대하는 feed_identifier 칼럼이 없는 채로 남고, isPosted()를 처음 호출하는 순간 no such column: feed_identifier로 실패하고 말 것입니다. migrateLegacyPostedItems()는 openAppDb()가 뭔가를 만들기 전에 그 모양을 확인해서 발견하면 마이그레이션하고, migrateLegacyBaselined()는 app_meta의 baselined 플래그에 대해 같은 일을 하되, 그 플래그가 속할 피드 행이 실제로 존재하게 된 다음 seedFeed()에서 호출됩니다. 둘 다 실행 전에 옛 모양인지부터 확인하므로, 기존 app.db가 아예 없는 새 설치라면 그냥 건너뛰고, 이미 마이그레이션된 것이라면 그 이후 실행마다 건너뜁니다. posted_items에는 이미 feed_identifier가 있고, app_meta는 이미 사라졌으니까요.

봇에서 인스턴스로

bot.ts는 이제 봇 하나가 아니라 여럿을 호스팅하므로 instance.ts가 됩니다.

instance.ts

import {
  createInstance,
  InProcessMessageQueue,
  MemoryKvStore,
} from "@fedify/botkit";
import { SqliteRepository } from "@fedify/botkit-sqlite";
import { mkdirSync } from "node:fs";
import { openAppDb, seedFeed } from "./db.ts";

// FEED_URL, BEHIND_PROXY는 앞서와 같은 방식으로 환경 변수에서 읽어 온다.
// 여기서는 생략한다.

mkdirSync("./data", { recursive: true });

const instance = createInstance<void>({
  kv: new MemoryKvStore(),
  queue: new InProcessMessageQueue(),
  repository: new SqliteRepository({ path: "./data/bot.db" }),
  behindProxy: BEHIND_PROXY,
  // 1부의 봇은 identifier를 명시적으로 지정한 적이 없으므로, createBot()이
  // 기본값으로 "bot"을 붙였다. "rssbot"은 어디까지나 사용자명이었을 뿐이다.
  // (아래의 legacyObjectUris가 아니라) 바로 이 identifier를 그대로 재사용하는
  // 것이 액터의 URI와 키, 팔로워 관계를 그대로 지켜 주는 열쇠다.
  // legacyObjectUris는 원격 서버가 이 봇이 인스턴스로 옮겨 오기 전부터
  // 캐시해 두고 있을 수 있는, 개별 오브젝트(게시물, 팔로우) URI의 *옛*
  // 형식만 다시 써 준다.
  legacyObjectUris: { identifier: "bot" },
});

const appDb = openAppDb("./data/app.db");

// 원래 피드를 봇의 실제(기본) identifier와 기존 사용자명 그대로 여기 행으로
// 이어받는다. 그래서 이 피드는 이제부터 등록되는 다른 모든 피드와 똑같은
// 동적 봇 그룹이 처리한다. 첫 실행 이후로는 아무 일도 하지 않는다.
seedFeed(appDb, { identifier: "bot", url: FEED_URL, slug: "rssbot" });

createInstance()는 createBot()이 쓰던 것과 같은 인프라 옵션들, 즉 kv, queue, repository, behindProxy를 그대로 받습니다. 새로 등장한 것은 legacyObjectUris입니다. 1부의 rssbot은 identifier 옵션을 명시적으로 지정한 적이 없으므로 createBot()이 기본값으로 "bot"을 붙였습니다. "rssbot"은 어디까지나 사용자명이었을 뿐입니다. legacyObjectUris가 아니라 바로 이 같은 identifier를 재사용하는 것이, 이제 서버에 봇 하나만 있는 게 아니라 여럿 중 하나가 된 지금도 원래 봇의 액터 URI와 암호화 키, 팔로워 관계를 온전히 지켜 주는 열쇠입니다. legacyObjectUris는 좀 더 좁은 범위, 즉 createInstance()로 옮기기 전에 게시된 개별 오브젝트 URI(게시물과 팔로우)의 옛 형식을 다룹니다. 원격 서버가 여전히 그 형식을 캐시하고 있다가 나중에 Accept나 Like로 돌려보낼 수 있기 때문입니다.

seedFeed()는 이 같은 봇을 평범한 행 하나로 이어받습니다. 같은 identifier, 같은 URL, 슬러그로는 같은 사용자명을 씁니다. INSERT OR IGNORE이므로 첫 실행 이후로는 매번 아무 일도 하지 않고, 이제부터 원래 봇은 나중에 등록되는 어떤 피드와도 똑같은 동적 봇 그룹이 처리하며, 다른 어디에도 별도의 특수 처리는 없습니다.

이게 가능하려면 미리 이뤄져야 할 마이그레이션이 있는데, 이 코드 자체가 하는 일은 아닙니다. BotKit은 이미 createBot() 배포의 리포지토리 데이터를 createInstance()가 기대하는 구조로 마이그레이션해 주지만, 그 배포가 createBot() 아래에서 BotKit 0.5.0 이상으로 처음 실행될 때 딱 한 번만 그렇습니다. 여기까지 왔다면 1부의 봇은 그저 실행된 것만으로 이미 그 과정을 마쳤을 것입니다. 만약 0.5.0에서 아예 실행된 적이 없는 배포를 옮기는 중이라면, 먼저 createBot()으로 한 번 실행하거나 리포지토리의 migrate() 메서드를 직접 호출하세요. 자세한 내용은 단일 봇 배포 마이그레이션하기를 참고하세요. createInstance()로 바꾸기 전후로 액터의 URI에 fedify lookup을 직접 돌려 보는 것도 해 볼 만합니다. 공개 키와 WebFinger 핸들이 완전히 똑같이 나와야 합니다.

인스턴스 하나에 피드 여럿

// 동적 봇 그룹: feeds 테이블의 행 하나마다 봇 하나씩을, 필요할 때마다
// 해석한다. 피드가 추가될 때마다 명령형으로 createBot()을 호출하는 대신,
// "피드마다 봇 하나"를 사전에 한 번의 등록으로 바꿔 주는 것이 바로 이
// 부분이다. 아래의 registryBot.onMention을 참고할 것.
const feedBots = instance.createBot(
  (_ctx, identifier) => {
    const feed = getFeedByIdentifier(appDb, identifier);
    if (feed == null) return null;
    return { username: feed.slug, name: feed.title ?? feed.url };
  },
  {
    mapUsername(_ctx, username) {
      const feed = getFeedBySlug(appDb, username);
      return feed?.identifier ?? null;
    },
  },
);

feedBots.onMention = async (session, message) => {
  const feed = getFeedByIdentifier(appDb, session.bot.identifier);
  if (feed == null) return;
  await message.reply(
    text`I'm watching ${
      link(feed.title ?? feed.url, feed.url)
    } and check for new posts every ${formatInterval(POLL_INTERVAL_MS)}.`,
  );
};

instance.createBot()에 고정된 식별자 대신 함수를 넘기면 BotGroup이 만들어집니다. 사전에 선언해 두는 대신 필요할 때마다 해석되는 봇들의 집합인데, BotKit 자체 문서에서 지역별 날씨 봇을 만들 때 쓰는 것과 같은 동적 봇 패턴입니다. 여기 디스패처가 하는 일도 같은 종류의 조회입니다. 식별자가 주어지면 feeds에서 대응하는 행을 찾고, 없으면 null을 반환해서 BotKit에게 이 식별자는 애초에 피드 봇이 아니라고 알려 줍니다. mapUsername은 그 반대 방향으로, identifier가 아니라 slug로 같은 조회를 수행합니다. 그래서 @xkcd-com@your-domain과, 무작위로 생성된 feed_a1b2c3d4e5f6을 식별자로 가진 액터가 양쪽 방향 모두에서 같은 봇으로 해석됩니다.

BotGroup은 자기만의 단일한 식별자를 갖지 않으므로, 이를 통해 게시하려면 어느 봇인지 말해 줘야 합니다. 1부에서 쓰던 bot.getSession(origin) 대신 feedBots.getSession(origin, identifier)를 씁니다. 폴링은 feeds의 모든 행을 돌면서, 피드마다 정확히 그렇게 한 번씩 합니다.

const FETCH_TIMEOUT_MS = 30_000;

async function pollFeed(feed: FeedRow): Promise<void> {
  const parsed = await fetchFeed(
    feed.url,
    AbortSignal.timeout(FETCH_TIMEOUT_MS),
  );
  if (parsed.title != null && parsed.title !== feed.title) {
    updateFeedTitle(appDb, feed.identifier, parsed.title);
  }
  const items = [...parsed.items].reverse(); // 피드는 최신 글이 맨 앞에 온다

  // 이 피드가 처음 폴링되는 순간 현재 첫 페이지 전체를 팔로워에게
  // 쏟아붓지 않기 위함이다. 이후 폴링에서 새로 나타나는 글만 "새 글"로 친다.
  const isFirstEverPoll = !feed.baselined;

  const session = await feedBots.getSession(ORIGIN, feed.identifier);
  let publishedCount = 0;
  for (const item of items) {
    const key = itemKey(item);
    if (key == null || isPosted(appDb, feed.identifier, key)) continue;
    if (!isFirstEverPoll) {
      await session.publish(
        text`${item.title ?? "(untitled)"}

${link(item.url ?? feed.url)}`,
      );
      publishedCount++;
    }
    markPosted(appDb, feed.identifier, key);
  }
  if (isFirstEverPoll) markFeedBaselined(appDb, feed.identifier);
  console.log(
    isFirstEverPoll
      ? `Baseline: ${items.length} existing item(s) from ${feed.url}.`
      : `Posted ${publishedCount} new item(s) from ${feed.url}.`,
  );
}

async function pollAll(): Promise<void> {
  for (const feed of listFeeds(appDb)) {
    try {
      await pollFeed(feed);
    } catch (error) {
      console.error(`Failed to poll feed ${feed.url}:`, error);
    }
  }
}

여기서는 식별자와 “이미 게시했음” 상태가 어디서 오는지를 빼면 1부의 폴링 로직과 다를 게 없습니다. 바깥 스코프에서 붙잡아 온 변수 대신 feed.identifier를 쓰고, 하나로 공유하던 Set 대신 그 identifier로 범위를 좁힌 isPosted/markPosted를 쓰고, fetchFeed()에 명시적인 AbortSignal.timeout()을 건다는 점이 다를 뿐입니다.

pollAll()은 피드를 하나씩 순서대로 기다리고, pollAllOnce()의 polling 가드 덕분에 현재 주기가 끝나기 전까지는 어떤 나중 틱도 새 주기를 시작할 수 없습니다.

이 타임아웃이 없다면, 연결은 받아 놓고 응답은 영영 하지 않는 서버 하나 때문에 그 서버가 물려 있는 피드뿐 아니라 인스턴스의 모든 피드가 영원히 멈춰 버릴 것입니다. 타임아웃이 있으면 이는 그저 평범한 피드별 실패, 이를테면 요청 시간 초과나 피드가 갑자기 이상한 XML을 반환하는 것과 다를 바 없어지고, pollAll()의 try/catch가 이를 똑같은 방식으로 처리합니다. 로그를 남기고 다음 피드로 넘어가는 식으로요.

멘션으로 피드 등록하기

지금까지 그룹에 봇을 하나 더한다는 것은 결국 feeds에 행을 하나 넣는다는 뜻이었습니다. feedBots의 디스패처는 그 테이블에 나타나는 식별자라면 무엇이든, 시작할 때부터 있던 것이든 방금 삽입된 것이든 상관없이 이미 알아서 해석해 주므로, 일단 피드가 등록되고 나면 instance.createBot()을 다시 호출할 필요가 전혀 없습니다. 바로 이것이 정적 봇 그룹 대신 동적 봇 그룹을 쓰는 이유입니다. 등록이 배포가 아니라 데이터베이스 쓰기 한 번으로 끝나는 것 말이죠.

작은 정적 봇 registry가 그 쓰기를 위한 대문 역할을 합니다.

function extractUrl(input: string): string | null {
  const match = input.match(/https?:\/\/\S+/)?.[0];
  return match != null && URL.canParse(match) ? match : null;
}

// 정적 봇: 등록 창구. 피드 URL과 함께 이 봇을 멘션하면 feeds 테이블에
// 행이 하나 추가된다. feedBots의 디스패처는 그 새 행을, 다음번에 그
// identifier가 해석될 때 알아서 집어 든다.
//
// 이 코드는 누가 멘션을 보냈는지 확인하지 않고, 아래의 폴링 루프가 가져오기
// 전에 그 URL을 검증하지도 않는다. 실제 배포에 필요한 것은 튜토리얼의
// "더 해볼 만한 것들" 절을 참고할 것.
const registryBot = instance.createBot("registry", {
  username: "registry",
  name: "Feed Registry",
  summary: text`Mention me with a feed URL to register a new feed bot.`,
});

registryBot.onMention = async (_session, message) => {
  const url = extractUrl(message.text);
  if (url == null) {
    await message.reply(text`Please include a feed URL in your mention.`);
    return;
  }
  const existing = getFeedByUrl(appDb, url);
  if (existing != null) {
    await message.reply(
      text`Already watching that feed: ${
        mention(`@${existing.slug}@${new URL(ORIGIN).host}`)
      }.`,
    );
    return;
  }
  const feed = addFeed(appDb, url);
  await message.reply(
    text`Registered! Give it a few minutes, then look for ${
      mention(`@${feed.slug}@${new URL(ORIGIN).host}`)
    }.`,
  );
};

extractUrl()은 멘션 텍스트 안에서 URL처럼 생기고 URL.canParse()를 통과하는 첫 번째 것을 찾습니다. 잘린 http://%처럼 형식이 잘못된 것은 걸러 내는데, 그렇지 않으면 addFeed()의 new URL() 호출까지 그대로 흘러 들어가 예외를 던지게 됩니다. 이미 감시 중인 URL을 등록하려 하면 feeds.url의 UNIQUE 제약에 부딪혀 죽는 대신 답장으로 알려 줍니다.

두 답장 모두 새 봇의 핸들을 mention()으로 만들지, 템플릿에 그대로 끼워 넣은 평범한 @${slug}@${host} 문자열로 만들지 않습니다. 평범한 텍스트로 적은 핸들은 그냥 텍스트일 뿐입니다. 링크로 렌더링되지도 않고, mention()이 붙여 주는 Mention 태그가 없으니 실제 멘션처럼 동작하지도 않습니다. mention()은 사람이 검색하듯 그 핸들을 실제로 찾아보고, 그 조회가 실패할 때만 평범한 텍스트로 물러나므로, 새 봇의 액터를 어쩌다 아직 해석할 수 없는 상황이라도 답장이 깨지는 대신 우아하게 낮은 수준으로 대응합니다.

registryBot.onMention이 하지 않는 일도 있습니다. 누가 묻고 있는지 확인하지 않고, pollFeed()에 곧 넘길 그 URL이 실제로 가져오기에 안전한 곳을 가리키는지도 확인하지 않습니다. 둘 다 의도적으로 남겨 둔 빈틈이고, 둘 다 여기가 아니라 아래 더 해볼 만한 것들에서 다룹니다.

두 런타임에서 인스턴스 검증하기

1부의 첫 실행과 같은 모양입니다. 다만 이제 단일 봇이 아니라 인스턴스를 호스팅하는 파일을 대상으로 합니다.

Deno

deno serve --allow-net --allow-env --allow-read=./data --allow-write=./data --watch instance.ts

Node.js

npx srvx serve --port 8000 --entry ./instance.ts

앞서와 같은 방식으로 터널을 열고, ActivityPub.Academy의 계정에서 피드 URL과 함께 등록 봇을 멘션합니다. 스킴을 포함한 전체 URL을 그대로 입력하세요. extractUrl()은 http://나 https:// 링크만 매칭하는데, Mastodon은 자동 링크된 URL을 보여 줄 때 스킴을 감춰서 표시하므로, 아래에는 스킴이 보이지 않는 것입니다.

Hey @registry, please watch https://xkcd.com/rss.xml

등록 봇에 대한 멘션과 그 답장: “Registered! Give it a few minutes, then look for @xkcd-com@…”

답장의 핸들은 mention()으로 만들어졌으므로, 그 답장을 이루는 실제 Note 객체에는 제대로 된 Mention 태그가 붙어 있고, 새 봇에게 직접 주소가 지정되어 있습니다. 그럴듯하게 보이도록 형식만 갖춘 게 아니라요. 위 스크린샷에서 ActivityPub.Academy는 이를 클릭 가능한 링크로 렌더링하지 않지만, 그 밑에 있는 데이터 자체는 정확합니다. 등록 봇 자신의 웹 페이지는 같은 답장을, 링크가 온전히 살아 있는 채로 보여 줍니다.

등록 봇 자신의 프로필 페이지. 새 봇의 핸들이 동작하는 링크로 렌더링된 같은 “Registered!” 게시물이 보인다

1~2분 뒤, 새 봇이 자기 슬러그로 나타납니다. 이 봇과 이관된 원래 봇을 둘 다 팔로우해 보면, 이 둘이 서로의 별칭이 아니라 진짜로 별개인 액터임을 확인할 수 있습니다.

세 개의 별개 봇을 보여 주는 어느 계정의 팔로잉 목록: rssbot 핸들로 이관된 원래 봇, 새로 등록된 xkcd.com 피드, 그리고 별도의 Node.js 실행에서 나온 세 번째 봇

새 봇을 직접 멘션하면, 등록 봇이 아니라 feedBots.onMention이 답합니다.

새 피드 봇에 대한 멘션과 그 답장: “I'm watching xkcd.com and check for new posts every 2 minutes.”

여기까지의 모든 과정은 Node.js에서도 똑같이 동작합니다. instance.ts를 두 번째 호스트명 뒤에서 터널링하고, 같은 방식으로 피드를 등록하면 같은 핸들이 WebFinger를 통해 그대로 해석됩니다.

서버에 배포하기

터미널이 아니라 프로세스 매니저 아래에서 instance.ts를 실행하는 것은 직접 호스팅하기의 systemd·Caddy 구성을 거의 그대로 따릅니다. 런타임 설치, botkit 사용자 만들기, Caddy 설정, 서비스 활성화까지 전체 과정은 그 문서에 있습니다. 이 봇에 특유한 것 두 가지만 짚고 넘어가면 되는데, 둘 다 examples/rss-bot/deploy/의 systemd 유닛과 Caddyfile에 이미 반영되어 있습니다.

ExecStart는 이 튜토리얼에서 쭉 써 온 것과 같은, 범위를 좁힌 Deno 권한(--allow-net --allow-env --allow-read=./data --allow-write=./data)을 쓰지, 직접 호스팅하기의 범용 -A를 쓰지 않습니다. 그리고 ProtectSystem=strict가 ReadWritePaths에 나열되지 않은 한 작업 디렉터리 전체를 읽기 전용으로 만들어 버리는데, 이 봇은 bot.db와 app.db를 ./data 아래에 쓰므로 유닛에 다음을 추가합니다.

ReadWritePaths=/opt/botkit/data

그 경로는 서비스가 시작되기 전에 botkit 사용자 소유로 이미 존재해야 합니다.

sudo mkdir -p /opt/botkit/data
sudo chown botkit:botkit /opt/botkit/data

직접 호스팅하기 자체의 mkdir -p /opt/botkit 단계 바로 다음에 실행하면 됩니다. 이걸 건너뛰면 유닛은 아예 시작조차 하지 못하고 종료 코드 226/NAMESPACE를 냅니다. systemd는 이미 존재하는 ReadWritePaths 항목만 허용 목록에 올리기 때문입니다.

Node.js에서는 이번에는 srvx가 npx를 통해서만 실행되는 게 아니라, 실제로 봇의 의존성과 함께 배포돼야 합니다. package.json에 일반 의존성으로 들어 있으므로, npm ci(또는 배포를 빌드한 패키지 매니저에 맞는 대응 명령)가 ExecStart가 호출하는 바로 그 바이너리인 /opt/botkit/node_modules/.bin/srvx를 설치해 줍니다.

Caddyfile은 그 밖의 나머지 부분에서 직접 호스팅하기를 그대로 따릅니다. ACME 연락처 이메일을 이용한 자동 HTTPS, localhost:8000으로의 리버스 프록시, 그리고 같은 기본 보안 헤더까지요.

더 해볼 만한 것들

아래 내용은 *examples/rss-bot/*에는 구현되어 있지 않습니다. 실제 배포라면 대체로 아래 순서로 손보게 될 만한 것들입니다.

  • 피드가 알려 주는 갱신 힌트: RSS의 선택 사항인 <ttl> 엘리먼트와 Syndication 모듈의 <sy:updatePeriod>, <sy:updateFrequency>, <sy:updateBase>는 모두 피드가 봇이 모든 피드에 똑같이 POLL_INTERVAL_MS를 추측해 적용하는 대신, 스스로 폴링 주기를 제안할 수 있게 해 줍니다. Atom에는 이에 대응하는 표준이 없습니다.
  • HTTP 조건부 요청: 매 폴링마다 If-Modified-Since나 If-None-Match를 함께 보내고 304 Not Modified 응답을 처리하면, 전혀 바뀌지 않은 피드를 다시 파싱하는 일을 건너뛸 수 있습니다.
  • WebSub: <atom:link rel="hub" href="…">를 광고하는 피드라면, setInterval() 대신 웹훅으로 폴링을 기다리는 게 아니라 갱신을 직접 밀어 넣을 수 있습니다.
  • 피드를 등록할 수 있는 사람의 허용 목록: 지금은 URL이 담긴 멘션이 등록 봇에게 오기만 하면 누구든 그 URL을 등록할 수 있습니다.
  • 멘션된 URL을 가져오기 전에 검증하기: registryBot.onMention은 extractUrl()이 찾은 것을 그대로 pollFeed()의 fetch() 호출로 넘길 뿐, localhost나 내부 주소를 가리키는 것을 막을 장치가 전혀 없습니다. @fedify/vocab-runtime의 validatePublicUrl()이 정확히 이런 용도로 존재하니, 처음부터 새로 짤 필요는 없습니다.

  1. 호스트명은 사용하는 터널링 서비스에 따라 실제로는 다르게 나옵니다. ↩︎

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

@hackerspub@hackers.pub

Hackers' Pub에 드디어 개인 계정 외에 조직(organization) 계정을 만들 수 있게 되었습니다. 조직 계정은 Hackers' Pub에서 특정 조직·프로젝트의 공식 계정을 만들기 위한 용도입니다. GitHub이나 GitLab, Codeberg 등에서 조직 기능을 사용해 보셨다면, 혹은 Facebook에서 페이지 기능을 사용해 보셨다면, 비슷한 개념으로 받아들이셔도 됩니다. 조직 계정에는 하나 이상의 개인 계정이 속하게 되며, 구성원들 누구나 조직 명의로 단문(note) 및 게시글(article)을 올릴 수 있게 됩니다. 단, 조직 기능은 새 웹 프런트엔드(web-next)에서만 사용 가능합니다.

조직 계정은 개인 계정과 많은 면에서 공통점이 있습니다. 이름과 아바타도 가질 수 있고, 약력(bio)도 입력 가능합니다. 다른 계정을 팔로할 수도 있고, 팔로워도 가질 수 있습니다. 댓글도 쓸 수 있고 에모지 리액션도 달 수 있습니다.

반면, 조직 계정은 개인 계정과는 달리 로그인할 수 없습니다. 로그인 할 수 없으니 이메일이나 패스키 등도 갖지 않습니다. 대신, 조직 구성원의 개인 계정으로 로그인한 뒤, 조직 명의를 선택하는 식으로 동작합니다. 또한, 북마크나 초고(draft)는 여전히 개인 계정에만 저장할 수 있습니다.

여러분 조직·프로젝트의 구성원이 단 한 명이라고 하더라도, 공식 계정을 만들 때는 조직 계정으로 생성하는 것을 권장합니다.

ActivityPub

개인 계정의 경우 ActivityPub 객체 상으로 Person 타입으로 표현되는 반면, 조직 계정의 경우에는 Organization 타입으로 표현됩니다.

"type": "Organization"

조직 만들기

조직은 누구나 설정 → 계정 → 조직 만들기에 가셔서 만드실 수 있습니다. 개인 계정과 마찬가지로, 조직 계정 역시 하나의 초대장을 소모합니다.

설정 → 계정 → 조직 만들기 폼

개인 계정 ↔ 조직 계정 사이의 스위치

조직 구성원은 언제든지 자신의 개인 계정과 조직 계정 사이에서 스위치가 가능합니다. 하나 이상의 조직에 몸 담고 있을 경우, 좌측 사이드바의 하단에 계정 스위치 UI가 보이게 됩니다.

계정 스위치 UI

단문 및 게시글 작성

조직 구성원은 단문 및 게시글을 작성할 때 해당 콘텐츠를 어떤 명의로 올릴 것인지 결정할 수 있습니다. 크게 세 종류의 선택지가 있습니다.

개인 명의

이전과 같이, 개인 계정이 쓴 콘텐츠로 올라갑니다.

조직 명의

조직으로서 작성한 콘텐츠로 올라가며, 다른 사람은 이 콘텐츠가 조직 구성원 중 누가 작성했는지 알 수 없습니다.

공동 저자

조직을 대표하여 개인 계정이 쓴 콘텐츠로 올라갑니다. 다른 사람에게는 해당 조직의 어느 구성원이 작성했는지 보입니다. 이 게시글이 바로 @hackerspub 조직 계정과 @hongminhee 개인 계정의 공동 명의로 쓰여진 예입니다.

ActivityPub

공동 저자는 ActivityPub 객체에서는 attributedTo 속성에 Organization 타입의 액터와 Person 타입의 액터가 함께 들어가는 것으로 표현됩니다.

"attributedTo": [
  "https://hackers.pub/ap/actors/019efc7c-70ad-7e2b-87dd-b1d36190cdee",
  "https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c"
]

단, 아직 복수의 attributedTo 속성을 올바르게 해석하지 못하는 ActivityPub 소프트웨어가 많습니다. 그런 소프트웨어에서는 조직 계정의 명의로만 보일 수 있습니다.

콘텐츠 명의 선택 UI

이미 있는 조직 계정들

다음은 Hackers' Pub에 이미 개설된 조직 계정들의 목록입니다:

fedidev.kr

한국 연합우주 개발자 모임

한국에 거주하거나 한국어를 사용하는 연합우주(fediverse) 개발자들의 모임입니다.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

知人(지인)인 @siliconsjang 님이 오늘 SiliconBeest v1.0.0을 公開(공개)했습니다. Fedify와 Cloudflare를 基盤(기반)으로 만든 소프트웨어인데, Workers, D1, R2, Queues 등 서버리스 스택 위에서 全部(전부) 돌아갑니다.

發想(발상)의 出發點(출발점)이 재밌습니다. Cloudflare 障礙(장애) 때 聯合宇宙(연합우주) 서버들이 덩달아 다운되는 걸 보고, 「그럼 아예 Cloudflare 위에서 돌리면 되지 않나?」라는 생각에서 始作(시작)했다고 하네요.

費用(비용) 面(면)에서는 小規模(소규모) 인스턴스는 Cloudflare 無料(무료) 플랜, 조금 더 큰 規模(규모)는 月(월) $5 플랜으로 堪當(감당)할 수 있도록 하는 게 目標(목표)라고 합니다. 아직 初期(초기) 버전이라 未具顯(미구현) 機能(기능)이 많고, Mastodon 및 Misskey API 互換(호환)은 長期(장기) 目標(목표)로 보고 있다네요.

Fedify를 써주시는 분이라 반갑기도 하고, 應援(응원)하고 싶어 紹介(소개)합니다.

소스 코드는 AGPL 3.0으로 GitHub에 공개되어 있습니다.

github.com

GitHub - SJang1/siliconbeest: Fediverse in Cloudflare Workers + live serverless code

Fediverse in Cloudflare Workers + live serverless code - SJang1/siliconbeest

@siliconsjang@hackers.pub

안녕하세요! Hello everyone!

SiliconBeest v1.0.0 공개

마스토돈 API 호환을 목표로 하는 Cloudflare 엣지 컴퓨팅 기반 서버리스 연합우주 소프트웨어, SiliconBeest v1.0.0을 공개하게 되어 기쁩니다.

I'm pleased to announce SiliconBeest v1.0.0, a serverless fediverse software project built on Cloudflare edge computing, aiming for Mastodon API compatibility.

SiliconBeest Logo - a wildebeest with silicon on it


설명 (Description)

ko

  • SiliconBeest는 Cloudflare Workers 환경에서 동작하는 연합우주 프로젝트입니다.
  • Cloudflare 장애가 발생했을 때 다수의 연합우주 서버가 함께 접속 불가 상태가 되는 것을 보며, 연합우주 역시 Cloudflare 인프라에 상당히 의존하고 있다는 점에 착안했습니다.
  • 그렇다면 아예 Cloudflare 위에서 동작하는 연합우주 소프트웨어를 만들어보자는 생각에서 시작했습니다.
  • Cloudflare Inc.에서 개발했던 Wildebeest 프로젝트의 아이디어와 일부 코드를 참고했습니다.
  • 프로젝트 이름은 제 닉네임인 silicon(sjang) 이랑 Cloudflare의 Wildebeest를 조합해 SiliconBeest로 정했습니다.
  • 적은 사용자 수와 작은 규모의 연합을 기준으로는 Cloudflare 무료 플랜에서도 운영할 수 있도록, 조금 더 큰 규모의 연합에서는 월 $5 플랜으로도 감당할 수 있도록 만드는 것을 목표로 하고 있습니다.
  • API 측면에서 SiliconBeest의 목표는 Mastodon 및 Misskey API와의 호환입니다. 다만 이론적으로 가능한 것과 실제 구현은 별개의 문제이기 때문에, 해당 부분은 아직 개발 중이며 장기적인 목표로 보고 있습니다.

en

  • SiliconBeest is a fediverse project designed to run on Cloudflare Workers.
  • After seeing many fediverse servers become unavailable when Cloudflare had outages, I realized that the fediverse already relies heavily on Cloudflare infrastructure.
  • So I thought: why not build fediverse software directly on top of Cloudflare?
  • This project was inspired by Cloudflare Inc.’s Wildebeest project, and it also references some of its ideas and code.
  • The project name, SiliconBeest, comes from my nickname silicon(sjang) combined with Cloudflare’s Wildebeest.
  • I’m still working on making it as inexpensive to run as possible. For now, the goal is to support a small number of users with a small federation footprint on the free plan, and a medium federation footprint on the $5 plan.
  • From an API perspective, SiliconBeest aims to be compatible with both Mastodon and Misskey APIs. However, as many people know, full compatibility is difficult in practice, so this remains a long-term goal rather than something fully implemented today.

아직은 초기 버전이라 구현되지 않은 부분도 많지만, Cloudflare Workers, D1, R2, Queues 등 Cloudflare의 서버리스 인프라 위에서 연합우주 소프트웨어를 얼마나 가볍고 저렴하게 운영할 수 있는지 실험하고 있습니다.

This is still an early version, and many parts are not implemented yet. However, SiliconBeest is an experiment in how lightweight and affordable fediverse software can be when built on top of Cloudflare’s serverless infrastructure, such as Workers, D1, R2, and Queues.

현재 v1.0.0에서는 기본적인 구조와 핵심 기능을 먼저 정리하는 데 집중했으며, 앞으로 Mastodon API 호환성, federation 안정성, 관리 도구, 문서화 등을 점진적으로 개선해나갈 예정입니다.

In v1.0.0, I focused on organizing the basic architecture and core functionality first. Going forward, I plan to gradually improve Mastodon API compatibility, federation stability, admin tooling, and documentation.

관심 있으신 분들은 GitHub 저장소를 확인해주시고, 이슈나 피드백도 언제든 환영합니다.

If you’re interested, please check out the GitHub repository. Issues, feedback, and suggestions are always welcome.

https://github.com/SJang1/siliconbeest


설치 및 배포 방법 (Installation and Deployment)

SiliconBeest는 GitHub 템플릿과 Cloudflare를 이용해 비교적 간단하게 배포할 수 있습니다.

  1. GitHub 템플릿에서 새 저장소를 생성합니다.
  2. Cloudflare에서 필요한 리소스와 환경을 설정합니다.
  3. Cloudflare API 토큰과 필요한 환경변수를 GitHub Secrets에 저장합니다.
  4. GitHub Actions를 통해 자동 배포를 진행합니다.
  5. 배포가 완료되면 인스턴스 설정을 마무리합니다.

아직 설치 과정은 계속 다듬고 있으며, 개선사항이 많음을 알고 있습니다. 추후 보강해 나갈 예정이며, 이에 대한 PR도 환영입니다.

SiliconBeest can be deployed relatively easily using a GitHub template and Cloudflare.

  1. Create a new repository from the GitHub template.
  2. Set up the required resources and environment on Cloudflare.
  3. Store the Cloudflare API token and required environment variables in GitHub Secrets.
  4. Deploy automatically using GitHub Actions.
  5. Once deployment is complete, finish configuring your instance.

The installation process is still being refined, and I’m aware that there is plenty of room for improvement. I plan to keep improving the documentation and deployment flow over time, and related PRs are always welcome.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

2026(타이베이, 8월 8–9일) Fediverse & Social Web 트랙 발표자 모집이 시작되었습니다! , , 오픈 소셜 웹 관련 주제라면 무엇이든 환영합니다. 마감은 5월 9일이고, COSCUP 참가는 무료입니다.

👉 https://hackers.pub/@fedidevkr/2026/fediverse-social-web-track-at-coscup-2026-cfp-ko

hackers.pub

COSCUP 2026 연합우주 & 소셜 웹 트랙: 발표자 모집

한국 연합우주 개발자 모임(FediDev KR)과 일본의 FediLUG가 2026년 대만 타이베이에서 개최되는 COSCUP 2026의 연합우주(Fediverse) 및 소셜 웹 트랙 발표자를 모집합니다. 이번 트랙은 액티비티펍(ActivityPub) 프로토콜 구현, 전용 클라이언트 및 라이브러리 개발, 인스턴스 운영 노하우, 그리고 연합 커뮤니티의 거버넌스와 같은 다양한 주제를 폭넓게 다룹니다. 동아시아 주요 오픈소스 컨퍼런스에서 처음으로 열리는 연합우주 전용 세션인 만큼, 개발자와 운영자들이 모여 기술적 통찰을 나누고 지역 커뮤니티의 결속을 다지는 중요한 기회가 될 것입니다.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

다른 언어로 읽기: English (영어), 日本語 (일본어).


한국 연합우주 개발자 모임(FediDev KR)과 FediLUG(일본)이 COSCUP 2026 연합우주(fediverse) & 소셜 웹 트랙을 열고, 발표 제안을 받습니다.

COSCUP은 매년 대만 타이베이에서 열리는 참가비 무료의 자유·오픈 소스 소프트웨어 컨퍼런스입니다. FOSDEM의 동아시아판이라고 생각하시면 됩니다. 올해는 8월 8–9일 국립대만과학기술대학교에서 UbuCon Asia 2026과 공동 개최됩니다.

연합우주 & 소셜 웹 트랙은 하루 종일, 총 6시간 진행됩니다. 동아시아의 주요 오픈소스 컨퍼런스에서 열리는 첫 번째 연합우주 전용 트랙으로, 이 자리가 동아시아 연합우주 커뮤니티의 정기적인 모임으로 이어지기를 바랍니다.

발표 형식

기본 발표 시간은 30분입니다. 더 길거나 짧은 시간이 필요하다면 제출 시 희망 시간을 적어주세요.

주제

연합우주 및 오픈 소셜 웹과 관련된 주제라면 무엇이든 환영합니다.

  • ActivityPub 또는 관련 프로토콜 구현
  • ActivityPub 기반 소프트웨어용 클라이언트
  • 연합우주 개발을 위한 라이브러리, 툴킷, 프레임워크
  • 검색, 온보딩, 모더레이션 등 지원 서비스
  • 인스턴스 운영 및 관리
  • 거버넌스, 정책, 연합 커뮤니티 운영의 사회적 측면
  • 더 넓은 의미의 오픈 소셜 웹과 상호운용성

주요 일정

  • 제출 시작: 2026년 3월 28일
  • 제출 마감: 2026년 5월 9일 (AoE, 세계 어느 시간대 기준으로도 해당 날짜 내)
  • 결과 통보: 2026년 6월 9일
  • 컨퍼런스: 2026년 8월 8–9일

제출 방법

https://pretalx.coscup.org/coscup-2026/cfp에서 제출하실 수 있습니다. 트랙 드롭다운에서 Fediverse & Social Web을 선택해 주세요.

발표 제안은 영어 또는 중국어로 작성해 주세요. COSCUP은 세션 설명을 영어와 중국어로 함께 게시하지만, 번역은 채택 이후에 이루어지므로 제출 시 두 언어를 모두 작성할 필요는 없습니다.

모든 세션은 녹화되어 CC BY-SA 4.0으로 공개됩니다. 녹화하거나 해당 조건으로 공개할 수 없는 내용이 포함되어 있다면 제출 시 명시해 주세요.

행동 강령

모든 발표자와 참가자는 COSCUP 행동 강령(영문)을 숙지하고 준수해야 합니다.

문의

트랙, 주제, 연합우주 전반에 대한 문의는 contact@fedidev.kr 또는 연합우주 계정 @fedidevkr 쪽으로 연락해 주세요.

@COSCUP 2026(臺北(타이베이), 8月(월) 8–9日(일))에서 저희 Fediverse & Social Web 트랙이 承認(승인)되었습니다! , , 오픈 소셜 웹을 主題(주제)로 하루 終日(종일), 總(총) 6時間(시간)을 進行(진행)할 豫定(예정)입니다.

發表者(발표자) 募集(모집) CFP는 아직 열리지 않았지만, 始作(시작)되는 대로 바로 公知(공지)하겠습니다. 期待(기대)해 주세요!

@COSCUP 2026(臺北(타이베이), 8月(월) 8–9日(일))에서 저희 Fediverse & Social Web 트랙이 承認(승인)되었습니다! , , 오픈 소셜 웹을 主題(주제)로 하루 終日(종일), 總(총) 6時間(시간)을 進行(진행)할 豫定(예정)입니다.

發表者(발표자) 募集(모집) CFP는 아직 열리지 않았지만, 始作(시작)되는 대로 바로 公知(공지)하겠습니다. 期待(기대)해 주세요!

@COSCUP 2026(臺北(타이베이), 8月(월) 8–9日(일))에서 저희 Fediverse & Social Web 트랙이 承認(승인)되었습니다! , , 오픈 소셜 웹을 主題(주제)로 하루 終日(종일), 總(총) 6時間(시간)을 進行(진행)할 豫定(예정)입니다.

發表者(발표자) 募集(모집) CFP는 아직 열리지 않았지만, 始作(시작)되는 대로 바로 公知(공지)하겠습니다. 期待(기대)해 주세요!

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

東(동)아시아에도 FediCon 같은 行事(행사)가 있으면 좋겠다는 말을 여러 番(번) 해왔는데요. 獨立的(독립적)인 컨퍼런스는 아직 어렵더라도, 작은 첫걸음으로 생각해보고 있는 게 있습니다.

@COSCUP 2026(臺北(타이베이), 8月(월) 8日(일)–9日(일))이 커뮤니티 트랙 提案(제안)을 받고 있어요. FOSDEM의 Social Web devroom 같은 느낌으로, 거기서 Social Web 트랙을 열 수 있지 않을까 하고 構想(구상) 중입니다.

아직 確定(확정)된 건 아무것도 없지만, , , 或(혹)은 소셜 웹 全般(전반)을 다루고 있고 發表(발표)나 共同(공동) 오거나이징에 關心(관심)이 있으신 분이 있다면 이야기 걸어주세요.

https://floss.social/@COSCUP/116152356550445285

floss.social

COSCUP (@COSCUP@floss.social)

🚀 COSCUP 2026 Call for Participation is now open! 🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited. 🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served. 👉 Apply here: https://s.coscup.org/26communityen #COSCUP2026 #OpenSource #Community

@COSCUP@floss.social

🚀 COSCUP 2026 Call for Participation is now open!

🎤 Community Tracks – Run a open-source agenda with talks, panels, or workshops. Apply by Mar 23. Spots are limited.

🛠 Community Booths – Showcase your project, recruit members, and connect. Apply by Jun 9. First come, first served.

👉 Apply here: s.coscup.org/26communityen

blog.coscup.org

COSCUP 2026 Call for Participation, 議程軌與攤位即日起開放申請

無論您是開放原始碼的開發者、推廣者、使用者,都歡迎您來參加 COSCUP「開源人年會」

@hollo@hollo.social · Reply to Hollo :hollo:

보안 업데이트: Hollo 0.6.19 릴리스

Fedify의 HTML 파싱 코드에서 발견된 보안 취약점을 수정한 Hollo 0.6.19를 릴리스했습니다.

이 취약점(CVE-2025-68475)은 ReDoS(정규 표현식 서비스 거부) 문제로, 공격자가 연합 작업 중 특수하게 조작된 HTML 응답을 보내 서비스 장애를 유발할 수 있습니다. 악성 페이로드는 작지만(약 170바이트), Node.js 이벤트 루프를 장시간 차단할 수 있습니다.

모든 Hollo 운영자분들께 즉시 버전 0.6.19로 업그레이드하실 것을 강력히 권고드립니다.

항목 상세
CVE CVE-2025-68475
심각도 높음 (CVSS 7.5)
조치 Hollo 0.6.19로 업그레이드

github.com

ReDoS Vulnerability in HTML Parsing Regex

Hi Fedify team! 👋 Thank you for your work on Fedify—it's a fantastic library for building federated applications. While reviewing the codebase, I discovered a Regular Expression Denial of Servic...

@hollo@hollo.social · Reply to Hollo :hollo:

보안 업데이트: Hollo 0.6.19 릴리스

Fedify의 HTML 파싱 코드에서 발견된 보안 취약점을 수정한 Hollo 0.6.19를 릴리스했습니다.

이 취약점(CVE-2025-68475)은 ReDoS(정규 표현식 서비스 거부) 문제로, 공격자가 연합 작업 중 특수하게 조작된 HTML 응답을 보내 서비스 장애를 유발할 수 있습니다. 악성 페이로드는 작지만(약 170바이트), Node.js 이벤트 루프를 장시간 차단할 수 있습니다.

모든 Hollo 운영자분들께 즉시 버전 0.6.19로 업그레이드하실 것을 강력히 권고드립니다.

항목 상세
CVE CVE-2025-68475
심각도 높음 (CVSS 7.5)
조치 Hollo 0.6.19로 업그레이드

github.com

ReDoS Vulnerability in HTML Parsing Regex

Hi Fedify team! 👋 Thank you for your work on Fedify—it's a fantastic library for building federated applications. While reviewing the codebase, I discovered a Regular Expression Denial of Servic...

@thaumiel999@mastodon.social

@activitypub.blog
워드프레스 액티비티펍 플러그인의 한국어 설명 페이지를 대대적으로 수정했습니다.
자동번역을 전체적으로 검토하고 맥락에 맞게 수정해 가독성과 번역 품질을 대폭 개선했습니다!

이제 한국어 사용자들도 페디버스와 워드프레스의 연결을 보다 더 쉽게 이해하고 활용할 수 있습니다. 앞으로도 최신 문서와 기능 번역을 계속 업데이트할 예정이니 많은 관심과 피드백 부탁드립니다.
@pfefferle @obenland



ko.wordpress.org/plugins/activ

ko.wordpress.org

액티비티펍(ActivityPub)

ActivityPub 프로토콜은 ActivityStreams 2.0 데이터 포맷을 기반으로 하는 탈중앙화된 소셜 네트워킹 프로토콜입니다.

@thaumiel999@mastodon.social

@activitypub.blog
워드프레스 액티비티펍 플러그인의 한국어 설명 페이지를 대대적으로 수정했습니다.
자동번역을 전체적으로 검토하고 맥락에 맞게 수정해 가독성과 번역 품질을 대폭 개선했습니다!

이제 한국어 사용자들도 페디버스와 워드프레스의 연결을 보다 더 쉽게 이해하고 활용할 수 있습니다. 앞으로도 최신 문서와 기능 번역을 계속 업데이트할 예정이니 많은 관심과 피드백 부탁드립니다.
@pfefferle @obenland



ko.wordpress.org/plugins/activ

ko.wordpress.org

액티비티펍(ActivityPub)

ActivityPub 프로토콜은 ActivityStreams 2.0 데이터 포맷을 기반으로 하는 탈중앙화된 소셜 네트워킹 프로토콜입니다.

@thaumiel999@mastodon.social

@activitypub.blog
워드프레스 액티비티펍 플러그인의 한국어 설명 페이지를 대대적으로 수정했습니다.
자동번역을 전체적으로 검토하고 맥락에 맞게 수정해 가독성과 번역 품질을 대폭 개선했습니다!

이제 한국어 사용자들도 페디버스와 워드프레스의 연결을 보다 더 쉽게 이해하고 활용할 수 있습니다. 앞으로도 최신 문서와 기능 번역을 계속 업데이트할 예정이니 많은 관심과 피드백 부탁드립니다.
@pfefferle @obenland



ko.wordpress.org/plugins/activ

ko.wordpress.org

액티비티펍(ActivityPub)

ActivityPub 프로토콜은 ActivityStreams 2.0 데이터 포맷을 기반으로 하는 탈중앙화된 소셜 네트워킹 프로토콜입니다.

@thaumiel999@mastodon.social

@activitypub.blog
워드프레스 액티비티펍 플러그인의 한국어 설명 페이지를 대대적으로 수정했습니다.
자동번역을 전체적으로 검토하고 맥락에 맞게 수정해 가독성과 번역 품질을 대폭 개선했습니다!

이제 한국어 사용자들도 페디버스와 워드프레스의 연결을 보다 더 쉽게 이해하고 활용할 수 있습니다. 앞으로도 최신 문서와 기능 번역을 계속 업데이트할 예정이니 많은 관심과 피드백 부탁드립니다.
@pfefferle @obenland



ko.wordpress.org/plugins/activ

ko.wordpress.org

액티비티펍(ActivityPub)

ActivityPub 프로토콜은 ActivityStreams 2.0 데이터 포맷을 기반으로 하는 탈중앙화된 소셜 네트워킹 프로토콜입니다.

@thaumiel999@mastodon.social

@activitypub.blog
워드프레스 액티비티펍 플러그인의 한국어 설명 페이지를 대대적으로 수정했습니다.
자동번역을 전체적으로 검토하고 맥락에 맞게 수정해 가독성과 번역 품질을 대폭 개선했습니다!

이제 한국어 사용자들도 페디버스와 워드프레스의 연결을 보다 더 쉽게 이해하고 활용할 수 있습니다. 앞으로도 최신 문서와 기능 번역을 계속 업데이트할 예정이니 많은 관심과 피드백 부탁드립니다.
@pfefferle @obenland



ko.wordpress.org/plugins/activ

ko.wordpress.org

액티비티펍(ActivityPub)

ActivityPub 프로토콜은 ActivityStreams 2.0 데이터 포맷을 기반으로 하는 탈중앙화된 소셜 네트워킹 프로토콜입니다.

@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hongminhee@hollo.social

제가 開發(개발)하고 있는 프로젝트 Hackers' Pub의 베타 테스터를 모십니다!

이 프로젝트는 (fediverse)板(판) velog 같은 것으로, 소프트웨어 開發者(개발자)를 爲(위)한 基盤(기반)의 SNS 兼(겸) 블로그 플랫폼입니다. AGPL-3.0 라이선스로 소스 코드가 公開(공개)되어 있을 뿐 아니라, GitHub에서 프로젝트를 公開的(공개적)으로 進行(진행)하고 있습니다.

職業(직업)으로든 趣味(취미)로든 소프트웨어를 開發(개발)하시는 분들, 聯合宇宙(연합우주)를 좋아하시는 분들, 새로운 플랫폼을 써 보고 싶으신 분들은 부디 參與(참여)해 주시기 바랍니다! 關心(관심) 있으신 분들은 答(답)글이나 DM으로 이메일 住所(주소)를 보내주시면 됩니다.

github.com

GitHub - dahlia/hackerspub: ActivityPub-enabled social network for hackers

ActivityPub-enabled social network for hackers. Contribute to dahlia/hackerspub development by creating an account on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

안녕하세요! :hollo:

Hollo의 새로운 계획에 대해 여러분의 의견을 듣고자 합니다.

지금까지 Hollo는 셀프 호스팅을 기본 원칙으로 삼아왔습니다. 이는 앞으로도 변함없이 유지될 것이며, 소스 코드는 계속해서 AGPLv3 라이선스로 공개됩니다.

최근 저희는 프로젝트의 지속 가능한 발전을 위해, Open Collective(@opencollective)를 통해 일정 금액 이상을 정기적으로 후원해 주시는 분들을 위한 호스팅 서비스 제공을 검토하고 있습니다.

이는 기술적인 부분에 신경 쓰지 않고도 Hollo를 이용하고 싶으신 분들을 위한 추가 옵션이 될 것입니다. 물론 지금처럼 직접 설치하고 운영하시는 것도 계속 가능합니다.

아래 인용된 영어 게시물의 투표에 참여해 주시면 감사하겠습니다! 📊

  1. 좋은 생각입니다! 호스팅 서비스를 이용하고 싶어요.
  2. 괜찮네요! 전 셀프 호스팅을 계속하지만 응원합니다.
  3. 다른 방식으로 후원을 늘리는 게 좋겠어요.
  4. 현재처럼 순수 셀프 호스팅으로 남았으면 좋겠어요.

💭 추가 의견이나 제안이 있으시다면 댓글로 남겨주세요!

https://hollo.social/@hollo/01950344-1c55-7f43-8afc-b0a1ee8b4abf

hollo.social

#Hollo everyone! :hollo: We'd…

#Hollo everyone! :hollo: We'd like to hear your thoughts on something we've been considering. As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license. We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective@opencollective.com. This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now. What are your thoughts on this idea? Please vote below! 📊 💭 Have additional thoughts or suggestions? Feel free to share them in the comments! #poll #fediverse

@hollo@hollo.social

everyone! :hollo:

We'd like to hear your thoughts on something we've been considering.

As you know, Hollo has always been focused on self-hosting—this won't change, and our source code will continue to be available under the AGPLv3 license.

We're exploring ways to make the project more sustainable, and we're considering offering a hosting service for those who regularly support us with a certain amount through @opencollective.

This would be an additional option for those who want to use Hollo without managing the technical aspects themselves. Of course, you'll still be able to self-host just like you do now.

What are your thoughts on this idea? Please vote below! 📊

💭 Have additional thoughts or suggestions? Feel free to share them in the comments!

  • 1️⃣ Great idea! I'd be interested in supporting and using the hosted service.26 (45%)
  • 2️⃣ Sounds good! I'll stick to self-hosting but support the initiative.29 (50%)
  • 3️⃣ I think we should explore other ways to increase support.2 (3%)
  • 4️⃣ I prefer Hollo to remain purely self-hosted.1 (2%)
@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.

@hollo@hollo.social · Reply to Hollo :hollo:

저장소가 @dahlia/hollo에서 @fedify-dev/hollo로 이전되었습니다. 이에 따라 이미지 레지스트리도 ghcr.io/dahlia/hollo에서 ghcr.io/fedify-dev/hollo로 이전되었습니다.

기존 이미지 레지스트리는 계속 접근 가능하지만, 새로운 태그는 더 이상 추가되지 않을 예정입니다. Hollo를 사용 중이신 모든 분들은 새로운 레지스트리 주소로 업데이트해 주시기 바랍니다.

Docker 설정을 다음과 같이 변경해 주세요:

  • 기존 이미지 주소: ghcr.io/dahlia/hollo:latest
  • 새 이미지 주소: ghcr.io/fedify-dev/hollo:latest

이번 이전은 프로젝트의 더 나은 운영과 지속적인 개발을 위해 진행되었습니다. 원활한 전환에 협조해 주셔서 감사합니다. :hollo:

https://hollo.social/@fedify/0194a851-581d-779c-b777-dc39e753ef14

hollo.social

We've just moved the #Fedify p…

We've just moved the #Fedify project and related repositories to our new GitHub organization account, [@fedify-dev]! 🎉 Here's what moved: - [@dahlia/fedify](https://github.com/dahlia/fedify) → [@fedify-dev/fedify](https://github.com/fedify-dev/fedify) - [@dahlia/botkit](https://github.com/dahlia/botkit) → [@fedify-dev/botkit](https://github.com/fedify-dev/botkit) - [@dahlia/hollo](https://github.com/dahlia/hollo) → [@fedify-dev/hollo](https://github.com/fedify-dev/hollo) - [@dahlia/fedify-amqp](https://github.com/dahlia/fedify-amqp) → [@fedify-dev/amqp](https://github.com/fedify-dev/amqp) - [@dahlia/fedify-h3](https://github.com/dahlia/fedify-h3) → [@fedify-dev/h3](https://github.com/fedify-dev/h3) - [@dahlia/fedify-express](https://github.com/dahlia/fedify-express) → [@fedify-dev/express](https://github.com/fedify-dev/express) - [@dahlia/fedify-postgres](https://github.com/dahlia/fedify-postgres) → [@fedify-dev/postgres](https://github.com/fedify-dev/postgres) - [@dahlia/fedify-redis](https://github.com/dahlia/fedify-redis) → [@fedify-dev/redis](https://github.com/fedify-dev/redis) - [@dahlia/markdown-it-hashtag](https://github.com/dahlia/markdown-it-hashtag) → [@fedify-dev/markdown-it-hashtag](https://github.com/fedify-dev/markdown-it-hashtag) - [@dahlia/markdown-it-mention](https://github.com/dahlia/markdown-it-mention) → [@fedify-dev/markdown-it-mention](https://github.com/fedify-dev/markdown-it-mention) - [@dahlia/fedichatbot](https://github.com/dahlia/fedichatbot) → [@fedify-dev/fedichatbot](https://github.com/fedify-dev/fedichatbot) - [@dahlia/microblog](https://github.com/dahlia/microblog) → [@fedify-dev/microblog](https://github.com/fedify-dev/microblog) All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged. Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home! :fedify: New GitHub organization: <https://github.com/fedify-dev>. [@fedify-dev]: https://github.com/fedify-dev

We've just moved the project and related repositories to our new GitHub organization account, @fedify-dev! 🎉

Here's what moved:

All repositories have been transferred and GitHub's automatic redirects are in place, so existing links will continue to work. Also, the project's core functionality and development process remain unchanged.

Thanks to everyone who participated in our naming poll. Looking forward to Fedify's continued growth under its new organizational home!

:fedify: New GitHub organization: https://github.com/fedify-dev.

github.com

Fedify

A collection of development tools for fediverse. Fedify has 12 repositories available. Follow their code on GitHub.