반응형
반응형

반응형

https://dfir.pubpub.org/pub/h78di10n/release/2

 

USB Forensics – Recover more Volume Serial Numbers (VSNs) with the Windows 10 Partition/Diagnostic Event Log · DFIR Review

Synopsis Forensics Question: How many Volume Serial Numbers (VSNs) of previously connected devices can be recovered from a single Windows event log?OS Version: Microsoft Windows 10 Pro 2004 Build 19041 (Original Tests)Microsoft Windows 10 Pro 20H2 Build 19

dfir.pubpub.org

저자: Alexandros Vasilaras 1 , Evangelos Dragonas 2 , Dimitrios Katsoulis 3

vasilaras@digitalforensics.gr , dragonas@digitalforensics.gr , katsulis@digitalforensics.gr

라이선스:  Creative Commons Attribution 4.0 국제 라이선스(CC-BY 4.0)

 

 

- 개요

질문 : Windows 이벤트 로그에서 이전에 연결된 장치의 VSN을 몇 개나 복구할 수 있는가

OS 버전 : Windows 10 Pro 2004 build 19041

도구 : JLECmd 1.4.0.0, LECmd 1.4.0.0, AXIOM 4.7, Partition-4DiagnosticParser

 

 

- 배경

윈도우 10에 파티션/진단 이벤트 로그가 처음 도입됨.

C:\Windows\System32\winevt\Logs\ Microsoft-Windows-Partition%4Diagnostic.evtx

이 로그에서 여러 볼륨이 있는 장치의 최대 3개의 VSN을 찾을 수 있음.

해당 로그를 분석해서 VSN을 추출하는 도구를 개발함.

 

해당 이벤트 로그에는 이동식 디스크 (USB 스틱, SD카드, 외장 하드 등)과 컴퓨터 내부 디스크에 대한 정보가 저장됨.

 

- 이벤트 로그 필드 일부 설명

[시스템 섹션]

  • EventID: 항상 1006
  • TimeCreated [SystemTime]: 타임스탬프
  • EventRecordId: 로그 파일 내의 각 기록에 할당된 고유 번호
  • Computer: 컴퓨터 이름
  • Security [UserID]: 유저의 SID값. 항상 S-1-5-18임 (User SYSTEM).

 

[이벤트 데이터 섹션]

  • DiskNumber: In our analysis, we found this number varying in a range between 0-3, with 0 being assigned to the disk running the operating system (OS).
  • IsSystemCritical: This value is usually false, apart from when DiskNumber was 0.
  • BytesPerSector: Bytes per sector (usually 512-defined by the manufacturer) of the device.
  • Capacity: The total capacity of the device (in bytes).
  • Manufacter: The manufacturer of the device. Not always accurate.
  • Model: Model of the device. Not always accurate.
  • SerialNumber: Serial number of the device.
  • ParentId: Information about the device, including Vendor ID, Product ID, Serial Number (potential correlation with registry files).
  • PartitionStyle: The partition scheme of the device. MBR is 0. GPT (GUID Partition Table) is 1.
  • PartitionCount: Number of partitions of the device. Not to be trusted.
  • Mbr: Raw dump of the first 512 bytes of the Master Boot Record of the device. Contains device’s Disk Signature and Master Partition Table.
  • Vbr0: Raw dump of the Volume Boot Record of the device’s 1st partition. Contains the VSN of this partition.
  • Vbr1: Raw dump of the Volume Boot Record of the device’s 2nd partition. Contains the VSN of this partition.
  • Vbr2: Raw dump of the Volume Boot Record of the device’s 3rd partition. Contains the VSN of this partition.
  • Vbr3: In our analysis, we found that this field is not populated with the raw dump of the Volume Boot Record of the device’s 4th partition. If someone wants to determine the existence of a 4th partition, he should examine the Master Partition Table.

 

위에서 볼 수 있듯 한 장치의 두 번째 및 세 번째 파티션의 VSN도 저장한다. 사용자가 자신의 장치를 포맷하더라도(할당된 VSN 손실) 이벤트 로그에서 이전 VSN을 검색할 수 있다.

 

- 조건 및 한계점

이 연구는 Microsoft-Windows-Partition%4Diagnostic.evtx 이벤트 로그에 대한 포렌식 분석에만 중점을 둠. 레지스트리 등의 다른 아티팩트를 조사하지 않는다.

이 연구는 할당되지않음 또는 MBR 파티션이 있는 장치로 제한됨. GPT 파티션 장치는 이 이벤트 로글르 다른 방식으로 기록함. 또한 4번째 파티션이 존재할 경우 4번째 파티션에 대한 정보는 저장되지 않음.

이 연구는 FAT32, NTFS, exFAT, EXT3 파일 시스템을 사용하는 장치만을 대상으로함. 다른 파일시스템을 사용하는 장치는 이 이벤트 로그를 다른 방식으로 기록함.

해당 이벤트 로그의 수명은 짧음. 주요한 윈도우 10 업데이트될 때 이벤트 로그가 지워짐. 다만, 19041에서 19042로의 업데이트와 같이 사소한 업데이트로는 지워지지 않음.

장치유형(SSD), 연결유형 (USB <- SATA 디스크)는 이벤트 로그를 다른 방식으로 기록함.

 

- 방법론

VSN은 파티션의 고유하게 하는 한 가지 기능임.

VSN은 VBR 내부에 있으며, 오프셋과 길이는 파일시스템에 따라 다름.

VSN은 LNK파일 및 점프 모곩에서 대상 파일이 있는 볼륨을 가리키는데 사용됨.

 

MBR 파티션 테이블이 있는 장치를 식별하는 다른 기능은 디스크 서명임.

디스크 서명은 MBR의 offset 0x1b8(4byte, little endian)에 있으며 파티션이 만들어질 때 할당 됨.

 

--- 직접 시연을 하기 위해서 사용된 환경 및 장비가 다릅니다. ---

OS : Windows 10 home 21H2 19044.1415
usb device : A(unknown) 64GB(MBR, 4 NTFS partition), B(unknown) 8GB (MBR, 1 FAT32 partition & 1 EXT3 partiton)
software : JLECmd 1.4.0.0, LECmd 1.4.0.0, AXIOM 4.7

 

 

  • 1단계 : 이 단계에서 A 64GB USB 장치는 테스트 컴퓨터에 여러 번 연결/해제되었습니다. 장치가 연결되어 있는 동안 LNK 파일과 점프 목록을 생성하기 위해 여러 파일(4개 파티션 모두에 위치)에 액세스했습니다. 일정량의 데이터가 생성된 후 이벤트 로그, LNK 파일 및 점프 목록에 대한 포렌식 검사를 진행했습니다.
  • 2단계 : 이 단계에서 A 64GB USB 장치의 모든 파티션을 삭제하고 3개의 새로운 NTFS 파티션을 생성했습니다. 1단계(연결/연결 해제, 모든 파티션의 파일 액세스)와 동일한 과정을 반복했습니다. 일정량의 데이터가 생성된 후 이벤트 로그, LNK 파일 및 점프 목록에 대한 포렌식 검사를 진행했습니다.
  • 3단계 : 이 단계에서 B 8GB USB 장치는 테스트 컴퓨터에 여러 번 연결/해제되었습니다. 잠시 후 우리는 이벤트 로그에 대한 포렌식 조사를 진행했습니다.
  • 4단계 : 이 단계에서 B 8GB USB 장치의 FAT32 파티션을 삭제하고 새로운 exFAT 파티션을 생성했습니다. 3단계(연결/연결 해제)와 동일한 과정을 반복했습니다. 잠시 후 우리는 이벤트 로그에 대한 포렌식 조사를 진행했습니다.

