https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-an-sms-reminder-tool-for-teachers-using-google-classroom-dr/Blog_Google-Classroom_Node_1200x600.png

Google Classroomを使った教師用SMSリマインダーツールの構築

最終更新日 May 4, 2021

所要時間:1 分

私は数年前から教育テクノロジーに携わっているが、教師からよく聞く課題のひとつは、生徒が自分のメールアカウントをチェックしないということだ。例えば リマインドのようなビジネスがある。

このウォークスルーでは、Google Classroomで教師が生徒に今後の課題を知らせるためのSMSリマインダーアプリを作成します。ここでは Google Classroom APIを使って認証を行い、コースと課題のデータを取得します。 Vonage Messages APIを使います。

申請計画

始める前に、アプリケーションのコア機能とアーキテクチャを理解しましょう。このチュートリアルでは、3つのユーザーストーリーを扱います:

  • 教師はGoogleアカウントを使ってアプリケーションにログインできます。

  • 教師は直近の課題のリストを見ることができ、生徒に思い出させたい課題を選択することができます。

  • 教師はSMSで各生徒に次の課題を思い出させることができます。

アプリケーションと2つのAPI(Google ClassroomとVonage Messages)の間のデータの流れを見てみよう:

Planning an SMS reminder tool for teachers using Google Classroom

ここでは ノードExpressこのデモではNodeとExpressを使うが、GoogleもVonageもほとんどの主要なプログラミング言語でAPIクライアントを提供している。先に進みたい場合は Githubからコードをダウンロードしてにあるコードをダウンロードし、Readmeの "Quick Start "セクションに従ってアプリを立ち上げて実行することができる。

前提条件

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

アプリケーションの構築

ステップ1:新規Expressアプリケーションの作成

まず、新しいExpressアプリケーションを作りましょう。 新しいExpressアプリケーションを作成しよう。テンプレートと セッションストレージにを使う新しいExpressアプリケーションを作りましょう。ここでは エクスプレス・アプリケーション・ジェネレーターを使います:

npx express-generator --hbs --git classroom-reminders

このコマンドは classroom-remindersという新しいディレクトリが作成されます。そのディレクトリに移動し、Expressセッション・ストレージ・パッケージと、Expressが提供するその他のパッケージをインストールします:

npm i --save express-session && npm i

セッション・パッケージを使うには、このパッケージを app.jsファイルに追加する必要がある。コメントで示された場所に以下の行を追加する:

// Add this line to the top of your app.js file
var session = require("express-session");
...
var app = express();
...
// And this block after the app has been created
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: true,
  saveUninitialized: true,
}));
...

これまでのところ、すべてがうまくいっていることを確認したい場合は、以下を実行してください。 SESSION_SECRET=<A SECURE STRING FOR PROTECTING SESSIONS> npm startを実行し localhost:3000にアクセスする。デフォルトのExpressのウェルカムページが表示されるはずだ。

ステップ2:Google認証を追加する

新しいExpressアプリケーションができたので、GoogleのOAuthクライアントを使った認証を追加してみよう。

まだの場合は、Google APIポータルで新しいプロジェクトを作成し、そこにOAuth 2.0の認証情報を追加します。 OAuth 2.0 認証情報を追加します。.このチュートリアルでは OAuth コールバック URL を localhost:3000しかし、このアプリケーションを本番環境にデプロイする場合は、コールバック URL を変更したくなるでしょう。

Creating a Client ID in the Google Developer ConsoleCreating a Client ID in the Google Developer Console

Googleは、このチュートリアルで使用するクライアントIDとクライアントシークレットを生成します。

次に Google API npmパッケージ:

npm i --save googleapis

コードを整理しやすくするために、Google APIコード専用の新しいファイルを classroom-remindersという名前の新しいフォルダを作成します。という名前の新しいフォルダを作成し、その中に helpers/という名前の新しいフォルダを作成し、その中に google-api.js.このファイルに以下を追加する:

