avatar

val.town 實戰,快速實作一個 Bluesky 轉貼機器人

簡介

之前曾經在週報裡面有提過 val.town 這個好用的小工具,如果是第一次聽到這個服務的名稱的話,這是他的自我介紹 ⬇️

Val Town is a social programming environment to write, run, deploy and share code.

你可以初看會以為就是一個線上可以直接跑程式的小服務,也沒有特別的,不過如果實際去玩它會發現作者的一些巧思在裡面😄。為什麼它會自稱為 social programming 的原因,就是它不單是能提供撰寫小型、模組化的程式,它還可以重用這些可能是別人已經寫好的功能,直接引入到你的程式裡面來使用。

此外,val.town 本身是用 Deno 當基礎開發的,所以想當然可以直接使用 Deno 生態圈裡面的模組功能來使用(npm specifiers & direct HTTP import)。把上述這兩個東西組合使用之後,你會發現原本可能需要花一段時間開發的功能,現在跟拼積木一樣組合一下可能就完工了!😆

如果你看到這邊對這小工具開始感興趣的話,歡迎花一點時間看一下它們的介紹影片,可以快速上手👍。

實戰

自從拿到 bluesky 邀請之後就很喜歡這個新興社群(這部分感興趣也可以參考我之前寫的這篇文章),不過畢竟還在封測所以使用者人數當然沒辦法跟 Twitter、Mastodon 這類服務相比,不過因為它們蠻看重開發者所以早早就提供不少開發資源,所以雖然還在封測期間,你如果有些基本開發能力的話,是可以開始兜弄一些好玩的東西😆。

因為測試中,所以很常需要在不同平台中切換,然後轉文章可能也得手動來處理,然後就突然想到🔽 怎麼不試試用 val.town 來弄一個簡單的轉文功能!

目標 & 需要的前置作業

等確認方向之後就衝刺啦!

踩雷之路

其實沒什麼雷啦,用 val.town 來開發真的有達到預期的效果,很快就把殼搭起來了,開發過程中有一些花了一點時間才處理的項目,這邊就特別註記一下:

檢查排程週期以內的新貼文

這點一開始想了一陣不知道要怎麼搭 val.town 來實作,原本想說一般的 rss reader 應該是有把 last checking date 的值記錄起來然後拿來跟 RSS feed 裡面比對,但覺得要弄到 DB 有點麻煩。

後來發現 val.town 上面排程的行為,會有一個 Interval 的參數傳遞進來,它大概像這樣:

{
  "id": "fb0302bb-e7ac-49a5-916f-c3a5d9e91f3a",
  "code": "",
  "delay": 3600000,
  "author": "eb90236d-72cf-4e4f-86d0-548f41a11285",
  "registeredAt": "2023-05-25T18:37:26.342Z",
  "clearedAt": null,
  "lastRunAt": null
}

因為有 delay 這個數值,就可以知道執行的排程的頻率,然後要做的就變成「檢查週期之內發佈的新貼文」,就可以啦!然後再接上作者寫好檢查 RSS feed 的函式,就可以達到這個效果了🔽

const lastRunAt = new Date(new Date().getTime() - delay);

// 這就是 val.town 呼叫已實作函式的方法
// 很方便吧😃
const items = await @stevekrouse.newRSSItems({
    url,         // RSS feed link
    lastRunAt,   // 上面計算出來需要檢查的排程週期
});

在 val.town 裡面處理各種引用模組

能解析出需要轉發的新貼文之後,主要就是處理 bluesky 貼文的 API 行為了,這邊也參考了幾篇很有幫助的文章或範例,特別感謝一下😄🙇‍♂️

原本想說應該是很簡單的流程

  1. 引入官方 SDK -> @atproto/api
  2. 登入 bluesky -> BSkyAgent.login,也就是 com.atproto.server.createSession 這部分。
  3. 發文 -> app.bsky.feed.post
TypeError: BskyAgent is not a constructor

不過後來卻一直收到如上的 BSkyAgent 的錯誤,想說怎麼可能有問題,反覆測試了好幾次,也把官方文件中關於模組引用的部分看了好幾回也找不到原因,後來在快要放棄的時候終於無意間在 discord 頁面找到這篇解答😅

真的是浪費了好多時間阿😢。因為 bluesky SDK 目前匯出的形式,記得在引入 @atproto/api 模組的時候,用下面方法賦值,才能正確取用變數。

