note アプリチーム 技術投資Days 2025
おはようございます。waturaです。note アプリチームで技術投資Daysと称して、普段の業務でなかなか手がつけられない技術テーマやアイディア、課題に取り組み、その成果を発表することで自身の成長や、プロダクトや業務への活用の機会を設けたい。 をやりました。
アプリ開発で今後役立つことか? **ほぼNO
**価値のあるものを作ったか?**NO
**この情報はみんなの役に立つか?**たぶんNO
**楽しかったか?**YES
**成長できたか?たぶんYES?
という感じでやってみました。
やったこと
AT Protocol上に建てられたサービスのセルフホスト
PDSにWebSocketで接続して自前Typeの表示
自前LexiconでPDSで投稿
投稿した内容の取得
おまけ(本体)
レギュレーション
オレオレレギュレーションです。他のチームメンバーにはそんなレギュレーションはありません。
Claude Code, CodexのようなAIにコードをかかせない
AI自体は使ってもいいがコピペしない
理解が全然できないコードを写経しない
AIは便利な検索ツール的な1年くらい前の世界観でやっていく感じです。
さすがに、AIゼロでググるところまで完全に自分でやるよーは辛いので、許可することにしました。
エディタとかでも、ほぼ使わない方向で進めました。
なお、やってみた結果、Claude Code使っててもそこまで超加速しなかったのではないかという気がします。
BlueskyとAT Protocol
Blueskyは分散型SNSの一つです。分散型なのですが、BskyはMastodonと違いオレオレBskyみたいなものはありません。
しかし、Bskyの採用している仕組み的にはBskyと同じデータを使った別のSNSを作ったりできます。
それを実現しているのが、Bskyが採用しているAT Protocolというプロトコルです。このプロトコル自体の開発もBluesky社なのでイコールな雰囲気もありますが、基本的にはオープンに使えるプロトコルです。
AT Protoの中にPersonal Data Serverという仕組みがあります。これが、AT Protoを分散型にしている仕組みで、投稿やいいねなどのアクション、データを管理しているサーバーです。Bskyのものを使うことも、自分で建てることもできます。また、Bsky社以外の実装もあります。
このPDSにBskyから接続して使うみたいな感じになっています。
世の中にはすでにPDSとAT Protoを使い作られたサービスがいくつもあります。
例
ブログ Leaflet https://about.leaflet.pub/
画像SNS Grain Social https://grain.social/
GitHubみたいなコード管理 Tangled https://tangled.org/
まだまだありますが、また、とりあえず3つだけ。
現時点において、Privateなデータを扱う仕組みがないので、頑張って定義、実装してほしいなと思っています。
ちなみにPDSはセルフホストしており、私のBskyハンドルは @wtr.app です。
AT Protocol上に建てられたサービスのセルフホストする
TangledというAT Proto上に構築されたGitHub見たいなサービスがあります。とりあえず、これをホストしてみることにしました。
TangledにもPDSのような仕組みがあり、Knotと呼ばれます。このKnotがセルフホスト可能で、Tangledから接続して使えるみたいな感じになっています。
が、微妙に上手くいかなかったのでメモです。
.well-known
PDSをルートドメインに設置するのは、推奨されておらず、サブドメイン下に設置するものとなっています。
そのあたりの関係もあり、.well-knownにアクセスするとblogのエラー画面が表示されて残念な感じになっていました。
traefikで雑に名指しで転送するように指定しました。
(雑に書くとtraefikはやってきた通信を適切なウェブサーバーに振り分けるプログラムです。)
TangledのアドレスをIPv6で解決してるのに、IPv6では繋がらなかった
docker の設定がアレだったのか、IPv6で繋がらないので、認証できないという状態になっていました。IPv4使えよという指示をいれて解決しました。
CloudFlare TunnelでSSHとWebを共存できない
CloudFlare TunnelはWebsocketを使って、80,443ポートとか使えないような環境にあるサーバーに接続できる仕組みです。
事前に「このドメイン」にアクセスが来たら、「ここに転送」するみたいなルールを定義しておく感じになっています。
「このドメイン」という仕組みがネックでknot.wtr.appを443接続だったらtraefikで、22だったらSSHにするみたいなことができないようなのです。
解決策は、
別のアドレスにする
Cloudflare Tunnelやめる
のどっちかという感じでした。
残念なことに自宅環境が、ポートは少ないしIPも動的で自由がないのでCloudflare Tunnelか類似のサービスが必須です。ということで、Tangledをセルフホストするのは諦めました。
(note社のテックチャレンジ制度でVPS借りるのも考えなくはないです。普通にRPi動かすよりも早いし)
同一ホスト名でプロトコル別ルーティング実質不可なためTangledをセルフホストするのは諦めました。
やったこと1つ目はやれなかったこととなりました。
PDSにWebSocketで接続して自前Typeの表示
PDSがあってFirehoseがあってというところまでは、事前に知っていたのですが、そこから先はよく知らない状態での手探りで進めていきました。
Firehose
BskyにFirehoseという大量にデータが流れてくる仕組みがあります。Bskyへのデータの書き込みが全部流れてくる系の仕組みです。
これが、Bsky本体だけではなくPDSにもあり、PDSに対するデータの書き込みを垂れ流してみることにしました。
FirehoseはWebsocketを使っていて、更新がソケットを通じてどんどん流れてくるという感じになっています。
このとき、流れてくるデータはCBORというバイナリ形式で、流れてきます。
CBORは複数のCBORをくっつけて流すことができて、Bskyの場合CBORCBORみたいな状態でデータが送られてきます。
一つ目のCBORにヘッダー情報で、二つ目に中身という感じです。
やることは、
Websocketを購読する
CBORを分割する
ヘッダーをみて欲しい内容なら2つ目をパースする
パースした結果を受かってなんかする(今回はできていない)
このうち1-3の途中まで行いました。3でCARという具体的な中身をパースするところまで時間がたりなくてできていませんでした。
自前LexiconでPDSに投稿
AT Protoにはおおむね任意のデータを保存する仕組みがあります。
Lexiconというデータ構造の定義を作成し、そのデータ構造にしたがって投稿すると、PDSとかにデータが保存されるという仕組みになります。
Lexiconを使ってデータ構造を定義できるというところで、AT ProtoをつかったBskyではないブログシステムを作ったりできるのです。
Lexicon - AT Protocol A schema definition language. atproto.com
この仕様書にしたがってデータ構造を定義すればいいようです。blobもあったりするので画像でも動画でもなんでもできなくはないです。出来なくはないだけで、推奨される分けではなさそうですが。。。
というわけで、
Quick start guide to building applications on AT Protocol - AT Protocol In this guide, we’re going to build a simple multi-user app t atproto.com
Quick StartにあったLexiconを app.wtr.bsky.status という名前でつくって、bsky.wtr.appに投稿してみました。
wtr.appとして投稿するには、当然認証する必要があります。認証はパスワード認証とOAuth認証があります。今回は、時間がなさすぎるということでID/PasswordでJWTを取得するという手抜きを行ないました。
JWTは
/xrpc/com.atproto.server.createSession にポストでID/Passwordをなげたら帰ってきます。ほかにも、
did
- twitterとかとちがって、didという文字列が個人を識別するしくみになります
handle
email
emailConfirmed
accessJwt
refreshJwt
active
とかが帰ってきます。
このうち、didとjwtが必須なので記録しておきます。
データの投稿 /xrpc/com.atproto.repo.createRecord にjsonをpostしたらPDSに書き込まれます。
そのさい、
repo, collection, record.$type が重要なキーになります。
repo: did
collection: lexiconのid (app.wtr.bsky.status)
record.$type:lexiconのid (app.wtr.bsky.status)
record にはlexiconで定義したそのたもろもろのデータ構造をいれてPOSTできます。
POSTしたときに、Websocketをつないでいたら、POSTされたデータが流れてくるのを確認できます。
投稿した内容の取得
POSTができるならGETもほぼ同じです。JWTをヘッダーにいれて、
/xrpc/com.atproto.repo.listRecords?repo=did:plc:didididididididididididididid&collection=app.wtr.bsky.status
これにアクセスすると↑でPOSTしたデータを取得できます。簡単。
たったこれだけの内容を3日間も書けて行ないました。
おまけという名の本題
マルチプラットフォームでうごくライブラリを作るとしたら、Kotlinですかね?FlutterとかReactNativeで全部作る感じですかね?
note・TALESアプリではKotlinでマルチプラットフォーム化をやりはじめています。
こういうマルチプラットフォームやるぜ!っていうときに、まったく出てこないが、実は使われまくっているマルチプラットフォームでうごくものがあります。
C言語です。
C言語です。
C言語ですが、さすがにこの期に及んでCは書きたくないので、Cライブラリとして呼び出せる言語を使ってみました。
Zigです。今回Zigを初めてさわったので、「たったこれだけ」をやるのに丸2日もかかってしまいました。
そして、SwiftとかKotlinって楽だなぁ。どんだけ楽なんだぁってなりました。 ただ、基本となるデータのやり取りについては、できるようになったのでここからは
今回、Zigをつかって上記のBskyへアクセスするどうのこうのを作りました。
そして、最後の投稿した内容の取得は、iOSアプリ上で取得しています。
Zigでかかれたコードで取得しています。
時間がかかった点
今どきの言語ならあるでしょ〜っていう機能がない
DateTimeをらくらくフォーマット
JSONのデコード・エンコードが大変
CBORの分割がライブラリにない
- CBORのデコード自体はライブラリがありそれが利用できた
iOSはtlsを触らせてくれない
Zigは独自実装しており、iOS標準のものを触らせてくれない
独自でpemを持つ必要があるが、ファイルパスはiOS側で提供しないといけない
pemをメモリに展開しておくことはできるが、それを直接CertManagerには入れられなかった
実際使い続けるにはTLSの証明書を自分で持ち続けるのは微妙な気がするので悩ましい
- ネットワーク通信はネイティブ側になげるという方法もあるが、今回はアプリ内にpemを持たせた
AIの知識が古すぎる
今日現在、Zigのバージョンは0.15.2です。そして、0.1xがかわるだけで、マジかよ。。。ってくらいstdの仕様が変わっています。
変わりすぎていて,どうぞサンプルコードですとかってAIが提示してくるコードはぜんぜんうごきません。0.15ですというコードと0.15.2では。。。ってなったりします。
そして、当然のように存在しないstdライブラリの機能を使ってきたりします。
利用者がすくなくて、しかも、更新がめっちゃ速い言語だとつらいねーーーっていうのを久しぶりに感じました。
心残り
やったことがしょぼすぎる
Zigのcomptimeを使えていない
コードがめっちゃ汚い
本業には1mmも関係ない
コード
それぞれのタスクをこなすにはこの順番で書き下すよね。というコードでしかないです。他の言語と違う箇所は、Allocatorというメモリを確保する機構とdeferを使ったメモリ解放のコーモを入れる箇所ですね。
test allocatorというメモリ解放を忘れていると、leakしてるよとしっかりワーニングをだしてくれる素晴らしい機能があるので利用しました。
iOSから呼び出す
やること
Zig で関数を export し、ライブラリをビルドする
Xcodeにビルドした hogehoge.a をいれる
BridgingHeaderをつくり、exportした関数のシグネチャを書く
呼び出す
注意点としては、やはりメモリ管理まわりになります。
今回の例ではZig側でメモリを確保しています。
なので、Swift側にかえってきたあと、使い終わったらZigに渡してfreeしてあげる必要があります。
Swift側でもZigみたいにdefer祭にすればいいですね!
なお、XCFrameworkとして、複数のターゲットに向けた `.a` をまとめてしまえば、楽になります。が、今回技術投資Daysの間に終了しなかったので、そこまではやっていません。
#ifndef BridgingHeader_h
#define BridgingHeader_h
#include <stdint.h>
#include <stddef.h>
typedef struct {
const uint8_t *ptr;
size_t len;
} ByteBuffer;
ByteBuffer getStatus(const char *ca_path);
void freeBuffer(ByteBuffer buf);
#endif
Zig側では以下の2つのメソッドをexportしていました。
export fn getStatus(ca_path_ptr: ?[*:0]const u8) ByteBuffer
export fn freeBuffer(buf: ByteBuffer) void
前述したように、ZigからiOSの証明書周りにアクセス出来ませんでした。なので、引数としてファイルpathをポインタで受け取るようにしています。
let caPath = Bundle.main.path(forResource: "cacert", ofType: "pem")
let buffer = getStatus(caPath)
SwiftからZigに渡すときは、これで渡せます。
Zig側ではこういうかんじで、path_ptrをつかったら読込めます。
var ca_bundle = std.crypto.Certificate.Bundle{};
if (ca_path_ptr) |path_ptr| {
const ca_path = std.mem.span(path_ptr);
// try ca_bundle.addCertsFromPem(allocator, pem) みたいなのがほしかった
try ca_bundle.addCertsFromFilePathAbsolute(allocator, ca_path);
}
// 1. メモリを確保して生成
var client = std.http.Client{
.allocator = allocator,
.ca_bundle = ca_bundle,
};
// 2. 終わったら開放
defer client.deinit();
const resp = try client.fetch(options);
if (resp.status.phrase()) |response| {
// Debug用の表示
// std.log.debugはXcodeのログのところにも流れてくる。なので両方書いて見ている。
std.log.debug("url: {s}, status: {s}\n", .{ url_string, response });
std.debug.print("url: {s}, status: {s}\n", .{ url_string, response });
}
if (resp.status == .ok) {
return try body.toOwnedSlice();
} else {
std.debug.panic("error", .{});
}
(改めてコードをみたら、 ca_bundle.addCertsFromFilePathAbsolute(allocator, ca_path) をfreeしていなさそうなので、リークしていそうです。)
暇があればもうちょっとZigを触ってみたいと思うものの,ここまでメモリ管理とかきにしないといけない環境で動かすことはないので、普通にもっと大富豪のようにメモリをつかえばいいのではという気もしています。
組み込み要素のプログラミングとかやる予定がないですし。