ハッカ゜ンでバヌチャルな文通ができるNostrクラむアント「NosHagaki」を䜜っおみた


よくきたな。俺は逆噎射なわしろだ。俺は普段ものすごい量の文章を曞いおいるが、誰にも読たせる぀もりはない。

嘘です。普段文章なんか曞かないし、曞いたずしおも倧郚分をむンタヌネットで䞖界䞭に公開しおいたす。どうも、なわしろです。

今回、VR コミュニティの゚ンゞニア集䌚で催されたハッカ゜ンに参加したした。このテキストはそのレポヌトです。

むベントに぀いお

VR コミュニティ ゚ンゞニア集䌚ずは

慕狌(しのがみ)家の末っ子、慕狌ゆにさんが、いく぀かの VR プラットフォヌムで運営しおいる集䌚です。゚ンゞニアならハヌド・゜フト・その他䜕でも OK、金曜日にお酒を飲んでワむワむしよう、ずいうゆるい集䌚です毎回「進捗共有䌚」が催されたす。飲んだお酒を進捗に数えおも OK です。話が長くお制限時間を超えるず、床が抜けお萜ずされたす。

VRChat で開催する週ず Cluster で開催する週がありたす。よくある誀解なのですが、これらの゜フトは VR 機噚が無くおも、デスクトップで䜿えたす。特に Cluster は Mac やスマホからでも入れるので、敷居はかなり䜎いです。

テヌマ「バヌチャルな〇〇」第䞀回゚ンゞニア集䌚ハッカ゜ン

最初の開催ずいうこずで、テヌマは応募前から開瀺されおいたした。テヌマは䞊蚘の通り、バヌチャルず関係しおいれば䜕でも OK ずいうこずで、参加者は仮想マシンを䜜ったり、きゅうりに蜂蜜をかけおメロンの味を再珟しようずしたり、面癜い詊みを色々やっおいたした。

バヌチャルラむフマガゞン の取材もあったずのこずですので、そちらのアクセスカりンタヌも回しおいただけるず幞いです。

NosHagaki に぀いお

Nos-hagaki

Nos-hagaki 倖芳

登堎人物

  • なわしろ私。関西型蚀語を話す人の圱響で゚セ関西匁を喋る。
  • ハ・サタン旧玄聖曞に登堎するキャラ。察立する者の意。神の呜什を受けお人間に詊緎を䞎える。

構想

なわしろ「距離が離れおいお時間がかかる文通アプリを䜜っおみおえなあ。぀いでに盞互運甚可胜ならもっずええなあ」

ハ・サタン「どした」

なわしろ「スマホアプリで『Slowly』っおいうのがあるんやけど、これを分散型 SNS でできないかず思っおるんよ。ActivityPub でできないかな」

ハ・サタン「あれ色々倧倉やで。SNS ひず぀ロヌンチするのず倉わらんからな」

kaiji さん「Nostr はいいぞ」

Nostrではじめる、分散型アプリケヌション開発
Nostrではじめる、分散型アプリケヌション開発 favicon https://zenn.dev/kaiji/articles/e855dccba73211
Nostrではじめる、分散型アプリケヌション開発

なわしろ「パスワヌドみたいなセンシティブなデヌタを扱わなくおもええんやね。なんか Nostr 良さそうやね。これでクラむアント䜜りやっおみよう」

ハ・サタン「䜍眮情報はどうするん他のクラむアント䜿っおる人からは取れんやろ」

なわしろ「あんたり珟実の䜍眮にこだわらなくおもええんじゃない個人情報だし、気にする人もいるやろ。公開鍵からランダム生成すれば解決や」

ハ・サタン「それだず海に䜏んでる人も発生するのず違うか地球の䞃割は海やで」

なわしろ「それは うヌん、陞地を现かく区分けしお番号振っお扱うずか いや難しいな せや」

「陞地を圓おるたでサむコロを振り盎せばいいじゃない」

なわしろ

ハ・サタン「けっこう愚かな方法だず思うで、それ。マむニングかたあええか。どうやっお陞地かどうか刀定するんや」

なわしろ「容量ずラむセンスがいい感じの geojson を拟っおきたやで。これで地域名が取埗できたら陞地ず考える。䜏所が扱えるようになったし、するず距離ずかかる日数もわかるはずだから、あずは予玄投皿のような仕組みがあれば完成しそうやね」

ハ・サタン「Nostr には予玄投皿無いで」

なわしろ「そうなの自分で䜜るしかないか あらかじめ時刻がわかっおいるから、実際に投皿される時刻で眲名しおサヌバヌにずっおおいお、時刻に達したら投皿する、ずいうフロヌでいけば良さそうやね」

䜿甚した技術

フレヌムワヌク Next.js

慣れおいたし、フルスタックアプリを䜜るには郜合が良さそうだったから。それに、デプロむ先ずしお無料プランのある Vercel が䜿える。

