길드용 로스트아크 디스코드 봇 (5) 업데이트 기능 버튼 추가

Jeuk Oh·2021년 8월 15일
2

변경 점

개요

사람들이 어떤 기능이 있는지 어떻게 쓰는지를 잘 몰라 활용을 못한다. 편의성과 접근성을 높이고자 버튼으로 접근하도록 한다.

아이디어

계정 업데이트같은 경우 가장 좋은 방법은 주기적인 시간마다 봇이 자동으로 해주는 것이다. 보통 하루주기가 적당할 듯하다. 하지만 일단 유저들이 아이템 레벨을 올렸을 때 즉각적인 갱신을 원한다면 (마치 OP.GG에서 롤 전적을 갱신하는 것처럼) 직접 클릭할 수 있게 하는 것이 좋을 것이다. 기존의 !업데이트 기능은 관리자가 아닌 이상 인자 없이 명령어만 치면 실행되었으니 이를 버튼으로 바꾸어주었다.

준비물

상호작용 관련 기능들은 아직 discord 1.7.3 버전 라이브러리에선 지원하지 않는다. 다음 업데이트가 2.0.0이 되면서 지원할 것이라고 하는데, 언제가 될 지 모르므로 버튼 기능을 지원해주는 외부 라이브러리를 찾아서 썼다.

  • discord_buttons_plugin

https://github.com/SilentJungle399/discord_buttons_plugin

버튼 라이브러리가 몇 개 더 있었지만 이게 가장 문서도 자세하고, 쓰임도 간단해보여서 이 친구를 사용했다.

코드

코드에 주석을 달아가며 한 줄씩 리뷰하겠다.

update 함수 -> 기존 업데이트 함수를 수정해서 버튼이 눌렸을 시 업데이트 하는 핵심 기능

# 클라이언트 데코레이터로 명령어, 이전 !업데이트 함수를 그대로 활용하되 몇가지가 바뀌었다.

@client.command(name='계정업데이트', aliases=['게정업뎃','업뎃','게정업데이트','업데이트','계정업뎃'],\
                    help='계정업데이트 ID\n 연동된 계정의 변경점을 업데이트합니다.',\
                    prefix="!")
    async def update(ctx, *args):
    	# 기존엔 아웃풋을 await ctx.send로 즉각적으로 display하였다면, 이제 버튼을 눌렀을 시에 보이게 해야하므로 아웃풋을 담아줄 리스트를 추가한다.
        outmessage = []
        # button 라이브러리의 ctx에는 버튼을 누른 사람의 정보가 Member class가 아닌 User class로 담겨있다. 
        # 어떤 경우이든 Member class의 유저 정보를 받도록 (이미 Member class를 사용하였으므로) author를 받는 함수를 추가하였다. 
        author = await author_return(ctx)
        # args가 있으면 관리자의 타인 계정 업데이트 상황. 없다면 args는 author가 된다.
        if author.guild_permissions.administrator and args:
            args = args
        else:
            args = (author.nick,)
		
        # args에 대해서 업데이트를 진행하되 먼저 기존 정보를 지우고 크롤링 데이터를 다시 재추가 방향으로 변환하였다.
        # 다만 여기서 로아 서버가 점검 중이라면 크롤링에서 에러가 나서 계정정보만 지우고 함수가 종료되는  문제가 있었다.
        # 크롤링 이후 크롤링 정보에 예외처리를 한번 거치고 기존 정보를 지워야했는데 생각이 부족했다.
        for Id in args:
            hash = DB.load(tablename='user_data', return_hash=True, nick=Id)
            if not hash:
                outmessage.append(await ctx.channel.send(f'DB에 {Id}님의 저장된 데이터가 없습니다. 계정 연동을 먼저 진행해주세요.'))
                return outmessage
            DB.delete(tablename='user_data', hash=hash)
            call_data = DB.call(hash, Id, update=True)
            # call data 자료형  [[server,이름,직업,템렙],[...]]
            # 디스코드 역할과 이름을 다시 적용해주고
            name, cl, lv = call_data[0][1:]
            role_name = role_out(lv)
            role = discord.utils.get(ctx.guild.roles, name=role_name)
            user = discord.utils.find(lambda m: m.nick == call_data[0][1], client.get_all_members())
            await nick_role_change(ctx, role, name, cl, user)
            
            # 메세지를 해당 채널에 send하여 디스플레이하고 메세지 클래스들을 담은 리스트를 리턴한다. 
            # (나중에 지워주기 위해서)
            outmessage.append(await ctx.channel.send(f'{author.nick} 님이 요청하신 {Id} 계정정보가 업데이트 되었습니다.\n'))
            outmessage.append(await ctx.channel.send(embed=embed_print(call_data)))

            return outmessage

create_update_button 함수 -> 버튼을 만드는 함수,


