このチュートリアルのコードはGitHubで利用可能です。また、Glitchではアプリのライブデモをご覧いただけます。

 

新しいTwitter APIの最初のエンドポイントが公開されました。詳細を確認して、公開されている会話でどのようなやり取りがなされているのかを把握できるアプリを構築しましょう。

 

アプリを構築したら、次のようなすぐに使いたくなる実用的な例に適用する方法をご確認ください。

 

  • 新しいミュージックビデオを探す:Twitterでシェアされているミュージックビデオを管理画面に表示します

  • リモート開発者の求人を探す:Twitterでシェアされているリモート開発者の求人情報がリスト化されるリモート開発者求人情報アプリを作成します

  • 個人向けの金融や貯蓄についての情報を得る:Twitterでやり取りされている個人向けの金融や貯蓄に関する公開の会話を取得します

 

設定

 

開始するために必要なこと

 

  • 承認済み開発者アカウントを用意し、新しい開発者ポータルエクスペリエンスをアクティブにしておく必要があります。アクセスには、開発者ポータルで作成されたプロジェクトにアタッチされた開発者アプリのアクティブキーとトークンを使用します。
  • Node.js
  • Npm(これはNodeとともに自動的にインストールされます。npm 5.2以上であることを確認してください)。
  • Npx(npm 5.2以上に含まれます)

 

まず、Node.jsをインストールします。NodeのウェブサイトのDownloads (ダウンロード)セクションを確認して、必要なソースコードやインストーラーをダウンロードします。Macで実行している場合は、Brewパッケージマネージャーを使用してNodeパッケージをインストールできます


ターミナルウィンドウを開き、npxを使用してcreate-react-appを使用するReactアプリをブートします。

 

      npx create-react-app real-time-tweet-streamer
    

 

