原神のステータスを取得するDiscordBotを作った話

こんにちは．初投稿です．
めっちゃわかりにくいとは思いますが、部分的にでも参考になれば幸いです．

原神っていろんなステータスがあるんですけど、
確認するときっていちいち原神を開かないといけないんですよね．

HoYoLabという公式ツールもあるのですが、
それで確認できるのはほんとに最小限という感じで、聖遺物のステータスなどは確認できません．

なのでDiscordから聖遺物を確認できるBotを作ってみました．
botを招待して確認したい方はこちらからどうぞ．
https://discord.com/api/oauth2/authorize?client_id=1014120722404220998&permissions=2147485696&scope=bot

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/2c6b398e-f22d-ad69-1ab8-21a9d1cd1185.png)
こんな感じです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/d93c0f72-431d-ec50-8cd8-f076e7793424.png)
キャラ情報も取得できます．
武器の詳細については現在作成中です．

# 今回作るbotの材料

原神のステータスを取得するためのAPIには、
ステータスを一発で画像にしてくれると有名な、
https://enka.network/
こちらのサイトを利用させていただきました．
画像などもこちらのサイトから取得できるので便利です．

botにはPycordを使います．
```py
python3 -m pip install pycord
```
でインストールします．
一応バージョンは2.0.0を使用します．

# コードを書く

とりあえずみんな大好き（？）なCogを使っていきます．

Cogというのは私もよくわかっていないのですが、
たくさんのコマンドをまとめられるやつだと思っています...
Cogsフォルダにコマンドを書いたpythonファイルを突っ込むと動きます．
（Cogについてよくわかれば追記します．）

まずはmainのやつから
```main.py
from discord.ext import commands

bot = commands.Bot()
path = "./cogs"

@bot.event
async def on_application_command_error(ctx, error):
    if isinstance(error, commands.CommandOnCooldown):
        await ctx.respond(error)
    if isinstance(error, commands.MissingPermissions):
        await ctx.respond(content="BOT管理者限定コマンドです", ephemeral=True)

@bot.event
async def on_ready():
    print(f"Bot名:{bot.user} On ready!!")

bot.load_extension('cogs.genshin', store=False)

bot.run("TOKEN")
```

最後の「TOKEN」にはbotのトークンを貼り付けてください．
botの作成の仕方は今回は省略とさせていただきます。

こちらのmain.pyと同じディレクトリに「cogs」というフォルダを作成してください．
その中に、「genshin.py」というファイルを作成します．

ファイル名を変える場合には、main.pyの
```py
bot.load_extension('cogs.genshin')
```
の部分を
```py
bot.load_extension('cogs.任意のファイル名.py')
```
としてください．