@client.command()
async def create_update_button(ctx):
       channel = discord.utils.get(client.get_all_channels(), name='계정_업데이트')
       print(channel.id)
       await buttons.send(
           content = "본인 계정 업데이트 기능을 제공합니다.\n Update 버튼을 누르면 본인의 계정 정보가 자동 업데이트 됩니다.",
           channel = channel.id,
           components = [ActionRow([Button(
                                           label = "Update",
                                           style = ButtonType().Primary,
                                           custom_id = "button_update"
                                           )]
                       )]
       )

계정_업데이트 라는 디스코드 채팅채널을 신설한 뒤 id를 주어, 해당 채널에 버튼을 만들어주는 함수다. 버튼은 custom_id를 키로
이어져 custom_id를 이름으로 하는 함수를 만들면, 버튼이 눌렸을 시의 실행 함수를 만들 수 있다.


button_update -> 버튼이 눌렸을 시 실행되는 함수

button 라이브러리에 치명적인 아쉬운 점이 있었는데, 버튼의 라이브러리의 ctx는 가능한 메서드가 ctx.reply밖에 없었고, ctx.reply는 버튼을 눌렀을 때 create_update_button의 buttons.send 메세지에 답장해주는 기능이었다.

아쉬운 점은 크게 2가지였는데

  • ctx.reply 메서드의 아웃풋이 message 클래스가 아닌 None이라는 점. 메세지에 따로 접근할 수가 없었다.
  • 버튼을 눌렀을 시 함수에 연결하고 작동하는 부분은 쉽지만, ctx.reply를 사용하지 않으면 원하는 결과가 나와도 버튼에 상호작용을 실패했다는 출력이 뜨고 만다. 매우 불편...

내가 의도했던 기능은 KorLARK 디스코드 서버 봇처럼, 버튼을 눌렀을 시, 결과를 출력해주고 결과는 몇 초 이후 자동으로 지워지는 기능이었다. 그렇지 않으면 버튼이 있는 메세지가 계속 다른 메세지에 쌓여 올라가면서 보이지 않게되는 불편함이 있다.

메세지 삭제는 메세지가 message 클래스라면 쉽게 지울 수 있지만, ctx.reply는 리턴이 None이라서 접근해서 삭제하기가 깔끔하지 않았다. (디스코드 라이브러리의 ctx.send는 message 클래스를 반환하여 매우 쉽게 지울 수 있다.)

하지만 ctx.reply를 안쓰고 ctx.send를 사용하게 되면 버튼이 눌렀을 시 보내는 interaction 세션에 대한 callback이 없어서 기능이 잘 작동해도 상호작용 실패라는 반응이 뜬다. 매우 보기 안좋다. 결국 ctx.reply를 직접 고치거나, callback을 직접 보내주는 수 밖에 없었다. 내가 선택한 방법은 후자였다.

라이브러리를 뜯어보니 버튼을 만들고 누를때 discord.http를 사용하여 직접 디스코드 서버에 세션을 보낸다.

자세한 설명은 이 글을 추천한다.
https://blog.yonghyeon.com/53

이 분 포스트를 보면서 도움이 많이 되었다.

아무튼 그래서 코드는...


#버튼이 눌렀을 시에, customd_id였던 button_update가 실행된다. ctx는 discord 라이브러리의 ctx와는 다르다. 누른 사람의
#정보를 Member가 아닌 User 클래스로 제공하는 등 매우 다르다.
@buttons.click
async def button_update(ctx):

	# reply를 사용하지 않을 것이기 때문에 직접 callback을 보낸다. 이 경우 update가 끝날때까지 봇이 생각하는 모양새를 주는
    # 새션을 보낸다.
	await client.http.request(
              Route("POST", f"/interactions/{ctx.id}/{ctx.token}/callback"),
              json={"type": 5},
          )
          
        # 앞선 update 함수에 ctx를 보내서 버튼 누른사람의 ID가 업데이트가 이뤄지게 한다. 아웃풋인 메세지 값들을 받아온다.
    	outmessage = await update(ctx)
		
        # 결과가 나왔으므로 앞선 callback을 삭제해주고
    	await client.http.request(
              Route('DELETE', f"/webhooks/{client.user.id}/{ctx.token}/messages/@original")
          					)
                            
        # 6초가 지난 뒤
    	await asyncio.sleep(6)
        # outmessage들을 삭제해주자.
    	for i in outmessage:
         	await i.delete()

먼가 조잡하지만 원하는 기능은 잘 작동이 되었다.

시연

먼저 계정_업데이트 채널을 만들고 !create_update_button 명령어를 타이핑해 버튼 메세지를 불러온다.

업데이트 버튼을 누를시에

크롤링과 업데이트가 완료될 때 까지 뜨는 생각하는 콜벡

이후 콜벡이 삭제되고 업데이트 메세지가 잘 뜬다.

그리고 6초 뒤에 모든 메세지가 지워지고 다시 버튼 메시지만 남는다.


느낀 점

확실히 버튼으로 명령어를 쓸 수 있도록 접근성을 높이니 사람들이 편하게 그리고 재미로 많이 사용해주는 모습을 볼 수 있었습니다.
(매우 뿌듯)

