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

リアクション待ち時間の改善、ログレベル見直し、ラジコ検索機能の追加 #43

Merged
merged 5 commits into from
Jan 14, 2021
80 changes: 41 additions & 39 deletions cogs/admincog.py

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions cogs/messagecog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from discord.ext import commands # Bot Commands Frameworkのインポート
from .modules.grouping import MakeTeam
from .modules.radiko import Radiko
from logging import getLogger

import discord
Expand Down Expand Up @@ -80,6 +81,30 @@ async def poll(self, ctx, arg1=None, *args):
for emoji, arg in zip(POLL_CHAR, args):
await message.add_reaction(emoji)

@commands.command(aliases=['rs','radiko','radikoKensaku','rk'], description='Radikoの番組表を検索する機能です')
async def radikoSearch(self, ctx, *args):
"""
このコマンドを実行すると、Radikoの番組表を検索することができます。
1番目の引数(キーワード): 検索する対象。**半角スペースがある場合、"(二重引用符)で囲って**ください。
2番目の引数(検索対象): 過去(past)、未来(future)を検索対象とします。未指定か不明な場合、allが採用されます
3番目の引数(地域): XX県かJP01(数字は県番号)と指定すると、その地域の番組表を検索します。未指定か不明の場合はデフォルトの地域が採用されます。
*あんまり検索結果が多いと困るので、一旦5件に制限しています。
"""
usage = '/radikoSearchの使い方\n 例:`/radikoSearch 福山雅治 東京都`\nRadikoの番組表を検索した結果(件数や番組の時間など)をチャンネルへ投稿します。詳しい使い方は`/help radikoSearch`で調べてください'

# 引数の数をチェック
if len(args) == 0:
await ctx.channel.send(usage)
elif len(args) > 3:
await ctx.channel.send(f'引数は3件までです!\n{usage}')
else:
radiko = Radiko()
embed = await radiko.radiko_search(*args)
if not radiko.r_err:
await ctx.channel.send(content=radiko.content, embed=embed)
else:
await ctx.channel.send(radiko.r_err)

@team.error
async def team_error(self, ctx, error):
if isinstance(error, commands.CommandError):
Expand All @@ -92,5 +117,11 @@ async def group_error(self, ctx, error):
logger.error(error)
await ctx.send(error)

@radikoSearch.error
async def radikoSearch_error(self, ctx, error):
if isinstance(error, commands.CommandError):
logger.error(error)
await ctx.send(error)

def setup(bot):
bot.add_cog(MessageCog(bot)) # MessageCogにBotを渡してインスタンス化し、Botにコグとして登録する
2 changes: 2 additions & 0 deletions cogs/modules/auditlogchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ async def get_ch(self, guild:discord.Guild):
logger.debug(self.channel)
if self.channel is not None:
return True

self.alc_err = '管理用のチャンネルが登録されていません。'
logger.error(self.alc_err)
return False
123 changes: 123 additions & 0 deletions cogs/modules/radiko.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from logging import getLogger

import aiohttp
import datetime, random, hashlib
import discord
import re
import mojimoji

logger = getLogger(__name__)

class Radiko:
PREF_CD = {'北海道':'01','青森県':'02','岩手県':'03','宮城県':'04','秋田県':'05','山形県':'06','福島県':'07','茨城県':'08','栃木県':'09','群馬県':'10','埼玉県':'11','千葉県':'12','東京都':'13','神奈川県':'14','新潟県':'15','富山県':'16','石川県':'17','福井県':'18','山梨県':'19','長野県':'20','岐阜県':'21','静岡県':'22','愛知県':'23','三重県':'24','滋賀県':'25','京都府':'26','大阪府':'27','兵庫県':'28','奈良県':'29','和歌山県':'30','鳥取県':'31','島根県':'32','岡山県':'33','広島県':'34','山口県':'35','徳島県':'36','香川県':'37','愛媛県':'38','高知県':'39','福岡県':'40','佐賀県':'41','長崎県':'42','熊本県':'43','大分県':'44','宮崎県':'45','鹿児島県':'46','沖縄県':'47'}
RADIKO_URL = 'http://radiko.jp/v3/api/program/search'
LIMIT_NUM = 5
DEFAULT_AREA = 'JP13'

def __init__(self):
self.content = ''
self.r_err = ''

async def radiko_search(self, keyword='', filter='', areaName=''):
# どっちも空文字になるなら、入れ替えて検索する
if self.convert_filter(filter) == self.convert_prefCd(areaName) == '':
filter,areaName = areaName,filter
response = await self.search_radiko_result(keyword, self.convert_filter(filter), self.convert_prefCd(areaName))