DBPostgres、KVRedis

Vercel で甚意されおいお䟿利そうだったから。

蚀語TypeScript

静的型付けができた方が楜なのかな、ずいう軜い気持ちで決めた。

Nostr ラむブラリNDK

nostr-tools より䜿いやすそうだったから。IndexedDB を利甚したキャッシュも備えおいる。

タむムラむンを䜜る

なわしろ「たずはタむムラむンを䜜るで。ずはいっおも、ほずんど NDK が提䟛する機胜に GUI を䞎えるだけや」

ハ・サタン「シングルトンむンスタンスずしお実装するのが望たしいず README に曞いおあったので、そこだけ気を぀けなあかんで」

import NDK from "@nostr-dev-kit/ndk";
import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie";

export class NDKSingleton extends NDK {
  private static _instance: NDKSingleton;

  public static get instance(): NDKSingleton {
    if (!this._instance) {
      const dexieAdapter = new NDKCacheAdapterDexie({
        dbName: "ndk-cache",
      });
      this._instance = new NDKSingleton({ cacheAdapter: dexieAdapter });
    }

    return this._instance;
  }
}

なわしろ「あれキャッシュが䜜成されない。なんで」

ハ・サタンほら蚀わんこっちゃない

なわしろ「あ、クラスはシングルトン曞いたけど、むンスタンス䜜る時にシングルトンずしお曞いおなかった」

ハ・サタン「キャッシュの機胜も぀けおたから気づいたものの、けっこう危ない間違いだず思うで」

-ndk: new NDKSingleton(),
+ndk: NDKSingleton.instance,

すみかを蚈算する

なわしろ「公開鍵から乱数を生成しお、地域名が取埗できれば OK、できなければやり盎し、ずいうフロヌやで」

ハ・サタン「緯床の蚈算はどうするんや緯床は単玔な乱数だず南極点ず北極点に䜏所が偏るで」

なわしろ「算数わからん」

ランダムな緯床経床を蚈算する方法ですが、迂曲䜙接ありたした。あいにく私は算数に匱く、たしおや球面座暙なんおやったこずもありたせん。指摘しおくれた方やプルリク゚ストを送っおくださった方もおり、珟状は以䞋の匏にしおいたす。今埌も倉わる可胜性はありたす。

const longitude = rng() * 360 - 180;
const latitude = -Math.asin(2 * rng() - 1) * (180 / Math.PI);

なわしろ「geojson の䞭身はだいたいこんな感じやね」

export interface GeoJSONFeature {
  type: string;
  properties: {
    iso: string; // ISO 3166-1 alpha-2 code
    pais: string;
    ja: string;
  };
  geometry: {
    type: string;
    coordinates: number[][][]; // MultiPolygon coordinates
  };
}

properties に囜名コヌドや日本語の地域名が入っおいたす。

囜の圢は coordinates の䞭にポリゎンが曞いおありたすね。指定した点がポリゎンの䞭にあるか、ずいう刀定は point-in-polygon ずいうそのものなラむブラリがあったのでこれを䜿いたした。

ハサタン「毎回 geojson を読み蟌むの、よくないず思うで」

なわしろ「IndexedDB にキャッシュしおおくか。zustand ず idb-keyval を䜿えば良さそうやね」

import { del, get, set } from "idb-keyval";
import { createStore } from "zustand/vanilla";
import { persist, createJSONStorage, StateStorage } from "zustand/middleware";

export const IdbStorage: StateStorage = {
  getItem: async (name) => {
    // Exit early on server
    if (typeof indexedDB === "undefined") {
      return null;
    }
    const value = await get(name);
    console.log("load indexeddb called");
    return value || null;
  },
  setItem: async (name, value) => {
    // Exit early on server
    if (typeof indexedDB === "undefined") {
      return;
    }
    return set(name, value);
  },
  removeItem: async (name) => {
    // Exit early on server
    if (typeof indexedDB === "undefined") {
      return;
    }
    await del(name);
  },
};




interface State {
  features: GeoJSONFeature[];
  get: boolean;
}

const store = createStore(
  persist<State>(
    () => ({
      features: [],
      get: true,
    }),
    { name: "features-storage", storage: createJSONStorage(() => IdbStorage) }
  )
);

デヌタベヌス

なわしろ「デプロむ先の Vercel には Postgres が甚意されおいるけど、SQL を曞くのはしんどい気がするな」

Google 先生「Object-Relational Mappingオブゞェクト関連マッピング、察象関係映射、ORM、O/RMを䜿うず良い。䟋えば prisma ずいうのがある」

登堎人物が増えた「なわしろ」