A usb의 환경 구성에 실패. 처음에 NTFS 파티션 4개로 나누고 각 파티션에 테스트용 파일까지 넣고 usb 재인식하니까 위 사진처럼 파티션 구성이 바뀌어서 인식됨. usb 자체 문제인지 특성인지 파악 못함.

B USB의 경우 FAT32와 EXT3 파티션 구성이 유지되었음.

그러나 FAT32 파티션을 exFAT 파티션으로 포맷하고 재인식하니 exFAT 파티션이 RAW로 인식되버림.

첫번째 파티션을 exFAT 대신 NTFS로 다시 포맷하여 진행함.

따라서 3~4단계만 진행.


 

1단계

그냥 위 사진 상태의 usb를 연결하고 파일에 접근한 뒤 로그를 확인해 보았다.

NTFS로 정상적으로 인식되는 첫 번째 파티션은 vbr이 제대로 기록되었고, 나머지는 vbr이 0000으로 기록되었지만 이를 통해 최소 4개의 파티션이 존재했음을 알 수 있다.

 

2단계

 

3단계

USB를 연결하고 FAT32 파티션 내에 있는 테스트 파일을 열어본 이후 Microsoft-Windows-Partition%4Diagnostic.evtx 해당 이벤트 로그를 확인한 내용이다.

FAT32 파티션의 vbr은 기록되었지만 EXT3 파티션은 인식할 수 없는 포맷이므로 vbr이 기록되지 않았다.

그래도 2개의 파티션이 있는 것을 확인할 수 있다.

 

4단계

역시 2개의 파티션이 존재함을 알 수 있고, 두 번째 EXT3 파티션은 여전히 0000으로 기록되어있으며

첫번째 파티션이 새로 포맷되었기 때문에 vbr값이 변경된 것을 알 수 있다.

mbr값은 동일하다.

 

- 결론

이 게시물에서 우리는 특히 장치 속성이 필요한 경우 "Microsoft-Windows-Partition%4Diagnostic.evtx" 검사의 중요성에 대해 설명했습니다. 이 유물이 몇 년 전에 발견되었지만 우리는 아직 그 풍부한 정보를 완전히 탐색하지 못했습니다. 그럼에도 불구하고 우리는 저장되어 있는 사용 가능한 모든 VSN을 수동으로 추출하는 방법과 이 프로세스를 자동화하는 도구를 개발하는 방법을 보여주었습니다. 이 이벤트 로그에 대한 추가 연구의 여지가 있지만 독자가 이 이벤트 로그에서 찾을 수 있는 주요 아티팩트 중 일부에서 가치를 찾을 수 있기를 바랍니다.

https://github.com/theAtropos4n6/Partition-4DiagnosticParser

 

 

- 앞으로

해당 이벤트 로그가 GPT 파티션을 사용하는 장치를 처리하는 방법과 다양한 파일 시스템을 처리하는 방법을 분석할 예정.

 

- DFIR 리뷰

 USB 드라이브에는 여러 볼륨이 포함될 수 있으며, 동일한 물리적인 USB 드라이브에서 여러 볼륨을 나올 수 있다는 사실을 간과할 수 있따는 점을 중요하게 지적함.

 

 

 

 

반응형

반응형

ASCTF에 출제한 리눅스 메모리 포렌식 문제 제작에  12시간 이상 소요된 것 같다. 사전 계획 빼고 문제 제작 자체만.

순조롭게 진행되었다면 절반도 안걸렸을테지만.. 처음 만들다보니 헤메는 것이 많았다.

그리고 만들고 보니 왜 CTF들에 리눅스 메모리 문제는 거의 없는지도 느낄 수 있었다.

 


리눅스 메모리 포렌식 문제 제작은 처음이지만, 최근에 한 번 풀어 본 적이 있다.

리눅스 메모리 포렌식의 특징은 분석 자체는 볼라티리티로 하면 되므로

윈도우랑 별 차이가 없기 때문에 쉽다.

그런데 이 볼라티리티를 사용하기 위한 환경 구성에 시간이 많이 소요된다.

 

프로필을 생성하는 방법은 쉽게 찾을 수 있다.

https://github.com/volatilityfoundation/volatility/wiki/Linux#creating-a-new-profile

이 과정에 비교적 많은 시간이 소요된다. 리눅스를 가상머신에 깔고 커널버전을 맞춰줘야 하니까..

 

 

리눅스 종류랑 리눅스 커널 버전이 매우 다양해서, 볼라티리티에서 모든 종류의 프로필을 배포할 수 없고

많은 프로필들을 한 번에 볼라티리티에 적용할 경우 볼라티리티의 성능이 매우 나빠진다.

 

https://github.com/volatilityfoundation/profiles

 

따라서 필요한 프로필만 그때 그때 만들어서 적용을 해서 사용을 해야한다.

 

이 부분을 노리고 문제 제작을 계획했다.


 

리눅스에서 메모리를 덤프할 때 사용할 수 있는 소프트웨어들에는 여러가지가 있다.

그 중에서 나는 들어본 적 있는 lime을 사용하기로 했고 관련 포스트를 찾아보았다,

 

https://lastcard.tistory.com/68

위 포스트를 찾아서, 문제 시나리오대로 구성을 한 뒤에 덤프를 위해 사용방법을 따라했다.

근데 사용 예시에 format이 raw로 설정되어있다. 밑에 옵션은 처다보지도 않고, 당연히 메모리 덤프는 raw지! 하고 raw로 덤프를 떴다.

 

그리고 볼라티리티를 돌렸는데 여기서 문제가 발생했다.

예시자료&nbsp;https://cpuu.postype.com/post/665132

이런 식으로 no base address space가 떴다. (사용했던 볼라티리티 버전은 2.6.1 최신이었다.)

문제 제작한 환경이 ubuntu 18.04.1 x64 linux version 5.4.0-42-generic 이었고, 동일한 환경에서 제작된 문제를 이전에 풀어본 적이 있었기 때문에 전혀 이해가 가지 않은 상황이었다.

반응형

그래서 좀 찾아보니까 

https://cpuu.postype.com/post/665132

리눅스 커널 4.8 부터는 KASLR이 적용되어 있어서 저런 현상이 발생되고 분석을 진행할 수 없다는 내용이었다.

추가로 kaslr이 적용된 4.8에서 사용할 수 있는 볼라티리티 버전이 있다는 내용도 있었다.

https://github.com/volatilityfoundation/volatility/pull/385

 

이 시점에서 의문이 들었다.

내가 풀었었던 동일한 환경의 문제는 어떻게 제작이 되었던 것일까?

1. 나랑 다른 방식으로 메모리를 덤프하였다.

2. kaslr을 해제하고 문제를 제작하였다.

 

2번 같은 경우 kaslr을 해제할 수 있다는 내용을 보고 선택지를 추가하게 되었다.

https://wogh8732.tistory.com/323