if response is None:
return

result_count = response['meta']['result_count']
self.content = f"検索結果は、{result_count}件です"
if result_count > self.LIMIT_NUM:
self.content += f'(ただし、{self.LIMIT_NUM}件を超えた部分については表示されません)'
elif result_count == 0:
return

embed_field = ''

count = 1
for key in response['data']:
data = ''
data += f"**title:{key['title']}**"
if key['start_time'][:10] == key['end_time'][:10]:
start2end = key['start_time'][:16] + '-' + key['end_time'][11:16]
else:
start2end = key['start_time'][:16] + '-' + key['end_time'][:16]
data += '\ndatetime:' + start2end
data += '\nstation_id:' + key['station_id']
data += '\nperformer:' + key['performer'] if key['performer'] != '' else '\nperformer:(なし)'

info_data = re.sub(r'<br *?/>', '@@', key['info'])
info_data = re.sub(r'<[^>]*?>', '', info_data)
info_data = re.sub(r'\t|\n|(&[lg]t; *)', '', info_data)
info_data = re.sub(r'@+ +?@+', '', info_data)
info_data = re.sub(r'@{2,}', ' ', info_data)
info_data = info_data.strip()
info_data = mojimoji.zen_to_han(info_data, kana=False)
if len(embed_field + data + '\ninfo:' + info_data) > 1700:
data += '\ninfo:(文字数超えのため削除)'
logger.debug('文字数超で削除されたinfo:' + info_data)
else:
data += f'\ninfo:{info_data}'

embed_field += f'{str(count)}件目: {data}\n'
count = count + 1
if count > self.LIMIT_NUM:
break

embed = discord.Embed(title = f'keyword:{keyword}, filter:{filter}, areaName:{areaName} の結果です', description=embed_field,type='rich', inline=False)
return embed

def generate_uid(self):
"""
rdk_uid生成関数
"""
rnd = random.random() * 1000000000
ms = datetime.timedelta.total_seconds(datetime.datetime.now() - datetime.datetime(1970, 1, 1)) * 1000
return hashlib.md5(str(rnd + ms).encode('utf-8')).hexdigest()

def convert_prefCd(self, prefName):
if ('都' in prefName or '道' in prefName or '府' in prefName or '県' in prefName) and self.PREF_CD[prefName].isdecimal():
return 'JP' + self.PREF_CD[prefName]
elif 'JP' in prefName:
return prefName
else:
return ''

def convert_filter(self, filter):
if '未来' in filter or 'future' in filter:
return 'future'
elif '過去' in filter or 'past' in filter:
return 'past'
else:
return ''

async def search_radiko_result(self, keyword='', filter='', area_id=DEFAULT_AREA):
# ref. https://turtlechan.hatenablog.com/entry/2019/06/25/195451
area_id_with_defalut_area = self.DEFAULT_AREA if area_id == '' else area_id
params = {
'key': keyword,
'filter': filter,
'start_day': '',
'end_day': '',
'area_id': area_id_with_defalut_area,
'region_id': '',
'cul_area_id': area_id_with_defalut_area,
'page_idx': '0',
'uid': self.generate_uid(),
'row_limit': '10',
'app_id': 'pc',
'action_id': '0',
}
response = None

async with aiohttp.ClientSession() as session:
async with session.get(self.RADIKO_URL, params=params) as r:
if r.status == 200:
response = await r.json()
return response
else:
self.r_err = 'Radikoの番組表検索に失敗しました。'
logger.warn(self.r_err)
return
11 changes: 7 additions & 4 deletions cogs/modules/reactionchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async def get_discord_attachment_file(self):
Attachment_file_date = last_message[0].created_at
file_path = join(dirname(__file__), 'files' + os.sep + self.FILE)
await last_message[0].attachments[0].save(file_path)
logger.debug(f'channel_file_save:{guild.name}')
logger.info(f'channel_file_save:{guild.name}')
else:
logger.warn(f'{guild}: に所定のチャンネルがありません')
else:
Expand Down Expand Up @@ -91,7 +91,7 @@ async def set_discord_attachment_file(self, guild:discord.Guild):
overwrites = dict(zip(target, permissions))

