マテ茶を飲もう

ネットの海を泳いでも見つからなかった情報を残します

【完全無料】Oracle Cloudでdiscord.js botを常時ホストする方法

対象者

低スペックで構わないので、個人用botを動かせる完全無料サーバーを探している人。

2024年4月時点での簡単なホスト方法を記します。M2 MacBook Airで実行しました。

目次:

Oracle Cloudに登録

公式サイトから、手順に従って登録してください。

数年前はOracleのキャパ問題(?)によってアカウント申請が通りにくかった覚えがありますが、今回は一度で作成できました。

ただし、相変わらず Tokyo は Home Region として人気のようなので、一応 Japan Central (Osaka) を選びました。

SSH接続の準備

ターミナルを起動し、これから作成するoracleのサーバーにSSH接続するための鍵を生成しましょう。

例:

cd ~/.ssh
ssh-keygen -t ed25519 -f oracle

※異なるディレクトリーで生成しても構いませんが、場所を覚えておいてください。

lsコマンドの結果、oracle.puboracleの2ファイルが表示されることを確認して次に進みます。

コンピュートインスタンスを作成

Oracle Cloudにログインし、Launch resources --> Create a VM instance に移動します。

Image and Shape で、Always Free-eligible というタグがついているものを選択します。 本当は VM.Standard.A1.Flex を選びたかったんですが、キャパ不足のようなエラー(詳しくは覚えてない)が表示されてしまったので、VM.Standard.E2.1.Micro にしました。

次に、先ほど生成したSSH公開鍵oracle.pubを登録します。

ターミナルで

cat ~/.ssh/oracle.pub | pbcopy

を実行し、クリップボードにコピーします。その後Oracleのページに戻り、Add SSH Keys --> Paste public keys に移動してペーストすればokです。

以上を終えたら、最低限の設定は完了です。

作成ボタンを押し、画面が遷移するのを待ちます。 遷移後の画面で、ステータスが RUNNING になれば完了です。 タブを開いたまま、次に進みます。

インスタンスSSH接続・最低限の設定

先ほど生成したSSH鍵と、インスタンスの詳細ページに表示されているPublic IP address(赤塗り部分)、その下のusernameを用います。 生成直後のusernameは全員共通でopcのようです。

ターミナルで以下のコマンドを打ちます。

ssh -i ~/.ssh/oracle opc@(public ip address)

その結果、以下のような表示になれば接続できています。

[opc@(インスタンスの名前) ~] $

これは任意ですが、接続の度に秘密鍵の場所やIPアドレスを入力するのは面倒なので、SSHの設定ファイル(大抵~/.ssh/config)に以下を追記すると楽になります。

Host (任意の名前。例:oracle-discord)
    HostName (上記IPアドレス)
    User opc
    IdentifyFile ~/.ssh/oracle

上記のように設定すると、ssh oracle-discordと入力するだけでインスタンスに接続できます。

次に、SSH接続した状態で最低限の設定変更を行います。

タイムゾーンの変更

おそらく、初期状態では日本標準時(JST)ではなくグリニッジ標準時(GMT)に設定されています。

[opc@(インスタンスの名前) ~]$ date

の結果に、JSTではなくGMTが表示されている場合、以下のコマンドでタイムゾーンを変更しましょう。

[opc@(インスタンスの名前) ~]$ sudo timedatectl set-timezone Asia/Tokyo

その後、上記のdateコマンドを実行し、GMTではなくJSTと表示されていることを確認します。

日本語パックのインストール

[opc@(インスタンスの名前) ~]$ localectl status

の結果がLANG=ja_JP.utf8になっていない場合、以下の流れに沿ってロケールを日本語に設定しましょう。

[opc@(インスタンスの名前) ~]$ sudo yum install glibc-langpack-ja

↓途中でy Enter、Complete! と表示されるまで数分待つ

[opc@(インスタンスの名前) ~]$ sudo localectl set-locale LANG=ja_JP.utf8

再度localectl statusを実行し、LANG=ja_JP.utf8が表示されていればOKです。

gitをインストール

[opc@(インスタンスの名前) ~]$ sudo yum install git-all
[opc@(インスタンスの名前)~]$ git -v
git version 2.39.3

のようになるのを確かめる。

ローカルのファイルを移す