난 여기서 1번에 의심을 가졌으면 금방 해결되었을 문제였지만 나는 4.8 kaslr 버전용 볼라티리티가 따로 있어? 에 흥미를 느끼고 4.8 환경에서 문제를 제작해서 해당 볼라티리티 버전만을 사용하게 해서 문제를 풀게 하면 재밌겠다는 생각이 들었다. 그리고 이게 안되면 kaslr을 해제해서 만들자로 plan b를 세웠다.

 

(아무 의미가 없는 행동이었다. bneuburg가 제작한 4.8 kaslr 지원 버전에는 2개가 있는데, 모두 2017년에 올라온 것이고 지금은 이미 최신버전 볼라티리티에 기본 탑재되어 있는 기능이다. 근데 난 몰랐다. (아니 나 2017년도 내용임을 확인안하고 뭐했냐..))


 

기존에 사용하던 18.04.1에서는 커널 버전을 4.8로 내릴 수 없어서 4.8로 탑재되어 있는 16.10을 다운받아 환경을 구성하였다.

 

https://ko.wikipedia.org/wiki/%EC%9A%B0%EB%B6%84%ED%88%AC_%EB%B2%84%EC%A0%84_%EC%97%AD%EC%82%AC

 

그리고 똑같은 짓을 하고

똑같이 raw로 덤프를 떠서

볼라티리티를 돌렸고

똑같은 결과가 나왔다. No base address.

 

왜 안되지 하고 깃헙 페이지를 자세히 봤다. 이전엔 자세히 안보고 걍 지원되는 버전 정도만 확인 했었다.

이걸 딱 읽고 아 라임 포멧이어야 하는구나를 알게 되었다.

 

그리고 이전에 읽었던 lime 사용방법 블로그로 돌아와서

https://lastcard.tistory.com/68

방법2-1에는 포멧을 lime으로 써두셨더라..

그래서 처음부터 다 잘못됐음을 깨닳고 처음 환경에서 lime으로 뽑아내서 최신버전 볼라티리티를 돌렸고

분석에 성공했다.

 

이렇게 메모리 덤프를 볼라티리티에 인식하게 하는 것에 성공했다.



다음 두번째 문제가 있었다.

처음 계획했던 시나리오는 크롬이나 파이어폭스 검색 기록을 확인해서 특정 시간에 검색한 키워드를 찾아내는 것이었다.

 

근데 문제는 일단 chromehistory firefoxhistory 플러그인이 리눅스에서는 지원되지 않았다.

 

그러면 히스토리 파일을 뽑아내야하는데, 파일을 추출 할때 윈도우처럼 filescan 같은 플러그인이 없고 find_file 플러그인 하나로 파일의 메모리 주소 뽑아내고 파일 추출을 다 해야한다.

find_file로 파일의 메모리 주소를 뽑아내려면 파일의 full 절대 경로를 알아야 한다. 크롬은 기본 설치 경로를 생각하고 히스토리 파일을 뽑아낼 수 있겠지만 파이어폭스는 랜덤 문자열의 profile 이름으로 생성된 폴더에 있기 때문에 해당 폴더명을 알아내야하는 과정이 필요한데, lime 포멧은 R-Studio 같은거에서도 분석이 안되서 파일 구조를 확인할 수 없고

bash에서 해당 파일명을 주고자 bash로 직접 해당 폴더까지 접근하는 것을 시나리오 넣기엔 너무 부자연스러웠다. 문제 제작을 위해 고의로 넣은 내용 느낌. 개인적으로 이런거 안 좋아한다.

뭐 어떻게 해서 풀 절대경로를 줘서 파일을 추출하더라도 내용이 제대로 들어있지 않았다.

그래서 히스토리 파일을 뽑아내는 것은 의미가 없었다.


그래서 크롬/파이어폭스 프로세스를 덤프를 떠서 찾아내야 하는데,

yarascan으로 http 뽑아내서 링크들을 뽑아내는 것은 가능한데, 시간 값을 뽑아내려면 뭐 어떻게 잘 해서 뽑아내야할건데 난 모른다.

 

그래서 문제 지문에 구글 검색으로 첫 번째로 검색한 내용은? 같은걸 할까 했지만 이 경우 구글 검색 링크 "https://www.google.com/search?q=" 로 yarascan 돌리면 바로 나와버리기 때문에 난이도가 매우 낮았다. 이건 볼라티리티 안쓰고 그냥 grep으로도 뽑아낼 수 있으므로 

브라우저 검색 기록을 찾아내는 것은 포기하고

 

어디서 많이 본 프로세스 뽑아내서 실행하기가 되었다.

근데 또 문제가 있었다. bash에 입력한 명령어는 볼라티리티 플러그인으로 확인할 수 있지만, 출력된 내용은 플러그인으로 확인할 수 없는 것으로 알고 있다.

하지만 그냥 raw로 출력 내용이 남아 있기 때문에 strings랑 grep으로 다 뽑아낼 수 있다.

grep에 키워드가 최대한 걸리지 않도록 플래그를 base64 이중으로 인코딩하는 등의 노력을 했지만

"ASCTF"가 키워드에 걸렸다. "ASCTF{"만 생각하고 이건 생각 못했다. 이건 롸업에도 써놨다.

 

이걸 롸업 작성하면서 알아서 해당 프로세스의 pid값을 플래그에 추가하는 걸로 문제를 수정하였다.

pid값은 볼라티리티로만 뽑아낼 수 있으니까..


혀튼 리눅스 메모리 포렌식 문제를 제작해보고 다시 풀어보면서 

리눅스 메모리 분석은 윈도우 메모리 분석이랑 비슷하거나 할 수 있는게 더 적었고

실제 분석보다 환경 구성의 비중이 크고 오래걸리고 어렵고 귀찮고 짜증나기 때문에

CTF에서 리눅스 메모리 문제를 보기 어려웠던 이유를 알 수 있었다.

 

분석 자체만 가지고 어렵게 만들 수만 있다면 프로필 파일은 같이 제공해줘도 되긴 하는데

 

 

 

응애

나 뉴비

반응형
반응형

Q1 objectaid class diagram install error.

-> download objectaid-1.2.2.zip or ojectaid-1.2.4.zip from here.

Q2 objectaid class diagram "finish" button is not working.

-> use old version of eclipse.

 


 

오늘까지 객프 과제 마감인데, 오늘 과제하다가 3 2번 화났다.

첫번째는 저번주랑 이번주 할 일이 너무 많았다.

첫번째는 UML만들어야되는데 objectaid 사이트가 터진지 오래라 플러그인을 바로 다운받지 못했고

두번째는 다이어그램생성하는데 finish버튼을 눌러도 아무 반응이 없었던 것이었던거시다.

 

 

이클립스에서 UML 간편하게 만들려면 objectaid 플러그인을 사용해야한다.

직접 플러그인을 다운을 받던 이클립스 통해서 설치를 하던 objectaid사이트에서 데이터를 받아오게 되어있다.

그런데 사이트가 몇 달 전에 터진거 같다.  정확히는 도메인 만료되서 연장 안한거 같은데... 6월까지는 살아있던거 같던데..

objectaid.com

 

혀튼 그래서 플러그인 설치하려면 파일을 직접 구하는 수 밖에 없었다. 그래서 여러 사이트랑 블로그 찾아서 파일 2개를 구했다.

https://drive.google.com/file/d/1JNdkcMxQ-hGPKcoS-JaoiwtMgCYiJ7qP/view?usp=sharing 

 