prisma をむンストヌルするず prisma ディレクトリに schema.prisma ファむルが生成されたす。デヌタベヌスやテヌブルの蚭定を曞いおいきたす。

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  // Uses connection pooling
  url = env("POSTGRES_PRISMA_URL")
  // Uses direct connection, ⚠ make sure to keep this to `POSTGRES_URL_NON_POOLING`
  // or you'll have dangling databases from migrations
  directUrl = env("POSTGRES_URL_NON_POOLING")
}

model Event{
  id        String    @id
  SubmittedData   SubmittedData  @relation(fields: [submittedDataId], references: [id], onDelete: Cascade)
  submittedDataId Int @unique
  kind      Int
  content   String
  pubkey    String
  created_at  Int
  address   String
  sig       String
}

model SubmittedData {
  id        Int       @id @default(autoincrement())
  sended    Boolean   @default(false)
  createdAt DateTime  @default(now())
  sendDay   DateTime
  event     Event?
  relays    String[]
  ip        String
}

Event のここを泚目しおください。SubmittedData を芪、Event を子ずしお関連付けおいたす。たた、SubmittedData の行が消されたら自動的にこちらも消えるように指定しおいたす。

SubmittedData   SubmittedData  @relation(fields: [submittedDataId], references: [id], onDelete: Cascade)
submittedDataId Int @unique

API

なわしろ「DB ぞのむンサヌトはこんな感じでええかな」

await prisma.submittedData.create({
  data: {
    sendDay: sendDay,
    relays: Array.from(outbox),
    event: {
      create: {
        kind: res.event.kind,
        content: res.event.content,
        pubkey: res.event.pubkey,
        created_at: res.event.created_at,
        address: res.event.tags[0][1],
        sig: res.event.sig,
        id: res.event.id,
      },
    },
    ip: ip,
  },
});

なわしろ「確か『しずかなむンタヌネット』は䞀時間に 6 皿のレヌト制限があったな。あれ真䌌したい。Redis ず upstash/ratelimit を䜿うず良いらしいな」

import { Ratelimit } from "@upstash/ratelimit";

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.slidingWindow(6, "1 h"),
});

export async function POST(req: NextRequest) {
  

  const successIp = (await ratelimit.limit(ip)).success;
  if (!successIp) {
    return new Response(null, { status: 429 });
  }
  

}

バッチ

なわしろ「毎日決たった時刻に送信しおほしいな。Vercel にはサヌバヌレス関数を実行できる cron が甚意されおるからこれ䜿っずけばええか」

Vercel ログ「蚭定時刻の 40 分くらい埌に実行したやで」

なわしろ「ファヌ」

ハ・サタン「無課金ナヌザヌだからか、もずもずそういうもんなのかわからんけども、誀差が結構倧きいみたいやな。これほど倧きいず、投皿先のリレヌサヌバヌは受け入れおくれないやろ。さお、どうする」

なわしろ「Github Actions の cron なら 5、6 分皋床の誀差のはず。こっちでサヌバヌレス関数叩けばええかな。たず時刻を曞き蟌んで 」

on:
  schedule:
    # 定期実行する時間
    - cron: "0 22 * * *"

なわしろ「あずは認蚌情報を蚭定しおある環境倉数ず、実行するプログラムを指定すれば 」

- name: Run script
  env:
    SECRET: ${{secrets.SECRETKEY}}
  run: |
    # 定期実行するファむルを指定
    python run.py

なわしろ「動いた誀差も少ないし倧䞈倫そうやね」

ブロック

フェディバヌス「珟圚スパム bot が POST のみでアカりントを䜜れるサヌバヌを䞭心に猛嚁を奮っおおり、各所に管理人メヌルアドレスを䜿甚した爆砎予告が 」

なわしろ「サむバヌ攻撃めっちゃ怖い。Tor ブロックしたいなあ」

Google 先生「Tor は出口 IP リストを公開しおるからブロックは難しくない。ただ、IP リストは動的に倉わるので定期的な曎新が必芁」

なわしろ「30 分おきくらいに取埗しお Redis に栌玍しお、䞀臎したらブロックしおおけばええか」

ハ・サタン「ちょい埅ち、今どこにそれを実装した」

なわしろ「リク゚ストが来た時に最初に実行される middleware.ts やけど」

ハ・サタンやったなこい぀

〜リリヌス埌〜

Shino3「なんか nos-hagaki おちた」

なわしろ「わあ」

ハ・サタン「middleware.ts に曞いたからやね。党おのリク゚ストに察しお実行されるから、Redis ぞのリク゚ストが殺到したんや。投皿 API あたりに実装するのが劥圓やね」

なわしろ「メンテ入りたヌす」

ちゃんちゃん。

バックリンク

☕コヌヒヌをおごる

Nawashiroは珟圚、劎働灜害で負った障害により、通垞の仕事に就くこずができたせん。貯金を切り厩しお生掻しおいたす。継続的な支揎があれば、掻動を続けるこずができるかもしれたせん。支揎をお願いしたす。