ローカルで行うこと

  1. 開発ディレクトリーで用いているnode.jsのバージョンを調べる。私が使っていた v16.20.2 はいつの間にかサポートが終了していたので、これを機にOracle Linuxが対応しているバージョンのうち最も新しいLTSである v18.20.2 に上げました。

  2. GitHubにpush(参考:githubでpushするまでの手順 #GitHub - Qiita)

クラウドサーバーで行うこと

  1. 上記インスタンスSSH接続

    ssh oracle-discord

  2. GitHubリポジトリーをクローン

    [opc@(インスタンスの名前) ~]$git clone https://github.com/~~~.git

  3. node.jsとnvmをインストール

    まず、oracle公式サイトでnode.jsの対応表を見ます。 2024年4月時点では、v18.x までのみ対応しているようです(node.js自体の最新LTSは v20.12.2)。

    公式サイトに沿ってインストールしても良いですが、いずれnvm(Node Version Manager)が必要になるので、

    まずはnvmをインストール→指定バージョンのnode.jsをインストール

    という順番で進めます(参考: How to Install Node.js and NPM on Oracle Linux | Atlantic.Net )。

    nvmのインストール:

    [opc@(インスタンスの名前) ~]$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash

    [opc@(インスタンスの名前) ~]$ source ~/.bashrc

    [opc@(インスタンスの名前) ~]$ nvm -vの結果、0.39.7のように表示されればOK。

    node.jsのインストール:

    [opc@(インスタンスの名前) ~]$ nvm ls-remoteでインストール可能なバージョン一覧を確認。v18.20.2をインストールする場合↓

    [opc@(インスタンスの名前) ~]$ nvm install 18

    [opc@(インスタンスの名前) ~]$ nvm use 18

    [opc@(インスタンスの名前) ~]$ node -vの結果、v.18.20.2と表示されればOK。

  4. ローカルと同じように実行し、必要なモジュールをインストール

    私の場合、環境変数を読み込むための dotenv モジュールが無いというエラーが出たので、npm install dotenvを実行して解決しました。

  5. foreverで常時起動

    サーバーとの接続を切ってもbotを動かし続けるため、foreverというデーモンを導入します。

    [opc@(インスタンスの名前) ~]$ npm install -g forever (グローバルオプション -g を忘れないように注意)

    インストール後、メインファイルが格納されているディレクトリーに移動し、startコマンドを実行します。

    [opc@(インスタンスの名前) ~]$ forever start index.js

    実行中のスクリプトの一覧や停止方法など:Node.jsのスクリプトをデーモン化する!

  6. 動作確認

    [opc@(インスタンスの名前) ~]$ exitでサーバーとの接続を切った後も、botがオンラインになっていれば成功です👏

macOS Sonomaでメモのポップアップを無効にする方法

Appleの余計なお世話を無効にしたい

macOS SonomaがインストールされたMacを買うと、スクリーン右下にカーソルを移動させるとメモアプリが表示されると思います。Appleの純正メモアプリを使いたくない人向けに、これを無効にする方法を記します。Montery以前とは多少違いますが、大体一緒です。

手順

  1. システム設定 アプリを開き、デスクトップとDockに移動。一番下までスクロールし、ホットコーナー...をクリック。
  2. クイックメモをクリックし、-を選択。
  3. メモアプリを起動する以外にも色々設定できるようです。要らないけど。

MacのTerminalにpureをインストールする方法

Macにデフォルトで搭載されているTerminalは、目に優しくない。pureというテーマを導入すると良いらしいので、新しいMacBook Proにインストールしようとしましたが、公式サイトの通りにやっても上手くいかなかったので、僕のケースの解決策を記しておきます。

追記:M2, M3でも同様に出来ました。

環境

1. Preztoをインストール

Preztoは、zshのconfiguration frameworkです。まずはこれをインストールし、その設定ファイル(.zpreztorc)でpureを選択します。 ターミナルで以下のコマンドを実行します。

git clone --recursive https://github.com/sorin-ionescu/prezto.git "${ZDOTDIR:-$HOME}/.zprezto"
setopt EXTENDED_GLOB
for rcfile in "${ZDOTDIR:-$HOME}"/.zprezto/runcoms/^README.md(.N); do
  ln -s "$rcfile" "${ZDOTDIR:-$HOME}/.${rcfile:t}"
done

その後ターミナルを再起動すると、3色(>>>)のシンプルな表示になっていると思います。なお、既に .zshrc ファイルが存在する状態で行うと上手くいかない場合があるようです。その場合は、ターミナルにrm ~/.zshrcと入力してファイルを削除してから実施してください(rmコマンドは自己責任で)。

2. Preztoの設定を変更

既にデフォルトのターミナルよりはだいぶ見やすいですが、pureを入れると更に分かりやすくなります。vi ~/.zpreztorcでPreztoの設定ファイルを開き、iキーを押して以下の通り変更します。

2.1 テーマを変更

ファイルの中央付近にある

zstyle ':prezto:module:prompt' theme 'sorin'

zstyle ':prezto:module:prompt' theme 'pure'

に変更。

2.2 補完機能とシンタックスハイライトを追加

'syntax-highlighting' \'autosuggestions' \を追加して、

zstyle ':prezto:load' pmodule \
  'environment' \
  'terminal' \
  'editor' \
  'history' \
  'directory' \
  'spectrum' \
  'utility' \
  'completion' \
  'syntax-highlighting' \
  'autosuggestions' \
  'prompt'

とする。

esc:wqと入力して終了。この時点で変更が反映されているはずですが、上手くいかない人はターミナルを再起動してみてください。

これはただの好みですが、テーマはIcebergがオススメです。こちらの記事を参考にしました。

Discord.jsで新メンバーが入った時の処理を自動化した

僕が運営しているスキーチーム用のbotをdiscord.js V14で作ったので、その概要と作成方法を共有。

前提

チームの入会フォーム(Google Form)が提出されると、discordサーバーへの招待リンクがメールで届くようになっている。discordサーバーは大まかにコーチ&現役フォルダーと現役フォルダーに分かれ、セキュリティーの観点から、○期ロールが付与されるまでの間は前者しか閲覧できないようになっている。discordのユーザーネームは本名と無関係に設定している人が多いので、僕がこんな感じでやっていた:

新入生がコーチ&現役フォルダー内にある自己紹介チャンネルに投稿 → 管理者が個人情報シート(入会フォームの回答をまとめたspreadsheet)を見て何期生かを確認 → 新入生のニックネームを本名(○期)に変更 & ○期ロールを付与 → 個人情報シートに新入生のdiscord IDを入力

5分もあれば終わる処理だけど、まぁまぁ面倒くさいので、勉強を兼ねてbotを作ってみることにした。

目標

  • 上記フローを自動化する。
  • 新メンバーと管理者のみが閲覧できる新入生ウェルカムチャンネルを作り、いわゆる ようこそ画面 の代わりとする。新たにサーバーに入った新入生は、まずこのチャンネルと自己紹介チャンネルのみ閲覧できるようになっており、新入生ウェルカムチャンネルに自分の名前を入力すると他のチャンネルも閲覧できるようになる。その後、ウェルカムチャンネルはアクセス出来なくなる。

実行環境

  • マシン:Ventura 13.4 のMac Mini
  • エディター:VS Code
  • インストールが必要なパッケージ
    • Node.js:今回はv18.15.0を使った
    • discord.js:v14を推奨
    • dotenv
    • axios
    • nodemon

コード

全体構成

今回の機能に関係するものを抜き出した。(こういうツリー構造をスムーズに書くにはどうすれば良いんでしょう。ターミナルでtreeコマンドを打ち、その出力を<pre>タグ内に貼り付けているけど、そのままだと表示がズレる。)

discord_bot
├── .env
├── .gitignore
├── events
│     ├── guildMemberAdd.js
│     ├── messageCreate.js
│     └── ready.js
├── index.js
├── newMember
│     ├── indexForNewMember.js
│     └── modifyDiscord.js
└── node_modules

全体の流れは次の通り:

サーバーに新メンバーが入る
↪︎(1) guildMemberAdd.js が実行され、NewMemberロールを付与する
↪︎新メンバーが新入生ウェルカムチャンネルに名前を送信する
↪︎(2) messageCreate.js が実行され、データのチェック, discordの各種設定(ニックネームの変更, ロールの変更など), 個人情報シートの更新を行う。
↪︎新メンバーが他のチャンネルを閲覧できるようになり、新入生ウェルカムチャンネルへのアクセスは失う。

各機能の解説

(0)根幹ファイル

nodemonで常時動かしているのはindex.js。なお、セキュリティー上公開できない情報(トークン, サーバーIDなど)は全て.envファイルに保存してある。それを別ファイルで用いたいときは、process.env.TOKENのように書く。

require('dotenv').config()
const { Client, Collection, Events, IntentsBitField } = require("discord.js")
const fs = require('node:fs')
const path = require('node:path')

//今回のbotに必要な権限を与える
const client = new Client({
    intents: [
        IntentsBitField.Flags.Guilds,
        IntentsBitField.Flags.GuildMembers,
        IntentsBitField.Flags.GuildMessages,
        IntentsBitField.Flags.MessageContent,
    ]
})

//各種イベントはeventsフォルダーに格納しているので、それを全て読み込む
const eventsPath = path.join(__dirname, 'events')
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'))

//readyイベントはbotの起動時のみ、その他のイベントには常時反応できるようにする
for (const file of eventFiles){
    const filePath = path.join(eventsPath, file)
    const event = require(filePath)
    if(event.once){
        client.once(event.name, (...args) => event.excute(...args))
    }else{
        client.on(event.name, (...args) => event.excute(...args))
    }
}

//botを起動
client.login(process.env.TOKEN)

(1)guildMemberAddイベント

サーバーに新メンバーが入ると、以下のコードが実行される。NewMemberロールのIDは書き換えてください。

guildMemberAdd.js

const {Events} = require('discord.js')

module.exports = {
   name: Events.GuildMemberAdd,
   async excute(member){
      //臨時のロールNewMemberを付与する。
      await member.roles.add('1116738049548755044')
   }
}

(2)messageCreateイベント

サーバー内のいずれかのチャンネルにメッセージが送信されると、以下のコードが実行される。そのチャンネルが新入生ウェルカムチャンネルであり、送信者がbot以外のときのみ、indexForNewMember.js ファイルに処理を引き渡す。

messageCreate.js

const {Events} = require('discord.js')

module.exports = {
    name: Events.MessageCreate,
    async excute(message){
        //新入生ウェルカムチャンネルにのみ反応する
        if(message.channelId==process.env.WELCOME_CHANNEL_ID && !message.author.bot){
            const indexForNewMember = require('../newMember/indexForNewMember.js')
            indexForNewMember(message)
        }
    }
}

indexForNewMember.js ファイルは以下の動作をまとめたもので、①の動作は同ディレクトリーの modifyDiscord.js ファイルで行っている。

  • 入力された名前が個人情報シートに記録されているかをチェック
  • ①新入生のロールとニックネームを変更する
  • 個人情報シートに新入生のdiscord IDを追記する
  • 新入生ウェルカムチャンネルの不要な投稿を削除する

indexForNewMember.js

const {Events} = require('discord.js')
const axios = require('axios')

module.exports = async (message)=>{
    const name = message.content
    const requestToPersonalInfoSheet = await axios.get(`${process.env.PERSONAL_INFO_SHEET}/search?sheet=名簿&氏名=${name}`)
    const record = requestToPersonalInfoSheet.data

    if(record.length===0){
        message.reply(`❌ ${name}さんのデータは個人情報シートに記録されていません。表記を確認して、もう一度お試しください。`)
    }else{
        const memberID = message.author.id
        const gen = record[0]['代'] //integer

        //新入生のDiscord設定を変更(ロールの変更, ニックネームの変更)。
        const modifyDiscord = require('./modifyDiscord.js')
        await modifyDiscord(message, name, gen)

        //個人情報シートを更新
        const discordInfoSheet = axios.post(`${process.env.PERSONAL_INFO_SHEET}?sheet=discord`, {名前: name, 代: gen, ID: memberID})

        //ウェルカムchの不要なメッセージを削除する
        const messagesToDelete = await message.channel.messages.fetch({after: '1116774827383066684'})
        message.channel.bulkDelete(messagesToDelete)
    }
}

modifyDiscord.js

const {Events} = require('discord.js')

module.exports = async (message, name, gen) => {
    //gen期のロールを付与する。ToDo管理botロールのランクを○期より上に設定しておく必要がある。
    const role = message.guild.roles.cache.find(role => role.name===`${gen}期`)
    if (!role) {
        console.log(`${gen}期ロールが存在しません。`); 
        return;
    }

    if(!message.member.roles.cache.has(role.id)){
        try {
            await message.member.roles.add(role)
        } catch (error) {
            console.log(`Error adding role to new member: ${error}`)
        }
    }

    //ニックネームを変更。これもToDo管理botより高いロールが既に付与されている人に対してはエラーを吐く。
    try {
        await message.member.setNickname(`${name}(${gen}期)`) 
    } catch (error) {
        console.log(`Unable to change the nickname. Error: ${error}`)
    }

    await message.reply(`${name}さんのDiscord設定が完了しました。他のチャンネルも覗いてみてください!`)

    //NewMemberロールを削除
    await message.member.roles.remove('1116738049548755044')
}

ソースコード

GitHubにアップロード済み。元々はdiscordサーバーで完結するtodo管理機能を作ろうと思っていたので、discord_todo_botというフォルダー名になっている。この記事に書かれていないGitHub上のコードは、大体todo機能関連。

クラウドサーバーで常時起動

別の記事を参照。