create-react-appの実行が終了したら、新しく作成されたreal-time-tweet-streamerディレクトリに移動し、package.json内のscriptsブロックを以下のscriptsブロックに置き換えます。これらの行は、必要に応じて開発中または本番環境でクライアントとサーバーのバックエンドコードを同時に実行するためのコマンドショートカットを提供します。

 

      cd real-time-tweet-streamer
    

 

 

      "scripts": {
  "start": "npm run development",
  "development": "NODE_ENV=development concurrently --kill-others \"npm run client\" \"npm run server\"",
  "production": "npm run build && NODE_ENV=production npm run server",
  "client": "react-scripts start",
  "server": "node server/server.js",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

    

 

scriptsセクションを更新すると、package.jsonは以下のようになります。

 

package.json

      {
  "name": "real-time-tweet-streamer",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "start": "npm run development",
    "development": "NODE_ENV=development concurrently --kill-others \"npm run client\" \"npm run server\"",
    "production": "npm run build && NODE_ENV=production npm run server",
    "client": "react-scripts start",
    "server": "node server/server.js",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

    

 

次に、src/サブディレクトリ内のすべてのファイルを削除します。

 

      rm src/*
    

 

その後、src/サブディレクトリ内にindex.jsという新しいファイルを作成します。このファイルのコードは以下のようになります。

 

      import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

ReactDOM.render(<App />, document.querySelector("#root"));

    

 

認証情報

 

フィルタリングされたストリームエンドポイントに接続するには、Twitter開発者ポータルのアプリのベアラートークンを使用して認証する必要があります。ベアラートークンを使用するには、以下の環境変数を設定する必要があります。シェルにbashを使用している場合は、ターミナルウィンドウで以下のコマンドを発行して設定できます。左右の角括弧を含む<YOUR BEARER TOKEN HERE>を、使用するベアラートークンに置き換えます。

 

      export TWITTER_BEARER_TOKEN=<YOUR BEARER TOKEN HERE>

    

 

サーバーサイドコード

 

まず、Twitter APIに実際にリクエストするNodeサーバーを実装する必要があります。このNodeサーバーは、ブラウザーベースのReactクライアントとTwitter APIの間のプロキシとして機能します。Nodeサーバーでは、フィルタリングされたストリームエンドポイントに接続するAPIエンドポイントを作成する必要があります。そうすると、Reactクライアントからのリクエストは、ローカルのNodeサーバーにプロキシされます。 

先に進む前に、プロジェクトのルートディレクトリにcdで移動し、以下の依存関係をインストールします

 

      npm install concurrently express body-parser util request http socket.io path http-proxy-middleware request react-router-dom axios socket.io-client react-twitter-embed
    

 

次に、プロジェクトのルートディレクトリ内に「server」という新しいサブディレクトリを作成し、そのサブディレクトリ内に「server.js」と呼ばれる新しいファイルを作成します。 

 

      mkdir server
touch server/server.js

    

 

このソースコードファイルには、フィルタリングされたストリームエンドポイントに接続してツイートを受信するためのバックエンドロジックがすべて含まれます。server.jsファイルの内容は以下のようになります。

 

server.js

 

      const express = require("express");
const bodyParser = require("body-parser");
const util = require("util");
const request = require("request");
const path = require("path");
const socketIo = require("socket.io");
const http = require("http");

const app = express();
let port = process.env.PORT || 3000;
const post = util.promisify(request.post);
const get = util.promisify(request.get);

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const server = http.createServer(app);
const io = socketIo(server);

const BEARER_TOKEN = process.env.TWITTER_BEARER_TOKEN;

let timeout = 0;

const streamURL = new URL(
  "https://api.x.com/2/tweets/search/stream?tweet.fields=context_annotations&expansions=author_id"
);

const rulesURL = new URL(
  "https://api.x.com/2/tweets/search/stream/rules"
);

const errorMessage = {
  title: "Please Wait",
  detail: "Waiting for new Tweets to be posted...",
};

const authMessage = {
  title: "Could not authenticate",
  details: [
    `Please make sure your bearer token is correct. 
      If using Glitch, remix this app and add it to the .env file`,
  ],
  type: "https://developer.twitter.com/en/docs/authentication",
};

const sleep = async (delay) => {
  return new Promise((resolve) => setTimeout(() => resolve(true), delay));
};

app.get("/api/rules", async (req, res) => {
  if (!BEARER_TOKEN) {
    res.status(400).send(authMessage);
  }

  const token = BEARER_TOKEN;
  const requestConfig = {
    url: rulesURL,
    auth: {
      bearer: token,
    },
    json: true,
  };

  try {
    const response = await get(requestConfig);

    if (response.statusCode !== 200) {
      if (response.statusCode === 403) {
        res.status(403).send(response.body);
      } else {
        throw new Error(response.body.error.message);
      }
    }

    res.send(response);
  } catch (e) {
    res.send(e);
  }
});

app.post("/api/rules", async (req, res) => {
  if (!BEARER_TOKEN) {
    res.status(400).send(authMessage);
  }

  const token = BEARER_TOKEN;
  const requestConfig = {
    url: rulesURL,
    auth: {
      bearer: token,
    },
    json: req.body,
  };

  try {
    const response = await post(requestConfig);

    if (response.statusCode === 200 || response.statusCode === 201) {
      res.send(response);
    } else {
      throw new Error(response);
    }
  } catch (e) {
    res.send(e);
  }
});

const streamTweets = (socket, token) => {
  let stream;

  const config = {
    url: streamURL,
    auth: {
      bearer: token,
    },
    timeout: 31000,
  };

  try {
    const stream = request.get(config);

    stream
      .on("data", (data) => {
        try {
          const json = JSON.parse(data);
          if (json.connection_issue) {
            socket.emit("error", json);
            reconnect(stream, socket, token);
          } else {
            if (json.data) {
              socket.emit("tweet", json);
            } else {
              socket.emit("authError", json);
            }
          }
        } catch (e) {
          socket.emit("heartbeat");
        }
      })
      .on("error", (error) => {
        // Connection timed out
        socket.emit("error", errorMessage);
        reconnect(stream, socket, token);
      });
  } catch (e) {
    socket.emit("authError", authMessage);
  }
};

const reconnect = async (stream, socket, token) => {
  timeout++;
  stream.abort();
  await sleep(2 ** timeout * 1000);
  streamTweets(socket, token);
};

io.on("connection", async (socket) => {
  try {
    const token = BEARER_TOKEN;
    io.emit("connect", "Client connected");
    const stream = streamTweets(io, token);
  } catch (e) {
    io.emit("authError", authMessage);
  }
});

console.log("NODE_ENV is", process.env.NODE_ENV);

if (process.env.NODE_ENV === "production") {
  app.use(express.static(path.join(__dirname, "../build")));
  app.get("*", (request, res) => {
    res.sendFile(path.join(__dirname, "../build", "index.html"));
  });
} else {
  port = 3001;
}

server.listen(port, () => console.log(`Listening on port ${port}`));
    

 

ルールを使用した、フィルタリングされたストリームエンドポイントのツイートのフィルタリング

 

フィルタリングされたストリームには、データを受信するストリーミングエンドポイントと、ルールの作成や削除に使用されるルールエンドポイントの2つのエンドポイントがあります。フィルタリングされたストリームエンドポイントには、ルールと呼ばれる検索クエリを定義し、自分に送信されるツイートの種類を設定する必要があります。ルールでは、一連の演算子を使用することによって、対象となるツイートのみを絞り込むことができます。アプリの構築後に使用できるいくつかのユースケースとそれに対応するルールも用意されています。

 

他のフィルタリングされたストリームエンドポイントは、単純なGET接続を使用するストリームエンドポイントです。接続が確立されると、永続的なHTTPストリーミング接続を介してJSON形式でツイートが配信されます。ストリームに接続している間、ルールにマッチするツイートのみを受信します。 

 

クライアントサイドコード

 

次のステップでは、以下のReactコンポーネントを設定します

 

App.js - 他のすべてのコンポーネントを順番にレンダリングする親コンポーネントです

NavBar.js - ツイートフィードと管理ルール間を移動するためのナビゲーションバーを表示します

Tweet.js - ページにTweetを表示します 

TweetFeed.js - 複数のTweetコンポーネントを「フィード」のような方法で一度にレンダリングします

Rule.js - ストリームに個別のルールをレンダリングします

RuleList.js - 複数のRuleコンポーネントをレンダリングし、ルールを追加するための入力フィールドを表示します

ErrorMessage.js - ステータスやエラーメッセージを画面に表示します

Spinner.js - 保留中のAPIコールの読み込みインジケーターを表示します


すべてのコンポーネントのスタイルを設定するには、Semantic UIを使用します。以下の行を~/real-time-tweet-streamer/publicディレクトリにあるindex.htmlファイルのheadタグに追加して、プロジェクトのSemantic UIにCSSを含めます。

 

      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
    

 

次に、Reactコンポーネントの作成を始めます。/srcサブディレクトリに「components」というディレクトリを作成します。上のソースコードファイルは、この新しいディレクトリに格納されます。まず、アプリケーションの親となる最上位のコンポーネントを作成します。このコンポーネントは、その他すべてのコンポーネントのレンダリングを担当します。

 

App.js

 

 

      import React from "react";
import { BrowserRouter, Route } from "react-router-dom";

import Navbar from "./Navbar";
import TweetFeed from "./TweetFeed";
import RuleList from "./RuleList";

class App extends React.Component {
  render() {
    return (
      <div className="ui container">
        <div className="introduction"></div>

        <h1 className="ui header">
          <div className="content">
            Real Time Tweet Streamer
            <div className="sub header">Powered by Twitter data</div>
          </div>
        </h1>

        <div className="ui container">
          <BrowserRouter>
            <Navbar />
            <Route exact path="/" component={RuleList} />
            <Route exact path="/rules" component={RuleList} />
            <Route exact path="/tweets" component={TweetFeed} />
          </BrowserRouter>
        </div>
      </div>
    );
  }
}

export default App;

    

 

次に、ナビゲーションバーのコンポーネントを作成します

 

Navbar.js

 

 

      import React from "react";
import { NavLink } from "react-router-dom";

const Navbar = () => {
  return (
    <div className="ui two item menu">
      <NavLink to="/tweets" className="item" target="_blank">
        New Tweets
      </NavLink>
      <NavLink to="/rules" className="item" target="_blank">
        Manage Rules
      </NavLink>
    </div>
  );
};

export default Navbar;
    

 

次に、すべての求人情報をレンダリングするための親コンポーネントを作成します。

 

TweetFeed.js

      import React, { useEffect, useReducer } from "react";
import Tweet from "./Tweet";
import socketIOClient from "socket.io-client";
import ErrorMessage from "./ErrorMessage";
import Spinner from "./Spinner";

const reducer = (state, action) => {
  switch (action.type) {
    case "add_tweet":
      return {
        ...state,
        tweets: [action.payload, ...state.tweets],
        error: null,
        isWaiting: false,
        errors: [],
      };
    case "show_error":
      return { ...state, error: action.payload, isWaiting: false };
    case "add_errors":
      return { ...state, errors: action.payload, isWaiting: false };
    case "update_waiting":
      return { ...state, error: null, isWaiting: true };
    default:
      return state;
  }
};

const TweetFeed = () => {
  const initialState = {
    tweets: [],
    error: {},
    isWaiting: true,
  };

  const [state, dispatch] = useReducer(reducer, initialState);
  const { tweets, error, isWaiting } = state;

  const streamTweets = () => {
    let socket;

    if (process.env.NODE_ENV === "development") {
      socket = socketIOClient("http://localhost:3001/");
    } else {
      socket = socketIOClient("/");
    }

    socket.on("connect", () => {});
    socket.on("tweet", (json) => {
      if (json.data) {
        dispatch({ type: "add_tweet", payload: json });
      }
    });
    socket.on("heartbeat", (data) => {
      dispatch({ type: "update_waiting" });
    });
    socket.on("error", (data) => {
      dispatch({ type: "show_error", payload: data });
    });
    socket.on("authError", (data) => {
      console.log("data =>", data);
      dispatch({ type: "add_errors", payload: [data] });
    });
  };

  const reconnectMessage = () => {
    const message = {
      title: "Reconnecting",
      detail: "Please wait while we reconnect to the stream.",
    };

    if (error && error.detail) {
      return (
        <div>
          <ErrorMessage key={error.title} error={error} styleType="warning" />
          <ErrorMessage
            key={message.title}
            error={message}
            styleType="success"
          />
          <Spinner />
        </div>
      );
    }
  };

  const errorMessage = () => {
    const { errors } = state;

    if (errors && errors.length > 0) {
      return errors.map((error) => (
        <ErrorMessage key={error.title} error={error} styleType="negative" />
      ));
    }
  };

  const waitingMessage = () => {
    const message = {
      title: "Still working",
      detail: "Waiting for new Tweets to be posted",
    };

    if (isWaiting) {
      return (
        <React.Fragment>
          <div>
            <ErrorMessage
              key={message.title}
              error={message}
              styleType="success"
            />
          </div>
          <Spinner />
        </React.Fragment>
      );
    }
  };

  useEffect(() => {
    streamTweets();
  }, []);

  const showTweets = () => {
    if (tweets.length > 0) {
      return (
        <React.Fragment>
          {tweets.map((tweet) => (
            <Tweet key={tweet.data.id} json={tweet} />
          ))}
        </React.Fragment>
      );
    }
  };

  return (
    <div>
      {reconnectMessage()}
      {errorMessage()}
      {waitingMessage()}
      {showTweets()}
    </div>
  );
};

export default TweetFeed;

    

 

次に、個別のツイートをレンダリングする、1つ前のコンポーネントの子コンポーネントを作成します。 

 

Tweet.js

 

 

      import React from "react";
import { TwitterTweetEmbed } from "react-twitter-embed";

const Tweet = ({ json }) => {
  const { id } = json.data;

  const options = {
    cards: "hidden",
    align: "center",
    width: "550",
    conversation: "none",
  };

  return <TwitterTweetEmbed options={options} tweetId={id} />;
};

export default Tweet;

    

 

次に、ストリームのすべてのルールをレンダリングし、新しいルールを作成するための入力コントロールを表示するコンポーネントを作成します。今回は、使用するルールは1つだけです。

 

RuleList.js

 

 

      import React, { useEffect, useReducer } from "react";
import axios from "axios";
import Rule from "./Rule";
import ErrorMessage from "./ErrorMessage";
import Spinner from "./Spinner";

const reducer = (state, action) => {
  switch (action.type) {
    case "show_rules":
      return { ...state, rules: action.payload, newRule: "" };
    case "add_rule":
      return {
        ...state,
        rules: [...state.rules, ...action.payload],
        newRule: "",
        errors: [],
      };
    case "add_errors":
      return { ...state, rules: state.rules, errors: action.payload };
    case "delete_rule":
      return {
        ...state,
        rules: [...state.rules.filter((rule) => rule.id !== action.payload)],
      };
    case "rule_changed":
      return { ...state, newRule: action.payload };
    case "change_loading_status":
      return { ...state, isLoading: action.payload };
    default:
      return state;
  }
};

const RuleList = () => {
  const initialState = { rules: [], newRule: "", isLoading: false, errors: [] };
  const [state, dispatch] = useReducer(reducer, initialState);
  const exampleRule = "from:twitterdev has:links";
  const ruleMeaning = `This example rule will match Tweets posted by 
     TwtterDev containing links`;
  const operatorsURL =
    "/content/developer-twitter/en/docs/labs/filtered-stream/operators";
  const rulesURL = "/api/rules";

  const createRule = async (e) => {
    e.preventDefault();
    const payload = { add: [{ value: state.newRule }] };

    dispatch({ type: "change_loading_status", payload: true });
    try {
      const response = await axios.post(rulesURL, payload);
      if (response.data.body.errors)
        dispatch({ type: "add_errors", payload: response.data.body.errors });
      else {
        dispatch({ type: "add_rule", payload: response.data.body.data });
      }
      dispatch({ type: "change_loading_status", payload: false });
    } catch (e) {
      dispatch({
        type: "add_errors",
        payload: [{ detail: e.message }],
      });
      dispatch({ type: "change_loading_status", payload: false });
    }
  };

  const deleteRule = async (id) => {
    const payload = { delete: { ids: [id] } };
    dispatch({ type: "change_loading_status", payload: true });
    await axios.post(rulesURL, payload);
    dispatch({ type: "delete_rule", payload: id });
    dispatch({ type: "change_loading_status", payload: false });
  };

  const errors = () => {
    const { errors } = state;

    if (errors && errors.length > 0) {
      return errors.map((error) => (
        <ErrorMessage key={error.title} error={error} styleType="negative" />
      ));
    }
  };

  const rules = () => {
    const { isLoading, rules } = state;

    const message = {
      title: "No rules present",
      details: [
        `There are currently no rules on this stream. Start by adding the rule 
        below.`,
        exampleRule,
        ruleMeaning,
      ],
      type: operatorsURL,
    };

    if (!isLoading) {
      if (rules && rules.length > 0) {
        return rules.map((rule) => (
          <Rule
            key={rule.id}
            data={rule}
            onRuleDelete={(id) => deleteRule(id)}
          />
        ));
      } else {
        return (
          <ErrorMessage
            key={message.title}
            error={message}
            styleType="warning"
          />
        );
      }
    } else {
      return <Spinner />;
    }
  };

  useEffect(() => {
    (async () => {
      dispatch({ type: "change_loading_status", payload: true });

      try {
        const response = await axios.get(rulesURL);
        const { data: payload = [] } = response.data.body;
        dispatch({
          type: "show_rules",
          payload,
        });
      } catch (e) {
        dispatch({ type: "add_errors", payload: [e.response.data] });
      }

      dispatch({ type: "change_loading_status", payload: false });
    })();
  }, []);

  return (
    <div>
      <form onSubmit={(e) => createRule(e)}>
        <div className="ui fluid action input">
          <input
            type="text"
            autoFocus={true}
            value={state.newRule}
            onChange={(e) =>
              dispatch({ type: "rule_changed", payload: e.target.value })
            }
          />
          <button type="submit" className="ui primary button">
            Add Rule
          </button>
        </div>
        {errors()}
        {rules()}
      </form>
    </div>
  );
};

export default RuleList;

    

 

次に、1つのルールの表示とルールの削除を行うRuleList.jsの子コンポーネントを作成します。

 

Rule.js

 

 

      import React from "react";

export const Rule = ({ data, onRuleDelete }) => {
  return (
    <div className="ui segment">
      <p>{data.value}</p>
      <div className="ui label">tag: {data.tag}</div>
      <button
        className="ui right floated negative button"
        onClick={() => onRuleDelete(data.id)}
      >
        Delete
      </button>
    </div>
  );
};

export default Rule;

    

 

次に、ステータスやエラーメッセージを表示するためのコンポーネントを作成します。

 

ErrorMessage.js

 

 

      import React from "react";

const ErrorMessage = ({ error, styleType }) => {
  const errorDetails = () => {
    if (error.details) {
      return error.details.map(detail => <p key={detail}>{detail}</p>);
    } else if (error.detail) {
      return <p key={error.detail}>{error.detail}</p>;
    }
  };

  const errorType = () => {
    if (error.type) {
      return (
        <em>
          See
          <a href={error.type} target="_blank" rel="noopener noreferrer">
            {" "}
            Twitter documentation{" "}
          </a>
          for further details.
        </em>
      );
    }
  };

  return (
    <div className={`ui message ${styleType}`}>
      <div className="header">{error.title}</div>
      {errorDetails()}
      {errorType()}
    </div>
  );
};

export default ErrorMessage;

    

 

最後に、API呼び出しが保留中になっているときに読み込みインジケーターを表示するコンポーネントを作成します。

 

Spinner.js

 

 

      import React from "react";

const Spinner = () => {
  return (
    <div>
      <div className="ui active centered large inline loader"></div>
    </div>
  );
};

export default Spinner;

    

 

プロキシ設定

 

最後のステップでは、クライアントからバックエンドサーバーへのリクエストをプロキシします。これを行うには、src/ディレクトリ内に「setupProxy.js」という新しいファイルを作成し、以下のコードを追加します。

 

setupProxy.js

 

      const { createProxyMiddleware } = require("http-proxy-middleware");

// This proxy redirects requests to /api endpoints to
// the Express server running on port 3001.
module.exports = function (app) {
  app.use(
    ["/api"],
    createProxyMiddleware({
      target: "http://localhost:3001",
    })
  );
};

    

 

これで、プロジェクトのルートディレクトリに移動して、以下のように入力することで、サーバーとクライアントの両方を起動することができるようになりました。

 

 

      npm start
    

 

このコマンドが完了すると、デフォルトのウェブブラウザーが自動的に起動し、http://localhost:3000に移動します。その後、アプリのルール管理画面に移動します。

 

これで、希望する種類のツイートをリッスンするためのアプリができました。次に、このアプリの実際の使用例を見ていきます

 

  • 新しいミュージックビデオを探す

  • リモート開発者の求人を探す

  • 個人向けの金融や貯蓄についての情報を得る

 

以下に記載されているそれぞれ例とそれに付随するルールでは、アプリのルールセクションに移動し、ルールをコピーして入力フィールドに貼り付けるだけで、ストリームに追加できます。ルールが追加されると、そのルールは数秒以内に有効になり、条件にマッチするツイートのみが送信されるようになります。

 

  • リモート開発者の求人

 

この最初の例では、リモート開発者の求人を探している 

と仮定します。このようなツイートを取得するには、以下のようなルールを使用します。

 

      (developer OR engineer) remote (context:66.961961812492148736 OR context:66.850073441055133696)

    

 

このルールの仕組みを理解するために、2つの部分に分解できます。キーワード部分とツイート注釈部分です。

 

キーワード演算子の使用

 

 

      (developer OR engineer) remote

    

 

ルールのキーワード部分を「developer」または「engineer」というキーワードと「remote」というキーワードを含むツイートとマッチングします。これらのキーワードだけでも、リモート開発者の求人情報を含むツイートを確実にマッチさせることはできますが、無関係なツイートにもマッチしてしまいます。たとえば、以下のツイートはこのルールにマッチします。

 

https://twitter.com/EraserFarm/status/1220013392766947332

 

これは必要なツイートではないため、このルールにさらに手を加える必要があります。このルールは無関係なツイートにマッチしますが、実際の求人情報とのマッチングにも成功しています。たとえば、ここまでのルールでは、以下のツイートにもマッチすることになります。

 

https://twitter.com/plataformatec/status/1225460423761317888

 

 

ツイート注釈:コンテキスト演算子の使用

 

ここで問題となるのは、求人情報を含むツイートを受信しているにもかかわらず、無関係なツイートも確認する必要があることです。できるだけ求人情報を含むツイートのみをマッチングする方法があればいいのですが、これらの関連するツイートにのみマッチングするルールで使用できる演算子はあるでしょうか? 

 

幸い、この場合Tweet注釈の機能を利用できます。フィルタリングされたストリームエンドポイントでこのツイートに送信されたTweetオブジェクトのペイロードの例を詳しく見てみましょう。ペイロード内には、ネストされた「context_annotations」フィールドがあります。コンテキスト注釈は、ペイロードで「context_annotations」フィールドとして配信されます。これらの注釈は、ツイートテキストに基づいて推測され、ドメインラベルやエンティティラベルになります。これらのラベルは、これまでは取得が難しかったトピックのツイートを発見するために使用できます。これらのフィールドが含まれるのはデータが存在する場合のみです。すべてのツイートにこのデータが存在するとは限りません。 

 

 

            "context_annotations": [
        {
          "domain": {
            "id": "66",
            "name": "Interests and Hobbies Category",
            "description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations"
          },
          "entity": {
            "id": 961961812492148736,
            "name": "Recruitment",
            "description": "Recruitment"
          }
        },
        {
          "domain": {
            "id": "66",
            "name": "Interests and Hobbies Category",
            "description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations"
          },
          "entity": {
            "id": "850073441055133696",
            "name": "Job search",
            "description": "Job search"
          }
        }
      ],

    

 

context_annotationsフィールド内のドメインとエンティティのIDでマッチングするには、「context」演算子を使用します。「context」演算子を使用し、フィルタリングされたストリームエンドポイントに指示を与え、特定のドメイン名とエンティティ名を含むツイートでマッチングすることができます。たとえば、次のようになります。

 

 

      (context:66.961961812492148736 OR context:66.850073441055133696)
    

 

上の演算子は「context:<domain id>.<entity id>」の形式に従います。上のペイロードの例にあるように、ドメインID 66は、「Interests and Hobbies Category」を表します。エンティティID 961961812492148736は「Recruitment」のエンティティを表し、エンティティID 850073441055133696は「Jobs search」のエンティティを表します。ドメインの詳細なリストとして、ツイート注釈のドキュメントに50以上のドメイン名の表が記載されています。

 

演算子の説明は省きますが、この2つ目のルールは、エンティティ名に「Recruitment」や「Jobs search」を含むツイートとマッチングします。

 

まとめると、この両方のルールを組み合わせた場合、「developer」または「engineer」というキーワードと「remote」というキーワードを含み、さらに「Recruitment」または「Jobs search」というエンティティ名が含まれる場合にのみマッチします

 

  • 新しいミュージックビデオを探す

 

    お勧めの新しいミュージックビデオを知りたい場合は、まず、「song」と「YouTube」というキーワードを含むツイートにマッチするシンプルなルールを

    使用します。また、実際に外部の動画にリンクするツイートを見ることもできます

 

 

      song youtube has:links

    

 

このツイートのペイロードを詳しく見てみると、より関連性の高いツイートをマッチングするのに便利ないくつかの注釈が付けられていることが分かります。「Pop」というエンティティラベルと「Music Genre」というドメイン名の注釈に注目してみましょう。

 

 

           "context_annotations": [
        {
          "domain": {
            "id": "10",
            "name": "Person",
            "description": "Named people in the world like Nelson Mandela"
          },
          "entity": {
            "id": "871815676998033408",
            "name": "Ally Brooke",
            "description": "Ally Brooke"
          }
        },
        {
          "domain": {
            "id": "54",
            "name": "Musician",
            "description": "A musician in the world, like Adele or Bob Dylan"
          },
          "entity": {
            "id": "871815676998033408",
            "name": "Ally Brooke",
            "description": "Ally Brooke"
          }
        },
        {
          "domain": {
            "id": "55",
            "name": "Music Genre",
            "description": "A category for a musical style, like Pop, Rock, or Rap"
          },
          "entity": {
            "id": "810938279801470977",
            "name": "Pop",
            "description": "Pop"
          }
        }
      ],

    

 

このルールを改良し、より関連性の高いツイートを絞り込むためには、以下のようにルールを更新できます。 

 

 

      song youtube has:links context:55.810938279801470977
    

 

これにより、先ほど使用したオリジナルのルールに手が加えられ、Music GenreのドメインラベルとPopのエンティティラベルが貼られたツイートのみに絞り込みます。

 

  • 個人向けの金融や貯蓄についての情報を得る

 

 

    最後の例として、個人向け金融に関心がある場合に、支出や貯蓄についての知識を得ることができます。また、詳細を知るために、オンライン記事へのリンクを含む元のツイートのみを取得したいとします。 

 

    これまでの例で行ったのと同じように処理し、以下のルールを追加するだけで、「personal」、「finance」、「savings」という言葉を含むツイートのみが送信されます。

 

 

      “personal finance savings”

    

 

ツイートのペイロードの1つを見ると、ネストされた注釈に、個人向けの金融に関するエンティティラベルが含まれています。これは、最も関連性の高いツイートに絞り込むのに役立ちます

 

 

      "annotations": {
    "context": [
    {
        "context_domain_id": 66,
        "context_domain_id_str": "66",
        "context_domain_name": "Interests and Hobbies Category",
        "context_domain_description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations",
        "context_entity_id": 847888632711061504,
        "context_entity_id_str": "847888632711061504",
        "context_entity_name": "Personal finance",
        "context_entity_description": "Personal finance"
    },

    

 

まとめ

 

フィルタリングされたストリームエンドポイントを使用して、公開されているツイートをサーバーや注釈にストリーミングすることで、気になるトピックに関するツイートをより簡単に取得するアプリを作成しました。フィルタリングされたストリームエンドポイントを使用すると、ツイート形式で大量のデータを取得し、ツイート注釈を使用することによって、その中から必要な情報を見つけることができます。 

 

他にも、このアプリの面白い使用方法を考えつきましたか?Twitterでフォローして、ツイートして私にお知らせください。このチュートリアルの作成にあたって、Twitter API以外のライブラリをいくつか使用しましたが、ニーズや要件が異なる場合があるため、ご利用にあたってはそれらのツールが適切かどうかを評価してください。

 

Glitchでアプリのライブデモをご覧ください。

 

 

次の手順