Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Add] TODOリクエスト機能の追加 #3

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

daima3629
Copy link
Owner

色々間違えたのでやり直しPRです

  • Command request
    • 第1引数にTODOリクエストを送りたいユーザーのメンション or ID or 名前を指定
    • 第2引数(可変)に送りたいTODO文字列を指定
    • リクエストは送り先のユーザーのDMに送られる
    • リクエストごとにIDが作成される(sha256)
    • IDを使い後述のコマンドでリクエストの承認, 拒否を行う
  • Command request_approve
  • 第1引数にリクエストIDを指定
  • 指定されたIDに対応するTODOリクエストを承認, 自身のTODOリストに追加する
  • Command request_deny
    • 第1引数にリクエストIDを指定
    • 指定されたIDに対応するTODOリクエストを拒否

※リクエスト承認, 拒否はリクエストをしたユーザーには通知されない, 今後通知されるよう改善する可能性あり

cogs/todo.py Outdated
"to": to_user.id,
"content": todo
}
self.bot.data["request"][hashed[:10]] = data
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sha256 の前10文字だけ取ってくるって被らないのかな?
あと hashed[:10] を下でも使っているので、request_id 的な変数を定義して代入すると分かりやすいと思う

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あと hashed[:10] を下でも使っているので、request_id 的な変数を定義して代入すると分かりやすいと思う

分かりやすいだけでなくで、修正漏れも防げる。いわゆるDRY原則というやつ。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

てか、ただ一意な ID 振りたいだけなら、UUID とか使えばいい気がする。

Copy link
Owner Author

@daima3629 daima3629 Jul 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このIDを入力してリクエストの承認・拒否を行うので、長すぎると手入力がとても大変になるんですよね...
先程shunさんから「36進数に圧縮すればいい」という意見を頂いたので、そうするように修正コミットを出しました!
624a42cこちらです!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このIDを入力してリクエストの承認・拒否を行うので、長すぎると手入力がとても大変になるんですよね...

大体ダブルクリックして ctrl+C だろうけどね。

てか、いつの間にかメッセージ ID をリクエストの ID にするようになってる。かしこ。

shunさんから「36進数に圧縮すればいい」という意見を頂いた

[0-9a-z] で 36 ってわけか。かしこすぎる。

cogs/todo.py Outdated
とコマンドを実行してください。"""
try:
await member.send(dm_msg)
except:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全ての Exception を握りつぶすんじゃなく、起こりうる Exception のみキャッチすると良いと思う。

cogs/todo.py Outdated

del self.bot.data["request"][req_id]
self.bot.save_data()
return await ctx.send(f"> リクエストを拒否しました。")
Copy link

@chun37 chun37 Jul 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return await ctx.send(f"> リクエストを拒否しました。")
return await ctx.send("> リクエストを拒否しました。")

無意味な f-strings を発見!

@chun37
Copy link

chun37 commented Jul 13, 2020

request_approverequest_deny の共通してるところが多いので、共通してる部分を関数化すると良いかも

commands.group() を使って、request create, request list, request approve, request denyみたいにすると、個人的に分かりやすいと思う。

@daima3629
Copy link
Owner Author

たくさんの指摘ありがとうございます...!
指摘を受けてなるべく早く修正コミットをだします!

@ikngtty
Copy link

ikngtty commented Jul 14, 2020

プルリクコメントに仕様書いてるけど、プルリクコメントだと流れちゃうよ。
多分自分でも忘れたりして見返すだろうし、ちゃんとした場所に残しておくのがオススメ。
READMEとか、helpコマンドとか。

@daima3629
Copy link
Owner Author

プルリクコメントに仕様書いてるけど、プルリクコメントだと流れちゃうよ。
多分自分でも忘れたりして見返すだろうし、ちゃんとした場所に残しておくのがオススメ。
READMEとか、helpコマンドとか。

READMEはこのPRが無事Mergeされたあとにすぐ書こうと計画してます。
helpコマンド、というかUI全般もその後一新して見やすくしようと思ってます
ご指摘ありがとうございます。

@ikngtty
Copy link

ikngtty commented Jul 14, 2020

第2引数(可変)に送りたいTODO文字列を指定

第2引数が空でも動くように見える。空の TODO を送りつけられる仕様はどうなんだろう。
普通は「TODO パラメータ入れ忘れてますよ」っつってキャンセルすると思う。
(作者がそれでいいならまあいいか、程度)

@daima3629
Copy link
Owner Author

第2引数(可変)に送りたいTODO文字列を指定

第2引数が空でも動くように見える。空の TODO を送りつけられる仕様はどうなんだろう。
普通は「TODO パラメータ入れ忘れてますよ」っつってキャンセルすると思う。
(作者がそれでいいならまあいいか、程度)

確かにこの設計はまずいですね...修正します、ありがとうございます!

cogs/todo.py Outdated
Comment on lines 141 to 151
dm_msg = f"""
>>> {ctx.author.mention}さんからTODOリクエストが届きました。

