ããã«ãœã³ã§ããŒãã£ã«ãªæéãã§ããNostrã¯ã©ã€ã¢ã³ããNosHagakiããäœã£ãŠã¿ã
- categories:
- topics:
ãããããªã俺ã¯éåŽå°ãªãããã ãä¿ºã¯æ®æ®µãã®ãããéã®æç« ãæžããŠãããã誰ã«ãèªãŸããã€ããã¯ãªãã
åã§ããæ®æ®µæç« ãªããæžããªãããæžãããšããŠã倧éšåãã€ã³ã¿ãŒãããã§äžçäžã«å ¬éããŠããŸããã©ããããªãããã§ãã
ä»åãVR ã³ãã¥ããã£ã®ãšã³ãžãã¢éäŒã§å¬ãããããã«ãœã³ã«åå ããŸããããã®ããã¹ãã¯ãã®ã¬ããŒãã§ãã
ã€ãã³ãã«ã€ããŠ
VR ã³ãã¥ãã㣠ãšã³ãžãã¢éäŒãšã¯ïŒ
æ çŒå®¶ã®æ«ã£åãæ çŒãã«ããããããã€ãã® VR ãã©ãããã©ãŒã ã§éå¶ããŠããéäŒã§ãããšã³ãžãã¢ãªãããŒãã»ãœããã»ãã®ä»äœã§ã OKãéææ¥ã«ãé ã飲ãã§ã¯ã€ã¯ã€ãããããšãããããéäŒã§ãæ¯åãé²æå ±æäŒããå¬ãããŸãã飲ãã ãé ãé²æã«æ°ããŠã OK ã§ãã話ãé·ããŠå¶éæéãè¶ ãããšãåºãæããŠèœãšãããŸãã
VRChat ã§éå¬ããé±ãš Cluster ã§éå¬ããé±ããããŸãããããã誀解ãªã®ã§ããããããã®ãœãã㯠VR æ©åšãç¡ããŠãããã¹ã¯ãããã§äœ¿ããŸããç¹ã« Cluster 㯠Mac ãã¹ããããã§ãå ¥ããã®ã§ãæ·å± ã¯ããªãäœãã§ãã
ããŒããããŒãã£ã«ãªããã第äžåãšã³ãžãã¢éäŒããã«ãœã³
æåã®éå¬ãšããããšã§ãããŒãã¯å¿ååããé瀺ãããŠããŸãããããŒãã¯äžèšã®éããããŒãã£ã«ãšé¢ä¿ããŠããã°äœã§ã OK ãšããããšã§ãåå è ã¯ä»®æ³ãã·ã³ãäœã£ããããã ããã«èèããããŠã¡ãã³ã®å³ãåçŸããããšããããé¢çœã詊ã¿ãè²ã ãã£ãŠããŸããã
ããŒãã£ã«ã©ã€ããã¬ãžã³ ã®åæããã£ããšã®ããšã§ãã®ã§ããã¡ãã®ã¢ã¯ã»ã¹ã«ãŠã³ã¿ãŒãåããŠããã ãããšå¹žãã§ãã
NosHagaki ã«ã€ããŠ

ç»å Žäººç©
- ãªãããïŒç§ãé¢è¥¿åèšèªã話ã人ã®åœ±é¿ã§ãšã»é¢è¥¿åŒãåãã
- ãã»ãµã¿ã³ïŒæ§çŽèæžã«ç»å Žãããã£ã©ã察ç«ããè ã®æãç¥ã®åœä»€ãåããŠäººéã«è©Šç·Žãäžããã
æ§æ³
ãªããããè·é¢ãé¢ããŠããŠæéããããæéã¢ããªãäœã£ãŠã¿ãŠããªããã€ãã§ã«çžäºéçšå¯èœãªããã£ãšãããªãã
ãã»ãµã¿ã³ãã©ããïŒã
ãªããããã¹ããã¢ããªã§ãSlowlyãã£ãŠããã®ããããããã©ãããã忣å SNS ã§ã§ããªãããšæã£ãŠããããActivityPub ã§ã§ããªãããªã
ãã»ãµã¿ã³ãããè²ã 倧å€ãã§ãSNS ã²ãšã€ããŒã³ãããã®ãšå€ããããããªã
kaiji ããã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 ãã¡ã€ã«ãçæãããŸããããŒã¿ããŒã¹ãããŒãã«ã®èšå®ãæžããŠãããŸãã
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 ãããã«å®è£
ããã®ã劥åœããã
ãªããããã¡ã³ãå ¥ããŸãŒãã
ã¡ããã¡ããã