objectaid-1_2_2.zip

 

drive.google.com

https://drive.google.com/file/d/1E1YFbVNe-swgLsLvKXb46VQjAxa4Gxd_/view?usp=sharing 

 

objectaid-1.2.4.zip

 

drive.google.com

 

Archive 버튼 눌러서 다운받은 zip 파일 그대로 걸어주면 된다.

 

이렇게 해결되나 싶더니 문제가 또 발생했다.

wtf finish button is not working f u

finish 버튼을 아무리 눌러도 반응이 없었다. 뭐 오류메세지를 주는 것도 아니고 뭘 하려는 시늉이라도 해야지.. 아무 반응이 없었다.

그래서 또 다른 블로그들 보니까  옛날 버전에서는 된다는 건 확실했고

 

 

4.2.1 주노 다운받아서 플러그인 다시 설치해서 성공했다.

 

힘들었다.

반응형
반응형

(NTUSER.DAT는 삭제하면 안된다. 삭제할 이유가 없다.)


최근 CTF에서 NTUSER.DAT을 사용해서 문제를 몇 번 풀었는데, NTUSER.DAT을 통해 얻을 수 있는 유의미한 정보들이 뭐가 있는지 정리해 보았다. 아래 내용이 정확하진 않으니 참고만 해주세요..

 

NTUSER.DAT

윈도우 사용자 계정 프로필 정보가 포함된 레지스트리 파일

/Users/ 밑의 각 유저 폴더안에 존재

 

 

분석

이미지 파일에서 Autopsy로 분석하거나

NTUSER.DAT파일만 가지고 access data의 registry viewer나 REGA 등으로 분석이 가능하다.

 

또는 regriper 로 간단하게 볼 수도 있다.

forensic.korea.ac.kr/DFWIKI/index.php/RegRipper

 

RegRipper - Digital Forensic Wikipedia

Regripper는 Harlan Carvey에서 개발한 레지스트리 분석 도구로 오픈소스로 제공된다. 최신 업데이트는 2013년 4월이며, 최신 버전은 v2.8이다. 본 항에서는 최신버전인 v2.8버전을 사용하였다. Regripper는 C

forensic.korea.ac.kr

github.com/keydet89/RegRipper3.0

 

keydet89/RegRipper3.0

RegRipper3.0. Contribute to keydet89/RegRipper3.0 development by creating an account on GitHub.

github.com

 

regripper을 사용하면 txt 파일 형태로 정리된 내용을 볼 수 있다.

 

 

생성된 txt 파일에는 여러 정보가 담겨 있다.

그 중에서 유의미한, 쓸만한 정보들만 추려보았다.

 

 

국가/지역

 

최근 문서

최근 문서 목록에 남아 있는 기준이 기간인지, 개수인지는 정확하게 알아보지 않았으나, 본인 PC의 ntuser.dat을 확인해 본 결과, RecentDocs에 약 150개정도가 기록되어 있었다. 개수보다는 기간을 기준으로 오래된 내역은 삭제되는 듯 싶다.

 

RecentDocs\.txt 에서는 \.txt처럼 \.hwp \.pdf \.png 등 거의 모든 확장자별로 기록되어 있다.

 

기록된 내용은 앞에 붙은 번호 상관없이 최상단에 있는 내용이 가장 최근 내역이다.

 

자동 실행 응용프로그램

 

탐색기 경로창에 입력한 내역

 

인터넷 익스플로러 url창에 입력한 내역

ie 방문기록을 확인할 수 없는 경우에는 이 부분을 확인해 보자.

typedurlstime에는 각 기록별 시각도 나온다.

 

설치된 응용프로그램 삭제 내역

삭제한 시각도 같이 나온다.

 

윈도우 검색 기능을 이용하여 검색한 내역

반응형
반응형

최근 유행했던 주사위 게임을 디스코드 봇으로 만들어 보았다.

 


https://discord.com/api/oauth2/authorize?client_id=671993218438791169&permissions=34880&scope=bot

 

Discord - A New Way to Chat with Friends & Communities

Discord is the easiest way to communicate over voice, video, and text. Chat, hang out, and stay close with your friends and communities.

discord.com

제 봇을 추가해서 테스트해 볼 수 있습니다.

 

명령어: >야추

 


 

 

 

정확히는 디스코드랑 상호작용하는 부분만 만들고 실제로 주사위를 굴리거나 점수를 반환하는 실질적인 게임 부분은 깃허브에서 찾아서 사용했다.

 

사용한 API 코드:

 

https://github.com/dkpark95/yahtzee

 

dkpark95/yahtzee

Code for a simple game of Yahtzee built with JS/jQuery on top of a simple Express API. - dkpark95/yahtzee

github.com

위 API를 돌려서 값만 주고 받아서 사용하는 거다.

 


구조

 

project/

├── bot/

│ ├── app.js (봇)

│ └── emojiCharacters.js (이모지 모음)