try:
logger.debug(f'***{self.REACTION_CHANNEL}を作成しました!***')
logger.info(f'***{self.REACTION_CHANNEL}を作成しました!***')
get_control_channel = await guild.create_text_channel(name=self.REACTION_CHANNEL, overwrites=overwrites)
except discord.errors.Forbidden:
logger.error(f'***{self.REACTION_CHANNEL}の作成に失敗しました!***')
Expand All @@ -105,7 +105,7 @@ async def set_discord_attachment_file(self, guild:discord.Guild):
# チャンネルにファイルを添付する
file_path = join(dirname(__file__), 'files' + os.sep + self.FILE)
await get_control_channel.send(self.FILE, file=discord.File(file_path))
logger.debug(f'***{get_control_channel.name}へファイルを添付しました!***')
logger.info(f'***{get_control_channel.name}へファイルを添付しました!***')

logger.debug('set_discord_attachment_file is over!')

Expand Down Expand Up @@ -167,6 +167,7 @@ async def save(self, guild:discord.Guild):
except pickle.PickleError:
# 書き込みに失敗したらなにもしない
self.rc_err = '保管に失敗しました。'
logger.error(self.rc_err)

# 追加するリアクションチャネルが問題ないかチェック
def check(self, ctx, reaction:str, channel:str):
Expand Down Expand Up @@ -241,7 +242,9 @@ async def add(self, ctx, reaction:str, channel:str):
if await self.save(guild) is False:
return self.rc_err

return f'リアクションチャンネルの登録に成功しました!\n{reaction} → <#{get_channel.id}>'
msg = f'リアクションチャンネルの登録に成功しました!\n{reaction} → <#{get_channel.id}>'
logger.info(msg)
return msg

async def list(self, ctx):
guild = ctx.guild
Expand Down
36 changes: 19 additions & 17 deletions cogs/modules/scrapboxsidandpnames.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def setup(self, guild:discord.Guild):
if len(self.targets) > 0:
return True

self.sbsu_err = '展開対象のSCRAPBOXのSID、プロジェクトが登録されていません。'
self.sbsu_err = '展開対象のScrapboxのSID、プロジェクトが登録されていません。'
logger.error(self.sbsu_err)
return False

def check(self, message:discord.Message):
Expand All @@ -76,24 +77,25 @@ async def expand(self, message:discord.Message):

json = None
target_url = ''
if mo is not None:
if mo.group(1):
target_url = 'https://scrapbox.io/api/pages/'+ self.target_project + '/' + mo.group(1)
if mo is not None and mo.group(1):
target_url = 'https://scrapbox.io/api/pages/'+ self.target_project + '/' + mo.group(1)

async with aiohttp.ClientSession(cookies=cookies) as session:
async with session.get(target_url) as r:
if r.status == 200:
json = await r.json()
async with aiohttp.ClientSession(cookies=cookies) as session:
async with session.get(target_url) as r:
if r.status == 200:
json = await r.json()

if json is not None:
embed = discord.Embed(title = json['title'], description = " ".join(json['descriptions']), url=mo.group(0), type='rich')
embed.set_author(name=json['user']['displayName'], icon_url=json['user']['photo'])
if json is not None:
embed = discord.Embed(title = json['title'], description = " ".join(json['descriptions']), url=mo.group(0), type='rich')
embed.set_author(name=json['user']['displayName'], icon_url=json['user']['photo'])

if json['image']:
embed.set_thumbnail(url=json['image'])
if json['image']:
embed.set_thumbnail(url=json['image'])

updated = datetime.datetime.fromtimestamp(json['updated'])
embed.add_field(name='Updated', value=updated.strftime('%Y/%m/%d(%a) %H:%M:%S'))

return embed
updated = datetime.datetime.fromtimestamp(json['updated'])
embed.add_field(name='Updated', value=updated.strftime('%Y/%m/%d(%a) %H:%M:%S'))

return embed
else:
logger.debug('展開対象外のため、対応しない')
return
8 changes: 4 additions & 4 deletions cogs/onmessagecog.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,17 @@ async def on_message(self, message: discord.Message):
if message.author == botUser:# 自分は無視する
return

if self.scrapboxSidAndPnames.SCRAPBOX_URL_PATTERN in message.clean_content:
if self.scrapboxSidAndPnames.setup(message.guild):
await self.scrapbox_url_expand(message)
if self.scrapboxSidAndPnames.SCRAPBOX_URL_PATTERN in message.clean_content and self.scrapboxSidAndPnames.setup(message.guild):
await self.scrapbox_url_expand(message)
else:
return