内容:
・{todo}

リクエストID: `{req_id}`

このリクエストを承認する場合は`todo!request_approve {req_id}`
拒否する場合は`todo!request_deny {req_id}`
とコマンドを実行してください。"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python の 複数行文字列リテラルってインデント考慮してくれないんだ。
めちゃキモいな。
Python 界隈的には普通なのかもしれないけど、個人的には無理矢理にでも避けたいなこれ。

https://stackoverflow.com/a/33924077/12964914
というわけでちょっと探したけど、↑これぐらいしかワークアラウンド見つからんかった。

俺だったらこのワークアラウンド採用するかな。
まああくまで個人的価値観なので、採用するかは任せた。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これに関してはtextwrap.dedentという関数を使うことでインデントを治すのが一般的なようです。
あまり気にしていませんでしたが、暇があれば修正します、指摘ありがとうございます!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textwrap.dedentという関数を使うことでインデントを治すのが一般的

そうなんだ。なるへそ。じゃあそれで

cogs/todo.py Outdated


class TODOCog(commands.Cog):
def __init__(self, bot):
self.bot = bot

def make_request(self, ctx, to_user, todo):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_user 、「to」な「user」って意味なんだろうけど、「ユーザーに向けて」みたいに読めちゃうので混乱しちゃうな。
俺だったら user_send_to とかかな。まあ俺の案もそんなにイケてる感じしなくて微妙なラインなので、別に修正しなくてもまあいいかって感じではある。

cogs/todo.py Outdated


class TODOCog(commands.Cog):
def __init__(self, bot):
self.bot = bot

def make_request(self, ctx, to_user, todo):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

見てる感じ内部用メソッドっぽいので、メソッド名は _ つけてあげるのが丁寧かな。

cogs/todo.py Outdated
Comment on lines 14 to 19
data = {
"author": ctx.author.id,
"to": to_user.id,
"content": todo
}
self.bot.data["request"][hashed[:10]] = data
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dict 使ってるのは JSON との相互変換しやすいからなんだろうけど、dict でこういうデータ扱うと色々メンテ大変だよ。

e.g.

  • クラスと違って、データの仕様(どんなプロパティがあるか)がどこかにまとまってるわけではない。
  • スペルミスしてもエラーが出てくれない。

クラスで作ったオブジェクトでも JSON と相互変換はできる。
ちょっと手間かかるので、あくまでリファクタリング課題って感じだけど、ちゃんとクラス定義してみるといいと思う。(ちょっと重いタスクかもしれないので、今はやらずに課題としてどこかに残しておく程度でも可。)

cogs/todo.py Outdated
Comment on lines 20 to 21
self.bot.save_data()
return hashed[:10]
Copy link

@ikngtty ikngtty Jul 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

メソッド名が make_request だけど、内容は

  1. リクエストのデータを作成
  2. 作成したデータを bot オブジェクト内に格納
  3. 作成したデータを外部ファイルに保存(bot.save_data の呼び出し)
  4. 作成したリクエストの ID を返す

の4つだね。

問題点:

  1. 一つのメソッドが複数のことをやっている。単一責任原則に反している。
    • 一度に複数のことを実現しようとすると、人は混乱するし、バグを生みがち。
    • その他詳しくは「単一責任原則」でググれ。
  2. 名前が実体を表していない(内容 1 は合っているが、内容 2 〜 4 は名前に表れていない)。
    • 「メソッド名を見ただけで何をしているかが分かる」という状況が理想。そういう状況だと、脳に負担をかけずに読むことができる。今の状況だと、make_request を使っている部分を読む時に、「ええと、make_request は本当はリクエストの保存をしているから…」という感じで、make_request の中身まで頭に叩き込まないと読めないようになってしまっている。こういう状況では全体を俯瞰してチェックしたりするのが困難になる。

というわけで、内容に沿ってちゃんと分割して、それぞれに内容に即したメソッド名をつけることをオススメする。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここらへんはすぐにはできそうではないので、今後の課題とさせていただきたいと思います、ご指摘ありがとうございます!

cogs/todo.py Outdated


class TODOCog(commands.Cog):
def __init__(self, bot):
self.bot = bot

def make_request(self, ctx, to_user, todo):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

オブジェクト指向のコツの一つは、データごとにロジックを整理すること。
例えば今回の場合、リクエストに関するロジックを Request というクラスやモジュールにまとめるのが自然。