const google = require("googleapis").google;

const googleConfig = {
  clientId: process.env.GOOGLE_OAUTH_ID,
  clientSecret: process.env.GOOGLE_OAUTH_SECRET,
  redirect: process.env.GOOGLE_OAUTH_REDIRECT,
};

const createConnection = () => {
  return new google.auth.OAuth2(
    googleConfig.clientId,
    googleConfig.clientSecret,
    googleConfig.redirect
  );
};

const getConnectionUrl = (auth) => {
  return auth.generateAuthUrl({
    access_type: "offline",
    prompt: "consent",
    scope: [
      "https://www.googleapis.com/auth/userinfo.profile",
      "https://www.googleapis.com/auth/userinfo.email",
      "https://www.googleapis.com/auth/classroom.courses.readonly",
      "https://www.googleapis.com/auth/classroom.rosters.readonly",
      "https://www.googleapis.com/auth/classroom.coursework.students.readonly",
    ],
  });
};

/**
 * Exported functions
 */

module.exports.loginUrl = () => {
  const auth = createConnection();
  return getConnectionUrl(auth);
};

module.exports.getToken = async (code) => {
  const auth = createConnection();
  const data = await auth.getToken(code);
  return data.tokens;
};

module.exports.getCurrentUser = async (tokens) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .oauth2({
      auth,
      version: "v2",
    })
    .userinfo.get();

  return { ...res.data };
};

OAuthがどのように機能するかについての詳細は省く。 OAuthの仕組みしかし、エクスポートされた3つの関数はすべて、標準的なサーバーサイドOAuthワークフローの一部です。まず loginUrlは、ユーザーが認証する前に表示される一意のログインURLを生成します。関数 getToken関数は、Googleのサーバーが生成するワンタイムコードと、長期間のアクセストークンを交換します。この getCurrentUser関数はそのアクセストークンを使って、現在認証されているユーザの情報をGoogle APIから取得します。

もうひとつ、検討する価値があるのは scopes私たちが要求している

scope: [
  "https://www.googleapis.com/auth/userinfo.profile",
  "https://www.googleapis.com/auth/userinfo.email",
  "https://www.googleapis.com/auth/classroom.courses.readonly",
  "https://www.googleapis.com/auth/classroom.rosters.readonly",
  "https://www.googleapis.com/auth/classroom.coursework.students.readonly",
],

スコープはアプリケーションがアクセスできるデータを制限します。一般的に、アプリケーションを構築するためにはできるだけ少ないアクセスしか求めない方がよいので、ここではユーザーのプロファイル情報とGoogle Classroomの読み取りアクセスだけを求めています。

次に、Expressアプリケーションの routes/index.jsファイルを更新する。このルートは codeをチェックします。 google-apiが見つかったら、作成したヘルパーファイルを使用して、そのコードを認可トークンと交換します。そして、そのトークンを(リフレッシュトークンと有効期限とともに)セッションストレージに保存します。最後に、ユーザーを /assignmentsページにリダイレクトします:

const express = require("express");
const router = express.Router();
const googleApi = require("../helpers/google-api");

router.get("/", function (req, res, next) {
  if (req.query.code) {
    googleApi.getToken(req.query.code).then((tokens) => {
      req.session.tokens = tokens;
      req.session.save(() => {
        res.redirect("/assignments");
      });
    });
  } else {
    res.render("index", {
      loginUrl: googleApi.loginUrl(),
    });
  }
});

module.exports = router;

また views/index.hbsファイルを修正する必要がある:

<h1>Google Classroom Reminders</h1>
<p>Log in with Google to remind your students about their upcoming assignments.</p>

<p>
  <a href="{{ loginUrl }}">Login</a>
</p>

ここまでのアプリケーションをテストしたい場合は、Google OAuth ID、シークレット、リダイレクトURLを使って開始する必要がある:

