Pythonでconnpassメッセージを予約投稿する


# はじめに

こんにちには。私はconnpassでオンライン勉強会を運営してますが、作業の一部を自動化したことを今回共有します。誰にでも役に立つ可能性があるので最後まで読んでいたければ幸いです。

さて、コミュニティの運営にはさまざまな作業がありますが、この中にパターン化できることがたくさんあります。

例：
* イベントコンテンツの大枠準備
* Zoom、Slido,アンケートフォーム、短縮URL、QRコードの準備
* 視聴者への各種メール通知（イベント案内、配信先URLの案内、アンケート依頼） ★今回はここです！
* オープニングスライドの雛形作成
* 登壇者への当日準備通知
* Togetterのまとめ
* アンケート結果の集計、発表者へのフィードバック
* connpassの申込数やランキングがある数値に達するとツィートするbot

今回は[connpass](https://connpass.com/)にYoutubeの配信URLやSlidoの質問URLの事前案内や、アンケートフォームの依頼を予約投稿したり、イベントサイトの「参加者への情報」欄へ記入する操作を自動化する方法を紹介します。

予約投稿は以下の４種類を投稿します。
* イベント前日に配信URLや質問URLの事前案内メール
* イベント当日の配信URLや質問URLの事前案内メール
* イベント終了後にアンケート登録の依頼メール
* イベント翌日にアンケート登録の依頼メール（リマインダー）

尚、YoutubeやSlido、アンケートフォームの自動作成までには対応しておりません。これらのURLが決まった後にconnpassに登録する部分だけ自動化するものなのでご注意下さい。
コードは以下のGithubに登録しているので、設定ファイルを修正してご利用下さい。
https://github.com/abenben/connpass_event_messages

# 動作環境

今回動作した環境は以下の通りです。
動作しない場合は、<b><font color="red">ブラウザとドライバーの種類やバージョンが合っていない</font></b>ことが多そうなのでご注意ください。

* 機種：Macbook Pro(Intel Core i7）
* OS:macOS バージョン 12.0.1
* ブラウザ：Google Chrome バージョン: 96.0.4664.93（Official Build） （x86_64）

# パッケージのインストール

```bash
$ pip3 install selenium
# ドライバーはChomeのバージョンと併せてインストールすること
$ pip3 install chromedriver-binary==96.0.4664.45.0
```

# 設定ファイル（config.ini）

設定ファイルは[connpass][community][event]の３つの構成からなります。[connpass]にはログインするユーザー名とパスワードを設定します。[community]には運営しているコミュニティの名称（または事務局名）を設定します。[event]には毎回行われる勉強会の情報を設定します。
[connpass][community]は一度設定すれば変更頻度は少ないはずです。
[event]には日付や時間、勉強会のタイトル、各種URLを設定します。設定ファイル（config.ini）の中にもコメントが記入されているので詳細はそちらをご参考下さい。

```ini
[connpass]
# connpassのユーザー、パスワード
username = XXXXXXXXXX
password = YYYYYYYYYY

[community]
# コミュニティ名称
name = 金融エンジニア養成コミュニティ

[event]
# connpassのイベントID
connpass_id = 228873
# イベント開催日
date = 2021/12/15
# イベント開始時間、終了時間
start_time = 19:00
end_time = 21:00
# イベント直前に配信URLの案内メールを通知する時刻
notification_time = 18:00
# イベント終了後にアンケートメールを通知する時刻
survey_time = 21:30
# イベント翌日にアンケートメール（リマインダー）を通知する時刻
re-survey_time = 09:00
# イベント名称
title = フィンテックエンジニア養成勉強会#20
# イベントサブタイトル
subtitle = フィンテック トレンド2022〜未来の姿のヒントとなるインプットを一気に学ぶアンラーンNight！〜
# 配信用URL（YouTubeやZoomなどのURL）
online_url = https://bit.ly/fe20youtube
# 質問サービス（Slidoなど）のURL
question_url = https://bit.ly/fe20faq
# 質問サービスの質問コード（SlidoのID）
question_id = Fin20
# アンケートフォームのURL
survey_url = https://bit.ly/fe20form
# アンケートフォームの回答期限
survey_deadline = 2021/12/21
```

設定ファイルのサンプルとして利用したconnpassイベントは[こちら](https://fintech-engineer.connpass.com/event/228873/)になります。

# プログラムの解説

ポイントとなるプログラムの部分だけ説明します。

## configファイル

設定ファイルの管理には[configparser](https://docs.python.org/ja/3/library/configparser.html)を使います。Pythonの標準ライブラリなのでパッケージのインストールは不要です。

```python
import configparser

cfg = configparser.ConfigParser()

# test.confから設定を読む
cfg.read("./config.ini", encoding='utf-8')

# イベント開催日
EVENT_DATE = dt.strptime(cfg["event"]["date"], '%Y/%m/%d')
```

## ブラウザ操作の自動化

ブラウザの操作には[Selenium WebDriver](https://www.selenium.dev/ja/documentation/webdriver/)というものを利用します。
Seleniumというのは、複数のプログラミング言語から自動でブラウザーが操作できる実行環境のことです。例えば、PythonからChromeやFirefoxを起動して、あるサイトのJavaScrptを読み取って実行することなどができます。
このSeleniumを操するには、W3Cで標準化されたWebDriverのAPIを使う必要があります。それがSelenium WebDriverです。さらに動かすブラウザに対応したWebDriverをインストールする必要もあります。この記事では細かいWebドライバーのインストールについては割愛しますが、[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)が参考になりました。

### Chromeブラウザを起動する

環境によってChromeの起動はwebdriver.Chrome()だけ起動できますが、私の環境では実行形式の場所を指定することで起動できました。chromedriver_binaryのインストールと宣言を忘れがちなので注意して下さい。IDEでパッケージ宣言の最適化を自動に設定すると直接呼び出していないため削除されることもあるのでご注意下さい。

```python
from selenium import webdriver
from selenium.webdriver.common.by import By
import chromedriver_binary                  # [注意]ドライバー入れないと動きません

options = webdriver.chrome.options.Options()
options.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
driver = webdriver.Chrome(options=options)
```

### connpassにログインする

connpassのログインページでユーザーとパスワードを入力してログインをします。
ブラウザの要素名を指定して操作するにはfind_element(By.NAME,...）を利用します。
それ以外にも様々な制御方法があります。詳細はマニュアルをご覧下さい。

```python
driver.get("https://connpass.com/login/")
driver.find_element(By.NAME, "username").send_keys(***ユーザー***)
driver.find_element(By.NAME, "password").send_keys(***パスワード***)
driver.find_element(By.CSS_SELECTOR, ".p > .btn").click()
driver.find_element(By.CSS_SELECTOR, "tr:nth-child(2) > td").click()
```

### connpassイベントのメッセージを予約投稿

該当するconnpassイベントのイベントメッセージページ内の「新規メッセージ」タブで、件名、本文、送信予約日時を入力して「即時送信/予約」ボタンをクリックします。

```python
driver.get(EVENT_URL + "/message/new/")
driver.find_element(By.ID, "id_subject").send_keys(***タイトル***)
driver.find_element(By.NAME, "body").send_keys(***本文***)
driver.find_element(By.ID, "id_reserved_date").send_keys(***日付***)
driver.find_element(By.ID, "id_reserved_time").send_keys(***時間***)
driver.find_element(By.ID, "id_send").click()
```

### connpassイベントの「参加者への情報」欄に配信URL等を登録

該当するconnpassイベントページを編集モードをイベント編集画面に切り替える。
「参加者への情報」をクリックして編集状態にしてクリアした後に「保存」ボタンをクリックする。
再び、「参加者への情報」をクリックして編集状態にして本文を入力して「保存」ボタンをクリックする。

```python
driver.get(EVENT_URL)
driver.find_element(By.CSS_SELECTOR, ".icon_gray_edit").click()
driver.find_element(By.ID, "FieldParticipantOnlyInfo").click()
driver.find_element(By.NAME, "participant_only_info").clear()
driver.find_element(By.CSS_SELECTOR, ".save").click()
driver.find_element(By.ID, "FieldParticipantOnlyInfo").click()
driver.find_element(By.NAME, "participant_only_info").send_keys(***「参加者への情報」本文***)
driver.find_element(By.CSS_SELECTOR, ".save").click()
```

### Chromeブラウザを終了する

処理が完了したらブラウザを終了します。

```python
# ブラウザを終了
driver.quit()
```

# プログラムの実行方法

Githubから取得したプログラムは以下のコマンドで実行します。
ポイントとなる箇所の可読性を重視するため、main関数の準備、エラー処理の対応、クラス化は意図的にしませんでした。

```bash
$ python3 main.py
```

<details><summary><b>──────────────────┐<br>│&nbsp;&nbsp;&nbsp;&nbsp;完成形のソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;│<br>└──────────────────┘</b></summary><div>

```python
import configparser
from datetime import datetime as dt
from datetime import timedelta

from selenium import webdriver
from selenium.webdriver.common.by import By

import chromedriver_binary

# ----------------------------------------------------------
# 環境設定ファイルから情報取得
# ----------------------------------------------------------

cfg = configparser.ConfigParser()

# test.confから設定を読む
cfg.read("./config.ini", encoding='utf-8')

# イベント開催日
EVENT_DATE = dt.strptime(cfg["event"]["date"], '%Y/%m/%d')

# イベントURL
EVENT_URL = "https://connpass.com/event/{0}".format(cfg["event"]["connpass_id"])

# ----------------------------------------------------------
# イベントメッセージの投稿情報を編集する
# ----------------------------------------------------------

# メッセージのタイトル
DAY_BEFORE_NOTIFICATION_TITLE = "【 明日{start_time} 開始】{title} 配信URLのご連絡".format(
    start_time=cfg["event"]["start_time"],
    title=cfg["event"]["title"])
TODAY_NOTIFICATION_TITLE = "【 本日{start_time} 開始】{title} 配信URLのご連絡".format(
    start_time=cfg["event"]["start_time"],
    title=cfg["event"]["title"])
TODAY_SURVEY_TITLE = "【アンケートにご協力ください】{title}".format(
    title=cfg["event"]["title"])
DAY_AFTER_SURVEY_TITLE = "【アンケートにご協力ください】{title}（リマインダー）".format(
    title=cfg["event"]["title"])
TITLES = [DAY_BEFORE_NOTIFICATION_TITLE, TODAY_NOTIFICATION_TITLE, TODAY_SURVEY_TITLE, DAY_AFTER_SURVEY_TITLE]

# メッセージの本文
DAY_BEFORE_NOTIFICATION_MESSAGE = '''明日の配信先と質問サイトのURLをお知らせします。
イベント配信URL
　{date} {start_time}~
　{online_url}
発表者への質問はこちらのURLからお願いいたします。
　{question_url}
　(Slido.com #{question_id})
--
{community_name}'''.format(
    date=cfg["event"]["date"],
    start_time=cfg["event"]["start_time"],
    online_url=cfg["event"]["online_url"],
    question_url=cfg["event"]["question_url"],
    question_id=cfg["event"]["question_id"],
    community_name=cfg["community"]["name"])

TODAY_NOTIFICATION_MESSAGE = '''本日の配信先と質問サイトのURLをお知らせします。
（リマインダー）
イベント配信URL
　{date} {start_time}~
　{online_url}
発表者への質問はこちらのURLからお願いいたします。
　{question_url}
　(Slido.com #{question_id})
--
{community_name}'''.format(
    date=cfg["event"]["date"],
    start_time=cfg["event"]["start_time"],
    online_url=cfg["event"]["online_url"],
    question_url=cfg["event"]["question_url"],
    question_id=cfg["event"]["question_id"],
    community_name=cfg["community"]["name"])

TODAY_SURVEY_MESSAGE = '''皆様、本日はご視聴いただきありがとうございました。
今後の勉強会の満足度向上および登壇者へのフィードバックのためにアンケートを実施しております。
アンケートURL
　 {survey_url}
※所要時間：1,2分程度
※回答期限：{survey_deadline}
皆様の貴重なご意見、率直なご感想をお聞かせ下さい。
--
{community_name}'''.format(
    survey_url=cfg["event"]["survey_url"],
    survey_deadline=cfg["event"]["survey_deadline"],
    community_name=cfg["community"]["name"])

DAY_AFTER_SURVEY_MESSAGE = '''皆様、昨晩はご視聴いただきありがとうございました。
（リマインダー）
今後の勉強会の満足度向上および登壇者へのフィードバックのためにアンケートを実施しております。
アンケートURL
　 {survey_url}
※所要時間：1,2分程度
※回答期限：{survey_deadline}
皆様の貴重なご意見、率直なご感想をお聞かせ下さい。
--
{community_name}'''.format(
    survey_url=cfg["event"]["survey_url"],
    survey_deadline=cfg["event"]["survey_deadline"],
    community_name=cfg["community"]["name"])

MESSAGES = [DAY_BEFORE_NOTIFICATION_MESSAGE, TODAY_NOTIFICATION_MESSAGE, TODAY_SURVEY_MESSAGE, DAY_AFTER_SURVEY_MESSAGE]

# メッセージの投稿予約日
DATES = [EVENT_DATE - timedelta(1), EVENT_DATE, EVENT_DATE, EVENT_DATE + timedelta(1)]

# メッセージの投稿予約時間
TIMES = [cfg["event"]["notification_time"], cfg["event"]["notification_time"], cfg["event"]["survey_time"],
         cfg["event"]["re-survey_time"]]

# ----------------------------------------------------------
# 「参加者への情報」を編集する
# ----------------------------------------------------------

# connpassの「参加者への情報」へ登録する内容
event_box = '''【配信URL】
　{online_url}
【発表者への質問URL】
　{question_url}
　(Slido.com #{question_id})
【アンケートURL】
　 {survey_url}'''.format(
    online_url=cfg["event"]["online_url"],
    question_url=cfg["event"]["question_url"],
    question_id=cfg["event"]["question_id"],
    survey_url=cfg["event"]["survey_url"])

# ----------------------------------------------------------
# connpassサイトを操作して投稿予約、参加者への情報を編集する
# ----------------------------------------------------------

# ブラウザ起動
options = webdriver.chrome.options.Options()
options.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
driver = webdriver.Chrome(options=options)

# connpassログイン
driver.get("https://connpass.com/login/")
driver.find_element(By.NAME, "username").send_keys(cfg["connpass"]["username"])
driver.find_element(By.NAME, "password").send_keys(cfg["connpass"]["password"])
driver.find_element(By.CSS_SELECTOR, ".p > .btn").click()
driver.find_element(By.CSS_SELECTOR, "tr:nth-child(2) > td").click()

# イベントメッセージへの投稿予約
for title, message, date, time in zip(TITLES, MESSAGES, DATES, TIMES):
    driver.get(EVENT_URL + "/message/new/")
    driver.find_element(By.ID, "id_subject").send_keys(title)
    driver.find_element(By.NAME, "body").send_keys(message)
    driver.find_element(By.ID, "id_reserved_date").send_keys(date.strftime('%Y/%m/%d'))
    driver.find_element(By.ID, "id_reserved_time").send_keys(time)
    driver.find_element(By.ID, "id_send").click()

# connpassの「参加者への情報」に登録
driver.get(EVENT_URL)
driver.find_element(By.CSS_SELECTOR, ".icon_gray_edit").click()
driver.find_element(By.ID, "FieldParticipantOnlyInfo").click()
driver.find_element(By.NAME, "participant_only_info").clear()
driver.find_element(By.CSS_SELECTOR, ".save").click()
driver.find_element(By.ID, "FieldParticipantOnlyInfo").click()
driver.find_element(By.NAME, "participant_only_info").send_keys(event_box)
driver.find_element(By.CSS_SELECTOR, ".save").click()

# ブラウザを終了
driver.quit()
```

</div></details>

# 実行後のconnpassの内容

プログラムを実行後のconnpassは以下のようになります。

## イベントメッセージの予約投稿（connpass）

「はじめに」で説明したとおり、４種類の予約投稿が自動で登録されます。

![connpass_messages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27772/84094ced-3db0-0f7d-3ad1-0d93c8a61983.png)

実行した後は、各投稿が正しいか必ず確認しましょう。間違った通知をするとコミュニティメンバーから信頼が失われてしまいます。今までは誰かが登録して、他の人が確認していたので2人がかりの作業でしたが、1人だけで準備できるようになりました。

![connpass_message.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27772/2b911c86-2c9a-0f26-1c89-7c3855e50cbc.png)

## 参加者への情報（connpass）

「参加者への情報」欄にも自動で登録されます。
一度クリアしてから登録しているので、実行をやり直しても更新されます。
URLがリンクにならないので、ここはconnpassに改善して欲しいところですね。

![connpass_box.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27772/d37076e5-ef65-8ff8-39a8-c05c0e9c8211.png)

# ブラウザを操作するためのプログラミングコードの解析方法

ブラウザ上の各要素を制御するプログラミングはSeleniumのマニュアルを調べて書いたわけではありません。
SeleniumにはSelenium　IDEというブラウザ操作をレコーディングしたり専用のスクリプトで再生できる実行環境があります。ブラウザ別にプラグインがあるのでまずはこれをインストールして、実際にconnpassサイトにログイン、新規投稿、参加者の情報を編集した操作をレコーディングして、そのスクリプトをPython言語のPyTestファイル形式にExportできます。このコードを参考にしながら、自動化コードに置き換えていきます。詳細は[Selenium IDEが便利すぎる件　【スクレイピング】](https://freeheroblog.com/google-selenium-ide/)がわかりやすかったです。C#やJavaScript、Rubyのコードにすることもできるようです。昨今のRPAツールはパソコン上の操作をレコーディングして再生操作できますが、ブラウザに限定した作業であればSeleniumでもだいたいの事が行えます（ただし、それほどローコードでないため非エンジニアには難易度が高いです）。

# まとめ

メッセージのフォーマットが変えたい場合は、コード上でメッセージが直書きされてるので直接修正してご利用ください（後で外だしファイルにするかも）。
自分が関わっているITコミュニティ運営は、だいたい[HackMD](https://hackmd.io/)で管理を行い、Githubで管理しています（個人メールやパスワードなどは載せないように注意して運営している）。将来的には、HackMDを起点にconnpassイベントの大枠を作成したり、Zoom、YouTube、Slido、アンケートフォームの自動作成、短縮URLの作成。QRコード生成、オープニングスライドテンプレートの準備までをできるだけ自動化したいと思っています。この中には一部自動化したものもあるので、できる限り公開したいです（コードが汚い　w）。[Googleフォームはベータ版のAPIが公開された](https://www.publickey1.jp/blog/21/google_forms_apigoogle_forms.html)のでこれも早く試してみたいです。いずれにせよ、単純作業はできるだけ自動化して企画やふりかえり、プロモーション活動のような人にしかできないことに専念したいですよね。
2021年12月14日は毎月やってる<b><font color="green">[みんなのPython勉強会](https://startpython.connpass.com/event/231260/)</font></b>を開催します。早速このプログラムを使ってconnpassにメッセージを予覚投稿しました。是非こちらのオンライン勉強会にもお申し込みください。
それでは、みなさま、どうぞ良いお年をお迎えください！

# その他の参考ページ

* [pythonプログラムにおける設定ファイル管理モジュール～configparserの使い方と注意点～](https://qiita.com/mimitaro/items/3506a444f325c6f980b2)
* [Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)
* [Selenium IDEが便利すぎる件　【スクレイピング】](https://freeheroblog.com/google-selenium-ide/)