Cog はあくまで Discord とのやり取りを表現するクラス。
こういう IO 関係のロジックと、IO と関係ないプレーンなロジックを分離できるようになると、コードの見通しはすごいよくなる。
「Discord とのやりとりでエラーが起きてるようなら、Cog をチェックすればいい。」
「リクエストの保存の仕方でミスってるようなら、Request をチェックすればいい。」
という感じ。
あと、IO を切り離すとテストコードでテストすることも可能になったりする。

ちなみに、save_data も一つの IO といえる。この辺のロジックも分離できると更にスッキリするしテストしやすくなる。
俺だったら RequestRepository みたいなクラス作るかな。「リポジトリパターン」っていうデザインパターンがあるのでそれを使う。詳しくはググれ。

まあ、このへんは設計レベルのリファクタリングであって、重いタスクかつ発展課題なので、「やらなきゃマージできない〜〜〜」とか思わなくていいよ。「ほーん、そうなんや。まあとりあえずマージしちゃお。」でいい。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

分離ですか...ちょっとすぐにはできそうもないので、今は今後の課題、という形で処理させていただこうと思います。
教えていただきありがとうございます!

cogs/todo.py Outdated
await msg.add_reaction("👍")
await msg.add_reaction("👎")

def check(reac, user):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reac みたいに無理やり略すのは個人的には奨励しない。
多分一般的にもあんまやらないんじゃないかなー、まあでも Python 界隈はこういう略し方好みそうな気もするし、ちょっと自信ないや。

一番の理由として、脳内で読み辛いんだよね。
「リアクション ドット メッセージ イコール〜」って読みたい。
「リアク ドット メッセージ イコール〜」だと頭に入らない。

まあ個人的意見なので採用するかは自由。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discord.pyのリファレンスのコード例の中でそんな略し方をしていた気がしたのでそうしていたんですが、今見たら普通にreactionで書いてありました...
ありがとうございます!