GOOGLE_OAUTH_ID=<YOUR GOOGLE OAUTH ID> \
GOOGLE_OAUTH_SECRET=<YOUR GOOGLE OAUTH SECRET> \
GOOGLE_OAUTH_REDIRECT=http://localhost:3000/ \
SESSION_SECRET=<A SECURE STRING FOR PROTECTING SESSIONS>  \
npm start

今回は localhost:3000に移動すると、ログインリンクが表示されます:

Login screen for Google Classroom Reminders applicationLogin screen for Google Classroom Reminders application

ログインをクリックすると、グーグルが新しいアプリの承認プロセスを案内してくれる:

Permissions approval for Google Classroom Reminders applicationPermissions approval for Google Classroom Reminders application

あなたが私たちのアプリケーションを承認した後、あなたは次の場所に移動します。 localhost:3000/assignmentsに移動しますが、そのURLはまだ存在しません。次のセクションで作成します。

ステップ3: Google Classroomから先生の課題とコースを表示する

アプリケーションのログインプロセスが構築できたので、Google ClassroomのAPIからコースと課題を取得するためにユーザのアクセストークンを使用する必要があります。

まず、2つの新しい関数を helpers/google-api.jsファイルに追加する必要がある:

...
module.exports.getCourses = async (tokens) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.list({ teacherId: "me", courseStates: "ACTIVE" });

  return res.data.courses ? [...res.data.courses] : [];
};

module.exports.getCourseWorks = async (tokens, courseId) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.courseWork.list({ courseId: courseId, orderBy: "dueDate desc" });

  return res.data.courseWork ? [...res.data.courseWork] : [];
};

これらにより、以下のリストをリクエストすることができます。 コースそして コースワーク(課題を表す Google Classroom の名前) の一覧を Google Classroom API にリクエストできるようになります。次に routes/assignments.js:

const express = require("express");
const router = express.Router();
const googleApi = require("../helpers/google-api");

router.get("/", function (req, res, next) {
  if (!req.session.tokens) {
    res.redirect("/");
  }

  googleApi.getCourses(req.session.tokens).then(async (courses) => {
    Promise.all(
      courses.map(async (course) => {
        course.assignments = await googleApi.getCourseWorks(
          req.session.tokens,
          course.id
        );
        return course;
      })
    ).then((courses) => {
      res.render("assignments", { courses });
    });
  });
});

module.exports = router;

これはユーザのすべてのコースをループし、それぞれのコースの最新の課題を取得します。また、このルートを app.jsファイルに追加する必要があります:

var assignmentsRouter = require('./routes/assignments');
...
app.use('/', indexRouter);
app.use('/assignments', assignmentsRouter);
...

最後に、新しいビューファイル(views/assignments.hbs)を作成します:

<h1>Google Classroom Reminders</h1>
<p>Select an Assignment to remind your students about.</p>