# ScrapboxのURLを展開
async def scrapbox_url_expand(self, targetMessage: discord.Message):
if (self.scrapboxSidAndPnames.check(targetMessage)):
embed = await self.scrapboxSidAndPnames.expand(targetMessage)
await targetMessage.channel.send(embed=embed)
if embed is not None:
await targetMessage.channel.send(embed=embed)
else:
return

Expand Down
15 changes: 8 additions & 7 deletions cogs/reactionchannelercog.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ReactionChannelerCog(commands.Cog, name="リアクションチャンネラ
リアクションチャンネラー機能のカテゴリ(リアクションをもとに実行するアクション含む)。
"""
SPLIT_SIZE = 1900
TIMEOUT_TIME = 30.0

# ReactionChannelerCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
def __init__(self, bot):
Expand Down Expand Up @@ -53,7 +54,7 @@ async def add(self, ctx, reaction:str=None, channel:str=None):
"""
# リアクション、チャンネルがない場合は実施不可
if reaction is None or channel is None:
await ctx.channel.purge(limit=1)
await ctx.message.delete()
await ctx.channel.send('リアクションとチャンネルを指定してください。\nあなたのコマンド:`{0}`'.format(ctx.message.clean_content))
return
msg = await self.reaction_channel.add(ctx, reaction, channel)
Expand All @@ -74,23 +75,23 @@ async def list(self, ctx):
async def purge(self, ctx):
"""
リアクションチャンネラー(*)で反応する絵文字を全て削除します。
10秒以内に👌(ok_hand)のリアクションをつけないと実行されませんので、素早く対応ください。
30秒以内に👌(ok_hand)のリアクションをつけないと実行されませんので、素早く対応ください。
*指定した絵文字でリアクションされた時、チャンネルに通知する機能のこと
"""
command_author = ctx.author
# 念の為、確認する
confirm_text = f'全てのリアクションチャンネラーを削除しますか?\n 問題ない場合、10秒以内に👌(ok_hand)のリアクションをつけてください。\nあなたのコマンド:`{ctx.message.clean_content}`'
await ctx.channel.purge(limit=1)
confirm_text = f'全てのリアクションチャンネラーを削除しますか?\n 問題ない場合、30秒以内に👌(ok_hand)のリアクションをつけてください。\nあなたのコマンド:`{ctx.message.clean_content}`'
await ctx.message.delete()
await ctx.channel.send(confirm_text)

def check(reaction, user):
return user == command_author and str(reaction.emoji) == '👌'

# リアクション待ち
try:
reaction, user = await self.bot.wait_for('reaction_add', timeout=10.0, check=check)
reaction, user = await self.bot.wait_for('reaction_add', timeout=self.TIMEOUT_TIME, check=check)
except asyncio.TimeoutError:
await ctx.channel.send('→リアクションがなかったのでキャンセルしました!')
await ctx.channel.send('→リアクションがなかったので、リアクションチャンネラーの全削除をキャンセルしました!')
else:
msg = await self.reaction_channel.purge(ctx)
await ctx.channel.send(msg)
Expand All @@ -105,7 +106,7 @@ async def delete(self, ctx, reaction:str=None, channel:str=None):
"""
# リアクション、チャンネルがない場合は実施不可
if reaction is None or channel is None:
await ctx.channel.purge(limit=1)
await ctx.message.delete()
await ctx.channel.send('リアクションとチャンネルを指定してください。\nあなたのコマンド:`{0}`'.format(ctx.message.clean_content))
return
msg = await self.reaction_channel.delete(ctx, reaction, channel)
Expand Down
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Discord用のBot。discord.pyのBot Commands Frameworkを使用して実装。
`/vcmembers` ボイスチャンネルに接続しているメンバーリストを取得
![image(vcmembers)](https://github.com/tetsuya-ki/images/blob/main/discord-bot-heroku/vcmembers.png?raw=true)

`/radikoSearch` ラジコの番組表を検索する機能
![image(radikoSearch)](https://github.com/tetsuya-ki/images/blob/main/discord-bot-heroku/radiko_search.png?raw=true)

### 管理用カテゴリ(admincog.pyで実装)

`/channel` チャンネルを操作するコマンド(サブコマンド必須)。チャンネルの操作権限を渡すと、削除も可能だから嫌だなと思って作ったコマンド。
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ typing-extensions==3.7.4.3
websockets==6.0
wrapt==1.12.1
yarl==1.5.1
zope.interface==5.1.0
zope.interface==5.1.0
mojimoji==0.0.11