봇 개발을 하면서 처음 직면하는 문제들이 많았고 풀어나가면서 부족한 개발 실력에 도움이 많이 되지 않았나 생각합니다.
단순히 코딩 경험뿐만 아니라 사용자들의 입장에서 뭐가 불편하고, 무슨 기능이 필요한지 생각하면서 구현하는 과정이 처음이다 보니
어렵지만 재밌었습니다.

하지만 요즘은 로아를 예전처럼 열심히는 못하면서 아무래도 봇 개발에도 진척이 생기고 있습니다. 흑흑


다음 기능

아무래도 로아가 흥겜이 되다보니 길드 사람도 많아지고, 전체적인 스펙들이 누진적으로 올라가면서 문제가 생겼습니다.

가장 필요 레벨이 낮은 아르고스 가용 인원을 확인했을 때 현재 총 47개의 계정이 나타는 상황입니다.
너무 많다보니 오히려 있으나마나 한 기능이 되버렸습니다.

최대한 보여주는 계정수를 줄이기 위해 레이드 클리어 데이터를 모아서 제거해주려는 생각입니다.
하지만 경험상 유저들이 순순히 클리어 정보를 수기로 줄리가 없습니다. (저 또한 귀찮습니다 껄껄)

따라서 기존에 존재하던 모든 레이드 음성채널을 지운 뒤,
버튼으로 레이드 음성채널을 만들수 있게하고 닫을 땐 성공 실패 여부를 알려줘야하는 아이디어를 생각해보았습니다.

실패시 그냥 넘어가고 성공 시에는 채널에 있던 사람들의 클리어 캐릭터 정보를 물어봅니다.

정리 하면 다음으로 만드는 기능은 다음과 같습니다.

  1. 버튼을 활용하여, 레이드 음성채널을 만들고 자동으로 이동해줌 (KorLARK 서버처럼)

  2. 레이드가 완료 될 시 음성 채팅에 참여한 전원에게 클리어 캐릭터를 선택하라는 DM를 주어 클리어 데이터를 기록

  3. 이후 레이드 가능 인원을 표시할 때 이미 클리어 된 인원은 제거해서 보여줌 ( 로요일마다 초기화)

현재 2번까지는 어느정도 구현이 되어있는데, DM을 병렬로 주지않고 한명한명 순서대로 주게 되어 거기서 막혀있습니다.
async 공부를 해야 할 듯 합니다 ㅜ 점점 기능 구현이 어려워지면서 속도도 낮아지고 있는 상황...

공략과 보상을 보여주는 쉬운 기능을부터 만들면서 고민하는 것이 좋아보이지만,
요즘 바쁘고 귀찮아서... 앞으로 연재 텀이 더욱 늘어날 것 같습니다.

profile
개발을 재밌게 하고싶습니다.

11개의 댓글

comment-user-thumbnail
2021년 9월 9일

혹시 만드신 로아 봇을 제가 사용 해볼 수 있을까요?

답글 달기
comment-user-thumbnail
2021년 9월 13일

저도 길드를 운영하면서 이런 봇 만들고 싶은데... 도와주실 수 있으신가요??

답글 달기
comment-user-thumbnail
2021년 9월 20일

죄송하지만 코드는 공유해드리기가 어렵고, 도움이 필요하신 부분 말씀해주시면 아는 부분에선 열심히 알려드리겠습니다.

답글 달기
comment-user-thumbnail
2021년 11월 10일

훌룡하네요 저도 전문가한테 봇구입해서 개인나스에 올려서 사용중인데 들리는예기로는 파이선으로 만든 봇들이 내년 4월부터는 사용이 않된다 하는데 혹시 어떻게 해결되는지 아시나해서 문의 합니다 혹시 해당 코드 골드나 현금으로 구입은 가능한지도요

1개의 답글
comment-user-thumbnail
2021년 11월 16일

안녕하세요 선생님
어찌어찌 해서 뭔가 실행이 되는것 같은데
Ignoring exception in command 계정연동:
Traceback (most recent call last):
File "c:\Users\KeeUi\Documents\GitHub\lostark\discord\ext\commands\core.py", line 85, in wrapped
ret = await coro(*args, **kwargs)
File "c:\Users\KeeUi\Documents\GitHub\lostark\main.py", line 147, in link
call_data = DB.call(hash,Id)
AttributeError: type object 'DB' has no attribute 'call'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "c:\Users\KeeUi\Documents\GitHub\lostark\discord\ext\commands\bot.py", line 939, in invoke
await ctx.command.invoke(ctx)
File "c:\Users\KeeUi\Documents\GitHub\lostark\discord\ext\commands\core.py", line 863, in invoke
await injected(*ctx.args, **ctx.kwargs)
File "c:\Users\KeeUi\Documents\GitHub\lostark\discord\ext\commands\core.py", line 94, in wrapped
raise CommandInvokeError(exc) from exc
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: AttributeError: type object 'DB' has no attribute 'call'

라는 에러가 뜹니다... 혹시 조언을 구할 수 있을까요?

그리고 await ctx.send(embed_print(call_data)) 이쪽에 embed_print 이건 신경 안써도 되는건가요?

1개의 답글