└── yahtzee/ (https://github.com/dkpark95/yahtzee)

 

 

emojiCharacters.js 파일은 아래 링크를 참조하여 생성하시면 됩니다.

https://discordjs.guide/popular-topics/miscellaneous-examples.html#emoji-characters

 

Discord.js Guide

A guide made by the community of discord.js for its users.

discordjs.guide


코드 작성하기

 

 

개발환경 : UBUNTU 18.04.3

개발언어: NODE JS V12.13.0

모듈 : discord.js v12

텍스트 에디터: ATOM (편하신거 사용하시면 됩니다.)

 

nodejs와 discordjs의 버전을 확인해주세요. 버전이 다를 경우 문제가 생길 수 있습니다.

discord.js의 버전은 무조건 V12이어야 합니다.

 

 

 

app.js

1
2
3
4
5
6
7
8
9
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// Extract the required classes from the discord.js module
const Discord = require('discord.js');
const rp = require('request-promise');
const emojiCharacters = require('./emojiCharacters');
 
// Create an instance of a Discord client
const client = new Discord.Client();
 
const botName = 'M4ndU';
const devURL = 'https://mandu-mandu.tistory.com';
const yatchURL = 'http://127.0.0.1:3400';
 
const token = 'TOKEN_HERE';
 
const PREFIX = '?';
 
function showScore(game, i){
  if (game.scorecard[i].locked === true) {
    return game.scorecard[i].points;
  } else {
    var score = '0';
    if (game.scorecard[i].points===null) {
      score = 0;
    }else {
      score = game.scorecard[i].points;
    }
    return "0(+"+score+")";
  }
}
 
function showScoreEmoji(game, i){
  if (game.scorecard[i].locked === true) {
    return ' ✅ ';
  } else {
    return ' ❌ ';
  }
}
 
function showBonusScore(game){
  if (game.scorecard[7].points===null) {
    return `0(${game.scorecard[6].points}/63)(+35)`;
  } else {
    return `${game.scorecard[7].points===null?0:game.scorecard[7].points}(${game.scorecard[6].points===null?0:game.scorecard[6].points}/63)`;
  }
}
 
function showBonusEmoji(game){
  if (game.scorecard[7].points===null) {
    return ' ❌ ';
  } else if (game.scorecard[7].points=== 0) {
    return ' ⛔️ ';
  } else {
    return ' ✅ ';
  }
}
 
function embedScoreMsg(newGame, r, log, turn){
  var dices = "";
  for (var i = 0; i < 5; i++) {
    dices += emojiCharacters[`${newGame.round.dice[i].value}`] +' ';
  }
 
  var gameBoardEmbed = new Discord.MessageEmbed()
    .setColor('#0099ff')
    .setTitle(`야추! | 턴 ${turn}/13`)
    .setDescription('입력시간제한 : 1분')
    .setAuthor(botName, '''http://mandu-mandu.tistory.com/')
    .addFields(
      { name'주사위 🎲 ', value: `${dices}`, inline: true },
      { name'남은 횟수 🤹‍ ', value: `${r}`, inline: true },
      { name'점수 합계 🏆 ', value: `${newGame.scorecard[15].points===null?0:newGame.scorecard[15].points}`, inline: true },
      { name'-----------', value: '점수판', inline: false },
      { name: `#1: Ones${showScoreEmoji(newGame, 0)}`, value: `점수: ${ showScore(newGame, 0)}`, inline: true },
      { name: `#2: Twos${showScoreEmoji(newGame, 1)}`, value: `점수: ${ showScore(newGame, 1)}`, inline: true },
      { name: `#3: Threes${showScoreEmoji(newGame, 2)}`, value: `점수: ${ showScore(newGame, 2)}`, inline: true },
      { name: `#4: Fours${showScoreEmoji(newGame, 3)}`, value: `점수: ${ showScore(newGame, 3)}`, inline: true },
      { name: `#5: Fives${showScoreEmoji(newGame, 4)}`, value: `점수: ${ showScore(newGame, 4)}`, inline: true },
      { name: `#6: Sixs${showScoreEmoji(newGame, 5)}`, value: `점수: ${ showScore(newGame, 5)}`, inline: true },
      { name: `1-6 보너스!${showBonusEmoji(newGame)}`, value: `점수:${ showBonusScore(newGame)}`, inline: false },
      { name: `#7: Three of a Kind${showScoreEmoji(newGame, 8)}`, value: `점수: ${ showScore(newGame, 8)}`, inline: true },
      { name: `#8: Four of a Kind${showScoreEmoji(newGame, 9)}`, value: `점수: ${ showScore(newGame, 9)}`, inline: true },
      { name: `#9: Full House${showScoreEmoji(newGame, 10)}`, value: `점수: ${ showScore(newGame, 10)}`, inline: true },
      { name: `#10: Small Straight${showScoreEmoji(newGame, 11)}`, value: `점수: ${ showScore(newGame, 11)}`, inline: true },
      { name: `#11: Large Straight${showScoreEmoji(newGame, 12)}`, value: `점수: ${ showScore(newGame, 12)}`, inline: true },
      { name: `#12: Chance${showScoreEmoji(newGame, 13)}`, value: `점수: ${ showScore(newGame, 13)}`, inline: true },
      { name: `#13: Yahtzee${showScoreEmoji(newGame, 14)}`, value: `점수: ${ showScore(newGame, 14)}`, inline: true },
      { name'기록 📃', value: `${log}`, inline: false },
    )
    .setFooter('Made by M4ndU''');
 
    return gameBoardEmbed;
}
 
 
client.on('ready', async () => {
  console.log(`Logged in as ${client.user.tag}!`);
});
 
client.on('message', async msg => {
  if (msg.content.startsWith(PREFIX)) {
        const input = msg.content.slice(PREFIX.length).split(' ');
        var command = input.shift();
        var commandArgs = input;
 
    if (command === '야추') {
      //게임 시작
      const newGame = await rp({uri:yatchURL + '/api/game'}).then(function (repos) {
        return JSON.parse(repos);
      });
 
      var gameBoardEmbed = embedScoreMsg(newGame, 2,'.'1);
 
      var yachtmsg = await msg.channel.send(gameBoardEmbed).then(msg =>{
        return msg;
      });
 
      Promise.all([
            yachtmsg.react(emojiCharacters['1']),
            yachtmsg.react(emojiCharacters['2']),
            yachtmsg.react(emojiCharacters['3']),
        yachtmsg.react(emojiCharacters['4']),
        yachtmsg.react(emojiCharacters['5']),
        yachtmsg.react('🔁'), //주사위 굴리기
        yachtmsg.react('🔴'), //점수 바로 등록하기
        ]);
 
      //init
      var yachtGame = newGame;
      const filter = (reaction, user) => {
        return ['🔁','🔴'].includes(reaction.emoji.name&& user.id === msg.author.id;
      };
      var stopFlag = false;
      var exit = false;
      var options = {
          method: 'POST',
          uri:yatchURL + '/api/game/roll',
          body: yachtGame,
          json: true // Automatically stringifies the body to JSON
      };
      const filterm = response => {
        return ['1','2','3','4','5','6','7','8','9','10','11','12','13'].includes(response.content&& response.author.id === msg.author.id;
      };
      var choose = '0';
      var userReactions = new Object();
      //init
 
 
      //13턴 돌립니다.
      for (var gameTurn = 1; gameTurn < 14; gameTurn++) {
        //주사위 3라운드 굴리기
        for (var g = 2; g > 0; g--) {
          await yachtmsg.awaitReactions(filter, { max: 1, time: 60000, errors: ['time'] })
            .then(async function(collected) {
              const reaction = collected.first();
              if (reaction.emoji.name === '🔁') {
                for (var yrdl = 0; yrdl < 5; yrdl++) {
                  yachtGame.round.dice[yrdl].locked = yachtmsg.reactions.cache.get(emojiCharacters[`${yrdl+1}`]).users.cache.has(msg.author.id);
                }
                //roll
                  } else {
                stopFlag = true;
                g = 1;
                  }
            }).catch(async function(collected) {
              gameBoardEmbed = embedScoreMsg(yachtGame, 0,'시간초과.', gameTurn);
              yachtmsg.edit(gameBoardEmbed);
              stopFlag = true;
              exit = true;
            });
          options = {
              method: 'POST',
              uri:yatchURL + '/api/game/roll',
              body: yachtGame,
              json: true // Automatically stringifies the body to JSON
          };
          if (exit) {
            break;
          }
          if (!stopFlag) {
            yachtGame = await rp(options).then(function (repos) {
              return repos;
            });
          }
 
          if (g === 1) {
            gameBoardEmbed = embedScoreMsg(yachtGame, g-1,'점수로 등록할 번호를 입력하세요.', gameTurn);
            await yachtmsg.edit(gameBoardEmbed);
            break;
          } else {
            gameBoardEmbed = embedScoreMsg(yachtGame, g-1,'잠시 기다려주세요', gameTurn);
          }
 
          await yachtmsg.edit(gameBoardEmbed);
 
          //2 react remove
          await yachtmsg.reactions.resolve('🔁').users.remove(msg.author.id);
          await yachtmsg.reactions.resolve('🔴').users.remove(msg.author.id);
 
          gameBoardEmbed = embedScoreMsg(yachtGame, g-1,'아래 이모지를 클릭해주세요.', gameTurn);
          await yachtmsg.edit(gameBoardEmbed);
        }
        if (exit) {
          break;
        }
 
        //점수 등록과정
        choose = '0';
        stopFlag = false;
        for (var rcc = 0; rcc < 5; rcc++) {
          await msg.channel.awaitMessages(filterm, { max: 1, time: 60000, errors: ['time'] })
                .then(async collected => {
              var resmsg = collected.first();
                    if (isNaN(resmsg.content|| Number(resmsg.content> 14 || Number(resmsg.content< 1 || !Number.isInteger(Number(resmsg.content))) {
                gameBoardEmbed = embedScoreMsg(yachtGame, 0,'1에서 14사이의 정수를 입력.');
                yachtmsg.edit(gameBoardEmbed);
              }else {
                if (resmsg.content < 7) {
                  choose = Number(resmsg.content-1;
                } else {
                  choose = Number(resmsg.content+1;
                }
                if (yachtGame.scorecard[Number(choose)].locked) {
                  gameBoardEmbed = embedScoreMsg(yachtGame, 0,'이미 등록된 번호. 다른번호로 입력.', gameTurn);
                  yachtmsg.edit(gameBoardEmbed);
                }else {
                  options = {
                      method: 'PUT',
                      uri:yatchURL + '/api/game/select/' + yachtGame.scorecard[choose].id,
                      body: yachtGame,
                      json: true // Automatically stringifies the body to JSON
                  };
                  yachtGame = await rp(options).then(function (repos) {
                    return repos;
                  });
                  stopFlag = true;
                }
              }
              resmsg.delete();
                })
                .catch(collected => {
              gameBoardEmbed = embedScoreMsg(yachtGame, 0,'시간초과.', gameTurn);
              yachtmsg.edit(gameBoardEmbed);
              stopFlag = true;
              exit = true;
                });
          if (stopFlag || exit) {
            break;
          }
        }
 
        stopFlag = false;
        if (exit) {
          break;
        }
 
        if (gameTurn === 13) {
          gameBoardEmbed = embedScoreMsg(yachtGame, 0,'게임종료', gameTurn);
          break;
        }else {
          gameBoardEmbed = embedScoreMsg(yachtGame, 2,'기다려주세요', gameTurn+1);
        }
 
        await yachtmsg.edit(gameBoardEmbed);
        //all react remove
        await yachtmsg.reactions.resolve(emojiCharacters['1']).users.remove(msg.author.id);
        await yachtmsg.reactions.resolve(emojiCharacters['2']).users.remove(msg.author.id);
        await yachtmsg.reactions.resolve(emojiCharacters['3']).users.remove(msg.author.id);
        await yachtmsg.reactions.resolve(emojiCharacters['4']).users.remove(msg.author.id);
        await yachtmsg.reactions.resolve(emojiCharacters['5']).users.remove(msg.author.id);
        await yachtmsg.reactions.resolve('🔁').users.remove(msg.author.id);
        await yachtmsg.reactions.resolve('🔴').users.remove(msg.author.id);
 
        gameBoardEmbed = embedScoreMsg(yachtGame, 2,'아래 이모지를 클릭!', gameTurn+1);
        await yachtmsg.edit(gameBoardEmbed);
      }
 
    }
  }
});
 
client.login(token);
 
cs

 


https://discord.com/api/oauth2/authorize?client_id=671993218438791169&permissions=34880&scope=bot

 

Discord - A New Way to Chat with Friends & Communities

Discord is the easiest way to communicate over voice, video, and text. Chat, hang out, and stay close with your friends and communities.

discord.com

제 봇을 추가해서 테스트해 볼 수 있습니다.

 

명령어: >야추

반응형
반응형

2020년 5월에 새로운 Wargame사이트를 추천합니다!

 

 

ctf.no-named.kr:1234/

 

N0Named (노나메드) 팀에서 제작한 Wargame입니다.

 

 

 

Misc, Reversing, Forensic, Pwnable, Crypto 카테고리의 문제들을 풀어보실 수 있습니다.

 

현재 29문제가 등록되어 있으며, 앞으로도 꾸준히 문제가 추가될 예정입니다.

 

 

많은 이용 부탁드립니다!

반응형
반응형

직접 MBTI를 검사하는 봇은 아니고, 외부 사이트 등에서 mbti를 검사하고서 그 결과를 등록한 뒤, 디스코드 구성원들과 궁합을 보는 봇이다.

 

인터넷에 떠돌아 다니는 MBTI 궁합 차트를 기반으로 했다.

 

물론 아무런 근거 없는 궁합이기 때문에 재미로만 보자.

 

 


명령어별 기능

 

 

mbti

명령어 모음 (help 명령어와 동일)

 

1. mbti 검사

MBTI를 검사할 수 있는 링크를 출력한다.

 

2. mbti 보기 [MBTI]

MBTI 유형을 입력해주면, 해당 유형의 내용을 볼 수 있는 링크를 출력한다.

 

3. mbti 등록 [MBTI]

자신의 MBTI 유형을 등록한다. (기존에 등록했어도 덮어쓰기 가능.)

 

4. mbti 등록현황

mbti 등록 현황을 출력한다.

 

5. mbti 궁합 @A @B

@자리에 원하는 사람을 언급해주면 된다.

@A만 언급하면 A와 나 자신의 궁합 결과를 출력한다.

@A @B 모두 언급해주면, @A와 @B의 궁합 결과를 표시해준다.

 

 


개발환경 : UBUNTU 18.04.3

개발언어: NODE JS V12

모듈 : discord.js v12

텍스트 에디터: ATOM (편하신거 사용하시면 됩니다.)

 

봇 생성하는 것은 아래 포스트를 보고 오자.

https://mandu-mandu.tistory.com/385?category=693080

 

노드자스로 디스코드 봇 만들기(1) - 봇 생성부터 테스트까지

이 글만을 통해서 기본적으로 텍스트를 출력하는 디스코드 봇을 만들 수 있습니다. 1. 봇 생성하고 초대하기 2. 코드 작성하기 3. 테스트 하기 봇 생성하고 초대하기 먼저 봇 계정을 생성해 주어��

mandu-mandu.tistory.com

 

 

 

일단 위 궁합표를 배열로 만들었다. (노가다다 노가다...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//16가지 mbti 유형
const mbti = new Array("INFP","ENFP","INFJ","ENFJ""INTJ""ENTJ""INTP""ENTP""ISFP""ESFP""ISTP""ESTP""ISFJ""ESFJ""ISTJ""ESTJ");
 
//Relation : A, B, C, D, F
var mbtiR = new Array(16);
mbtiR[0]  = ["A"];
mbtiR[1]  = ["A""A"];
mbtiR[2]  = ["A""B""A"];
mbtiR[3]  = ["B""A""A""A"];
mbtiR[4]  = ["A""B""A""A""A"];
mbtiR[5]  = ["B""A""A""A""A""A"];
mbtiR[6]  = ["A""A""A""A""A""B""A"];
mbtiR[7]  = ["A""A""B""A""B""A""A""A"];
mbtiR[8]  = ["F""F""F""B""C""C""C""C""D"];
mbtiR[9]  = ["F""F""F""F""C""C""C""C""D""D"];
mbtiR[10= ["F""F""F""F""C""C""C""C""D""D""D"];
mbtiR[11= ["F""F""F""F""C""C""C""C""D""D""D""D"];
mbtiR[12= ["F""F""F""F""D""C""D""D""C""B""C""B""A"];
mbtiR[13= ["F""F""F""F""D""C""D""D""B""C""B""C""A""A"];
mbtiR[14= ["F""F""F""F""D""C""D""D""C""B""C""B""A""A""A"];
mbtiR[15= ["F""F""F""F""D""C""B""D""B""C""B""C""A""A""A""A"];
 
cs

A, B, C, D, F 5단계이며, 위 표 기준으로 A가 파랑, F가 빨강이다.

 

 

 

 

 

 

 

이제 명령어 별로 6가지의 기능을 구현해야 하는데, 1번과 2번은 단술 입출력이기 때문에 아래 포스트를 보고 구현이 가능하다.

https://mandu-mandu.tistory.com/385?category=693080

 

노드자스로 디스코드 봇 만들기(1) - 봇 생성부터 테스트까지

이 글만을 통해서 기본적으로 텍스트를 출력하는 디스코드 봇을 만들 수 있습니다. 1. 봇 생성하고 초대하기 2. 코드 작성하기 3. 테스트 하기 봇 생성하고 초대하기 먼저 봇 계정을 생성해 주어��

mandu-mandu.tistory.com

https://mandu-mandu.tistory.com/386?category=693080

 

노드자스로 디스코드 봇 만들기(2) - 노래/사진/상태/삭제

이번에 추가할 기능은 아래와 같습니다. [ 로컬 노래파일 재생, 로컬 사진 전송, 상태메세지 설정, 메세지 지우기 ] 개발환경 : UBUNTU 18.04.3 개발언어: NODE JS V8 모듈 : discord.js v11 텍스트 에디터: ATOM

mandu-mandu.tistory.com

 

 

 


 

3번 4번 5번의 핵심 기능은 동일하다.

 

 

 

배열을 하나 만들어서

유저의 id (고유값), username, mbti 유형값을 받아서 저장하고

json으로 만들어서 외부 파일에 저장해둔다.

 

저장된 유저의 mbti값을 가져와서 궁합 값을 출력해주면 된다.

 

 

간단하다.

 

 

 

 

봇의 핵심인 3번과 5번 명령에 해당되는 코드:

 

1
2
3
4
5
6
7
8
9
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
if (msg.content.startsWith('mbti 등록 ')) {
    var reg_mbti = msg.content.split(' ');
 
    //type
    var type = 16;
    var input_mbti = reg_mbti[2].toUpperCase();
    for(var i=0; i<16; i++) {
      if (mbti[i] == input_mbti) {
        type = i;
        break;
      }
    }
    if(type == 16){
      msg.channel.send("실패!");
    }else {
 
      //json to Array
      var data = fs.readFileSync('./user_list.json''utf8');
      var user_list_arr = JSON.parse(data);
 
      //search
      var j = -1;
      for(var k=0; k<user_list_arr.length; k++) {
        if (user_list_arr[k].user_id == msg.member.user.id) {
          j = k;
          break;
        }
      }
 
      //patch or push
      if (j == -1) {
        var user_list = new Object();
        user_list.user_id = msg.member.user.id;
        user_list.username = msg.member.user.username;
        user_list.mbti  = type;
        user_list_arr.push(user_list);
      } else {
        user_list_arr[j].username = msg.member.user.username;
        user_list_arr[j].mbti  = type;
      }
 
      //Array to json
      data = JSON.stringify(user_list_arr);
      fs.writeFileSync('./user_list.json', data, 'utf8');
 
      msg.channel.send("성공!");
    }
  }
 
  if (msg.content.startsWith('mbti 궁합')) {
    var comp_mbti = msg.content.split(' ');
 
    //json to Array
    var data = fs.readFileSync('./user_list.json''utf8');
    var user_list_arr = JSON.parse(data);
 
    //search user 1
    var user_1 = "";
    if (comp_mbti[2=== undefined) {
      user_1 = "";
    } else {
      user_1 = comp_mbti[2].replace(/[^0-9]/g,'');
    }
 
    var user1type = 16;
    var user1name = "";
    for(var k=0; k<user_list_arr.length; k++) {
      if (user_list_arr[k].user_id == user_1) {
        user1type = user_list_arr[k].mbti;
        user1name = user_list_arr[k].username;
        break;
      }
    }
 
    //search user 2
    var user_2 = "";
    if (comp_mbti[3=== undefined) {
      user_2 = msg.member.user.id;
    } else if (comp_mbti[3== "") {
      comp_mbti[3= comp_mbti[4]; //중간에 띄어쓰기가 한 번 더 들어간 경우에도 정상적인 처리를 위해서
      user_2 = comp_mbti[3].replace(/[^0-9]/g,'');
    } else {
      user_2 = comp_mbti[3].replace(/[^0-9]/g,'');
    }
 
    var user2type = 16;
    var user2name = "";
    for(var k=0; k<user_list_arr.length; k++) {
      if (user_list_arr[k].user_id == user_2) {
        user2type = user_list_arr[k].mbti;
        user2name = user_list_arr[k].username;
        break;
      }
    }
 
    //result
    if (user1type == 16 || user2type == 16) {
      msg.channel.send(noresultEmbed);
    } else{
 
      if (user2type > user1type) {
        [user1type, user2type] = [user2type, user1type];
      }
 
      var result = mbtiR[user1type][user2type];
 
      var explain_text1 = " 와(과) ";
      var explain_text2 = " 의 궁합";
 
      var resultEmbed = new Discord.MessageEmbed()
          .setColor('#0099FF')
          .setTitle(user1name.concat(explain_text1,user2name,explain_text2))
        .addField('결과', result, true)
          .setFooter('Powered by node.js  Made by M4ndU''');
 
      msg.channel.send(resultEmbed);
 
    }
  }
cs

 

 

 

 

 

 

 

 

 

 

 


실행

 

user_list.json 파일을 하나 만들어두고 봇을 실행해야한다.

 

 

실행 후에 오류나면 아래 글에서 해결법을 찾아보자. 아래 글에도 없다면 구글링!

https://mandu-mandu.tistory.com/381

 

discord js 설치, 실행 오류 / 오디오 재생 오류 문제 해결

1. npm으로 discord.js 설치 후 봇 실행시 발생하는 오류 해결 /Client.js:39 } catch { ^ 봇 실행시 위와 같은 오류가 발생할 경우입니다. 저같은 경우에는 discord.js github에 나와있는 설치방법으로 설치를 했

mandu-mandu.tistory.com

 

반응형
반응형

이번에 추가할 기능은 아래와 같습니다.

 

[

로컬 노래파일 재생,

로컬 사진 전송,

상태메세지 설정,

메세지 지우기

]

 


개발환경 : UBUNTU 18.04.3

개발언어: NODE JS V8

모듈 : discord.js v11

텍스트 에디터: ATOM (편하신거 사용하시면 됩니다.)

 

 

 

 

로컬 경로에 존재하는 mp3파일을 재생하는 기능입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  if (msg.content === 'r.play') {
    // Only try to join the sender's voice channel if they are in one themselves
    if (msg.member.voiceChannel) {
      msg.member.voiceChannel.join()
        .then(connection => { // Connection is an instance of VoiceConnection
          msg.reply('playing music!');
          const dispatcher = connection.playFile('./music.mp3');
          dispatcher.on("end", end => {});
        })
        .catch(console.log);
    } else {
      msg.reply('먼저 방에 들어가');
    }
  }
  if (msg.content === 'r.leave') {
    // Only try to join the sender's voice channel if they are in one themselves
    if (msg.member.voiceChannel) {
      msg.member.voiceChannel.leave();
      msg.reply('bye!');
    } else {
      msg.reply('이미 나왔는데..');
    }
  }
 
cs

 

r.play는 내가 들어간 음성 채팅방으로 들어가서 music.mp3를 재생합니다. (7행에서 파일명, 경로를 설정할 수 있습니다.)

내가 음성채팅방에 들어가 있지 않은 경우 12행 메세지를 출력합니다.

 

r.leave로 방을 나갈 수 있습니다.

 

 

 

 

오디오 재생시 오디오가 재생되지 않고 바로 종료되는 문제

또는 Error: FFMPEG not found 에러 발생시 아래 포스트 참조

 

https://mandu-mandu.tistory.com/381

 

discord js 설치 후 catch 오류 / 오디오 재생 오류 문제 해결

1. npm으로 discord.js 설치 후 봇 실행시 발생하는 오류 해결 /Client.js:39 } catch { ^ 봇 실행시 위와 같은 오류가 발생할 경우입니다. 저같은 경우에는 discord.js github에 나와있는 설치방법으로 설치를 했..

mandu-mandu.tistory.com

 

 

 


로컬경로에 존재하는 이미지를 채팅방에 전송하는 기능입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  if (msg.content.startsWith('r.gif')) {
    const args = msg.content.split(' ').slice(1); // All arguments behind the command name with the prefix
    var no = args.join(' '); // Amount of messages which should be deleted
 
    if (!no) no = '1';
    if (isNaN(no) || no > 14return msg.reply('only number 1-14');
 
   // Create the attachment using Attachment
   var path = "./gif/";
   path = path.concat(no,".gif");
   console.log(path);
   const attachment = new Attachment(path);
   // Send the attachment in the message channel with a content
   msg.channel.send(attachment);
  }
cs

명령어는 r.gif n 입니다. n에는 정수가 들어갑니다. ex) r.gif 1

 

저는 gif 이미지를 보내도록 했습니다. 필요에 따라서 10번행의 확장자명을 gif에서 png, jpg등으로 바꾸실 수 있습니다.

저는 ./gif/ 디렉토리 안에 1.gif, 2.gif, 3.gif ... 파일을 넣어 놨습니다.

 

파일 명을 정수로 설정해주세요!

 

 

5번행은 명령어 뒤에 정수가 입력되지 않은 경우 자동으로 1로 하도록

6번행은 숫자인지 검증, 14까지만 입력되도록 한 것입니다.

 


 

상태메세지 설정은 아래 사진과 같이 별명 아래에 메세지를 표시하는 것입니다.

ready 안에 아래 코드 한줄을 추가만 해주시면 됩니다. (전체 코드는 밑에서 확인하실 수 있습니다.)

  client.user.setActivity('봇 만들자', { type: 'WATCHING' })

 

type에는 WATCHING (시청 중) 외에도 

  • PLAYING   (하는 중)
  • STREAMING
  • LISTENING

으로 설정하실 수 있습니다.

 

 

 


메세지 지우기 기능

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  if (msg.content.toLowerCase().startsWith("r.clear")) {
      const args = msg.content.split(' ').slice(1); // All arguments behind the command name with the prefix
      const amount = args.join(' '); // Amount of messages which should be deleted
 
      if (!amount) return msg.reply('You haven\'t given an amount of messages which should be deleted!'); // Checks if the `amount` parameter is given
      if (isNaN(amount)) return msg.reply('The amount parameter isn`t a number!'); // Checks if the `amount` parameter is a number. If not, the command throws an error
 
      if (amount > 10return msg.reply('You can`t delete more than 10 messages at once!'); // Checks if the `amount` integer is bigger than 100
      if (amount < 1return msg.reply('You have to delete at least 1 message!'); // Checks if the `amount` integer is smaller than 1
 
      msg.channel.fetchMessages({ limit: amount }).then(dmsg => { // Fetches the messages
      msg.channel.bulkDelete(dmsg // Bulk deletes all messages that have been fetched and are not older than 14 days (due to the Discord API)
      ).catch(console.log);});
    }
cs

 

r.clear n 과 같이 입력하면, n개의 메세지를 삭제해줍니다.

 

8행 9행에서 각각 최대 최소 개수를 설정할 수 있습니다.

 

 

 

 


전체 봇 소스코드 bot.js

1
2
3
4
5
6
7
8
9
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
 
// Create an instance of a Discord client
const client = new Client();
 
 
client.on('ready', () => {
  console.log(`Logged in as ${client.user.tag}!`);
  client.user.setActivity('봇 만들자', { type: 'WATCHING' })
});
 
client.on('message', msg => {
  if (msg.content.startsWith('r.help')) {
    msg.reply('Powered by node.js\nMade by M4ndU');
  }
  if (msg.content.startsWith('r.gif')) {
    const args = msg.content.split(' ').slice(1); // All arguments behind the command name with the prefix
    var no = args.join(' '); // Amount of messages which should be deleted
 
    if (!no) no = '1';
    if (isNaN(no) || no > 14return msg.reply('only number 1-14');
 
   // Create the attachment using Attachment
   var path = "./gif/";
   path = path.concat(no,".gif");
   console.log(path);
   const attachment = new Attachment(path);
   // Send the attachment in the message channel with a content
   msg.channel.send(attachment);
  }
 
 
  if (msg.content === 'r.play') {
    // Only try to join the sender's voice channel if they are in one themselves
    if (msg.member.voiceChannel) {
      msg.member.voiceChannel.join()
        .then(connection => { // Connection is an instance of VoiceConnection
          msg.reply('playing music!');
          const dispatcher = connection.playFile('./music.mp3');
          dispatcher.on("end", end => {});
        })
        .catch(console.log);
    } else {
      msg.reply('먼저 방에 들어가');
    }
  }
  if (msg.content === 'r.leave') {
    // Only try to join the sender's voice channel if they are in one themselves
    if (msg.member.voiceChannel) {
      msg.member.voiceChannel.leave();
      msg.reply('bye!');
    } else {
      msg.reply('이미 나왔는데..');
    }
  }
 
  if (msg.content.toLowerCase().startsWith("r.clear")) {
      const args = msg.content.split(' ').slice(1); // All arguments behind the command name with the prefix
      const amount = args.join(' '); // Amount of messages which should be deleted
 
      if (!amount) return msg.reply('You haven\'t given an amount of messages which should be deleted!'); // Checks if the `amount` parameter is given
      if (isNaN(amount)) return msg.reply('The amount parameter isn`t a number!'); // Checks if the `amount` parameter is a number. If not, the command throws an error
 
      if (amount > 10return msg.reply('You can`t delete more than 10 messages at once!'); // Checks if the `amount` integer is bigger than 100
      if (amount < 1return msg.reply('You have to delete at least 1 message!'); // Checks if the `amount` integer is smaller than 1
 
      msg.channel.fetchMessages({ limit: amount }).then(dmsg => { // Fetches the messages
      msg.channel.bulkDelete(dmsg // Bulk deletes all messages that have been fetched and are not older than 14 days (due to the Discord API)
      ).catch(console.log);});
    }
 
  if (msg.content === 'r.whoami') {
    // Send the user's avatar URL
    msg.reply(msg.author.avatarURL);
  }
});
 
client.login('token');
 
cs

 

79행 token에 반드시 자신의 봇의 토큰값을 입력하세요

반응형

+ Recent posts