const { default: atProto } = await import("npm:@atproto/api");
const { BskyAgent } = atProto;

意想外的卡關😅

最後這部分其實是自己龜毛才浮現的問題。只要是弄轉貼格式的時候有留意到某個媒體網站在 bluesky 上面的貼文是用了類似超連結的形式:

link example

然後就是一連串的實驗😅。原本以為它是可以直接吃 markdown 的格式,但直接送進去發現並沒有,它還是被轉成 plain text 的形式了。後來又發現一些範例用了 RichText 這個物件來使用,然後一樣在卡關不知道它正確用法的時候,又在 Bluesky 的 discord 群組裡面翻到了下面這個說明才初略了解它的大概用法⬇️

how to setup rich text

原來它 RichText 的用法是由內容 text 以及 facets 兩個元件組成,text 自然就是你希望發送的貼文內容。facets 就是替文字附上其他屬性的方式,用法就是把希望標記部分的位置以及希望賦予的屬性通過 facets 來定義。

Text Facets. Faceting is a good way to get an overview of a specific column of your data. Text faceting will organize unique items in the selected column by name and will give a count for how many rows or records possess that item name.
UNLV - https://guides.library.unlv.edu/open-refine/faceting

所以如果想做到上述像 Techmeme 的效果,就是用下面這種方式處理:

const rt = new RichText({
    text,  // 貼文內容
    facets: [{
        index: {
        byteStart: text.indexOf("Main Link"),
        byteEnd: (text.indexOf(" |") - 1),
      },
      features: [{
        $type: "app.bsky.richtext.facet#link",
        uri: MAIN_LINK_VALUE,
      }],
    }]
});

這樣再透過 feeds.post送出去即可,文中的 Main Link 就會被轉換成超連結的樣式。目前看起來只有支援 links & mentions,不過往後透過這個方式,要加上其他樣式的支援應該也不是難事了。

收工

現在透過上述的方式,就簡單用 val.town 搭起一個可以自定義 RSS 來源的排程轉文小工具啦!

發文的部分我有丟出來,有需要用來處理 bluesky 發文行為的話可以直接使用,其他部分因為客製行為比較多,就不放出來了,不過大家應該可以簡單處理自己需要的格式後接上 sendPostToBsky

排程轉文主程式

async function pollRSSFeedsAndRepost(intervalParams: Interval) {
  const { delay } = intervalParams;
  const lastRunAt = new Date(new Date().getTime() - delay);
  
  // 從 val.town secrets 裡面拿 API呼叫需要的兩個參數 -> identifier/password
  const identifier = @me.secrets.BLUESKY_IDENTIFIER;
  const password = @me.secrets.BLUESKY_PASSWORD;
  
  const rssSources = await Promise.all(
        Object.entries(@me.rssFeeds).map(async ([name, url]) => {
      // 這是上述提及,作者用來判斷新來源文章的函式
      let items = await @stevekrouse.newRSSItems({
        url,
        lastRunAt,
      });
      return { name, items };
    }),
  );
  
  // 批次把新文章轉發到 bluesky 對應的帳號
  for await (const rss of rssSources) {
    if (rss && rss.name && rss.items && rss.items.length > 0) {
      for await (const item of rss.items) {
        // 這邊是我自己處理貼文格式的部分,這段就根據自己需求實作即可
        const { text, facets } = @me.createBskyPostContent(
          rss.name,
          item,
        );
        if (text && facets) {
          await @me.sendPostToBsky({
            identifier,
            password,
            text,
            facets,
          });
        }
      }
    }
  }
}

自定義來源

// 這是我的設定,想加上新的來源就直接加上存檔就好
// 這邊的變數,就是主程式中 @me.rssFeeds 的部分
const rssFeeds = {
  "Deno": "https://rsshub.app/twitter/user/deno_land/exclude_rts_replies",
  "Node.js": "https://rsshub.app/twitter/user/nodejs/exclude_rts_replies",
};

大概就是這樣,成果如下,是不是超方便的😆

my sample

結語

寫完文章之後發現 val.town 也推出付費的服務 (Val Town Pro)了。如果覺得這個工具能夠幫上你不少的話,不妨支持一下作者,畢竟大家每個月都在付訂閱費了嘛 #誤🤣

powered by WebMention.io

You can tweet this post or discuss it on Twitter, the comments will show up here.
0Likes0Replies0Mentions

Be the first one to share this article!