{{#each courses}}
<h2>{{ this.name }}</h2>
  {{#if assignments}}
  <ul>
      {{#each assignments}}
        <li>
          <a href="/assignments/{{ ../id }}:{{ this.id }}">{{ this.title }}</a><br/>
          Due on {{ this.dueDate.month }}/{{ this.dueDate.day }}/{{ this.dueDate.year }}
        </li>
      {{/each}}
  </ul>
  {{else}}
  <p>No assignments found</p>
  {{/if}}
{{/each}}

前のステップと同じようにアプリケーションを起動し、再度ログインすると、Google Classroomのコースと課題のリストが表示されます:

Viewing Google Classroom courses and assignmentsViewing Google Classroom courses and assignments

この時点で、ユーザーはGoogleアカウントを使ってアプリケーションにログインし、Google Classroomコースの最新の課題リストを見ることができます。次に、ユーザがドリルダウンして各コースの学生と特定の課題を提出したかどうかを見ることができます。

ステップ4: 特定のGoogle Classroomの課題に対する先生の名簿と生徒の作品を表示する

コースに登録されている学生のリストを表示し、特定の課題を提出したかどうかを確認するには、Google Classroom APIのいくつかの新しいエンドポイントにアクセスする必要があります。

これらの新しい関数を google-api.jsヘルパー・ファイルに追加しましょう:

...
module.exports.getCourse = async (tokens, courseId) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.get({ id: courseId });

  return { ...res.data };
};

module.exports.getCourseRoster = async (tokens, courseId) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.students.list({ courseId: courseId });

  return res.data.students ? [...res.data.students] : [];
};

module.exports.getCourseWork = async (tokens, courseId, assignmentId) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.courseWork.get({ courseId: courseId, id: assignmentId });

  return { ...res.data };
};

module.exports.getStudentSubmissions = async (
  tokens,
  courseId,
  assignmentId
) => {
  const auth = createConnection();
  auth.setCredentials(tokens);

  const res = await google
    .classroom({ version: "v1", auth })
    .courses.courseWork.studentSubmissions.list({
      courseId: courseId,
      courseWorkId: assignmentId,
    });

  return res.data.studentSubmissions ? [...res.data.studentSubmissions] : [];
};

次に、新しいルートを routes/assignments.jsファイルに新しいルートを作ってみよう:

  • 単一コース

  • そのコースの学生名簿

  • 単一のコースワーク・オブジェクト

  • そのコースワークに対する学生の提出物

興味深いことに、Google Classroom APIはコースIDとコースワークIDの両方がない場合、単一のコースワークを取得することができません。両方のIDを1つのルートパラメータとして渡すために、前のステップでそれらを :で連結します。したがって、次の行は views/assignments.hbsファイルの

...
<a href="/assignments/{{ ../id }}:{{ this.id }}">{{ this.title }}</a>
...

ここで、新しいルートでこれら2つのIDを解析し、それらをファイル内で作成した適切な関数に渡す必要があります。 google-api.jsファイルに作成した適切な関数に渡す必要があります。次の行を routes/assignments.jsファイルに次の行を追加します:

...
router.get("/:id", function (req, res, next) {
  if (!req.session.tokens) {
    res.redirect("/");
  }

  const ids = req.params.id.split(":");
  const courseId = ids[0];
  const assignmentId = ids[1];

  Promise.all([
    googleApi.getCourse(req.session.tokens, courseId),
    googleApi.getCourseRoster(req.session.tokens, courseId),
    googleApi.getCourseWork(req.session.tokens, courseId, assignmentId),
    googleApi.getStudentSubmissions(req.session.tokens, courseId, assignmentId),
  ]).then(([course, students, courseWork, submissions]) => {
    // Match submissions to students
    if (
      students &&
      students.length > 0 &&
      submissions &&
      submissions.length > 0
    ) {
      students.map((student) => {
        student.submission = submissions.find(
          (submission) => submission.userId === student.userId
        );
        if (student.submission && student.submission.state === "TURNED_IN") {
          student.turnedIn = true;
        }
        return student;
      });
    }

    res.render("assignment", {
      course,
      students,
      courseWork,
      submissions,
    });
  });
});
...

最後に、特定の課題に対するすべての学生と提出状況を見るために、新しいビューが必要です。ファイルを views/assignment.hbsにファイルを作成し、以下を追加してください:

<h1>Google Classroom Reminders</h1>
<p>
  Send your students reminders about <a href="{{ courseWork.alternateLink }}">{{ courseWork.title }}</a>
  in <a href="{{ course.alternateLink }}">{{ course.name }}</a>.
</p>

{{#if students}}
<div>
  {{#each students}}
  <p>
    {{#if this.turnedIn}}
      <a href="{{ this.alternateLink }}" title="Assignment turned in">✅</a>
    {{else}}
      <span title="Assignment not turned in">❗️</span>
    {{/if}}
    <strong>{{ this.profile.name.fullName }}</strong>
  </p>
  {{/each}}
</div>
{{else}}
<p>No students found</p>
{{/if}}
<p><a href="/assignments">↩️ Back to all assignments</a></p>

今、アプリケーションを起動して再度ログインすると、特定の課題をドリルダウンし、各生徒の提出状況(または❗️)を見ることができます。

最後のステップでは、Vonage Messages APIを使って、ユーザが生徒にSMSメッセージを送れるようにします。

ステップ5: Vonage Messages APIを使用したテキストメッセージリマインダーの追加

Vonage Messages API はいくつかのチャネルでメッセージを送受信できるが、このアプリケーションでは SMS テキスト・メッセージだけを使う。

すでに Vonage APIアプリケーションを作成済みであると仮定すると次のステップはJavaScriptクライアントをインストールすることです。このクライアントに加えて google-libphonenumberパッケージを追加します:

npm i --save nexmo@beta google-libphonenumber

次に、電話番号をフォーマットしてNexmoライブラリ経由でSMSメッセージを送信するコード用に、別のヘルパーファイルを作成しましょう。新しいファイルを helpers/nexmo-api.js:

const Nexmo = require("nexmo");
const PNF = require("google-libphonenumber").PhoneNumberFormat;
const phoneUtil = require("google-libphonenumber").PhoneNumberUtil.getInstance();

const nexmo = new Nexmo({
  apiKey: process.env.NEXMO_API_KEY,
  apiSecret: process.env.NEXMO_API_SECRET,
  applicationId: process.env.NEXMO_APP_ID,
  privateKey: process.env.NEXMO_PRIVATE_KEY_PATH,
});

module.exports.sendSms = (telephone, message, callback) => {
  const formattedPhoneNumber = phoneUtil.format(
    phoneUtil.parseAndKeepRawInput(telephone, "US"),
    PNF.E164
  );
  nexmo.channel.send(
    { type: "sms", number: formattedPhoneNumber },
    { type: "sms", number: process.env.NEXMO_PHONE_NUMBER },
    {
      content: {
        type: "text",
        text: message,
      },
    },
    callback,
    { useBasicAuth: true }
  );
};

ユーザーからの入力を処理し、先ほど作成した sendSms関数を呼び出すために、新しいルートファイルを routes/messages.js:

const express = require("express");
const router = express.Router();
const nexmoApi = require("../helpers/nexmo-api");

router.post("/", function (req, res, next) {
  if (!req.session.tokens) {
    res.redirect("/");
  }
  const { telephones, messages } = req.body;
  Promise.all(
    telephones.map((telephone, key) => {
      if (telephone) {
        return nexmoApi.sendSms(telephone, messages[key]);
      }
    })
  ).then((results) => {
    res.redirect("/assignments");
  });
});

module.exports = router;

このファイルは、電話番号とメッセージの配列を繰り返し処理し、電話番号を持つものごとに sendSmsメソッドを呼び出します。また app.jsファイルも更新する必要があります:

var messagesRouter = require('./routes/messages');
...
app.use('/', indexRouter);
app.use('/assignments', assignmentsRouter);
app.use('/messages', messagesRouter);
...

GoogleのAPIでは生徒の電話番号にアクセスすることができないため、教師が生徒一人一人にメッセージとともにこれらの番号を入力する必要があります。では views/assignment.hbsファイルを編集して、各生徒にこれら2つのフォームフィールドと送信ボタンを追加しましょう:

<h1>Google Classroom Reminders</h1>
<p>
  Send your students reminders about <a href="{{ courseWork.alternateLink }}">{{ courseWork.title }}</a>
  in <a href="{{ course.alternateLink }}">{{ course.name }}</a>.
</p>

{{#if students}}
<form action="/messages" method="post">
  {{#each students}}
  <p>
    {{#if this.turnedIn}}
      <a href="{{ this.alternateLink }}" title="Assignment turned in">✅</a>
    {{else}}
      <span title="Assignment not turned in">❗️</span>
    {{/if}}
    <strong>{{ this.profile.name.fullName }}</strong>
  </p>
  <div>
    <label for="message-{{ this.userId }}" style="display: block;">Reminder message</label>
    <textarea id="message-{{ this.userId }}" name="messages" maxlength="140" minlength="3" rows="5" cols="30">Hey {{ this.profile.name.givenName }}, don't forget about your assignment for {{ ../course.name }}. It's due on {{ ../courseWork.dueDate.month }}/{{ ../courseWork.dueDate.day }}/{{ ../courseWork.dueDate.year }}</textarea>
  </div>
  <div>
    <label for="telephone-{{ this.userId }}" style="display: block;">Telephone</label>
    <input type="tel" id="telephone-{{ this.userId }}" autocomplete="off" name="telephones" />
  </div>
  {{/each}}
  <div style="margin-top: 10px;">
    <input type="submit" value="Send Reminders">
  </div>
</form>
{{else}}
<p>No students found</p>
{{/if}}
<p><a href="/assignments">↩️ Back to all assignments</a></p>

アプリケーションは基本的に完成しているが、使用する前にVonageの認証情報をすべて整理する必要がある。まず、Vonage API の秘密鍵をダウンロードして、新しいファイル .private_key.さて、GoogleとVonage APIの環境変数をすべて設定してアプリケーションを起動します:

GOOGLE_OAUTH_ID=<YOUR GOOGLE OAUTH ID> \
GOOGLE_OAUTH_SECRET=<YOUR GOOGLE OAUTH SECRET> \
GOOGLE_OAUTH_REDIRECT=http://localhost:3000/ \
SESSION_SECRET=<A SECURE STRING FOR PROTECTING SESSIONS>  \
NEXMO_API_KEY=<YOUR VONAGE MESSAGES API KEY> \
NEXMO_API_SECRET=<YOUR VONAGE MESSAGES API SECRET> \
NEXMO_PHONE_NUMBER=<YOUR VONAGE MESSAGES PHONE NUMBER> \
NEXMO_APP_ID=<YOUR VONAGE MESSAGES APP ID> \
NEXMO_PRIVATE_KEY_PATH=./.private_key \
npm start

今回、ログインして1つの課題を表示する際、各生徒の電話番号を入力し、送信するメッセージをカスタマイズすることができます。準備ができたら、"リマインダーを送信 "をクリックして全体をテストしてください。

Sending SMS Reminders using Google Classroom and the Vonage Messages APISending SMS Reminders using Google Classroom and the Vonage Messages API

次のステップ

このデモアプリケーションは比較的単純なユースケースをカバーしていますが、Google ClassroomのようなLMSと組み合わせることで、Vonage Messages APIがいかにパワフルなものになるかは一目瞭然です。このようなアプリケーションでユーザーエクスペリエンスを向上させる方法はいくつかあります:

  • 電話番号をデータベースに保存し、教師が毎回入力しなくても済むようにする。

  • デフォルトのリマインダーメッセージを変更できるようにする

  • Facebook、What's App、Viberなど他のメッセージング・チャンネルのサポートを追加する。

  • Google Classroomのデータをキャッシュしてパフォーマンスを向上させ、レート制限を回避します。

  • ユーザーフレンドリーなエラーメッセージと成功メッセージ

  • カスタムデザインとスタイリング

VonageとGoogle Classroomを使ったリマインダーシステムの導入について、ご質問やアイデアがありましたら、下記までご連絡ください。 ツイッターまたは Vonage開発者コミュニティSlack!

シェア:

https://a.storyblok.com/f/270183/384x384/627f04bbac/karl-hughes.png
Karl Hughes

Karl is a writer, speaker, and technology team lead. He’s currently the Chief Technology Officer at The Graide Network and runs CFP Land in his spare time.