```genshin.py
import discord
from discord.ui import Select,View,Button
from discord.ext import commands
from discord import Option, SlashCommandGroup
import aiohttp
from lib.yamlutil import yaml
import lib.getStat as getStat
from typing import List

dataYaml = yaml(path='genshin_avater.yaml')
data = dataYaml.load_yaml()
charactersYaml = yaml(path='characters.yaml')
characters = charactersYaml.load_yaml()
genshinJpYaml = yaml(path='genshinJp.yaml')
genshinJp = genshinJpYaml.load_yaml()
l: list[discord.SelectOption] = []

class TicTacToeButton(discord.ui.Button["TicTacToe"]):
    def __init__(self, label: str, uid: str, dict):
        super().__init__(style=discord.ButtonStyle.secondary, label=label)
        self.dict = dict
        self.uid = uid

    async def callback(self, interaction: discord.Interaction):
        assert self.view is not None
        view: TicTacToe = self.view

        self.style = discord.ButtonStyle.success
        content = self.label
        #ラベル（名前）からIDを割り出す
        #多分「名前：iD」ってなってるはず
        id = self.dict[self.label]
        print(interaction.user.id)
        for child in self.view.children:
            child.style = discord.ButtonStyle.gray
        await interaction.response.edit_message(content=content, embed=await getStat.get(self.uid, id), view=None)

class TicTacToe(discord.ui.View):
    children: List[TicTacToeButton]

    def __init__(self, data, uid):
        super().__init__(timeout=190)
        names = []
        dict = {}
        #入ってきたidを名前にしてリスト化
        for id in data:
            id = str(id)
            print(id)
            name = characters[id]["NameId"]
            print(name)
            name = genshinJp[name]
            print(name)
            names.append(name)
            dict.update({name: id})
            print(dict)
        #名前をラベル、ついでにdictとuidも送り付ける
        for v in names:
            self.add_item(TicTacToeButton(v,uid,dict))

class GenshinCog(commands.Cog):

    def __init__(self, bot):
        print('genshin初期化')
        self.bot = bot

    async def getApi(self,uid):
        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
        try:
            embed = discord.Embed( 
                                title=f"{resp['playerInfo']['nickname']}の原神ステータス",
                                color=0x1e90ff, 
                                description=f"uid: {uid}", 
                                url=url 
                                )
                                
            hoge = data[resp['playerInfo']['profilePicture']['avatarId']]['iconName']
            embed.set_thumbnail(url=f"https://enka.network/ui/{hoge}.png")
            try:
                embed.add_field(inline=False,name="ステータスメッセージ",value=resp['playerInfo']['signature'])
            except:
                print("hoge")
            embed.add_field(inline=False,name="冒険ランク",value=resp['playerInfo']['level'])
            embed.add_field(inline=False,name="世界ランク",value=resp['playerInfo']['worldLevel'])
            embed.add_field(inline=False,name="深境螺旋",value=f"第{resp['playerInfo']['towerFloorIndex']}層 第{resp['playerInfo']['towerLevelIndex']}間")
            return embed
        except:
            embed = discord.Embed( 
                    title=f"エラーが発生しました。APIを確認してからもう一度お試しください。\n{url}",
                    color=0x1e90ff, 
                    url=url 
                    )
            return embed

    genshin = SlashCommandGroup('genshinstat', 'test')

    @genshin.command(name="get", description="UIDからキャラ情報を取得します")
    async def genshin_get(
            self,
            ctx: discord.ApplicationContext,
            uid: Option(str, required=True, description='UIDを入力してください．'),
    ):
        await ctx.respond(content="アカウント情報読み込み中...", ephemeral=True)  
        embed = await GenshinCog.getApi(self,uid)
        await ctx.respond(content="キャラ情報読み込み中...", ephemeral=True)  

        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
                resalt = []
        for id in resp["playerInfo"]["showAvatarInfoList"]:
            resalt.append(id["avatarId"])
        await ctx.respond(content=None,embed=embed,view=TicTacToe(resalt,uid))

def setup(bot):
    bot.add_cog(GenshinCog(bot))
```
くっそ長いですね．
長すぎるので細かくは紹介しません．
多分ほんとはもっと省略できそうですが、私の技術力ではできませんでした...
簡単に解説していきます．

## コマンド部分
```py 
    @genshin.command(name="get", description="UIDからキャラ情報を取得します")
    async def genshin_get(
            self,
            ctx: discord.ApplicationContext,
            uid: Option(str, required=True, description='UIDを入力してください．'),
    ):
        await ctx.respond(content="アカウント情報読み込み中...", ephemeral=True)  
        embed = await GenshinCog.getApi(self,uid)
        await ctx.respond(content="キャラ情報読み込み中...", ephemeral=True)  

        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
                resalt = []
        for id in resp["playerInfo"]["showAvatarInfoList"]:
            resalt.append(id["avatarId"])
        await ctx.respond(content=None,embed=embed,view=TicTacToe(resalt,uid))
```
ここでスラッシュコマンドを定義しておきます．
やってることは単純で、
　①まずrespond（返信）をする（読み込みが長くタイムアウトしてしまう可能性があるため）
　②アカウントの情報（レベルなど）をembedとして取得
　③そのアカウントの持っているキャラ情報の取得
です．
キャラ情報については、キャラのIDをリストとして、ボタンの処理について書かれたクラスにUIDと一緒に渡します．