cogs/todo.py Outdated
@commands.command()
async def request(self, ctx, member: discord.Member, *, todo):
text = f"""
>>> `{str(member)}`さんにtodoリクエストを送ります。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なんで全体的に引用メッセージ(>>>がついてるやつ)なの?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

返信遅くなって申し訳ないです
そうしないと一行ずつ>をつけなくてはいけないので
現在のデザインとして、BOTのメッセージには全て引用符をつけるような感じになってます
でもやはりembedのほうが見やすいと思うので、今後デザインは一新しようと思ってます

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、今思いついたけど、いちいち引用にするのってどこかで忘れそうなので、

async def send_bot_message(ctx, message):
    await ctx.send(f">>> `{message}`)

みたいな util 関数作ってもいいかもねぇ。
embed に切り替えるのも一発でできるようになるかも。

ただ、なまじ共通化しすぎても柔軟性に欠けるから危険なんだよね。例外が色々出てきたりするとまた困ったりする。例外がどれだけ出てきそうかの将来予測次第かな。

Copy link

@ikngtty ikngtty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

思ったよりクリティカルな問題もなかったし、不格好なところもなかった。つまんねーの。
なのでとりあえず Approve で、全部ただのコメントにしておく。好きにマージして。

良い設計(クラス分けとか)に関する感覚みたいなのがまだ無さそうなので、そのへん勉強すると捗ると思う。

cogs/todo.py Outdated
@@ -47,13 +60,14 @@ async def on_message(self, message):
msg = await message.channel.send(msg)
await msg.add_reaction("👍")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

突然失礼します
レビュー見ました。
UTF-8のアイコンを使用するのは良いのですが、個人的に👍👍🏻など亜種も多数存在するため、定数を切ると良いと思いました

ICON_GOOD = "👍"

的な・・・?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かに定数を使ったほうがいいですね、ご指摘ありがとうございます!
反映させていただきます!

@ikngtty
Copy link

ikngtty commented Jul 22, 2020

c2b8478 「変数にまとめた」ってコミットメッセージだけど、変数と定数は違うぞ(細かい定義は言語によるところあるけど)

@daima3629
Copy link
Owner Author

c2b8478 「変数にまとめた」ってコミットメッセージだけど、変数と定数は違うぞ(細かい定義は言語によるところあるけど)

あっ...素でミスりました()
コミットやり直ししときます()

@daima3629
Copy link
Owner Author

皆さんのご指摘のおかげでかなりいいコードにすることができたと思います。
いくつかを除き、全てのレビューを反映させました。
今回は反映させなかったいくつかのレビューもいつかは反映させたいと思っています。
そろそろMergeをしようかと思っていますが、もし更になにかあればぜひお願いします。

cogs/todo.py Outdated
import numpy
import textwrap

REACTIONS = ["👍", "👎"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REACTIONS[0] が up で REACTIONS[1] が down なのは非自明。どっちがどっちかすぐ間違えちゃいそう。

REACTION_AGREEREACTION_DISAGREE だとか、
REACTION_YESREACTION_NO みたいな感じで、
役割を明らかにした名前にするのが良い。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、リアクションの集合を表す REACTIONS も必要みたいね。
そしたら

REACTIONS = [REACTION_YES, REACTION_NO]

みたいにするのがいい。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あーなるほど!
そうさせていただきます!

cogs/todo.py Outdated
Comment on lines 64 to 65
await msg.add_reaction(REACTIONS[0])
await msg.add_reaction(REACTIONS[1])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REACTIONS の中身全部 for 文とかで add するようにした方が間違い無さそう。
(例えばリアクションの種類を増やしたくなった時とかに、こういう箇所での追加漏れを防ぎやすい。)

cogs/todo.py Outdated
@@ -45,22 +61,23 @@ async def on_message(self, message):
msg += f"{todo}\n"
msg += "\nTODOに追加しますか?"
msg = await message.channel.send(msg)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message(受信したメッセージのオブジェクト)
msg(確認のために送信するメッセージのテキスト)
再代入して
msg(確認のために送信したメッセージのオブジェクト)
ってなんか凄く追いづらい気がする

変数名を
confirm_message_text
confirm_message
とかにすると分かりやすい

cogs/todo.py Outdated
return await ctx.send(f">>> {ctx.author.mention}のTODOリスト\n\nNone")

for i, todo in enumerate(l):
for i, todo in enumerate(todo_list):
todos += f"{i}: {todo}" + "\n"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todos の追加の仕方がスマートじゃない気がする
enumerate を使ってるのは良い
空文字用意 → 必要なものと改行を結合 → 最後の改行を消す
って処理するくらいなら、リスト内包表記使って join するのが良いと思う

cogs/todo.py Outdated

reaction, _ = await self.bot.wait_for("reaction_add", check=check)
if str(reaction.emoji) == REACTIONS[0]:
if not self.bot.data["todo"].get(str(message.author.id)):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not self.bot.data["todo"].get(str(message.author.id)):
if str(message.author.id) not in self.bot.data["todo"]:

get を使ってると、ぱっと見た時何書いてるかわからなかった

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_key の代用で key in dict が出来るので、そちらのほうが良いと思われる

Comment on lines +106 to +107
deleted_todo = self.bot.data["todo"][str(ctx.author.id)][num]
del self.bot.data["todo"][str(ctx.author.id)][num]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deleted_todo = self.bot.data["todo"][str(ctx.author.id)][num]
del self.bot.data["todo"][str(ctx.author.id)][num]
deleted_todo = todo_list[num]
del todo_list[num]

上で todo_list 定義してるんだし、そのまま使えば良さそう

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo_list内のデータを削除すればself.bot.data内のデータも一緒に削除できるんですか?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、検証してみたところ消されました
Pythonの参照渡しというのはdict.getでも働くんですね...
ありがとうございます!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python は全部「値渡し」だよ。「Python の引数は参照渡し」ってデマ書いてるサイト、めちゃくちゃ多いけど。

cf.
https://qiita.com/ur_kinsk/items/949dabe975bdc1affb82
https://qiita.com/raccy/items/d4c5e7995a8fc90109ee

このへん一度ちゃんと理解したら、検証しなくても色々一発で予想できるようになると思う。

cogs/todo.py Outdated
@request.command(aliases=["a"])
async def approve(self, ctx, req_id: str):
req_todo = self.bot.data["request"].get(req_id)
if not req_id:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not req_id:
if not req_todo:

ミス?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deny の方と統一されてないので、何が正しいかわからなくなった

Comment on lines +154 to +169
dm_msg = f"""\
>>> {ctx.author.mention}さんからTODOリクエストが届きました。

内容:
・{todo}

リクエストID: `{req_id}`

このリクエストを承認する場合は`todo!request approve {req_id}`
拒否する場合は`todo!request deny {req_id}`
とコマンドを実行してください。
"""
try:
await member.send(textwrap.dedent(dm_msg))
except discord.Forbidden:
await ctx.send(f"{member.mention}\n" + textwrap.dedent(dm_msg))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここIDだけではなく(TODOを追加するところと同じように)リアクションでできたら良さそうかな〜と思いました

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そこどうしようか迷ってたんですが、やっぱリアクション確認入れたほうがいいですね
ありがとうございます!

@daima3629
Copy link
Owner Author

少し設計を変更させていただきました。

  • Commandrequest, Commandrequest_approve, Commandrequest_denyをcommand.groupで統合
  • requestrequest createへ変更
  • request_approverequest_denyrequest respondへ統合, リアクションで承認するか拒否するか指定するように
  • (おまけ)それぞれエイリアスを設定

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants