にあえん

主にバックエンド・インフラ周りをやってます。a

React + Peerjsでグループチャットを実装してみる

こんにちは。ナナオです。 今回はReact + peer.jsを使ってグループチャットを実装しようと思います。 peer.jsとは? Peer.jsはWebRTCを使ってP2Pを実現するためのツールです。 P2Pとは、サーバーを介さずにクライアント同士で直接データやり取りを行う方法のことです。 WebRTCは とりあえず実装したもの 最初に作ったグループチャットページの実装は以下の通りです。 タイトルの通り、React + peer.jsと、CSSにはtailwindを使用しています。 また、グループの部屋IDはURLのハッシュから取得するようにしています。 グループは、グループの発行者とそれ以外のメンバーで構成されます。 (実装はほぼGeminiがやってくれました。便利すぎ。) import { useState, useEffect, useRef } from 'react'; import Peer from 'peerjs'; import type { DataConnection } from 'peerjs'; const ChatApp = () => { const [myId, setMyId] = useState(''); const [messages, setMessages] = useState<{ sender?: string; text: string; system?: boolean }[]>([]); const [messageInput, setMessageInput] = useState(''); const [conns, setConns] = useState<{ [key: string]: DataConnection }>({}); // 接続中の全Peerを管理 {peerId: connection} const [copyStatus, setCopyStatus] = useState('URLをコピー'); const peerRef = useRef<Peer>(null); const scrollRef = useRef<HTMLDivElement>(null); // 初期化処理 useEffect(() => { const peer = new Peer(); peerRef.current = peer; peer.on('open', (id) => { setMyId(id); // URLのハッシュ(#)からIDを取得 const targetId = window.location.hash.substring(1); // #を除いた文字列を取得 if (targetId) { connectToPeer(targetId); } }); peer.on('connection', (conn) => { setupConnection(conn); }); return () => peer.destroy(); }, []); // 常に最新メッセージまでスクロール useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); const setupConnection = (conn: DataConnection) => { conn.on('open', () => { setConns(prev => ({ ...prev, [conn.peer]: conn })); setMessages(prev => [...prev, { system: true, text: `${conn.peer.substring(0,5)}... が入室しました` }]); }); conn.on('data', (data) => { if (typeof data === 'string') { // 複数人でメッセージやり取りをできるように setMessages(prev => [...prev, { sender: conn.peer.substring(0,5), text: data }]); } }); conn.on('close', () => { setMessages(prev => [...prev, { system: true, text: '誰かが退室しました' }]); setConns(prev => { const newConns = { ...prev }; delete newConns[conn.peer]; return newConns; }); }); }; const connectToPeer = (id: string) => { if (conns[id] || !peerRef.current) return; // 接続済みならスキップ const conn = peerRef.current.connect(id); setupConnection(conn); }; const sendMessage = (e) => { e.preventDefault(); // 接続している全員に送信(ブロードキャスト) Object.values(conns).forEach(conn => { if (conn.open) { conn.send(messageInput); } }); setMessages(prev => [...prev, { sender: '自分', text: messageInput }]); setMessageInput(''); }; // 招待用URLをコピーする関数 const copyInviteLink = () => { const inviteUrl = `${window.location.origin}${window.location.pathname}#${myId}`; navigator.clipboard.writeText(inviteUrl).then(() => { setCopyStatus('コピー完了!'); setTimeout(() => setCopyStatus('URLをコピー'), 2000); }); }; return ( <div className="flex flex-col h-screen bg-slate-900 p-4 text-slate-100"> <div className="max-w-2xl mx-auto w-full flex flex-col h-full bg-slate-800 rounded-xl shadow-2xl overflow-hidden"> {/* ヘッダーセクション */} <div className="bg-slate-800 p-5 text-white"> <h1 className="text-xl font-bold mb-2 text-center text-indigo-400">P2P Chat</h1> <p className="text-[10px] text-slate-400">参加人数: {Object.keys(conns).length + 1}名</p> <div className="bg-slate-700 rounded-lg p-3 flex flex-col gap-2"> <div className="text-[10px] uppercase tracking-wider text-slate-400 font-semibold">Your ID</div> <div className="text-sm font-mono break-all text-indigo-200">{myId || '発行中...'}</div> {myId && ( <button onClick={copyInviteLink} className="mt-1 w-full bg-indigo-600 hover:bg-indigo-500 text-white text-xs py-2 rounded transition-colors font-bold" > {copyStatus} </button> )} </div> </div> {/* チャットログ */} <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-900/50"> {messages.map((msg, i) => ( <div key={i} className={`flex flex-col ${msg.sender === '自分' ? 'items-end' : 'items-start'}`}> {msg.system ? ( <span className="text-[10px] text-slate-500 italic mx-auto my-2 uppercase tracking-widest">{msg.text}</span> ) : ( <> <span className="text-[10px] text-slate-400 mb-1 ml-1">{msg.sender}</span> <div className={`px-4 py-2 rounded-2xl text-sm max-w-[80%] break-all ${ msg.sender === '自分' ? 'bg-indigo-600 text-white rounded-tr-none' : 'bg-slate-700 text-slate-100 rounded-tl-none border border-slate-600' }`}> {msg.text} </div> </> )} </div> ))} </div> {/* 入力エリア */} <form onSubmit={sendMessage} className="p-4 bg-slate-800 border-t border-slate-700 flex gap-2"> <input type="text" className="flex-1 bg-slate-700 border-none rounded-lg px-4 py-3 text-sm text-white focus:ring-2 focus:ring-indigo-500 outline-none" placeholder="全員にメッセージを送信..." value={messageInput} onChange={(e) => setMessageInput(e.target.value)} /> <button type="submit" className="bg-indigo-600 p-3 rounded-lg hover:bg-indigo-500 transition-colors"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current" viewBox="0 0 20 20"> <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" /> </svg> </button> </form> </div> </div> ); }; export default ChatApp; ポイントとしては以下の通りです。 ...

2026年1月3日 · にあえん

Syncthingのフォルダパスを変更する

こんにちは。ナナオです。 先日Syncthingについておススメする記事を書いたのですが、よくあることとして共有フォルダを設定した後でパスを変更したいケースがあります。 どのように対処すればよいか、備忘録として記録しておきます。 共有フォルダの移動方法 結論として、公式では以下のように記載されています。 FAQ — Syncthing documentation **Syncthingにはこれを直接行う方法がありません。**注意を怠ると危険を伴うためです。移動中に問題が発生し、他のデバイスに同期されるとデータ損失につながる可能性があります。 ローカルシステム上で同期フォルダをリネームまたは移動する簡単な方法は、Syncthing UIでフォルダを削除し、ディスク上で移動した後、新しいパスで再追加することです。 この操作は、フォルダが既にデバイス間で同期されている状態で行うことが重要です。そうしないと、移動後にどの変更が「優先されるか」が予測不能になります。他のデバイスで行われた変更が上書きされる可能性や、ローカルで行った変更が他のデバイスからの変更で上書きされる可能性があります。 別の方法として、Syncthingを停止し、ディスク上でフォルダ(.stfolderマーカーを含む)を移動した後、設定フォルダ内のconfig.xmlでパスを直接編集し(Syncthingの設定を参照)、Syncthingを再起動する方法があります。 ということで、UI上で一度フォルダを削除する必要があるようです。 やっていきましょう。 まず移動したいフォルダの編集を押します。 除去とはいを押します。 すると一覧からフォルダが消えます。 ただソースの実態は残っているので、別で削除しましょう。 削除したら新たにフォルダを設定します。 設定方法は昨日書いた記事で書いています。 除去されていないデバイスで一度共有設定を切って、再度共有します。 すると、先ほどフォルダを除去したデバイスで新規フォルダーとして検知されます。 これを新しいパスで保存して終わりです。 感想 やろうと思えばできるけど、ちょっと面倒なので最初からフォルダパスは慎重に決めるのがいいですね。。

2026年1月2日 · にあえん

Syncthingはいいぞ

あけましておめでとうございます。ナナオです。 去年はいろいろありまして一つもブログを挙げられず。。 今年はやったるぞ!ということで、元旦から挙げていこうと思います。 ということで、今回は2年ほど愛用しているSyncthingというツールをおすすめしていきたいと思います。 ちなみに自慢じゃないですが私のブログはほぼ手書きです。(AIを使っていない) 導入のきっかけ 私はメインにデスクトップ、サブにノートという両刀使いで開発をしているのですが、二年ほど前この二つの環境でのプロジェクトのソースコード共有の方法に頭を悩ませていました。 当時は基本的にGithubにプッシュして、それぞれの環境にそれをプルしてプロジェクトを共有するという開発をしていたのですが、個人開発でそれをしてもデスクトップで変更したものをノートでいちいちプルするのがとにかく面倒だったのです。 なんかないかなーと調べて、出てきたのが「Syncthing」というツールでした。 Syncthing Syncthingとは このツールは、指定したフォルダの内容をデバイス間で共有できるというシンプルなツールになっています。 ソースコードの共有を想定しているためか、gitignoreみたいなファイルを配置すればそのファイルを無視できるなど、ちょっとエンジニア指向なツールになっています。 WebGUIもあるため、操作性もよいです。 一度共有すれば、同一ネットワーク上に共有デバイスが存在する場合に勝手にフォルダ同期を行ってくれるので、Gitでプッシュ・プルするよりも格段に共有が楽になります。 使い方 Windowsではwingetで導入できます。 winget install Syncthing.Syncthing MacOS/Linuxではbrewで導入できます。 brew install syncthing インストールできたら起動します。 syncthing 起動するとブラウザが立ち上がります。 最初の画面 また、このサービスは常にバックグラウンドで立ち上げておく必要があるので、brew servicesで登録しておきましょう。 brew services start syncthing Windowsの場合はタスクスケジューラに登録しましょう。 Starting Syncthing Automatically — Syncthing documentation デバイスの登録 Syncthingをセットアップしたデバイスがローカルネットワーク上に二台以上できたなら、フォルダ共有の準備が整いました。 簡単です。 接続先デバイスの追加をクリックします。 IDとデバイス名を入力します。 画像では何も入力されていませんが、同一ネットワークに未登録かつSyncthing起動済みのデバイスがある場合IDが自動入力されます。 ちなみに、IDはトップ画面の「IDを表示」から参照できます。 フォルダの登録 フォルダの登録はWebUI上から行います。 「フォルダを作成」をクリックします。 フォルダ名とパスを埋めて、保存を押します。 これでフォルダを共有する準備ができました。 登録されたフォルダの共有 いよいよフォルダの共有を行います。 登録したフォルダを編集します。 共有タブから共有したいデバイスにチェックを入れ、保存を押します。 保存を押すと、対象のデバイスのWebUI上で共有を受け入れるかどうかが聞かれるので「追加」を選択します。 これで「フォルダの登録」で行ったのと同じ手順でフォルダ名とパスを埋めて保存を押すと、同期が開始されます。 感想 フォルダの共有作業が圧倒的に楽になるSyncthing、導入してからソースコード共有が非常に楽になりました。 同じような開発環境の方はぜひ試してみてください!

2026年1月1日 · にあえん

【Hackintosh】Wi-FiカードをIntelからbcm94360ngに乗り換えました

年の瀬ですね。どうも、ナナオです。 年末の整理ってほどでもないですが、以前Hackintosh化したThinkPad X270のWi-Fiカードを入れ替えて、HeliPortを使用しないで純正のWi-Fiモジュールを使用できるようにしました。 BCM94360NGというカードに乗り換えました。 切り替え手順に関しては、大体の手順はここを参考にすればいいはずです。 macOS 14 Sonoma 博通网卡驱动支持 まじで備忘録です。 最終的に一番参考になったリポジトリ GitHub - Edwardwich/BCM-WIFI-Sequoia ここの設定をほぼパクって設定して、OpenCore Legacy Patcherを当てたら成功しました。 Wi-Fi検知できた!! あとはこのツールにめちゃくちゃ助けてもらいました。 KextとかOpenCoreのバージョンを勝手に最新化してくれる便利なやつです。 GitHub - ic005k/OCAuxiliaryTools: Cross-platform GUI management tools for OpenCore(OCAT) 備考 Wi-Fiには繋がるようになったけど、スリープから復帰した時にタッチパッドがうまく動かないという事象が発生しています。 悲しい。。 ただまぁ今回の作業に比べたら全然大したことないんですが、、 また、リポジトリには今回の対応で完成したEFIフォルダを公開しています。 GitHub - satodaiki/X270-Hackintosh-Sequoia それではまた。 参考 SonomaでBroadcom Wi-Fiを有効にする – Boot macOS ThinkPad T460s にWiFi/BTカードBCM94360NGを取り付ける – Boot macOS

2024年12月30日 · にあえん

tauriを使ってXCodeでiPhoneをエミュレーションする

どうも、やっぱりRustが好きなナナオです。 前回、せっかくHackintosh化に成功したので、XCodeでなんかエミュレートしたいんですよね。 Lenovo ThinkPad X270にHackintoshでSequoiaを入れた | にあえん ということで今回はtauriを使ってみようと思います。 プロジェクトの作成 tauriのプロジェクトの作成は公式ドキュメントに従ってやっていきましょう。 Create a Project | Tauri とりあえずプロジェクト作成に必要なツールをダウンロードしていきましょう。 cargo install create-tauri-app --locked tauri cliというのもインストールしておきましょう。 Command Line Interface | Tauri cargo install tauri-cli --version "^2.0.0" --locked これで準備ができました。 プロジェクトを作成していきます。 cargo create-tauri-app 出来上がったsrc-tauriディレクトリ内で以下のコマンドを実行します。 cargo tauri dev とりあえずデスクトップアプリの起動には成功しました! iPhoneでエミュレートしてみる iOSで起動するには、同ディレクトリ内で以下のコマンドを実行する必要があります。 cargo tauri ios init しかし残念ながらエラーが出てしまいました。。 Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`). Info package `cocoapods` present: false Installing `cocoapods`... Error: Cask 'cocoapods' is not installed. `sudo` is required to install cocoapods using gem Error failed to install Apple dependencies: Failed to install `cocoapods`: No such file or directory (os error 2): No such file or directory (os error 2) cocoapodsというツールがインストールされていなかったようです。 ...

2024年12月22日 · にあえん

Lenovo ThinkPad X270にHackintoshでSequoiaを入れた

どうも、ナナオです。 最近は色々あってブログ更新ができていなかったんですが、それでもなんかPCは触っていたいな〜ってことで、ジャンクPCを漁ってHackintoshを入れてました。 ということで今回はその備忘録です。 準備(別にHackintoshとは関係ない) タイトルにもありますが、今回購入したジャンクPCはこちらです。 X270ちゃん! Lenovo ThinkPad X270です。 最初に買ってずっと使っていたジャンクPCがThinkPad X201で結構思い出深いところもあるんですが、やっぱりThinkPadはいじりやすいんですよね。 あとこれにした理由としては、メモリがDDR4に対応し始めたのがXシリーズだとこのモデルからというのもあります。 ということで、まず購入した最初の状態だとメモリが4GBだったので、32GBのものに変えました。(メーカー推奨じゃないので自己責任だよ!) ���i.com - SPD SDDR432S32G30 [SODIMM PC4-25600 32GB] ���i���r 次にHDDがついてないものを買ったので、適当な1TBのSSDを購入しました。 あと、キーボードが日本語配列だったのが気に入らなかったので英字配列のものに入れ替えました。 Amazon | Generic ラップトップ キーボード/US レイアウト ブラック ポイント英語/ 0C02291 に適用 x240 x240S x250 x260 x270 | Generic | パソコン用キーボード 通販 キーボードの変更方法は以下のブログを参考にしました。ありがとうございます! ThinkPad X270のキーボード交換をしてみた。 - 三日坊主の備忘録 ということで完成形はこちらです。 キーボード&メモリ換装後のX270 所々ひび割れてるのはジャンクあるあるですね。 キーもメモリも変えて気持ちよくなったところで、Hackintoshしていきましょう!! Sonomaを入れる インストールメディアの作り方などは検索すれば解説記事がいくらでも出てくると思うので、ここでは完成形のEFIを置いておきます。 GitHub - satodaiki/X270-Hackintosh-Sonoma BIOSの設定はこちらを参照しました。 GitHub - nicktimur/Lenovo-X270-Hackintosh-OpenCore-Sonoma BIOSの設定 こちらはIntelのM.2カードを利用していてもWi-Fiが普通に使えます! Sequoiaにアップグレード アップグレード作業はSonomaがインストールできた後に設定画面からしました。 ...

2024年12月17日 · にあえん

GoでgRPCをササッと実装してみる 1

こんにちは。ナナオです。 今回は触ろうと思って触ってなかったgRPCに入門してみようと思います。 gRPCとは? gRPCとは、GoogleがRPCを実現するために作った技術のことです。 詳細については以下のサイトがかなり細かく説明してくれています。 https://zenn.dev/hsaki/books/golang-grpc-starting gRPCサーバーを動かす まずはgRPCのリクエストとレスポンスを定義するためのprotoファイルを作成します。 適当なディレクトリを作成して、Goモジュールとして初期化しておきましょう。 mkdir go_grpc_playground && cd go_grpc_playground go mod init go_grpc_playground 次に、protoファイルを作成します。 (参考にしてるサイトをパクってるだけですが。。) // protoのバージョンの宣言 syntax = "proto3"; // protoファイルから自動生成させるGoのコードの置き先 option go_package = "pkg/grpc"; // packageの宣言 package myapp; // サービスの定義 service GreetingService { // サービスが持つメソッドの定義 rpc Hello (HelloRequest) returns (HelloResponse); } // 型の定義 message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } 次にこのprotoファイルからgoファイルを作成するためのモジュールをインストールします。 brewがあれば以下のコマンドで一発インストールできます。 brew install protobuf インストールするとprotocというコマンドが使用可能になります。 続けて、必要なモジュールを依存関係に追加しておきます。 go get -u google.golang.org/grpc go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc さて、今のディレクトリはこうなっています。 ...

2024年11月17日 · にあえん

Rustで標準入力受け取りたいとき

こんにちは。ナナオです。 AtCoderでRustが使えるので使いたいなーと思ったんですが、標準入力どう受け取ればいいんだ?ってなったので備忘録。 Rustで標準入力受け取るコード こんな感じにしました。 use std::io::{self, Read}; fn main() { let input = read_stdin(); print!("{}", input); } /// 標準入力を受け取る関数 fn read_stdin() -> String { let mut buf = String::new(); io::stdin().read_to_string(&mut buf).expect("Failed to read stdin."); buf.to_string() } これで大抵の競プロは乗り切れるはず。。

2024年11月10日 · にあえん

C++の入門 conanに少し触れる

こんにちは。ナナオです。 最近AtCoderなどでコーディングをしているのですが、やはり主流な言語はC++のようで、大体のひとがC++を使っています。 ということで、僕も今更ながらC++に入門してみようと思います。 環境構築 とりあえずパッケージ管理に何を使おうか調べたところ、vcpkgとconanの2つが使えるということですが、クロスプラットフォームであることからconanにしようと思います。 C/C++用パッケージマネージャのconanを使ってみた #Conan - Qiita pipからインストールできるようですが、pyenvを使っている都合でpythonのバージョン切り替えをしたら使えないみたいなことがあると面倒くさいのでbrewからインストールします。 brew install conan インストールが完了すると、conanコマンドが使用可能になります。 ❯ conan --version Conan version 2.9.2 チュートリアルにしたがって、まずはconanでパッケージを作成してみます。 Create your first Conan package — conan 2.24.0 documentation 新しくディレクトリを作成して、そこでconanコマンドを実行します。 mkdir cpp_playground && cd cpp_playground conan new cmake_lib -d name=cpp_playground -d version=1.0 また、conan自体にプロファイルを設定してあげる必要があるようなので、それも合わせてしておきます。 Besides the conanfile.txt, we need a Conan profile to build our project. Conan profiles allow users to define a configuration set for things like the compiler, build configuration, architecture, shared or static libraries, etc. Conan, by default, will not try to detect a profile automatically, so we need to create one. To let Conan try to guess the profile, based on the current operating system and installed tools, please run: conanfile.txtの他に、プロジェクトをビルドするにはConanプロファイルが必要だ。 Conanプロファイルは、コンパイラ、ビルド・コンフィギュレーション、アーキテクチャ、共有ライブラリやスタティック・ライブラリなどのコンフィギュレーション・セットを定義することができる。 Conanはデフォルトではプロファイルを自動検出しようとしないので、プロファイルを作成する必要があります。 そのため、プロファイルを作成する必要があります。現在のオペレーティング・システムとインストールされているツールに基づいて、コナンにプロファイルを推測させるには、以下を実行してください: ...

2024年11月10日 · にあえん

hasuraに入門してみた

こんにちは。ナナオです。 今回はPostgresサーバーから、そのままGraphQLとして運用することが可能なHasuraをイジってみたいと思います。 準備 とりあえずディレクトリを作っておきます。 mkdir hasura_playground ちゃっと検証したいので、とりあえずスキーマは以前触ったsqldefを使います。 【以前の記事はこちら】 マイグレーションの基盤とするSQLファイルを作成します。 touch schema.sql テーブルはUserとそれに紐づくPostテーブルを作る感じにしましょう。 CREATE EXTENSION "uuid-ossp"; CREATE TABLE public.user ( id uuid NOT NULL DEFAULT uuid_generate_v4(), name varchar(255), age integer ); CREATE TABLE public.post ( id uuid NOT NULL DEFAULT uuid_generate_v4(), title varchar(255), content text, user_id uuid ); dockerでPostgresを起動するためのdocker-compose.yamlファイルも実装しておきます。 version: '3' services: db: image: postgres:15 container_name: postgres ports: - 5432:5432 environment: - POSTGRES_PASSWORD=example これを起動します。 docker compose up -d あとは以下のコマンドで先程のテーブル定義を適用すればよいです。 psqldef --password=example -h localhost -p 5432 -U postgres postgres < schema.sql あとは適当にデータを追加しておきましょう。 ...

2024年11月3日 · にあえん