```py
class TicTacToe(discord.ui.View):
    children: List[TicTacToeButton]

    def __init__(self, data, uid):
        super().__init__(timeout=190)
        names = []
        dict = {}
        #入ってきたidを名前にしてリスト化
        for id in data:
            id = str(id)
            name = genshinJp[characters[str(id)]["NameId"]]
            names.append(name)
            dict.update({name: id})
        #名前をラベル、ついでにdictとuidも送り付ける
        for v in names:
            self.add_item(TicTacToeButton(v,uid,dict))
```
こちらはボタン部分です．
Discordで見るとこのようになっていますね．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/b8fd7861-ac72-6223-f33b-1209949bbdce.png)
ここから見たいキャラを選ぶ感じです．

ここにはリストになったキャラのidが突っ込まれてくるので、
idからキャラ名を取得
日本語化して表示しています．
また、キャラ詳細でidが必要なので、一緒に辞書型としてボタンの処理部分に丸投げします．

```py
class TicTacToeButton(discord.ui.Button["TicTacToe"]):
    def __init__(self, label: str, uid: str, dict):
        super().__init__(style=discord.ButtonStyle.secondary, label=label)
        self.dict = dict
        self.uid = uid

    async def callback(self, interaction: discord.Interaction):
        assert self.view is not None
        view: TicTacToe = self.view

        self.style = discord.ButtonStyle.success
        content = self.label
        #ラベル（名前）からIDを割り出す
        #多分「名前：iD」ってなってるはず
        id = self.dict[self.label]
        print(interaction.user.id)
        for child in self.view.children:
            child.style = discord.ButtonStyle.gray
        await interaction.response.edit_message(content=content, embed=await getStat.get(self.uid, id), view=None)
```
ボタンの処理部分です．
さっきのボタンの部分にidがくっついているので、押されたボタンのラベル（キャラ名）に関連したidを、キャラ詳細を表示するbotにuidと一緒に丸投げします．

```py
async def get(uid,id):
    url = f"https://enka.network/u/{uid}/__data.json"
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            resp = await response.json()
    name = characters[id]["NameId"]
    name = genshinJp[name]
    print(id)
    id = int(id)
    try:
        for n in resp['avatarInfoList']:
            if n["avatarId"] == id:
                chara = n
                print("hogehogheogheohgpoihvgp;ogiazwqp;oabwo")
                break
            else:
                continue
        for n in resp['playerInfo']["showAvatarInfoList"]:
            print(n["avatarId"])
            if n["avatarId"] == id:
                level = n["level"]
                print("hogehogehoge")
                break
            else:
                continue
    except:
        embed = discord.Embed( 
            title="エラー",
            color=0x1e90ff,
            description=f"キャラ詳細が非公開です。原神の設定で公開設定にしてください。", 
            url=url 
        )
        return embed
    try:
        embed = discord.Embed( 
                    title=name,
                    color=0x1e90ff, 
                    description=f"{level}lv", 
                    url=url 
                    )
        hoge = characters[str(id)]["sideIconName"]
        embed.set_thumbnail(url=f"https://enka.network/ui/{hoge}.png")
        embed.add_field(inline=True,name="キャラレベル",value=f"{level}lv")
        embed.add_field(inline=True,name="キャラ突破レベル",value=str(chara["propMap"]["1002"]["ival"]))
        embed.add_field(inline=True,name="HP",
            value=f'{str(round(chara["fightPropMap"]["1"]))} + {str(round(chara["fightPropMap"]["2000"]) - round(chara["fightPropMap"]["1"]))} = {str(round(chara["fightPropMap"]["2000"]))}'
        )
        embed.add_field(inline=True,name="攻撃力",
            value=f'{str(round(chara["fightPropMap"]["4"]))} + {str(round(chara["fightPropMap"]["2001"]) - round(chara["fightPropMap"]["4"]))} = {str(round(chara["fightPropMap"]["2001"]))}'
        )
        embed.add_field(inline=True,name="防御力",
            value=f'{str(round(chara["fightPropMap"]["7"]))} + {str(round(chara["fightPropMap"]["2002"]) - round(chara["fightPropMap"]["7"]))} = {str(round(chara["fightPropMap"]["2002"]))}'
        )
        embed.add_field(inline=True,name="会心率",
            value=f'{str(round(chara["fightPropMap"]["20"] *100))}%'
        )
        embed.add_field(inline=True,name="会心ダメージ",
            value=f'{str(round(chara["fightPropMap"]["22"]*100))}%'
        )
        embed.add_field(inline=True,name="元素チャージ効率",
            value=f'{str(round(chara["fightPropMap"]["23"]*100))}%'
        )
        embed.add_field(inline=True,name="元素熟知",
            value=f'{str(round(chara["fightPropMap"]["28"]))}'
        )
        buf = 1
        if round(chara["fightPropMap"]["30"]*100) > 0:
            embed.add_field(inline=True,name="物理ダメージ",
                value=f'{str(round(chara["fightPropMap"]["30"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["30"])
        elif round(chara["fightPropMap"]["40"]*100) > 0:
            embed.add_field(inline=True,name="炎元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["40"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["40"])
        elif round(chara["fightPropMap"]["41"]*100) > 0:
            embed.add_field(inline=True,name="雷元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["41"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["41"])
        elif round(chara["fightPropMap"]["42"]*100) > 0:
            embed.add_field(inline=True,name="水元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["42"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["42"])
        elif round(chara["fightPropMap"]["43"]*100) > 0:
            embed.add_field(inline=True,name="草元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["43"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["42"])
        elif round(chara["fightPropMap"]["44"]*100) > 0:
            embed.add_field(inline=True,name="風元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["44"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["44"])
        elif round(chara["fightPropMap"]["45"]*100) > 0:
            embed.add_field(inline=True,name="岩元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["45"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["45"])
        elif round(chara["fightPropMap"]["46"]*100) > 0:
            embed.add_field(inline=True,name="氷元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["46"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["46"])
        temp = []
        for myvalue in chara["skillLevelMap"].values():
            temp.append(f"{myvalue}")
        embed.add_field(inline=False,name="天賦レベル",
            value="\n".join(temp)
        )
        for n in chara["equipList"]:
            name = genshinJp[n["flat"]["setNameTextMapHash"]]
            equip = genshinJp[n["flat"]["equipType"]]
            main = genshinJp[n["flat"]["reliquaryMainstat"]["mainPropId"]]
            hoge = []
            for b in n["flat"]["reliquarySubstats"]:
                name_ = genshinJp[b["appendPropId"]]
                value_ = b["statValue"]
                hoge.append(f"{name_}：{value_}")
            embed.add_field(inline=True,name=f'聖遺物：{equip}\n{name}\n{main}：{n["flat"]["reliquaryMainstat"]["statValue"]}\n{n["reliquary"]["level"]-1}lv\n',
                value="\n".join(hoge)
            )
        return embed
    except KeyError:
        return embed
```
キャラ詳細表示部分です．
main.pyのディレクトリlibというフォルダを作り、その中にgetStat.pyというファイルを入れています．

こちらは長いだけなので特に複雑な処理はしていません．
丸投げされたUIDとキャラIDから、その人のキャラ詳細のJsonが帰ってくるので
それを分解して表示するだけです．

武器なども表示できると思いますが、時間がなくてまだできていません．
今後追記すると思います．

# おわりに
以上となります。
多分これから修正点がたくさん見つかると思いますが、随時追記していきます．
Qiitaを書くのは初めてなのでわかりずらいとは思いますが、少しでも参考になれば幸いです．
また、手伝っていただいた[にかわみかん](https://qiita.com/nikawamikan)さん、ありがとうございました。