├── .gitattributes ├── .gitignore ├── KakaoBotAPI.sln ├── KakaoBotAPI ├── Bot │ ├── ChatBot.cs │ ├── IKakaoBot.cs │ └── QuizBot.cs ├── KakaoBotAPI.csproj ├── Model │ ├── Quiz.cs │ ├── QuizUser.cs │ ├── Title.cs │ └── User.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Resources │ ├── bot_limitedWords.txt │ ├── cmd_shortcuts.txt │ ├── how_to_add_quiz_subjects.txt │ └── quiz_notice.txt └── Util │ ├── IniHelper.cs │ ├── ListUtil.cs │ └── XmlHelper.cs ├── KakaoTalkAPI ├── ClipboardManager.cs ├── KakaoTalk.cs ├── KakaoTalkAPI.csproj ├── Properties │ └── AssemblyInfo.cs └── WinAPI.cs ├── LICENSE.md ├── README.md └── SampleApplication ├── App.config ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── SampleApplication.csproj ├── SampleChatBot.cs ├── SampleQuizBot.cs └── bin ├── Debug └── data │ └── quiz │ └── [General]Saying │ ├── data.xml │ └── settings.ini └── Release └── data └── quiz └── [General]Saying ├── data.xml └── settings.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Custom 5 | KakaoTalkAPI/bin/ 6 | KakaoBotAPI/bin/ 7 | SampleApplication/bin/[Dd]ebug/* 8 | !SampleApplication/bin/[Dd]ebug/data/ 9 | SampleApplication/bin/[Dd]ebug/data/* 10 | !SampleApplication/bin/[Dd]ebug/data/quiz/ 11 | 12 | SampleApplication/bin/[Rr]elease/* 13 | !SampleApplication/bin/[Rr]elease/data/ 14 | SampleApplication/bin/[Rr]elease/data/* 15 | !SampleApplication/bin/[Rr]elease/data/quiz/ 16 | 17 | # User-specific files 18 | *.suo 19 | *.user 20 | *.userosscache 21 | *.sln.docstates 22 | 23 | # User-specific files (MonoDevelop/Xamarin Studio) 24 | *.userprefs 25 | 26 | # Build results 27 | #[Dd]ebug/ 28 | [Dd]ebugPublic/ 29 | #[Rr]elease/ 30 | [Rr]eleases/ 31 | x64/ 32 | x86/ 33 | bld/ 34 | #[Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | 38 | # Visual Studio 2015 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUNIT 48 | *.VisualState.xml 49 | TestResult.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # DNX 57 | project.lock.json 58 | project.fragment.lock.json 59 | artifacts/ 60 | 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # TFS 2012 Local Workspace 107 | $tf/ 108 | 109 | # Guidance Automation Toolkit 110 | *.gpState 111 | 112 | # ReSharper is a .NET coding add-in 113 | _ReSharper*/ 114 | *.[Rr]e[Ss]harper 115 | *.DotSettings.user 116 | 117 | # JustCode is a .NET coding add-in 118 | .JustCode 119 | 120 | # TeamCity is a build add-in 121 | _TeamCity* 122 | 123 | # DotCover is a Code Coverage Tool 124 | *.dotCover 125 | 126 | # NCrunch 127 | _NCrunch_* 128 | .*crunch*.local.xml 129 | nCrunchTemp_* 130 | 131 | # MightyMoose 132 | *.mm.* 133 | AutoTest.Net/ 134 | 135 | # Web workbench (sass) 136 | .sass-cache/ 137 | 138 | # Installshield output folder 139 | [Ee]xpress/ 140 | 141 | # DocProject is a documentation generator add-in 142 | DocProject/buildhelp/ 143 | DocProject/Help/*.HxT 144 | DocProject/Help/*.HxC 145 | DocProject/Help/*.hhc 146 | DocProject/Help/*.hhk 147 | DocProject/Help/*.hhp 148 | DocProject/Help/Html2 149 | DocProject/Help/html 150 | 151 | # Click-Once directory 152 | publish/ 153 | 154 | # Publish Web Output 155 | *.[Pp]ublish.xml 156 | *.azurePubxml 157 | # TODO: Comment the next line if you want to checkin your web deploy settings 158 | # but database connection strings (with potential passwords) will be unencrypted 159 | #*.pubxml 160 | *.publishproj 161 | 162 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 163 | # checkin your Azure Web App publish settings, but sensitive information contained 164 | # in these scripts will be unencrypted 165 | PublishScripts/ 166 | 167 | # NuGet Packages 168 | *.nupkg 169 | # The packages folder can be ignored because of Package Restore 170 | **/packages/* 171 | # except build/, which is used as an MSBuild target. 172 | !**/packages/build/ 173 | # Uncomment if necessary however generally it will be regenerated when needed 174 | #!**/packages/repositories.config 175 | # NuGet v3's project.json files produces more ignoreable files 176 | *.nuget.props 177 | *.nuget.targets 178 | 179 | # Microsoft Azure Build Output 180 | csx/ 181 | *.build.csdef 182 | 183 | # Microsoft Azure Emulator 184 | ecf/ 185 | rcf/ 186 | 187 | # Windows Store app package directories and files 188 | AppPackages/ 189 | BundleArtifacts/ 190 | Package.StoreAssociation.xml 191 | _pkginfo.txt 192 | 193 | # Visual Studio cache files 194 | # files ending in .cache can be ignored 195 | *.[Cc]ache 196 | # but keep track of directories ending in .cache 197 | !*.[Cc]ache/ 198 | 199 | # Others 200 | ClientBin/ 201 | ~$* 202 | *~ 203 | *.dbmdl 204 | *.dbproj.schemaview 205 | *.jfm 206 | *.pfx 207 | *.publishsettings 208 | node_modules/ 209 | orleans.codegen.cs 210 | 211 | # Since there are multiple workflows, uncomment next line to ignore bower_components 212 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 213 | #bower_components/ 214 | 215 | # RIA/Silverlight projects 216 | Generated_Code/ 217 | 218 | # Backup & report files from converting an old project file 219 | # to a newer Visual Studio version. Backup files are not needed, 220 | # because we have git ;-) 221 | _UpgradeReport_Files/ 222 | Backup*/ 223 | UpgradeLog*.XML 224 | UpgradeLog*.htm 225 | 226 | # SQL Server files 227 | *.mdf 228 | *.ldf 229 | 230 | # Business Intelligence projects 231 | *.rdl.data 232 | *.bim.layout 233 | *.bim_*.settings 234 | 235 | # Microsoft Fakes 236 | FakesAssemblies/ 237 | 238 | # GhostDoc plugin setting file 239 | *.GhostDoc.xml 240 | 241 | # Node.js Tools for Visual Studio 242 | .ntvs_analysis.dat 243 | 244 | # Visual Studio 6 build log 245 | *.plg 246 | 247 | # Visual Studio 6 workspace options file 248 | *.opt 249 | 250 | # Visual Studio LightSwitch build output 251 | **/*.HTMLClient/GeneratedArtifacts 252 | **/*.DesktopClient/GeneratedArtifacts 253 | **/*.DesktopClient/ModelManifest.xml 254 | **/*.Server/GeneratedArtifacts 255 | **/*.Server/ModelManifest.xml 256 | _Pvt_Extensions 257 | 258 | # Paket dependency manager 259 | .paket/paket.exe 260 | paket-files/ 261 | 262 | # FAKE - F# Make 263 | .fake/ 264 | 265 | # JetBrains Rider 266 | .idea/ 267 | *.sln.iml 268 | 269 | # CodeRush 270 | .cr/ 271 | 272 | # Python Tools for Visual Studio (PTVS) 273 | __pycache__/ 274 | *.pyc -------------------------------------------------------------------------------- /KakaoBotAPI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29025.244 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KakaoBotAPI", "KakaoBotAPI\KakaoBotAPI.csproj", "{564EA995-D572-4A10-A312-E0BC12E66424}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApplication", "SampleApplication\SampleApplication.csproj", "{AA6BF2C8-5537-4E32-89E7-D6A3608B1166}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KakaoTalkAPI", "KakaoTalkAPI\KakaoTalkAPI.csproj", "{BD0427C4-BA2A-48F8-8329-11574735F961}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {564EA995-D572-4A10-A312-E0BC12E66424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {564EA995-D572-4A10-A312-E0BC12E66424}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {564EA995-D572-4A10-A312-E0BC12E66424}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {564EA995-D572-4A10-A312-E0BC12E66424}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {AA6BF2C8-5537-4E32-89E7-D6A3608B1166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {AA6BF2C8-5537-4E32-89E7-D6A3608B1166}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {AA6BF2C8-5537-4E32-89E7-D6A3608B1166}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {AA6BF2C8-5537-4E32-89E7-D6A3608B1166}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {BD0427C4-BA2A-48F8-8329-11574735F961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BD0427C4-BA2A-48F8-8329-11574735F961}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BD0427C4-BA2A-48F8-8329-11574735F961}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BD0427C4-BA2A-48F8-8329-11574735F961}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {101A6F8A-9515-4950-9D8B-72A2ACD68467} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /KakaoBotAPI/Bot/ChatBot.cs: -------------------------------------------------------------------------------- 1 | using Less.API.NetFramework.KakaoBotAPI.Model; 2 | using Less.API.NetFramework.KakaoBotAPI.Util; 3 | using Less.API.NetFramework.KakaoTalkAPI; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading; 10 | 11 | namespace Less.API.NetFramework.KakaoBotAPI.Bot 12 | { 13 | /// 14 | /// KakaoBotAPI에서 제공하는 채팅봇 중 가장 기본 형태가 되는 클래스입니다. 15 | /// 만약 봇 커스터마이징 시 최고 레벨의 자유도를 원한다면, 이 클래스를 상속하여 사용하십시오. 16 | /// 17 | public abstract class ChatBot : IKakaoBot 18 | { 19 | /// 20 | /// 채팅창에 메시지나 이미지, 이모티콘 등을 보낸 후 적용할 지연 시간 21 | /// 이 값을 너무 적게 설정할 경우, 카카오톡의 매크로 방지에 걸릴 수 있으니 주의하십시오. 22 | /// 23 | public static int SendMessageInterval = 500; 24 | 25 | /// 26 | /// 채팅창에서 메시지 로그를 폴링으로 가져오는 데 적용할 지연 시간 27 | /// 이 값을 너무 적게 설정할 경우, 시스템에 과부하가 발생할 수 있으니 주의하십시오. 28 | /// 29 | public static int GetMessageInterval = 50; 30 | 31 | /// 32 | /// 데이터 파일들이 위치하는 최상단 경로 33 | /// 34 | protected const string DataPath = @"data\"; 35 | 36 | /// 37 | /// Well-Known 파일 확장자들(XML, INI 등)을 제외한 데이터 파일들의 기본 확장자 38 | /// 39 | protected const string DataFileDefaultExtension = ".dat"; 40 | 41 | /// 42 | /// 설정 파일들이 위치하는 최상단 경로 43 | /// 44 | protected const string ConfigPath = @"config\"; 45 | 46 | /// 47 | /// 설정 파일들의 기본 확장자 48 | /// 49 | protected const string ConfigFileDefaultExtension = ".cfg"; 50 | 51 | /// 52 | /// Profile 파일의 이름 53 | /// 54 | protected const string ProfileFileName = "profile"; 55 | 56 | /// 57 | /// Profile 파일의 확장자 58 | /// 59 | protected const string ProfileFileExtension = XmlHelper.FileExtension; 60 | 61 | /// 62 | /// 바로가기 명령어를 열거한 파일의 이름 63 | /// 64 | protected const string ShortcutFileName = "cmd_shortcuts"; 65 | 66 | /// 67 | /// 바로가기 명령어를 열거한 파일의 확장자 68 | /// 69 | protected const string ShortcutFileExtension = ConfigFileDefaultExtension; 70 | 71 | /// 72 | /// 봇의 금지어-대체어 목록을 열거한 파일의 이름 73 | /// 74 | protected const string BotLimitedWordsFileName = "bot_limitedWords"; 75 | 76 | /// 77 | /// 봇의 금지어-대체어 목록을 열거한 파일의 확장자 78 | /// 79 | protected const string BotLimitedWordsFileExtension = IniHelper.FileExtension; 80 | 81 | /// 82 | /// 텍스트 파일의 확장자 83 | /// 84 | protected const string TextFileExtension = ".txt"; 85 | 86 | /// 87 | /// 바로가기 명령어 목록을 담고 있는 배열 88 | /// GetOriginalCommand 메서드에서 초기화됩니다. 89 | /// 90 | protected string[] ShortcutTexts = null; 91 | 92 | /// 93 | /// 봇의 금지어 목록 94 | /// RefreshLimitedWordsList 메서드를 통해 데이터가 삽입됩니다. 95 | /// 96 | protected List LimitedWords = new List(); 97 | 98 | /// 99 | /// 봇의 대체어 목록 100 | /// RefreshLimitedWordsList 메서드를 통해 데이터가 삽입됩니다. 101 | /// 102 | protected List ReplacedWords = new List(); 103 | 104 | /// 105 | /// 자동으로 생성되는 새 User 객체의 채팅을 무시할지에 대한 여부 106 | /// true : 채팅을 무시합니다. 107 | /// false : 채팅을 무시하지 않습니다. 108 | /// 109 | protected static bool IsNewUserIgnored = false; 110 | 111 | /// 112 | /// 채팅방의 이름 113 | /// 이 값이 정확하지 않으면 봇이 동작하지 않으므로, 꼭 제대로 확인하십시오. 114 | /// 115 | public string RoomName { get; } 116 | 117 | /// 118 | /// 대상 채팅방의 유형 119 | /// ChatBot.TargetTypeOption.Self : 본인. 봇을 적용하는 상대가 본인인 경우 이 값으로 설정합니다. 120 | /// ChatBot.TargetTypeOption.Friend : 친구. 봇을 적용하는 채팅방이 친구와의 1:1 대화방인 경우 이 값으로 설정합니다. 121 | /// ChatBot.TargetTypeOption.Group : 단톡. 봇을 적용하는 채팅방이 단톡방인 경우 이 값으로 설정합니다. 122 | /// 123 | public TargetTypeOption Type { get; } 124 | 125 | /// 126 | /// 봇 돌리미의 해당 채팅방에서의 닉네임 127 | /// 돌리미에게만 특정 권한을 부여하여 명령어를 제한할 수 있도록 하기 위해 만들어진 값입니다. 128 | /// 129 | public string BotRunnerName { get; } 130 | 131 | /// 132 | /// 채팅방에 부여할 특별한 식별자 133 | /// 이 값은 각각의 봇 인스턴스를 구별하는 식별자입니다. 134 | /// 방 이름을 다르게 하더라도 식별자가 같으면 같은 설정 정보를 이용하도록 설계되었으며, 반대로 방 이름을 같게 하더라도 식별자가 다르면 다른 설정 정보를 이용하도록 설계되었습니다. 135 | /// 136 | public string Identifier { get; } 137 | 138 | /// 139 | /// 마지막으로 분석한 메시지의 인덱스 값 140 | /// GetMessagesUsingClipboard 메서드를 호출한 뒤에 메시지의 개수가 달라졌다면, 이 값을 증가 또는 감소시키는 원리입니다. 141 | /// 142 | public int LastMessageIndex { get; set; } 143 | 144 | /// 145 | /// 봇이 돌아가는 채팅방 인스턴스 146 | /// 147 | protected KakaoTalk.KTChatWindow Window; 148 | 149 | /// 150 | /// 봇 인스턴스에 대한 Main Thread 151 | /// 152 | protected Thread MainTaskRunner; 153 | 154 | /// 155 | /// 봇의 메인 Thread가 실행 중인지에 대한 여부 156 | /// true : StartMainTask 메서드가 호출되었으며, StopMainTask 메서드가 아직 호출되지 않은 상태를 나타냅니다. 157 | /// false : StartMainTask 메서드가 호출되지 않았거나, StopMainTask 메서드가 호출되어 봇이 종료된 상태를 나타냅니다. 158 | /// 159 | protected bool IsMainTaskRunning; 160 | 161 | /// 162 | /// 봇 인스턴스에 대한 유저 목록 163 | /// 164 | protected List Users = new List(); 165 | 166 | /// 167 | /// 대상 채팅방의 유형 168 | /// ChatBot.TargetTypeOption.Self : 본인. 봇을 적용하는 상대가 본인인 경우 이 값으로 설정합니다. 169 | /// ChatBot.TargetTypeOption.Friend : 친구. 봇을 적용하는 채팅방이 친구와의 1:1 대화방인 경우 이 값으로 설정합니다. 170 | /// ChatBot.TargetTypeOption.Group : 단톡. 봇을 적용하는 채팅방이 단톡방인 경우 이 값으로 설정합니다. 171 | /// 172 | public enum TargetTypeOption { Self = 1, Friend, Group } 173 | 174 | /// 175 | /// 채팅봇 객체를 생성합니다. 176 | /// 177 | /// 채팅방의 이름 178 | /// 대상 채팅방의 유형 179 | /// 봇 돌리미의 해당 채팅방에서의 닉네임 180 | /// 채팅방에 부여할 특별한 식별자 181 | public ChatBot(string roomName, TargetTypeOption type, string botRunnerName, string identifier) 182 | { 183 | RoomName = roomName; 184 | Type = type; 185 | BotRunnerName = botRunnerName; 186 | Identifier = identifier; 187 | } 188 | 189 | /// 190 | /// 봇 인스턴스를 시작합니다. 191 | /// 하나의 봇 인스턴스에 여러 개의 Thread에 기반한 작업을 수행할 경우, 이 메서드를 오버라이드하여 적절한 조치를 취하십시오. 192 | /// 193 | public virtual void Start() 194 | { 195 | StartMainTask(); 196 | } 197 | 198 | /// 199 | /// 봇 인스턴스를 중지합니다. 200 | /// 하나의 봇 인스턴스에 여러 개의 Thread에 기반한 작업을 수행할 경우, 이 메서드를 오버라이드하여 적절한 조치를 취하십시오. 201 | /// 202 | public virtual void Stop() 203 | { 204 | StopMainTask(); 205 | } 206 | 207 | /// 208 | /// 봇 객체를 참조하여 메시지를 전송합니다. 209 | /// 메시지를 전송하기 전에 금지어 목록에 걸리는 단어가 있는지 확인하고, 있을 경우 대체어로 바꾸어 전송합니다. 210 | /// 전송이 성공하려면, IsMainTaskRunning이 true여야 합니다. 211 | /// 212 | /// 전송할 메시지 213 | /// 전송의 성공 여부 214 | public bool SendMessage(string message) 215 | { 216 | for (int i = 0; i < LimitedWords.Count; i++) 217 | { 218 | if (message.Contains(LimitedWords[i]) && !ReplacedWords[i].Contains(LimitedWords[i])) message = message.Replace(LimitedWords[i], ReplacedWords[i]); 219 | } 220 | Window.SendText(message); 221 | 222 | return true; 223 | } 224 | 225 | /// 226 | /// 봇 객체를 참조하여 이미지를 전송합니다. 227 | /// 전송이 성공하려면, IsMainTaskRunning이 true여야 합니다. 228 | /// 229 | /// 전송할 이미지 파일의 경로 230 | /// 전송의 성공 여부 231 | public bool SendImage(string path) 232 | { 233 | Window.SendImageUsingClipboard(path); 234 | 235 | return true; 236 | } 237 | 238 | /// 239 | /// 봇 객체를 참조하여 이모티콘을 전송합니다. 240 | /// 전송이 성공하려면, IsMainTaskRunning이 true여야 합니다. 241 | /// 242 | /// 전송할 카카오톡 이모티콘 243 | /// 전송의 성공 여부 244 | public bool SendEmoticon(KakaoTalk.Emoticon emoticon) 245 | { 246 | Window.SendEmoticon(emoticon); 247 | 248 | return true; 249 | } 250 | 251 | /// 252 | /// 봇 인스턴스의 메인 Thread를 시작합니다. 253 | /// 254 | /// 봇 가동 완료 시 채팅창을 최소화할지 여부 255 | protected void StartMainTask(bool minimizeWindow = false) 256 | { 257 | if (!KakaoTalk.IsInitialized()) KakaoTalk.InitializeManually(); 258 | 259 | if (Type == TargetTypeOption.Self) Window = KakaoTalk.MainWindow.Friends.StartChattingWithMyself(RoomName, minimizeWindow); 260 | else if (Type == TargetTypeOption.Friend) Window = KakaoTalk.MainWindow.Friends.StartChattingWith(RoomName, minimizeWindow); 261 | else if (Type == TargetTypeOption.Group) Window = KakaoTalk.MainWindow.Chatting.StartChattingAt(RoomName, minimizeWindow); 262 | 263 | KakaoTalk.Message[] messages; 264 | while ((messages = Window.GetMessagesUsingClipboard()) == null) Thread.Sleep(GetMessageInterval); 265 | 266 | SendMainTaskStartNotice(); 267 | 268 | while ((messages = Window.GetMessagesUsingClipboard()) == null) Thread.Sleep(GetMessageInterval); 269 | LastMessageIndex = messages.Length - 1; 270 | MainTaskRunner = new Thread(new ThreadStart(RunMain)); 271 | MainTaskRunner.Start(); 272 | } 273 | 274 | /// 275 | /// 봇 인스턴스의 메인 Thread를 중지합니다. 276 | /// 277 | protected void StopMainTask() 278 | { 279 | IsMainTaskRunning = false; 280 | SendMainTaskStopNotice(); 281 | Window.Dispose(); 282 | } 283 | 284 | /// 285 | /// 봇의 메인 Thread가 수행할 명령을 기술합니다. 286 | /// 가능하면 이 메서드의 내용을 변경하지 마십시오. 꼭 필요한 경우에만 오버라이드하는 것을 권장합니다. 287 | /// 288 | protected virtual void RunMain() 289 | { 290 | IsMainTaskRunning = true; 291 | 292 | RefreshUserData(); 293 | RefreshLimitedWordsList(); 294 | InitializeBotSettings(); 295 | 296 | KakaoTalk.Message[] messages; 297 | KakaoTalk.MessageType messageType; 298 | string userName, content; 299 | DateTime sendTime; 300 | 301 | while (IsMainTaskRunning) 302 | { 303 | // 메시지 목록 얻어오기 304 | Thread.Sleep(GetMessageInterval); 305 | messages = Window.GetMessagesUsingClipboard(); 306 | if (messages == null) continue; 307 | if (messages.Length == LastMessageIndex + 1) continue; 308 | 309 | sendTime = DateTime.Now; 310 | 311 | // 메시지 목록 분석 312 | for (int i = LastMessageIndex + 1; i < messages.Length; i++) 313 | { 314 | messageType = messages[i].Type; 315 | userName = messages[i].UserName; 316 | content = messages[i].Content; 317 | 318 | LastMessageIndex++; 319 | if (messageType == KakaoTalk.MessageType.Unknown) continue; 320 | else if (messageType == KakaoTalk.MessageType.DateChange) SendDateChangeNotice(content, sendTime); 321 | else if (messageType == KakaoTalk.MessageType.UserJoin) SendUserJoinNotice(userName, sendTime); 322 | else if (messageType == KakaoTalk.MessageType.UserLeave) SendUserLeaveNotice(userName, sendTime); 323 | else if (messageType == KakaoTalk.MessageType.Talk) ProcessUserMessage(userName, content, sendTime); 324 | } 325 | } 326 | } 327 | 328 | /// 329 | /// 파일 시스템으로부터 유저 데이터를 불러옵니다. 330 | /// 만약 이 클래스 상속 시 새로운 유저 클래스를 같이 만든다면, 필요한 노드 데이터들을 전부 불러올 수 있도록 이 메서드를 오버라이드하여 사용하십시오. 331 | /// 332 | /// 유저 목록 333 | protected virtual void RefreshUserData() 334 | { 335 | var document = GetUserDataDocument(); 336 | var users = new List(); 337 | 338 | string nickname; 339 | bool isIgnored; 340 | string value; 341 | 342 | for (int i = 0; i < document.ChildNodes.Count; i++) 343 | { 344 | var node = document.ChildNodes[i]; 345 | 346 | nickname = node.GetData("nickname"); 347 | 348 | value = node.GetData("isIgnored"); 349 | if (value == "true") isIgnored = true; 350 | else if (value == "false") isIgnored = false; 351 | else throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 isIgnored 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 352 | 353 | users.Add(new User(nickname, isIgnored)); 354 | } 355 | 356 | Users = users; 357 | } 358 | 359 | /// 360 | /// 유저 데이터가 기록된 XML 문서를 가져옵니다. 361 | /// 362 | /// XML 문서 객체 363 | [MethodImpl(MethodImplOptions.Synchronized)] 364 | protected XmlHelper.Document GetUserDataDocument() 365 | { 366 | string path = ConfigPath + $"{Identifier}\\" + ProfileFileName + ProfileFileExtension; 367 | Directory.CreateDirectory(path.Substring(0, path.LastIndexOf('\\'))); 368 | 369 | var helper = new XmlHelper(path); 370 | if (!File.Exists(path)) helper.CreateFile("list", new List()); 371 | 372 | return helper.ReadFile(); 373 | } 374 | 375 | /// 376 | /// 파일 시스템에 유저 데이터를 저장합니다. 377 | /// 만약 이 클래스 상속 시 새로운 유저 클래스를 같이 만든다면, 이 메서드가 필요한 노드들을 전부 생성하여 저장할 수 있도록 오버라이드하여 사용하십시오. 378 | /// 379 | protected virtual void SaveUserData() 380 | { 381 | string path = ConfigPath + $"{Identifier}\\" + ProfileFileName + ProfileFileExtension; 382 | Directory.CreateDirectory(path.Substring(0, path.LastIndexOf('\\'))); 383 | 384 | var helper = new XmlHelper(path); 385 | var nodeList = new List(); 386 | XmlHelper.Node node; 387 | 388 | for (int i = 0; i < Users.Count; i++) 389 | { 390 | node = new XmlHelper.Node("user"); 391 | node.AddData("nickname", Users[i].Nickname); 392 | node.AddData("isIgnored", Users[i].IsIgnored ? "true" : "false"); 393 | 394 | nodeList.Add(node); 395 | } 396 | 397 | helper.CreateFile("list", nodeList); 398 | } 399 | 400 | /// 401 | /// 닉네임을 통해 유저 정보를 얻어옵니다. 402 | /// 만약 현재 해당 닉네임을 가진 유저가 기록에 존재하지 않으면 null을 반환합니다. 403 | /// 만약 이 클래스 상속 시 새로운 유저 클래스를 같이 만든다면, 이 메서드가 해당 유저 타입을 리턴하도록 new 키워드를 통하여 재정의하십시오. 404 | /// 405 | /// 유저의 닉네임 406 | /// 유저 객체 407 | protected virtual User FindUserByNickname(string userName) 408 | { 409 | foreach (User user in Users) if (user.Nickname == userName) return user; 410 | 411 | return null; 412 | } 413 | 414 | /// 415 | /// 해당 닉네임을 가진 유저 정보를 새로 등록합니다. 416 | /// 만약 이 클래스 상속 시 새로운 유저 클래스를 같이 만든다면, 이 메서드가 해당 유저 타입을 등록하고 리턴하도록 new 키워드를 통하여 재정의하십시오. 417 | /// 418 | /// 새로 등록될 유저의 닉네임 419 | /// 유저 객체 420 | protected virtual User AddNewUser(string userName) 421 | { 422 | var user = new User(userName, IsNewUserIgnored); 423 | Users.Add(user); 424 | SaveUserData(); 425 | return user; 426 | } 427 | 428 | /// 429 | /// 봇이 본격적으로 가동되기 전 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 430 | /// 431 | protected virtual void SendMainTaskStartNotice() 432 | { 433 | Window.SendText("채팅봇을 시작합니다."); 434 | Thread.Sleep(SendMessageInterval); 435 | } 436 | 437 | /// 438 | /// 봇이 종료될 때 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 439 | /// 440 | protected virtual void SendMainTaskStopNotice() 441 | { 442 | Window.SendText("채팅봇을 종료합니다."); 443 | Thread.Sleep(SendMessageInterval); 444 | } 445 | 446 | /// 447 | /// 날짜 변경 메시지가 출력되는 시점에 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 448 | /// 449 | /// 바뀐 요일에 대한 정보 450 | /// 요일 정보가 채팅창에 출력된 시각 451 | protected virtual void SendDateChangeNotice(string content, DateTime sendTime) 452 | { 453 | Window.SendText($"날짜가 변경되었습니다. ({content})"); 454 | Thread.Sleep(SendMessageInterval); 455 | } 456 | 457 | /// 458 | /// 유저 입장 시 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 459 | /// 460 | /// 입장한 유저의 닉네임 461 | /// 입장한 시각 462 | protected virtual void SendUserJoinNotice(string userName, DateTime sendTime) 463 | { 464 | Window.SendText($"{userName}님이 입장하셨습니다."); 465 | Thread.Sleep(SendMessageInterval); 466 | } 467 | 468 | /// 469 | /// 유저 퇴장 시 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 470 | /// 471 | /// 퇴장한 유저의 닉네임 472 | /// 퇴장한 시각 473 | protected virtual void SendUserLeaveNotice(string userName, DateTime sendTime) 474 | { 475 | Window.SendText($"{userName}님이 퇴장하셨습니다."); 476 | Thread.Sleep(SendMessageInterval); 477 | } 478 | 479 | /// 480 | /// 유저가 메시지를 입력한 경우 수행할 행동을 지정합니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 481 | /// 482 | /// 메시지를 보낸 유저의 닉네임 483 | /// 메시지 내용 484 | /// 메시지를 보낸 시각 485 | protected virtual void ProcessUserMessage(string userName, string content, DateTime sendTime) 486 | { 487 | if (FindUserByNickname(userName) == null) AddNewUser(userName); 488 | foreach (User user in Users) 489 | { 490 | if (user.Nickname == userName) 491 | { 492 | if (user.IsIgnored) return; 493 | else break; 494 | } 495 | } 496 | 497 | ParseMessage(userName, GetOriginalCommand(content), sendTime); 498 | } 499 | 500 | /// 501 | /// 유저가 보낸 메시지에 바로가기 명령어가 사용되었는지 확인하고, 만약 사용되었다면 원래 명령어로 복구하여 되돌려줍니다. 502 | /// 503 | /// 유저가 보낸 메시지의 내용 504 | /// 복원된 최종 명령어 505 | protected string GetOriginalCommand(string content) 506 | { 507 | if (ShortcutTexts == null) ShortcutTexts = ReadShortcutFile(); 508 | 509 | string text; 510 | string[] sentWords = content.Split(' '); 511 | string[] leftWords, rightWords; 512 | bool isCorrectShortcut; 513 | string[] resultWords = null; 514 | var result = new StringBuilder(); 515 | foreach (string s in ShortcutTexts) 516 | { 517 | text = s.Trim(); 518 | if (text.Length == 0) continue; 519 | if (text.IndexOf(';') == 0) continue; 520 | int equalSignIndex = text.IndexOf('='); 521 | if (equalSignIndex == 0 || equalSignIndex == text.Length - 1) continue; 522 | 523 | leftWords = text.Substring(0, equalSignIndex).TrimEnd().Split(' '); 524 | isCorrectShortcut = leftWords.Length <= sentWords.Length ? true : false; 525 | if (isCorrectShortcut) 526 | { 527 | for (int i = 0; i < leftWords.Length; i++) 528 | { 529 | if (leftWords[i].ToLower() != sentWords[i].ToLower()) { isCorrectShortcut = false; break; } 530 | } 531 | } 532 | if (!isCorrectShortcut) continue; 533 | 534 | rightWords = text.Substring(equalSignIndex + 1).TrimStart().Split(' '); 535 | resultWords = new string[rightWords.Length + (sentWords.Length - leftWords.Length)]; 536 | for (int i = 0; i < rightWords.Length; i++) resultWords[i] = rightWords[i]; 537 | for (int i = rightWords.Length; i < resultWords.Length; i++) resultWords[i] = sentWords[i - rightWords.Length + leftWords.Length]; 538 | for (int i = 0; i < resultWords.Length; i++) result.Append($"{resultWords[i]} "); 539 | result.Remove(result.Length - 1, 1); 540 | } 541 | 542 | if (resultWords == null) return content; 543 | else return result.ToString(); 544 | } 545 | 546 | /// 547 | /// 바로가기 명령어를 열거한 파일의 내용을 가져옵니다. 548 | /// 549 | /// 파일의 내용 550 | [MethodImpl(MethodImplOptions.Synchronized)] 551 | private string[] ReadShortcutFile() 552 | { 553 | string path = ConfigPath + $"{Identifier}\\" + ShortcutFileName + ShortcutFileExtension; 554 | Directory.CreateDirectory(path.Substring(0, path.LastIndexOf('\\'))); 555 | 556 | if (!File.Exists(path)) GenerateShortcutFile(path); 557 | return File.ReadAllText(path).Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); 558 | } 559 | 560 | /// 561 | /// 바로가기 명령어 파일을 생성합니다. 562 | /// 563 | /// 바로가기 파일의 경로 564 | private void GenerateShortcutFile(string path) 565 | { 566 | string message = Properties.Resources.cmd_shortcuts; 567 | 568 | File.WriteAllLines(path, message.Split(new string[] { "\r\n" }, StringSplitOptions.None), new UTF8Encoding(false)); 569 | } 570 | 571 | /// 572 | /// 금지어-대체어 목록을 갱신합니다. 573 | /// 574 | protected void RefreshLimitedWordsList() 575 | { 576 | string[] lines = ReadLimitedWordsFile(); 577 | var limitedWords = new List(); 578 | var replacedWords = new List(); 579 | for (int i = 0; i < lines.Length; i++) 580 | { 581 | if (lines[i].Trim() == "[Contents]") 582 | { 583 | for (int j = i + 1; j < lines.Length; j++) 584 | { 585 | if (lines[j].IndexOf(";") == 0) continue; 586 | if (lines[j].Split('=').Length != 2) continue; 587 | string[] pair = lines[j].Split('='); 588 | string key = pair[0].Trim(); 589 | string value = pair[1].Trim(); 590 | if (key.Length == 0 || value.Length == 0) continue; 591 | limitedWords.Add(key); 592 | replacedWords.Add(value); 593 | } 594 | } 595 | } 596 | 597 | LimitedWords = limitedWords; 598 | ReplacedWords = replacedWords; 599 | } 600 | 601 | /// 602 | /// 금지어-대체어 목록을 열거한 파일의 내용을 가져옵니다. 603 | /// 604 | /// 605 | [MethodImpl(MethodImplOptions.Synchronized)] 606 | private string[] ReadLimitedWordsFile() 607 | { 608 | string path = ConfigPath + $"{Identifier}\\" + BotLimitedWordsFileName + BotLimitedWordsFileExtension; 609 | Directory.CreateDirectory(path.Substring(0, path.LastIndexOf('\\'))); 610 | 611 | if (!File.Exists(path)) GenerateBotLimitedWordsFile(path); 612 | 613 | return File.ReadAllLines(path); 614 | } 615 | 616 | /// 617 | /// 금지어-대체어 목록 파일을 생성합니다. 618 | /// 619 | /// 금지어-대체어 목록 파일의 경로 620 | [MethodImpl(MethodImplOptions.Synchronized)] 621 | private void GenerateBotLimitedWordsFile(string path) 622 | { 623 | string message = Properties.Resources.bot_limitedWords; 624 | 625 | File.WriteAllLines(path, message.Split(new string[] { "\r\n" }, StringSplitOptions.None), Encoding.Unicode); 626 | } 627 | 628 | /// 629 | /// 본격적으로 메시지 분석을 시작하기 전에, 필요한 초기화 작업을 진행할 수 있도록 작성된 메서드입니다. 630 | /// 631 | protected abstract void InitializeBotSettings(); 632 | 633 | /// 634 | /// 본격적인 메시지 분석 작업을 시작합니다. 635 | /// 636 | /// 메시지를 보낸 유저의 닉네임 637 | /// 메시지 내용 638 | /// 메시지를 보낸 시각 639 | protected abstract void ParseMessage(string userName, string content, DateTime sendTime); 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /KakaoBotAPI/Bot/IKakaoBot.cs: -------------------------------------------------------------------------------- 1 | using Less.API.NetFramework.KakaoTalkAPI; 2 | 3 | namespace Less.API.NetFramework.KakaoBotAPI.Bot 4 | { 5 | /// 6 | /// 모든 카카오봇 클래스의 모태가 되는 인터페이스 7 | /// 8 | interface IKakaoBot 9 | { 10 | /// 11 | /// 봇 인스턴스를 시작합니다. 12 | /// 13 | void Start(); 14 | 15 | /// 16 | /// 봇 인스턴스를 중지합니다. 17 | /// 18 | void Stop(); 19 | 20 | /// 21 | /// 봇 객체를 참조하여 메시지를 전송합니다. 22 | /// 23 | /// 전송할 메시지 24 | /// 전송의 성공 여부 25 | bool SendMessage(string message); 26 | 27 | /// 28 | /// 봇 객체를 참조하여 이미지를 전송합니다. 29 | /// 30 | /// 전송할 이미지 파일의 경로 31 | /// 전송의 성공 여부 32 | bool SendImage(string path); 33 | 34 | /// 35 | /// 봇 객체를 참조하여 이모티콘을 전송합니다. 36 | /// 37 | /// 전송할 카카오톡 이모티콘 38 | /// 전송의 성공 여부 39 | bool SendEmoticon(KakaoTalk.Emoticon emoticon); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /KakaoBotAPI/Bot/QuizBot.cs: -------------------------------------------------------------------------------- 1 | using Less.API.NetFramework.KakaoBotAPI.Model; 2 | using Less.API.NetFramework.KakaoBotAPI.Util; 3 | using Less.API.NetFramework.KakaoTalkAPI; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading; 10 | 11 | namespace Less.API.NetFramework.KakaoBotAPI.Bot 12 | { 13 | public abstract class QuizBot : ChatBot 14 | { 15 | /// 16 | /// 자동으로 생성되는 새 QuizUser 객체의 경험치 17 | /// 18 | protected static int NewUserExperience = 0; 19 | 20 | /// 21 | /// 자동으로 생성되는 새 QuizUser 객체의 레벨 22 | /// 23 | protected static int NewUserLevel = 1; 24 | 25 | /// 26 | /// 자동으로 생성되는 새 QuizUser 객체의 머니 27 | /// 28 | protected static int NewUserMoney = 0; 29 | 30 | /// 31 | /// 자동으로 생성되는 새 QuizUser 객체의 세대 32 | /// 33 | protected static int NewUserGeneration = 1; 34 | 35 | /// 36 | /// 자동으로 생성되는 새 QuizUser 객체에 적용되는 타이틀 37 | /// 38 | protected static Title NewUserCurrentTitle = new Title("새로운타이틀"); 39 | 40 | /// 41 | /// 자동으로 생성되는 새 QuizUser 객체가 적용 가능한 타이틀 목록 42 | /// 43 | protected static List NewUserAvailableTitles = new List<Title>() { NewUserCurrentTitle }; 44 | 45 | /// <summary> 46 | /// 퀴즈 데이터가 위치하는 최상위 경로 47 | /// </summary> 48 | protected const string QuizPath = DataPath + @"quiz\"; 49 | 50 | /// <summary> 51 | /// 퀴즈 설정 파일의 이름 52 | /// </summary> 53 | protected const string QuizSettingsFileName = "settings"; 54 | 55 | /// <summary> 56 | /// 퀴즈 설정 파일의 확장자 57 | /// </summary> 58 | protected const string QuizSettingsFileExtension = IniHelper.FileExtension; 59 | 60 | /// <summary> 61 | /// 퀴즈 설정 파일의 Section 이름 62 | /// </summary> 63 | protected const string QuizSettingsSection = "Settings"; 64 | 65 | /// <summary> 66 | /// 퀴즈 데이터 파일의 이름 67 | /// </summary> 68 | protected const string QuizDataFileName = "data"; 69 | 70 | /// <summary> 71 | /// 퀴즈 데이터 파일의 확장자 72 | /// </summary> 73 | protected const string QuizDataFileExtension = XmlHelper.FileExtension; 74 | 75 | /// <summary> 76 | /// 퀴즈 목록 77 | /// </summary> 78 | protected List<Quiz> QuizList = new List<Quiz>(); 79 | 80 | /// <summary> 81 | /// 현재 퀴즈가 진행 중인지 여부 82 | /// true : 퀴즈가 진행 중입니다. 83 | /// false : 퀴즈가 진행 중이 아닙니다. 84 | /// </summary> 85 | protected bool IsQuizTaskRunning = false; 86 | 87 | /// <summary> 88 | /// 퀴즈 시작 시 나오는 공지 목록 파일의 이름 89 | /// </summary> 90 | protected const string QuizNoticeFileName = "quiz_notice"; 91 | 92 | /// <summary> 93 | /// 퀴즈 시작 시 나오는 공지 목록 파일의 확장자 94 | /// </summary> 95 | protected const string QuizNoticeFileExtension = ConfigFileDefaultExtension; 96 | 97 | /// <summary> 98 | /// 퀴즈 Thread의 메시지 폴링 간격 99 | /// </summary> 100 | protected static int QuizScanInterval = 200; 101 | 102 | /// <summary> 103 | /// 봇 인스턴스에 대한 Quiz Thread 104 | /// </summary> 105 | protected Thread QuizTaskRunner; 106 | 107 | /// <summary> 108 | /// 봇 인스턴스 내부에서 사용되는 랜덤 객체 109 | /// </summary> 110 | protected Random BotRandom = new Random(); 111 | 112 | /// <summary> 113 | /// 퀴즈 주제 추가 방법을 설명하는 파일의 이름 114 | /// </summary> 115 | protected const string QuizAddSubjectName = "퀴즈 주제 추가 방법"; 116 | 117 | /// <summary> 118 | /// 퀴즈 주제 추가 방법을 설명하는 파일의 확장자 119 | /// </summary> 120 | protected const string QuizAddSubjectExtension = TextFileExtension; 121 | 122 | /// <summary> 123 | /// 퀴즈봇 객체를 생성합니다. 124 | /// </summary> 125 | /// <param name="roomName">채팅방의 이름</param> 126 | /// <param name="type">대상 채팅방의 유형</param> 127 | /// <param name="botRunnerName">봇 돌리미의 해당 채팅방에서의 닉네임</param> 128 | /// <param name="identifier">채팅방에 부여할 특별한 식별자</param> 129 | public QuizBot(string roomName, TargetTypeOption type, string botRunnerName, string identifier) : base(roomName, type, botRunnerName, identifier) { } 130 | 131 | /// <summary> 132 | /// 퀴즈봇을 시작합니다. 133 | /// </summary> 134 | public override void Start() 135 | { 136 | base.Start(); 137 | } 138 | 139 | /// <summary> 140 | /// 퀴즈봇을 중지합니다. 141 | /// </summary> 142 | public override void Stop() 143 | { 144 | StopQuiz(); 145 | base.Stop(); 146 | } 147 | 148 | /// <summary> 149 | /// 본격적으로 메시지 분석을 시작하기 전에, 필요한 초기화 작업을 진행합니다. 150 | /// </summary> 151 | protected override void InitializeBotSettings() 152 | { 153 | RefreshQuizList(); 154 | 155 | string path = QuizAddSubjectName + QuizAddSubjectExtension; 156 | if (!File.Exists(path)) GenerateHowToAddQuizSubjectFile(path); 157 | } 158 | 159 | /// <summary> 160 | /// 유저가 메시지를 입력한 경우 수행할 행동을 지정합니다. 161 | /// </summary> 162 | /// <param name="userName">메시지를 보낸 유저의 닉네임</param> 163 | /// <param name="content">메시지 내용</param> 164 | /// <param name="sendTime">메시지를 보낸 시각</param> 165 | protected override void ProcessUserMessage(string userName, string content, DateTime sendTime) 166 | { 167 | if (FindUserByNickname(userName) == null) AddNewUser(userName); 168 | foreach (QuizUser user in Users) 169 | { 170 | if (user.Nickname == userName) 171 | { 172 | if (user.IsIgnored) return; 173 | else break; 174 | } 175 | } 176 | 177 | ParseMessage(userName, GetOriginalCommand(content), sendTime); 178 | } 179 | 180 | /// <summary> 181 | /// 퀴즈를 시작합니다. 182 | /// </summary> 183 | /// <param name="quizType">퀴즈의 유형</param> 184 | /// <param name="subjects">주제 목록</param> 185 | /// <param name="requestQuizCount">요청하는 퀴즈의 총 개수</param> 186 | /// <param name="minQuizCount">퀴즈 최소 개수</param> 187 | /// <param name="quizTimeLimit">퀴즈의 제한시간</param> 188 | /// <param name="bonusExperience">퀴즈 정답 시 획득 경험치</param> 189 | /// <param name="bonusMoney">퀴즈 정답 시 획득 머니</param> 190 | /// <param name="idleTimeLimit">퀴즈의 잠수 제한시간</param> 191 | /// <param name="showSubject">주제 표시 여부</param> 192 | protected void StartQuiz(Quiz.TypeOption quizType, string[] subjects, int requestQuizCount, int minQuizCount, int quizTimeLimit, int bonusExperience, int bonusMoney, int idleTimeLimit = 180, bool showSubject = true) 193 | { 194 | IsQuizTaskRunning = true; 195 | Thread.Sleep(1000); 196 | 197 | List<Quiz.Data> quizDataList = GetQuizDataList(quizType, subjects, requestQuizCount); 198 | if (quizDataList.Count < requestQuizCount) requestQuizCount = quizDataList.Count; 199 | if (requestQuizCount < minQuizCount) 200 | { 201 | Thread.Sleep(SendMessageInterval); 202 | OnQuizCountInvalid(minQuizCount, requestQuizCount); 203 | IsQuizTaskRunning = false; 204 | return; 205 | } 206 | 207 | string[] notices = GetQuizNoticesFromFile(); 208 | Thread.Sleep(SendMessageInterval); 209 | SendMessage($"{notices[BotRandom.Next(notices.Length)]}"); 210 | Thread.Sleep(2000); 211 | 212 | bool isRandom = subjects.Length > 1 ? true : false; 213 | 214 | OnQuizReady(); 215 | RunQuiz(quizType, subjects, requestQuizCount, quizTimeLimit, bonusExperience, bonusMoney, idleTimeLimit, showSubject, isRandom, quizDataList); 216 | 217 | if (IsQuizTaskRunning) IsQuizTaskRunning = false; 218 | Thread.Sleep(2000); 219 | KakaoTalk.Message[] messages; 220 | if (IsMainTaskRunning) 221 | { 222 | while ((messages = Window.GetMessagesUsingClipboard()) == null) Thread.Sleep(GetMessageInterval); 223 | LastMessageIndex = (messages.Length - 1) + 1; 224 | OnQuizFinish(); 225 | } 226 | } 227 | 228 | /// <summary> 229 | /// 퀴즈를 중지합니다. 230 | /// </summary> 231 | protected void StopQuiz() 232 | { 233 | IsQuizTaskRunning = false; 234 | } 235 | 236 | /// <summary> 237 | /// 퀴즈 리스트를 갱신합니다. 238 | /// </summary> 239 | [MethodImpl(MethodImplOptions.Synchronized)] 240 | protected virtual void RefreshQuizList() 241 | { 242 | /* 퀴즈 리스트 */ 243 | var newQuizList = new List<Quiz>(); 244 | 245 | /* 설정 변수 */ 246 | Quiz.TypeOption quizType; 247 | string mainSubject; 248 | List<string> childSubjects; 249 | bool isCaseSensitive; 250 | bool useMultiChoice; 251 | Quiz.ChoiceExtractMethodOption choiceExtractMethod; 252 | int choiceCount; 253 | 254 | /* 데이터 변수 */ 255 | string question; 256 | string answer; 257 | string explanation; 258 | string type; 259 | string beforeImagePath; 260 | string afterImagePath; 261 | string childSubject; 262 | string regDateStr; 263 | DateTime regDate; 264 | List<Quiz.Data> dataList; 265 | 266 | /* 임시 변수 */ 267 | string value; 268 | 269 | Directory.CreateDirectory(QuizPath); 270 | string[] directories = Directory.GetDirectories(QuizPath); 271 | 272 | for (int i = 0; i < directories.Length; i++) 273 | { 274 | /* 퀴즈 설정 값 로드 */ 275 | string path = directories[i] + "\\" + QuizSettingsFileName + QuizSettingsFileExtension; 276 | if (!File.Exists(path)) throw new ArgumentException($"{path} 파일이 누락되었습니다."); 277 | var iniHelper = new IniHelper(path); 278 | 279 | value = iniHelper.Read("quizType", QuizSettingsSection); 280 | if (value == "일반") quizType = Quiz.TypeOption.General; 281 | else if (value == "초성") quizType = Quiz.TypeOption.Chosung; 282 | else throw new ArgumentException($"지원되지 않는 퀴즈 유형입니다. (\"{value}\" << {path})"); 283 | 284 | mainSubject = iniHelper.Read("mainSubject", QuizSettingsSection); 285 | 286 | childSubjects = new List<string>(); 287 | value = iniHelper.Read("childSubjects", QuizSettingsSection); 288 | string[] tempArr = null; 289 | if (value != "") 290 | { 291 | tempArr = value.Split(','); 292 | for (int j = 0; j < tempArr.Length; j++) childSubjects.Add(tempArr[j].Trim()); 293 | } 294 | 295 | value = iniHelper.Read("isCaseSensitive", QuizSettingsSection); 296 | if (value == "true") isCaseSensitive = true; 297 | else if (value == "false") isCaseSensitive = false; 298 | else throw new ArgumentException($"대소문자 구분 정답 처리 여부에 잘못된 값이 설정되었습니다. (\"{value}\" << {path})"); 299 | 300 | value = iniHelper.Read("useMultiChoice", QuizSettingsSection); 301 | if (value == "true") useMultiChoice = true; 302 | else if (value == "false") useMultiChoice = false; 303 | else throw new ArgumentException($"객관식 여부에 잘못된 값이 설정되었습니다. (\"{value}\" << {path})"); 304 | 305 | value = iniHelper.Read("choiceExtractMethod", QuizSettingsSection); 306 | if (value == "") choiceExtractMethod = Quiz.ChoiceExtractMethodOption.None; 307 | else if (value == "RICS") choiceExtractMethod = Quiz.ChoiceExtractMethodOption.RICS; 308 | else if (value == "RAPT") choiceExtractMethod = Quiz.ChoiceExtractMethodOption.RAPT; 309 | else throw new ArgumentException($"객관식 선택지 추출 방식에 잘못된 값이 설정되었습니다. (\"{value}\" << {path})"); 310 | 311 | value = iniHelper.Read("choiceCount", QuizSettingsSection); 312 | if (value == "") value = "0"; 313 | try { choiceCount = int.Parse(value); } 314 | catch (Exception) { throw new ArgumentException($"객관식 선택지 개수에 잘못된 값이 설정되었습니다. (\"{value}\" << {path})"); } 315 | 316 | /* 퀴즈 데이터 로드 */ 317 | path = directories[i] + "\\" + QuizDataFileName + QuizDataFileExtension; 318 | if (!File.Exists(path)) throw new ArgumentException($"{path} 파일이 누락되었습니다."); 319 | var xmlHelper = new XmlHelper(path); 320 | 321 | question = null; 322 | answer = null; 323 | explanation = null; 324 | type = null; 325 | beforeImagePath = null; 326 | afterImagePath = null; 327 | childSubject = null; 328 | regDateStr = null; 329 | dataList = new List<Quiz.Data>(); 330 | var document = xmlHelper.ReadFile(); 331 | if (document.RootElementName != "list") throw new ArgumentException($"{QuizDataFileName + QuizDataFileExtension} 파일의 Parent Element의 이름은 \"list\"여야 합니다. (현재: {document.RootElementName})"); 332 | for (int j = 0; j < document.ChildNodes.Count; j++) 333 | { 334 | var node = document.ChildNodes[j]; 335 | if (node.Name != "data") throw new ArgumentException($"{QuizDataFileName + QuizDataFileExtension} 파일의 Child Element의 이름은 \"data\"여야 합니다. ({j + 1}번째 Child: {node.Name})"); 336 | 337 | foreach (XmlHelper.NodeData nodeData in node.DataList) 338 | { 339 | switch (nodeData.Key) 340 | { 341 | case "question": question = nodeData.Value; break; 342 | case "answer": answer = nodeData.Value; break; 343 | case "explanation": explanation = nodeData.Value; break; 344 | case "type": type = nodeData.Value; break; 345 | case "beforeImagePath": beforeImagePath = nodeData.Value; break; 346 | case "afterImagePath": afterImagePath = nodeData.Value; break; 347 | case "childSubject": childSubject = nodeData.Value; break; 348 | case "regDate": regDateStr = nodeData.Value; break; 349 | } 350 | } 351 | if (question == null) throw new ArgumentException($"{QuizDataFileName + QuizDataFileExtension} 파일의 {j + 1}번째 data에 question 요소가 누락되었습니다."); 352 | if (answer == null) throw new ArgumentException($"{QuizDataFileName + QuizDataFileExtension} 파일의 {j + 1}번째 data에 answer 요소가 누락되었습니다."); 353 | if (regDateStr == null) throw new ArgumentException($"{QuizDataFileName + QuizDataFileExtension} 파일의 {j + 1}번째 data에 regDate 요소가 누락되었습니다."); 354 | else 355 | { 356 | int year = int.Parse(regDateStr.Substring(0, 4)); 357 | int month = int.Parse(regDateStr.Substring(5, 2)); 358 | int day = int.Parse(regDateStr.Substring(8, 2)); 359 | int hour = int.Parse(regDateStr.Substring(11, 2)); 360 | int minute = int.Parse(regDateStr.Substring(14, 2)); 361 | int second = int.Parse(regDateStr.Substring(17, 2)); 362 | regDate = new DateTime(year, month, day, hour, minute, second); 363 | } 364 | 365 | dataList.Add(new Quiz.Data(mainSubject, question, answer, explanation, type, null, beforeImagePath, afterImagePath, childSubject, isCaseSensitive, regDate)); // TODO : choices를 data.xml 파일에서 입력받을 수도 있도록 처리 366 | } 367 | 368 | newQuizList.Add(new Quiz(quizType, mainSubject, childSubjects, isCaseSensitive, useMultiChoice, choiceExtractMethod, choiceCount, dataList)); 369 | } 370 | 371 | QuizList = newQuizList; 372 | } 373 | 374 | /// <summary> 375 | /// 파일 시스템으로부터 유저 데이터를 불러옵니다. 376 | /// </summary> 377 | /// <returns>퀴즈유저 목록</returns> 378 | protected override void RefreshUserData() 379 | { 380 | var document = GetUserDataDocument(); 381 | var users = new List<User>(); 382 | 383 | string nickname; 384 | bool isIgnored; 385 | int experience, level, money, generation; 386 | Title currentTitle; 387 | List<Title> availableTitles; 388 | string[] tempArray; 389 | string value; 390 | 391 | for (int i = 0; i < document.ChildNodes.Count; i++) 392 | { 393 | var node = document.ChildNodes[i]; 394 | 395 | nickname = node.GetData("nickname"); 396 | 397 | value = node.GetData("isIgnored"); 398 | if (value == "true") isIgnored = true; 399 | else if (value == "false") isIgnored = false; 400 | else throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 isIgnored 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 401 | 402 | if (node.GetData("experience") == null) experience = NewUserExperience; 403 | else if (!int.TryParse(node.GetData("experience"), out experience)) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 experience 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 404 | 405 | if (node.GetData("level") == null) level = NewUserLevel; 406 | else if (!int.TryParse(node.GetData("level"), out level)) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 level 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 407 | 408 | if (node.GetData("money") == null) money = NewUserMoney; 409 | else if (!int.TryParse(node.GetData("money"), out money)) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 money 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 410 | 411 | if (node.GetData("generation") == null) generation = NewUserGeneration; 412 | else if (!int.TryParse(node.GetData("generation"), out generation)) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 generation 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 413 | 414 | if (node.GetData("currentTitle") == null) currentTitle = NewUserCurrentTitle; 415 | else currentTitle = new Title(node.GetData("currentTitle")); 416 | 417 | availableTitles = new List<Title>(); 418 | if (node.GetData("availableTitles") == null) availableTitles = NewUserAvailableTitles; 419 | else 420 | { 421 | tempArray = node.GetData("availableTitles").Split(','); 422 | if (tempArray.Length == 0 || (tempArray.Length == 1 && tempArray[0] == "")) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 availableTitles 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 423 | else 424 | { 425 | for (int j = 0; j < tempArray.Length; j++) 426 | { 427 | if (tempArray[j] == currentTitle.Name) break; 428 | else if (j == tempArray.Length - 1) throw new ArgumentException($"식별자 {Identifier} 봇의 유저 데이터 파일에 availableTitles 값이 잘못 설정되었습니다. ({i + 1}번째 항목)"); 429 | } 430 | } 431 | for (int j = 0; j < tempArray.Length; j++) availableTitles.Add(new Title(tempArray[j])); 432 | } 433 | 434 | users.Add(new QuizUser(nickname, isIgnored, experience, level, money, generation, currentTitle, availableTitles)); 435 | } 436 | 437 | Users = users; 438 | SaveUserData(); 439 | } 440 | 441 | 442 | /// <summary> 443 | /// 파일 시스템에 유저 데이터를 저장합니다. 444 | /// </summary> 445 | protected override void SaveUserData() 446 | { 447 | string path = ConfigPath + $"{Identifier}\\" + ProfileFileName + ProfileFileExtension; 448 | 449 | var helper = new XmlHelper(path); 450 | var nodeList = new List<XmlHelper.Node>(); 451 | XmlHelper.Node node; 452 | string temp; 453 | 454 | for (int i = 0; i < Users.Count; i++) 455 | { 456 | node = new XmlHelper.Node("user"); 457 | node.AddData("nickname", Users[i].Nickname); 458 | node.AddData("isIgnored", Users[i].IsIgnored ? "true" : "false"); 459 | node.AddData("experience", (Users[i] as QuizUser).Experience); 460 | node.AddData("level", (Users[i] as QuizUser).Level); 461 | node.AddData("money", (Users[i] as QuizUser).Money); 462 | node.AddData("generation", (Users[i] as QuizUser).Generation); 463 | node.AddData("currentTitle", (Users[i] as QuizUser).CurrentTitle.Name); 464 | temp = ""; 465 | for (int j = 0; j < (Users[i] as QuizUser).AvailableTitles.Count; j++) temp += (Users[i] as QuizUser).AvailableTitles[j].Name + ","; 466 | node.AddData("availableTitles", temp.Substring(0, temp.Length - 1)); 467 | 468 | nodeList.Add(node); 469 | } 470 | 471 | helper.CreateFile("list", nodeList); 472 | } 473 | 474 | /// <summary> 475 | /// 해당 닉네임을 가진 유저 정보를 새로 등록합니다. 476 | /// </summary> 477 | /// <param name="userName">새로 등록될 유저의 닉네임</param> 478 | /// <returns>퀴즈유저 객체</returns> 479 | protected new QuizUser AddNewUser(string userName) 480 | { 481 | var user = new QuizUser(userName, IsNewUserIgnored, NewUserExperience, NewUserLevel, NewUserMoney, NewUserGeneration, NewUserCurrentTitle, NewUserAvailableTitles); 482 | Users.Add(user); 483 | SaveUserData(); 484 | return user; 485 | } 486 | 487 | /// <summary> 488 | /// 닉네임을 통해 유저 정보를 얻어옵니다.<para/> 489 | /// 만약 현재 해당 닉네임을 가진 유저가 기록에 존재하지 않으면 null을 반환합니다. 490 | /// </summary> 491 | /// <param name="userName">유저의 닉네임</param> 492 | /// <returns>퀴즈유저 객체</returns> 493 | protected new QuizUser FindUserByNickname(string userName) 494 | { 495 | return (QuizUser)base.FindUserByNickname(userName); 496 | } 497 | 498 | /// <summary> 499 | /// 파일에서 퀴즈 출제 전에 표시하는 공지사항을 가져옵니다. 500 | /// </summary> 501 | /// <returns>공지사항 스트링 배열</returns> 502 | protected string[] GetQuizNoticesFromFile() 503 | { 504 | string[] tempArr = ReadQuizNoticeFile(); 505 | var notices = new List<string>(); 506 | for (int i = 0; i < tempArr.Length; i++) 507 | { 508 | string line = tempArr[i].Trim(); 509 | if (line == "" || line.IndexOf(";") == 0) continue; 510 | notices.Add(line); 511 | } 512 | 513 | return notices.ToArray(); 514 | } 515 | 516 | /// <summary> 517 | /// 퀴즈 공지사항 파일을 읽어들입니다. 518 | /// </summary> 519 | /// <returns>전체 파일 내용 배열</returns> 520 | [MethodImpl(MethodImplOptions.Synchronized)] 521 | private string[] ReadQuizNoticeFile() 522 | { 523 | string path = ConfigPath + $"{Identifier}\\" + QuizNoticeFileName + QuizNoticeFileExtension; 524 | Directory.CreateDirectory(path.Substring(0, path.LastIndexOf('\\'))); 525 | 526 | if (!File.Exists(path)) GenerateQuizNoticeFile(path); 527 | 528 | return File.ReadAllLines(path); 529 | } 530 | 531 | /// <summary> 532 | /// 퀴즈 공지사항 파일을 생성합니다. 533 | /// </summary> 534 | /// <param name="path">퀴즈 공지사항 파일 경로</param> 535 | private void GenerateQuizNoticeFile(string path) 536 | { 537 | string message = Properties.Resources.quiz_notice; 538 | 539 | File.WriteAllLines(path, message.Split(new string[] { "\r\n" }, StringSplitOptions.None), new UTF8Encoding(false)); 540 | } 541 | 542 | /// <summary> 543 | /// 퀴즈 데이터 목록을 얻어옵니다. 544 | /// </summary> 545 | /// <param name="requestedQuizType">요청된 퀴즈 유형</param> 546 | /// <param name="subjects">주제 목록</param> 547 | /// <param name="quizCount">퀴즈 개수</param> 548 | /// <returns>퀴즈 데이터 목록</returns> 549 | protected List<Quiz.Data> GetQuizDataList(Quiz.TypeOption requestedQuizType, string[] subjects, int quizCount) 550 | { 551 | var data2dList = new List<List<Quiz.Data>>(); 552 | List<Quiz.Data> tempList; 553 | bool matchesChildSubject; 554 | 555 | /* resultDataList 초기화 */ 556 | for (int i = 0; i < subjects.Length; i++) 557 | { 558 | foreach (Quiz quiz in QuizList) 559 | { 560 | if (quiz.Type == requestedQuizType) 561 | { 562 | string mainSubject = quiz.MainSubject; 563 | if (mainSubject == subjects[i]) 564 | { 565 | tempList = new List<Quiz.Data>(); 566 | for (int j = 0; j < quiz.DataList.Count; j++) 567 | { 568 | var data = quiz.DataList[j]; 569 | if (quiz.UseMultiChoice == true) data.Choices = GetChoiceList(quiz, data); 570 | tempList.Add(data); 571 | } 572 | data2dList.Add(tempList); 573 | break; 574 | } 575 | else 576 | { 577 | matchesChildSubject = false; 578 | foreach (string childSubject in quiz.ChildSubjects) 579 | { 580 | if ($"{mainSubject}-{childSubject}" == subjects[i]) 581 | { 582 | tempList = new List<Quiz.Data>(); 583 | for (int j = 0; j < quiz.DataList.Count; j++) 584 | { 585 | var data = quiz.DataList[j]; 586 | if (data.ChildSubject == childSubject) 587 | { 588 | if (quiz.UseMultiChoice == true) data.Choices = GetChoiceList(quiz, data); 589 | tempList.Add(data); 590 | } 591 | } 592 | data2dList.Add(tempList); 593 | matchesChildSubject = true; 594 | } 595 | } 596 | if (matchesChildSubject) break; 597 | } 598 | } 599 | } 600 | } 601 | 602 | /* data2dList Shuffle */ 603 | int shuffleCount = 3; 604 | for (int i = 0; i < data2dList.Count; i++) 605 | { 606 | for (int j = 0; j < shuffleCount; j++) 607 | { 608 | ListUtil.Shuffle(data2dList[i]); 609 | } 610 | } 611 | 612 | /* 배열 리스트, 문항 수 초기화 */ 613 | List<Quiz.Data[]> dataArrList = new List<Quiz.Data[]>(); 614 | int totalCount = 0; 615 | for (int i = 0; i < data2dList.Count; i++) 616 | { 617 | dataArrList.Add(new Quiz.Data[data2dList[i].Count]); 618 | totalCount += data2dList[i].Count; 619 | } 620 | if (totalCount > quizCount) totalCount = quizCount; 621 | 622 | /* 문제 선택 및 결과 리스트로 이동 */ 623 | var resultDataList = new List<Quiz.Data>(); 624 | for (int i = 0; i < totalCount; i++) 625 | { 626 | int randomValue = BotRandom.Next(dataArrList.Count); 627 | for (int j = 0; j < dataArrList[randomValue].Length; j++) 628 | { 629 | if (dataArrList[randomValue][j] == null) 630 | { 631 | dataArrList[randomValue][j] = data2dList[randomValue][j]; 632 | resultDataList.Add(dataArrList[randomValue][j]); 633 | break; 634 | } 635 | else if (j == dataArrList[randomValue].Length - 1) { i--; } 636 | } 637 | } 638 | 639 | /* 결과 리스트 shuffle (리스트 내에서 다시 셔플) */ 640 | for (int i = 0; i < shuffleCount; i++) ListUtil.Shuffle(resultDataList); 641 | 642 | return resultDataList; 643 | } 644 | 645 | /// <summary> 646 | /// 퀴즈 선택지 목록을 가져옵니다. 647 | /// </summary> 648 | /// <param name="quiz">퀴즈 객체</param> 649 | /// <param name="data">퀴즈 데이터 객체</param> 650 | /// <returns></returns> 651 | protected List<string> GetChoiceList(Quiz quiz, Quiz.Data data) 652 | { 653 | var choiceList = new List<string>(); 654 | var choiceCandidates = new List<string>(); 655 | bool shouldRecalc; 656 | 657 | if (quiz.ChoiceCount > quiz.DataList.Count) throw new ArgumentException($"객관식에서 퀴즈 선택지 수가 문항 수보다 많습니다. ({quiz.Type}-{quiz.MainSubject})"); 658 | 659 | choiceList.Add(data.Answer); 660 | if (quiz.ChoiceExtractMethod == Quiz.ChoiceExtractMethodOption.RICS) 661 | { 662 | for (int i = 0; i < quiz.ChoiceCount - 1; i++) 663 | { 664 | string value = quiz.DataList[BotRandom.Next(quiz.DataList.Count)].Answer; 665 | shouldRecalc = false; 666 | for (int j = 0; j < choiceList.Count; j++) 667 | { 668 | if (choiceList[j] == value) { shouldRecalc = true; break; } 669 | } 670 | if (shouldRecalc) { i--; continue; } 671 | choiceList.Add(value); 672 | } 673 | } 674 | else if (quiz.ChoiceExtractMethod == Quiz.ChoiceExtractMethodOption.RAPT) 675 | { 676 | for (int i = 0; i < quiz.DataList.Count; i++) if (quiz.DataList[i].Type == data.Type) choiceCandidates.Add(quiz.DataList[i].Answer); 677 | if (quiz.ChoiceCount > choiceCandidates.Count) throw new ArgumentException($"RAPT 객관식에서 퀴즈 선택지 수가 가능한 선택지 수보다 많습니다. ({quiz.Type}-{quiz.MainSubject})"); 678 | 679 | for (int i = 0; i < quiz.ChoiceCount - 1; i++) 680 | { 681 | string value = choiceCandidates[BotRandom.Next(choiceCandidates.Count)]; 682 | shouldRecalc = false; 683 | for (int j = 0; j < choiceList.Count; j++) 684 | { 685 | if (choiceList[j] == value) { shouldRecalc = true; break; } 686 | } 687 | if (shouldRecalc) { i--; continue; } 688 | choiceList.Add(value); 689 | } 690 | } 691 | for (int i = 0; i < 3; i++) ListUtil.Shuffle(choiceList); 692 | 693 | return choiceList; 694 | } 695 | 696 | /// <summary> 697 | /// 퀴즈 실행부입니다. 698 | /// </summary> 699 | /// <param name="quizType">퀴즈의 유형</param> 700 | /// <param name="subjects">주제 목록</param> 701 | /// <param name="requestQuizCount">요청하는 퀴즈의 총 개수</param> 702 | /// <param name="quizTimeLimit">퀴즈의 제한시간</param> 703 | /// <param name="bonusExperience">퀴즈 정답 시 획득 경험치</param> 704 | /// <param name="bonusMoney">퀴즈 정답 시 획득 머니</param> 705 | /// <param name="idleTimeLimit">퀴즈의 잠수 제한시간</param> 706 | /// <param name="showSubject">주제 표시 여부</param> 707 | /// <param name="isRandom">주제 랜덤 여부</param> 708 | /// <param name="quizDataList">퀴즈 데이터 목록</param> 709 | protected void RunQuiz(Quiz.TypeOption quizType, string[] subjects, int requestQuizCount, int quizTimeLimit, int bonusExperience, int bonusMoney, int idleTimeLimit, bool showSubject, bool isRandom, List<Quiz.Data> quizDataList) 710 | { 711 | KakaoTalk.Message[] messages; 712 | KakaoTalk.MessageType messageType; 713 | string userName; 714 | string content; 715 | DateTime sendTime; 716 | QuizUser user; 717 | int lastInputTick = Environment.TickCount; 718 | 719 | for (int i = 0; i < requestQuizCount; i++) // == quizDataList.Count 720 | { 721 | int currentQuiz = i + 1; 722 | 723 | var quizData = quizDataList[i]; 724 | string subject = quizData.MainSubject; 725 | if (quizData.ChildSubject != null) subject += $"-{quizData.ChildSubject}"; 726 | string question = quizData.Question; 727 | string answer = quizData.Answer; 728 | string explanation = quizData.Explanation; 729 | string beforeImagePath = quizData.BeforeImagePath; 730 | string afterImagePath = quizData.AfterImagePath; 731 | bool isCaseSensitive = quizData.IsCaseSensitive; 732 | 733 | while ((messages = Window.GetMessagesUsingClipboard()) == null) Thread.Sleep(GetMessageInterval); 734 | LastMessageIndex = (messages.Length - 1) + 1; // 뒤에 바로 SendMessage를 하므로, +1 해서 초기화 735 | OnQuizQuestionSend(isRandom, showSubject, subject, currentQuiz, requestQuizCount, question); 736 | 737 | if (beforeImagePath != null) OnQuizBeforeImageSend(beforeImagePath); 738 | 739 | if (quizData.Choices != null) OnQuizChoicesSend(quizData.Choices); 740 | 741 | int beginTick = Environment.TickCount; 742 | bool shouldContinue = true; 743 | while (IsQuizTaskRunning && shouldContinue) 744 | { 745 | Thread.Sleep(QuizScanInterval); 746 | if (Environment.TickCount > lastInputTick + (idleTimeLimit * 1000)) 747 | { 748 | IsQuizTaskRunning = false; 749 | OnQuizIdleLimitExceed(); 750 | break; 751 | } 752 | else if (Environment.TickCount > beginTick + (quizTimeLimit * 1000)) // 시간 제한 초과 753 | { 754 | OnQuizTimeLimitExceed(answer); 755 | shouldContinue = false; 756 | } 757 | else 758 | { 759 | while ((messages = Window.GetMessagesUsingClipboard()) == null) Thread.Sleep(GetMessageInterval); 760 | sendTime = DateTime.Now; 761 | for (int j = LastMessageIndex; j < messages.Length; j++) 762 | { 763 | messageType = messages[j].Type; 764 | userName = messages[j].UserName; 765 | content = messages[j].Content; 766 | 767 | LastMessageIndex++; 768 | user = FindUserByNickname(userName); 769 | if (user == null) 770 | { 771 | AddNewUser(userName); 772 | user = FindUserByNickname(userName); 773 | } 774 | 775 | if (!isCaseSensitive) { content = content.ToLower(); answer = answer.ToLower(); } 776 | 777 | if (IsQuizTaskRunning) // 다른 곳에서 StopQuiz 요청에 의해 IsQuizTaskRunning은 false가 될 수 있음. 따라서 검사 시마다 확인. 778 | { 779 | if (messageType == KakaoTalk.MessageType.Unknown) continue; 780 | else if (messageType == KakaoTalk.MessageType.DateChange) SendDateChangeNotice(content, sendTime); 781 | else if (messageType == KakaoTalk.MessageType.UserJoin) SendUserJoinNotice(userName, sendTime); 782 | else if (messageType == KakaoTalk.MessageType.UserLeave) SendUserLeaveNotice(userName, sendTime); 783 | else if (messageType == KakaoTalk.MessageType.Talk) 784 | { 785 | if (content == answer) 786 | { 787 | lastInputTick = Environment.TickCount; 788 | OnQuizAnswerCorrect(answer, user, bonusExperience, bonusMoney); 789 | shouldContinue = false; 790 | break; 791 | } 792 | else ProcessUserMessage(userName, content, sendTime); 793 | } 794 | } 795 | else break; 796 | } 797 | } 798 | if (IsQuizTaskRunning && !shouldContinue) 799 | { 800 | if (afterImagePath != null) OnQuizAfterImageSend(afterImagePath); 801 | if (explanation != null) OnQuizExplanationSend(explanation); 802 | else Thread.Sleep(1500); 803 | } 804 | } 805 | if (!IsQuizTaskRunning) break; 806 | else if (i == requestQuizCount - 1) 807 | { 808 | OnQuizAllCompleted(); 809 | IsQuizTaskRunning = false; 810 | } 811 | } 812 | } 813 | 814 | /// <summary> 815 | /// 유저의 Profile을 업데이트하는 메서드입니다.<para/> 816 | /// 이 메서드는 유저가 퀴즈 정답을 맞혔을 경우에 자동으로 호출되므로, 정답 시마다 특정 액션을 취하고 싶다면 이 메서드를 오버라이드하여 사용하십시오. 817 | /// </summary> 818 | /// <param name="user">유저 객체</param> 819 | /// <param name="bonusExperience">추가 경험치</param> 820 | /// <param name="bonusMoney">추가 머니</param> 821 | protected abstract void UpdateUserProfile(QuizUser user, int bonusExperience, int bonusMoney); 822 | 823 | /// <summary> 824 | /// 퀴즈 주제 추가 방법을 설명하는 파일을 생성합니다. 825 | /// </summary> 826 | /// <param name="path">주제 추가 방법 파일 경로</param> 827 | [MethodImpl(MethodImplOptions.Synchronized)] 828 | private void GenerateHowToAddQuizSubjectFile(string path) 829 | { 830 | string message = Properties.Resources.how_to_add_quiz_subjects; 831 | 832 | File.WriteAllLines(path, message.Split(new string[] { "\r\n" }, StringSplitOptions.None), Encoding.Unicode); 833 | } 834 | 835 | /// <summary> 836 | /// 요청한 퀴즈 개수가 부족할 경우 추가적으로 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 퀴즈 개수 부족 알림"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 837 | /// </summary> 838 | /// <param name="minQuizCount">퀴즈 최소 개수</param> 839 | /// <param name="requestQuizCount">요청하는 퀴즈의 총 개수</param> 840 | protected virtual void OnQuizCountInvalid(int minQuizCount, int requestQuizCount) 841 | { 842 | SendMessage($"퀴즈 문항 수가 최솟값보다 작습니다. (최소: {minQuizCount}개, 현재: {requestQuizCount}개)"); 843 | Thread.Sleep(SendMessageInterval); 844 | } 845 | 846 | /// <summary> 847 | /// 퀴즈의 실행 준비가 완료된 시점에 추가적으로 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 퀴즈 시작 알림"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 848 | /// </summary> 849 | protected virtual void OnQuizReady() 850 | { 851 | SendMessage("퀴즈를 시작합니다."); 852 | Thread.Sleep(SendMessageInterval); 853 | } 854 | 855 | /// <summary> 856 | /// 퀴즈가 전부 끝난 시점에 추가적으로 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 퀴즈 종료 알림"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 857 | /// </summary> 858 | protected virtual void OnQuizFinish() 859 | { 860 | SendMessage("퀴즈가 종료되었습니다."); 861 | Thread.Sleep(SendMessageInterval); 862 | } 863 | 864 | /// <summary> 865 | /// 퀴즈의 문제를 전송하는 시점에 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 퀴즈 주제 및 문제 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 866 | /// </summary> 867 | /// <param name="isRandom">주제 랜덤 여부</param> 868 | /// <param name="showSubject">주제 표시 여부</param> 869 | /// <param name="subject">현재 문항의 주제</param> 870 | /// <param name="currentQuiz">현재 문항 번호</param> 871 | /// <param name="requestQuizCount">요청하는 퀴즈의 총 개수</param> 872 | /// <param name="question">현재 문항의 문제</param> 873 | protected virtual void OnQuizQuestionSend(bool isRandom, bool showSubject, string subject, int currentQuiz, int requestQuizCount, string question) 874 | { 875 | string randomText = isRandom ? "랜덤 " : ""; 876 | SendMessage($"[{randomText}" + (showSubject ? subject : "") + $" {currentQuiz}/{requestQuizCount}]{question}"); 877 | } 878 | 879 | /// <summary> 880 | /// 퀴즈의 문제를 풀기 전 이미지를 전송하는 시점에 할 행동을 지정합니다. 기본 설정은 "SendImage 메서드를 통한 이미지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 881 | /// </summary> 882 | /// <param name="beforeImagePath">문제 풀이 전 전송하는 이미지</param> 883 | protected virtual void OnQuizBeforeImageSend(string beforeImagePath) 884 | { 885 | Thread.Sleep(1500); 886 | SendImage(beforeImagePath); 887 | } 888 | 889 | /// <summary> 890 | /// 퀴즈의 선택지를 전송하는 시점에 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 선택지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 891 | /// </summary> 892 | /// <param name="choices"></param> 893 | protected virtual void OnQuizChoicesSend(List<string> choices) 894 | { 895 | string content = ""; 896 | for (int j = 0; j < choices.Count; j++) content += $"{j + 1}. {choices[j]}\n"; 897 | content = content.Substring(0, content.Length - 1); 898 | 899 | Thread.Sleep(2000); 900 | SendMessage(content); 901 | } 902 | 903 | /// <summary> 904 | /// 퀴즈의 잠수 제한 시간이 초과되었을 경우 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 퀴즈 중단 메시지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 905 | /// </summary> 906 | protected virtual void OnQuizIdleLimitExceed() 907 | { 908 | SendMessage("장시간 유효한 입력이 발생하지 않아 문제 풀이를 중단합니다. 잠시만 기다려주세요..."); 909 | } 910 | 911 | /// <summary> 912 | /// 퀴즈 풀이의 제한 시간이 초과되었을 경우 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 안내 문구 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 913 | /// </summary> 914 | /// <param name="answer">퀴즈의 정답</param> 915 | protected virtual void OnQuizTimeLimitExceed(string answer) 916 | { 917 | SendMessage($"정답자가 없어서 다음 문제로 넘어갑니다. 정답: {answer}"); 918 | Thread.Sleep(1500); 919 | } 920 | 921 | /// <summary> 922 | /// 퀴즈의 정답을 맞힌 시점에 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 안내 메시지 전송 및 유저 Profile 업데이트"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 923 | /// </summary> 924 | /// <param name="answer">퀴즈의 정답</param> 925 | /// <param name="answeredUser">정답을 맞힌 유저</param> 926 | /// <param name="bonusExperience">퀴즈 정답 시 획득 경험치</param> 927 | /// <param name="bonusMoney">퀴즈 정답 시 획득 머니</param> 928 | protected virtual void OnQuizAnswerCorrect(string answer, QuizUser answeredUser, int bonusExperience, int bonusMoney) 929 | { 930 | SendMessage($"정답: {answer}, 정답자: [{answeredUser.CurrentTitle.Name}]{answeredUser.Nickname} (Lv. {answeredUser.Level}), 경험치: {answeredUser.Experience + bonusExperience}(+{bonusExperience}), 머니: {answeredUser.Money + bonusMoney}(+{bonusMoney})"); 931 | UpdateUserProfile(answeredUser, bonusExperience, bonusMoney); 932 | Thread.Sleep(1500); 933 | } 934 | 935 | /// <summary> 936 | /// 퀴즈의 문제를 푼 후 이미지를 전송하는 시점에 할 행동을 지정합니다. 기본 설정은 "SendImage 메서드를 통한 이미지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 937 | /// </summary> 938 | /// <param name="afterImagePath">문제 풀이 후 전송하는 이미지</param> 939 | protected virtual void OnQuizAfterImageSend(string afterImagePath) 940 | { 941 | SendImage(afterImagePath); 942 | Thread.Sleep(1500); 943 | } 944 | 945 | /// <summary> 946 | /// 퀴즈의 해설을 전송하는 시점에 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 해설 메시지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 947 | /// </summary> 948 | /// <param name="explanation">퀴즈의 설명</param> 949 | protected virtual void OnQuizExplanationSend(string explanation) 950 | { 951 | SendMessage($"[해설]{explanation}"); 952 | Thread.Sleep(3500); 953 | } 954 | 955 | /// <summary> 956 | /// 퀴즈 문제를 모두 푼 시점에 할 행동을 지정합니다. 기본 설정은 "SendMessage 메서드를 통한 안내 메시지 전송"입니다. 필요할 경우 이 메서드를 오버라이드하여 사용하십시오. 957 | /// </summary> 958 | protected virtual void OnQuizAllCompleted() 959 | { 960 | SendMessage("문제를 다 풀었습니다. 잠시만 기다려주세요..."); 961 | } 962 | } 963 | } 964 | -------------------------------------------------------------------------------- /KakaoBotAPI/KakaoBotAPI.csproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> 4 | <PropertyGroup> 5 | <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> 6 | <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> 7 | <ProjectGuid>{564EA995-D572-4A10-A312-E0BC12E66424}</ProjectGuid> 8 | <OutputType>Library</OutputType> 9 | <AppDesignerFolder>Properties</AppDesignerFolder> 10 | <RootNamespace>Less.API.NetFramework.KakaoBotAPI</RootNamespace> 11 | <AssemblyName>KakaoBotAPI</AssemblyName> 12 | <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> 13 | <FileAlignment>512</FileAlignment> 14 | <Deterministic>true</Deterministic> 15 | <TargetFrameworkProfile /> 16 | </PropertyGroup> 17 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 18 | <DebugSymbols>true</DebugSymbols> 19 | <DebugType>full</DebugType> 20 | <Optimize>false</Optimize> 21 | <OutputPath>bin\Debug\</OutputPath> 22 | <DefineConstants>DEBUG;TRACE</DefineConstants> 23 | <ErrorReport>prompt</ErrorReport> 24 | <WarningLevel>4</WarningLevel> 25 | </PropertyGroup> 26 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> 27 | <DebugType>pdbonly</DebugType> 28 | <Optimize>true</Optimize> 29 | <OutputPath>bin\Release\</OutputPath> 30 | <DefineConstants>TRACE</DefineConstants> 31 | <ErrorReport>prompt</ErrorReport> 32 | <WarningLevel>4</WarningLevel> 33 | </PropertyGroup> 34 | <ItemGroup> 35 | <Reference Include="System" /> 36 | <Reference Include="System.Core" /> 37 | <Reference Include="System.Xml.Linq" /> 38 | <Reference Include="System.Data.DataSetExtensions" /> 39 | <Reference Include="Microsoft.CSharp" /> 40 | <Reference Include="System.Data" /> 41 | <Reference Include="System.Net.Http" /> 42 | <Reference Include="System.Xml" /> 43 | </ItemGroup> 44 | <ItemGroup> 45 | <Compile Include="Bot\ChatBot.cs" /> 46 | <Compile Include="Bot\IKakaoBot.cs" /> 47 | <Compile Include="Model\Quiz.cs" /> 48 | <Compile Include="Bot\QuizBot.cs" /> 49 | <Compile Include="Model\QuizUser.cs" /> 50 | <Compile Include="Model\Title.cs" /> 51 | <Compile Include="Model\User.cs" /> 52 | <Compile Include="Properties\AssemblyInfo.cs" /> 53 | <Compile Include="Properties\Resources.Designer.cs"> 54 | <AutoGen>True</AutoGen> 55 | <DesignTime>True</DesignTime> 56 | <DependentUpon>Resources.resx</DependentUpon> 57 | </Compile> 58 | <Compile Include="Util\IniHelper.cs" /> 59 | <Compile Include="Util\ListUtil.cs" /> 60 | <Compile Include="Util\XmlHelper.cs" /> 61 | </ItemGroup> 62 | <ItemGroup> 63 | <ProjectReference Include="..\KakaoTalkAPI\KakaoTalkAPI.csproj"> 64 | <Project>{bd0427c4-ba2a-48f8-8329-11574735f961}</Project> 65 | <Name>KakaoTalkAPI</Name> 66 | </ProjectReference> 67 | </ItemGroup> 68 | <ItemGroup> 69 | <EmbeddedResource Include="Properties\Resources.resx"> 70 | <Generator>ResXFileCodeGenerator</Generator> 71 | <LastGenOutput>Resources.Designer.cs</LastGenOutput> 72 | </EmbeddedResource> 73 | </ItemGroup> 74 | <ItemGroup> 75 | <None Include="Resources\cmd_shortcuts.txt" /> 76 | </ItemGroup> 77 | <ItemGroup> 78 | <None Include="Resources\bot_limitedWords.txt" /> 79 | </ItemGroup> 80 | <ItemGroup> 81 | <None Include="Resources\quiz_notice.txt" /> 82 | </ItemGroup> 83 | <ItemGroup> 84 | <None Include="Resources\how_to_add_quiz_subjects.txt" /> 85 | </ItemGroup> 86 | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> 87 | </Project> -------------------------------------------------------------------------------- /KakaoBotAPI/Model/Quiz.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Less.API.NetFramework.KakaoBotAPI.Model 5 | { 6 | /// <summary> 7 | /// QuizBot에서 기본적으로 사용되는 퀴즈에 관한 모델 클래스입니다. 8 | /// </summary> 9 | public class Quiz 10 | { 11 | /// <summary> 12 | /// 퀴즈의 유형<para/> 13 | /// Quiz.TypeOption.Gerneral : 일반 퀴즈<para/> 14 | /// Quiz.TypeOption.Chosung : 초성 퀴즈 15 | /// </summary> 16 | public TypeOption Type { get; } 17 | 18 | /// <summary> 19 | /// 퀴즈의 메인 주제<para/> 20 | /// ChildSubject(= 하위 주제)와 구분되는 개념입니다. 21 | /// </summary> 22 | public string MainSubject { get; } 23 | 24 | /// <summary> 25 | /// 퀴즈의 하위 주제 목록<para/> 26 | /// 하나의 메인 주제당 여러 개의 하위 주제를 가질 수 있습니다. 27 | /// </summary> 28 | public List<string> ChildSubjects { get; } 29 | 30 | /// <summary> 31 | /// 퀴즈 정답의 알파벳 대소문자 구분 여부<para/> 32 | /// true : 대소문자를 구분합니다.<para/> 33 | /// false : 대소문자를 구분하지 않습니다. 34 | /// </summary> 35 | public bool IsCaseSensitive { get; } 36 | 37 | /// <summary> 38 | /// 퀴즈를 객관식으로 처리할지 여부<para/> 39 | /// true : 객관식<para/> 40 | /// false : 주관식 41 | /// </summary> 42 | public bool UseMultiChoice { get; } 43 | 44 | /// <summary> 45 | /// 퀴즈가 객관식일 경우의 선택지를 추출하는 방식<para/> 46 | /// Quiz.ChoiceExtractMethodOption.None : 객관식이 아닐 때 사용합니다.<para/> 47 | /// Quiz.ChoiceExtractMethodOption.RICS : 해당 주제 내에서 랜덤으로 선택합니다. (Random In Current Subject)<para/> 48 | /// Quiz.ChoiceExtractMethodOption.RAPT : 미리 정의된 유형(= Quiz.Data.Type)에 준하여 랜덤으로 선택합니다. 49 | /// </summary> 50 | public ChoiceExtractMethodOption ChoiceExtractMethod { get; } 51 | 52 | /// <summary> 53 | /// 퀴즈가 객관식일 경우의 선택지 개수<para/> 54 | /// 객관식일 경우 1 이상을, 그렇지 않다면 아무 값이나 입력합니다. (권장 : 0) 55 | /// </summary> 56 | public int ChoiceCount { get; } 57 | 58 | /// <summary> 59 | /// 퀴즈에 대한 실제 데이터의 목록<para/> 60 | /// 하나의 인스턴스에 대하여 Quiz.Data 객체의 목록을 받아와서 일단 저장한 후에 이용하는 방식입니다. 61 | /// </summary> 62 | public List<Data> DataList { get; } 63 | 64 | /// <summary> 65 | /// 퀴즈 유형에 대한 옵션 66 | /// Quiz.TypeOption.General : 일반 퀴즈<para/> 67 | /// Quiz.TypeOption.Chosung : 초성 퀴즈 68 | /// </summary> 69 | public enum TypeOption { General, Chosung }; 70 | 71 | /// <summary> 72 | /// 객관식 선택지 추출 방식에 대한 옵션<para/> 73 | /// Quiz.ChoiceExtractMethodOption.None : 객관식이 아닐 때 사용합니다.<para/> 74 | /// Quiz.ChoiceExtractMethodOption.RICS : 해당 주제 내에서 랜덤으로 선택합니다. (Random In Current Subject)<para/> 75 | /// Quiz.ChoiceExtractMethodOption.RAPT : 미리 정의된 유형(= Quiz.Data.Type)에 준하여 랜덤으로 선택합니다. 76 | /// </summary> 77 | public enum ChoiceExtractMethodOption { None, RICS, RAPT }; 78 | 79 | /// <summary> 80 | /// 퀴즈 객체를 생성합니다. 81 | /// </summary> 82 | /// <param name="type">퀴즈의 유형</param> 83 | /// <param name="mainSubject">퀴즈의 메인 주제</param> 84 | /// <param name="childSubjects">퀴즈의 하위 주제 목록</param> 85 | /// <param name="isCaseSensitive">퀴즈 정답의 알파벳 대소문자 구분 여부</param> 86 | /// <param name="useMultiChoice">퀴즈를 객관식으로 처리할지 여부</param> 87 | /// <param name="choiceExtractMethod">퀴즈가 객관식일 경우의 선택지를 추출하는 방식</param> 88 | /// <param name="choiceCount">퀴즈가 객관식일 경우의 선택지 개수</param> 89 | /// <param name="dataList">퀴즈에 대한 실제 데이터의 목록</param> 90 | public Quiz(TypeOption type, string mainSubject, List<string> childSubjects, bool isCaseSensitive, bool useMultiChoice, ChoiceExtractMethodOption choiceExtractMethod, int choiceCount, List<Data> dataList) 91 | { 92 | Type = type; 93 | MainSubject = mainSubject; 94 | ChildSubjects = childSubjects; 95 | IsCaseSensitive = isCaseSensitive; 96 | UseMultiChoice = useMultiChoice; 97 | ChoiceExtractMethod = choiceExtractMethod; 98 | ChoiceCount = choiceCount; 99 | DataList = dataList; 100 | } 101 | 102 | /// <summary> 103 | /// QuizBot에서 기본적으로 사용되는 퀴즈 데이터에 관한 모델 클래스입니다. 104 | /// </summary> 105 | public class Data 106 | { 107 | /// <summary> 108 | /// 퀴즈 데이터의 메인 주제 109 | /// </summary> 110 | public string MainSubject { get; } 111 | 112 | /// <summary> 113 | /// 해당 퀴즈 데이터의 질문 114 | /// </summary> 115 | public string Question { get; } 116 | 117 | /// <summary> 118 | /// 해당 퀴즈 데이터의 정답 119 | /// </summary> 120 | public string Answer { get; } 121 | 122 | /// <summary> 123 | /// 해당 퀴즈 데이터의 설명<para/> 124 | /// 만약 별도의 설명이 존재하지 않는다면 null로 설정하십시오. 125 | /// </summary> 126 | public string Explanation { get; } 127 | 128 | /// <summary> 129 | /// 해당 퀴즈 데이터의 유형<para/> 130 | /// 퀴즈의 객관식 선택지 추출 방식을 Quiz.ChoiceExtractMethodOption.RAPT로 설정할 경우에 이 값이 이용됩니다.<para/> 131 | /// 객관식이 아니고, 별도로 필요하지 않을 경우 null로 설정하십시오. 132 | /// </summary> 133 | public string Type { get; } 134 | 135 | /// <summary> 136 | /// 해당 퀴즈 데이터의 선택지 목록<para/> 137 | /// 객관식이 아니라면 null로 설정하십시오. 138 | /// </summary> 139 | public List<string> Choices { get; set; } 140 | 141 | /// <summary> 142 | /// 해당 퀴즈 데이터의 선택지 노출 전에 전송되는 이미지의 경로<para/> 143 | /// 이미지를 전송하지 않는다면 null로 설정하십시오. 144 | /// </summary> 145 | public string BeforeImagePath { get; } 146 | 147 | /// <summary> 148 | /// 해당 퀴즈 데이터의 정답 공개 후에 전송되는 이미지의 경로<para/> 149 | /// 이미지를 전송하지 않는다면 null로 설정하십시오. 150 | /// </summary> 151 | public string AfterImagePath { get; } 152 | 153 | /// <summary> 154 | /// 해당 퀴즈 데이터의 하위 주제 155 | /// </summary> 156 | public string ChildSubject { get; } 157 | 158 | /// <summary> 159 | /// 해당 퀴즈 데이터의 정답 처리 시 알파벳 대소문자 구분 여부 160 | /// </summary> 161 | public bool IsCaseSensitive { get; } 162 | 163 | /// <summary> 164 | /// 해당 퀴즈 데이터가 등록된 일시<para/> 165 | /// 데이터 정렬 등에 활용하기 위해 부여된 값입니다. 166 | /// </summary> 167 | public DateTime RegDate { get; } 168 | 169 | /// <summary> 170 | /// 퀴즈 데이터 객체를 생성합니다. 171 | /// </summary> 172 | /// <param name="mainSubject">퀴즈 데이터의 메인 주제</param> 173 | /// <param name="question">해당 퀴즈 데이터의 질문</param> 174 | /// <param name="answer">해당 퀴즈 데이터의 정답</param> 175 | /// <param name="explanation">해당 퀴즈 데이터의 설명</param> 176 | /// <param name="type">해당 퀴즈 데이터의 유형</param> 177 | /// <param name="choices">해당 퀴즈 데이터의 선택지 목록</param> 178 | /// <param name="beforeImagePath">해당 퀴즈 데이터의 선택지 노출 전에 전송되는 이미지의 경로</param> 179 | /// <param name="afterImagePath">해당 퀴즈 데이터의 정답 공개 후에 전송되는 이미지의 경로</param> 180 | /// <param name="childSubject">해당 퀴즈 데이터의 하위 주제</param> 181 | /// <param name="isCaseSensitive">해당 퀴즈 데이터의 정답 처리 시 알파벳 대소문자 구분 여부</param> 182 | /// <param name="regDate">해당 퀴즈 데이터가 등록된 일시</param> 183 | public Data(string mainSubject, string question, string answer, string explanation, string type, List<string> choices, string beforeImagePath, string afterImagePath, string childSubject, bool isCaseSensitive, DateTime regDate) 184 | { 185 | MainSubject = mainSubject; 186 | Question = question; 187 | Answer = answer; 188 | Explanation = explanation; 189 | Type = type; 190 | Choices = choices; 191 | BeforeImagePath = beforeImagePath; 192 | AfterImagePath = afterImagePath; 193 | ChildSubject = childSubject; 194 | IsCaseSensitive = isCaseSensitive; 195 | RegDate = regDate; 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /KakaoBotAPI/Model/QuizUser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Less.API.NetFramework.KakaoBotAPI.Model 4 | { 5 | /// <summary> 6 | /// QuizBot에서 기본적으로 사용되는 유저에 대한 모델 클래스입니다. 7 | /// </summary> 8 | public class QuizUser : User 9 | { 10 | /// <summary> 11 | /// 유저의 경험치 12 | /// </summary> 13 | public int Experience { get; set; } 14 | 15 | /// <summary> 16 | /// 유저의 레벨 17 | /// </summary> 18 | public int Level { get; set; } 19 | 20 | /// <summary> 21 | /// 유저의 머니 22 | /// </summary> 23 | public int Money { get; set; } 24 | 25 | /// <summary> 26 | /// 유저의 세대<para/> 27 | /// 올드 유저들을 구분하여, 추가적인 혜택 등을 부여할 수 있도록 하기 위한 개념입니다. 28 | /// </summary> 29 | public int Generation { get; } 30 | 31 | /// <summary> 32 | /// 유저에게 현재 적용된 타이틀 33 | /// </summary> 34 | public Title CurrentTitle { get; set; } 35 | 36 | /// <summary> 37 | /// 유저가 적용 가능한 타이틀 목록 38 | /// </summary> 39 | public List<Title> AvailableTitles { get; } 40 | 41 | /// <summary> 42 | /// 퀴즈 유저 객체를 생성합니다. 43 | /// </summary> 44 | /// <param name="nickname">유저의 닉네임</param> 45 | /// <param name="isIgnored">유저의 채팅을 무시할지에 대한 여부</param> 46 | /// <param name="experience">유저의 경험치</param> 47 | /// <param name="level">유저의 레벨</param> 48 | /// <param name="money">유저의 머니</param> 49 | /// <param name="generation">유저의 세대</param> 50 | /// <param name="currentTitle">유저에게 현재 적용된 타이틀</param> 51 | /// <param name="availableTitles">유저가 적용 가능한 타이틀 목록</param> 52 | public QuizUser(string nickname, bool isIgnored, int experience, int level, int money, int generation, Title currentTitle, List<Title> availableTitles) : base(nickname, isIgnored) 53 | { 54 | Experience = experience; 55 | Level = level; 56 | Money = money; 57 | Generation = generation; 58 | CurrentTitle = currentTitle; 59 | AvailableTitles = availableTitles; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /KakaoBotAPI/Model/Title.cs: -------------------------------------------------------------------------------- 1 | namespace Less.API.NetFramework.KakaoBotAPI.Model 2 | { 3 | /// <summary> 4 | /// QuizUser가 가지는 속성 중 하나로, 닉네임 외에 부가적으로 사용하는 칭호를 나타내는 모델 클래스입니다. 5 | /// </summary> 6 | public class Title 7 | { 8 | /// <summary> 9 | /// 타이틀의 이름 10 | /// </summary> 11 | public string Name { get; } 12 | 13 | /// <summary> 14 | /// 타이틀 객체를 생성합니다. 15 | /// </summary> 16 | /// <param name="name">타이틀의 이름</param> 17 | public Title(string name) 18 | { 19 | Name = name; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /KakaoBotAPI/Model/User.cs: -------------------------------------------------------------------------------- 1 | namespace Less.API.NetFramework.KakaoBotAPI.Model 2 | { 3 | /// <summary> 4 | /// ChatBot에서 기본적으로 사용되는 유저에 대한 모델 클래스입니다. 5 | /// </summary> 6 | public class User 7 | { 8 | /// <summary> 9 | /// 유저의 닉네임 10 | /// </summary> 11 | public string Nickname { get; } 12 | 13 | /// <summary> 14 | /// 유저의 채팅을 무시할지에 대한 여부<para/> 15 | /// true : 채팅을 무시합니다.<para/> 16 | /// false : 채팅을 무시하지 않습니다. 17 | /// </summary> 18 | public bool IsIgnored { get; set; } 19 | 20 | /// <summary> 21 | /// 유저 객체를 생성합니다. 22 | /// </summary> 23 | /// <param name="nickname">유저의 닉네임</param> 24 | /// <param name="isIgnored">유저의 채팅을 무시할지에 대한 여부</param> 25 | public User(string nickname, bool isIgnored) 26 | { 27 | Nickname = nickname; 28 | IsIgnored = isIgnored; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /KakaoBotAPI/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("KakaoBotAPI")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("KakaoBotAPI")] 13 | [assembly: AssemblyCopyright("Copyright © 2018 Less")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("564ea995-d572-4a10-a312-e0bc12e66424")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.1.0.0")] 36 | [assembly: AssemblyFileVersion("1.1.0.0")] 37 | -------------------------------------------------------------------------------- /KakaoBotAPI/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // <auto-generated> 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // </auto-generated> 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Less.API.NetFramework.KakaoBotAPI.Properties { 12 | using System; 13 | 14 | 15 | /// <summary> 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// </summary> 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// <summary> 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// </summary> 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Less.API.NetFramework.KakaoBotAPI.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// <summary> 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// </summary> 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// <summary> 64 | /// Looks up a localized string similar to ; 금지어 및 대체어 목록을 "[Contents]" 아래에 입력합니다. 65 | ///; (예: 금지어 = 대체어) 66 | ///; 세미콜론(;)으로 시작하는 문장은 주석으로 인식합니다. 67 | ///[Contents]. 68 | /// </summary> 69 | internal static string bot_limitedWords { 70 | get { 71 | return ResourceManager.GetString("bot_limitedWords", resourceCulture); 72 | } 73 | } 74 | 75 | /// <summary> 76 | /// Looks up a localized string similar to ; 바로가기 명령어 (줄인 명령어 = 실제 명령어) 77 | ///; 예시 : !포켓몬 1 = !퀴즈 초성 포켓몬-1세대 78 | ///; 영어의 경우 대소문자를 구분하지 않습니다. 79 | ///; 세미콜론(;)으로 시작하는 문장은 주석으로 인식합니다.. 80 | /// </summary> 81 | internal static string cmd_shortcuts { 82 | get { 83 | return ResourceManager.GetString("cmd_shortcuts", resourceCulture); 84 | } 85 | } 86 | 87 | /// <summary> 88 | /// Looks up a localized string similar to 퀴즈봇을 돌리기 위해서는, 반드시 해당 주제에 대한 퀴즈 데이터 폴더 및 내부 파일이 존재해야 합니다. 89 | ///폴더 및 파일을 추가하는 방법은 아래와 같습니다. 90 | /// 91 | ///◎ 폴더 생성 92 | ///- data/quiz 경로에 폴더를 하나 생성합니다. 93 | ///(폴더 이름은 아무렇게나 해도 됩니다) 94 | /// 95 | ///◎ settings.ini 파일 추가 96 | ///생성한 폴더 내에 settings.ini라는 이름으로 파일을 생성합니다. 인코딩은 유니코드(= UTF-16)로 합니다. 97 | /// 98 | ///파일 최상단에 "[Settings]" 입력 (ini 파일의 섹션 값) 99 | /// 100 | ///- "quizType = " 뒤에 퀴즈 유형 입력 ("일반" 또는 "초성") 101 | /// 102 | ///- "mainSubject = " 뒤에 주제 입력 103 | /// 104 | ///- "childSubjects = " 뒤에 하위주제의 목록을 쉼표(,)로 구분하여 입력합니다. (하위주제가 없으면 입력하지 않아도 됩니다.) 105 | /// 106 | ///- "isCaseSensitive = " 뒤에 true 또는 false 입력 107 | ///(만약 정답에 알파벳이 포함되어 있는 [rest of string was truncated]";. 108 | /// </summary> 109 | internal static string how_to_add_quiz_subjects { 110 | get { 111 | return ResourceManager.GetString("how_to_add_quiz_subjects", resourceCulture); 112 | } 113 | } 114 | 115 | /// <summary> 116 | /// Looks up a localized string similar to ; 여기에는 퀴즈 시작 전에 나오는 알림 메시지를 작성합니다. 117 | ///; 세미콜론(;)으로 시작하는 문장은 주석으로 인식합니다. 118 | ///공지 파일을 수정하여 유저들과 최신 소식을 공유해보세요.. 119 | /// </summary> 120 | internal static string quiz_notice { 121 | get { 122 | return ResourceManager.GetString("quiz_notice", resourceCulture); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /KakaoBotAPI/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <root> 3 | <!-- 4 | Microsoft ResX Schema 5 | 6 | Version 2.0 7 | 8 | The primary goals of this format is to allow a simple XML format 9 | that is mostly human readable. The generation and parsing of the 10 | various data types are done through the TypeConverter classes 11 | associated with the data types. 12 | 13 | Example: 14 | 15 | ... ado.net/XML headers & schema ... 16 | <resheader name="resmimetype">text/microsoft-resx</resheader> 17 | <resheader name="version">2.0</resheader> 18 | <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> 19 | <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> 20 | <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> 21 | <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> 22 | <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> 23 | <value>[base64 mime encoded serialized .NET Framework object]</value> 24 | </data> 25 | <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> 26 | <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> 27 | <comment>This is a comment</comment> 28 | </data> 29 | 30 | There are any number of "resheader" rows that contain simple 31 | name/value pairs. 32 | 33 | Each data row contains a name, and value. The row also contains a 34 | type or mimetype. Type corresponds to a .NET class that support 35 | text/value conversion through the TypeConverter architecture. 36 | Classes that don't support this are serialized and stored with the 37 | mimetype set. 38 | 39 | The mimetype is used for serialized objects, and tells the 40 | ResXResourceReader how to depersist the object. This is currently not 41 | extensible. For a given mimetype the value must be set accordingly: 42 | 43 | Note - application/x-microsoft.net.object.binary.base64 is the format 44 | that the ResXResourceWriter will generate, however the reader can 45 | read any of the formats listed below. 46 | 47 | mimetype: application/x-microsoft.net.object.binary.base64 48 | value : The object must be serialized with 49 | : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 50 | : and then encoded with base64 encoding. 51 | 52 | mimetype: application/x-microsoft.net.object.soap.base64 53 | value : The object must be serialized with 54 | : System.Runtime.Serialization.Formatters.Soap.SoapFormatter 55 | : and then encoded with base64 encoding. 56 | 57 | mimetype: application/x-microsoft.net.object.bytearray.base64 58 | value : The object must be serialized into a byte array 59 | : using a System.ComponentModel.TypeConverter 60 | : and then encoded with base64 encoding. 61 | --> 62 | <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> 63 | <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> 64 | <xsd:element name="root" msdata:IsDataSet="true"> 65 | <xsd:complexType> 66 | <xsd:choice maxOccurs="unbounded"> 67 | <xsd:element name="metadata"> 68 | <xsd:complexType> 69 | <xsd:sequence> 70 | <xsd:element name="value" type="xsd:string" minOccurs="0" /> 71 | </xsd:sequence> 72 | <xsd:attribute name="name" use="required" type="xsd:string" /> 73 | <xsd:attribute name="type" type="xsd:string" /> 74 | <xsd:attribute name="mimetype" type="xsd:string" /> 75 | <xsd:attribute ref="xml:space" /> 76 | </xsd:complexType> 77 | </xsd:element> 78 | <xsd:element name="assembly"> 79 | <xsd:complexType> 80 | <xsd:attribute name="alias" type="xsd:string" /> 81 | <xsd:attribute name="name" type="xsd:string" /> 82 | </xsd:complexType> 83 | </xsd:element> 84 | <xsd:element name="data"> 85 | <xsd:complexType> 86 | <xsd:sequence> 87 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> 88 | <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> 89 | </xsd:sequence> 90 | <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> 91 | <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> 92 | <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> 93 | <xsd:attribute ref="xml:space" /> 94 | </xsd:complexType> 95 | </xsd:element> 96 | <xsd:element name="resheader"> 97 | <xsd:complexType> 98 | <xsd:sequence> 99 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> 100 | </xsd:sequence> 101 | <xsd:attribute name="name" type="xsd:string" use="required" /> 102 | </xsd:complexType> 103 | </xsd:element> 104 | </xsd:choice> 105 | </xsd:complexType> 106 | </xsd:element> 107 | </xsd:schema> 108 | <resheader name="resmimetype"> 109 | <value>text/microsoft-resx</value> 110 | </resheader> 111 | <resheader name="version"> 112 | <value>2.0</value> 113 | </resheader> 114 | <resheader name="reader"> 115 | <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> 116 | </resheader> 117 | <resheader name="writer"> 118 | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> 119 | </resheader> 120 | <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> 121 | <data name="bot_limitedWords" type="System.Resources.ResXFileRef, System.Windows.Forms"> 122 | <value>..\Resources\bot_limitedWords.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;ks_c_5601-1987</value> 123 | </data> 124 | <data name="cmd_shortcuts" type="System.Resources.ResXFileRef, System.Windows.Forms"> 125 | <value>..\Resources\cmd_shortcuts.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;ks_c_5601-1987</value> 126 | </data> 127 | <data name="how_to_add_quiz_subjects" type="System.Resources.ResXFileRef, System.Windows.Forms"> 128 | <value>..\Resources\how_to_add_quiz_subjects.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;ks_c_5601-1987</value> 129 | </data> 130 | <data name="quiz_notice" type="System.Resources.ResXFileRef, System.Windows.Forms"> 131 | <value>..\Resources\quiz_notice.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;ks_c_5601-1987</value> 132 | </data> 133 | </root> -------------------------------------------------------------------------------- /KakaoBotAPI/Resources/bot_limitedWords.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/KakaoBotAPI/Resources/bot_limitedWords.txt -------------------------------------------------------------------------------- /KakaoBotAPI/Resources/cmd_shortcuts.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/KakaoBotAPI/Resources/cmd_shortcuts.txt -------------------------------------------------------------------------------- /KakaoBotAPI/Resources/how_to_add_quiz_subjects.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/KakaoBotAPI/Resources/how_to_add_quiz_subjects.txt -------------------------------------------------------------------------------- /KakaoBotAPI/Resources/quiz_notice.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/KakaoBotAPI/Resources/quiz_notice.txt -------------------------------------------------------------------------------- /KakaoBotAPI/Util/IniHelper.cs: -------------------------------------------------------------------------------- 1 | // Reference : https://stackoverflow.com/questions/217902/reading-writing-an-ini-file - Answer by Danny Beckett 2 | 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace Less.API.NetFramework.KakaoBotAPI.Util 8 | { 9 | /// <summary> 10 | /// INI 파일을 처리하는 데 도움을 주는 유틸리티 클래스입니다. 11 | /// </summary> 12 | public class IniHelper 13 | { 14 | /// <summary> 15 | /// INI 파일의 기본 확장자 16 | /// </summary> 17 | public const string FileExtension = ".ini"; 18 | 19 | /// <summary> 20 | /// INI 파일의 경로<para/> 21 | /// 생성자 내부에서 변형된 값이 저장되므로, getter만을 허용합니다. 22 | /// </summary> 23 | public string Path { get; } 24 | 25 | /// <summary> 26 | /// INI 헬퍼 객체를 생성합니다. 27 | /// </summary> 28 | /// <param name="path">INI 파일의 경로</param> 29 | public IniHelper(string path) 30 | { 31 | Path = new FileInfo(path.Contains(FileExtension) && path.IndexOf(FileExtension) == path.Length - 4 ? path : path + FileExtension).FullName; 32 | } 33 | 34 | /// <summary> 35 | /// INI 파일의 해당 section에서 특정 key를 가진 값을 읽어들입니다. 36 | /// </summary> 37 | /// <param name="key">Key</param> 38 | /// <param name="section">Section</param> 39 | /// <returns>Value</returns> 40 | public string Read(string key, string section) 41 | { 42 | var value = new StringBuilder(255); 43 | GetPrivateProfileString(section, key, "", value, 255, Path); 44 | return value.ToString(); 45 | } 46 | 47 | /// <summary> 48 | /// INI 파일의 특정 section에 key-value pair를 작성합니다. 49 | /// </summary> 50 | /// <param name="key">Key</param> 51 | /// <param name="value">Value</param> 52 | /// <param name="section">Section</param> 53 | public void Write(string key, string value, string section) 54 | { 55 | WritePrivateProfileString(section, key, value, Path); 56 | } 57 | 58 | 59 | /* INI 처리용 Windows API 함수 목록 */ 60 | 61 | [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)] 62 | static extern long WritePrivateProfileString(string section, string key, string value, string filePath); 63 | 64 | [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)] 65 | static extern int GetPrivateProfileString(string section, string key, string defaultStr, StringBuilder retVal, int size, string filePath); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /KakaoBotAPI/Util/ListUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Less.API.NetFramework.KakaoBotAPI.Util 5 | { 6 | internal class ListUtil 7 | { 8 | private static Random random = new Random(); 9 | 10 | public static void Shuffle<T>(IList<T> list) 11 | { 12 | int n = list.Count; 13 | while (n > 1) 14 | { 15 | n--; 16 | int k = random.Next(n + 1); 17 | T value = list[k]; 18 | list[k] = list[n]; 19 | list[n] = value; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KakaoBotAPI/Util/XmlHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Xml; 6 | 7 | namespace Less.API.NetFramework.KakaoBotAPI.Util 8 | { 9 | /// <summary> 10 | /// XML 파일을 처리하는 데 도움을 주는 유틸리티 클래스입니다. 11 | /// </summary> 12 | public class XmlHelper 13 | { 14 | /// <summary> 15 | /// XML 파일의 기본 확장자 16 | /// </summary> 17 | public const string FileExtension = ".xml"; 18 | 19 | /// <summary> 20 | /// XML 파일의 경로<para/> 21 | /// 생성자 내부에서 변형된 값이 저장되므로, getter만을 허용합니다. 22 | /// </summary> 23 | public string Path { get; } 24 | 25 | /// <summary> 26 | /// XML 파일 최상단 요소의 태그명<para/> 27 | /// 이 값은 XML 파일 생성 시에만 사용되며, XML 파일을 읽어들일 경우는 ReadFile 메서드의 반환 값인 Document 객체 내의 속성이 사용됩니다. 28 | /// </summary> 29 | string RootElementName; 30 | 31 | /// <summary> 32 | /// 최상단 요소 내부의 자식 노드 목록<para/> 33 | /// 이 값은 XML 파일 생성 시에만 사용되며, XML 파일을 읽어들일 경우는 ReadFile 메서드의 반환 값인 Document 객체 내의 속성이 사용됩니다. 34 | /// </summary> 35 | List<Node> ChildNodes; 36 | 37 | XmlWriterSettings WriterSettings; 38 | 39 | const int ThreadProcessInterval = 20; 40 | 41 | /// <summary> 42 | /// XML 헬퍼 객체를 생성합니다. 43 | /// </summary> 44 | /// <param name="path">XML 파일의 경로</param> 45 | public XmlHelper(string path) 46 | { 47 | Path = new FileInfo(path.Contains(FileExtension) && path.IndexOf(FileExtension) == path.Length - 4 ? path : path + FileExtension).FullName; 48 | 49 | WriterSettings = new XmlWriterSettings(); 50 | WriterSettings.Encoding = new UTF8Encoding(false); 51 | WriterSettings.Indent = true; 52 | } 53 | 54 | /// <summary> 55 | /// XML 파일을 생성합니다.<para/> 56 | /// 만약 기존 파일이 존재한다면, 덮어쓰기가 진행됩니다. 57 | /// </summary> 58 | /// <param name="rootElementName">XML 파일 최상단 요소의 태그명</param> 59 | /// <param name="childNodes">최상단 요소 내부의 자식 노드 목록</param> 60 | public void CreateFile(string rootElementName, List<Node> childNodes) 61 | { 62 | RootElementName = rootElementName; 63 | ChildNodes = childNodes; 64 | new Thread(new ThreadStart(CreateTask)).Start(); 65 | lock (this) Monitor.Wait(this); 66 | } 67 | 68 | /// <summary> 69 | /// XML 파일을 읽어들입니다.<para/> 70 | /// 반환 값인 XmlHelper.Document 객체를 이용하여 노드들의 값을 얻어올 수 있습니다. 71 | /// </summary> 72 | /// <returns>XML 문서 객체</returns> 73 | public Document ReadFile() 74 | { 75 | new Thread(new ThreadStart(ReadTask)).Start(); 76 | lock (this) Monitor.Wait(this); 77 | return new Document(RootElementName, ChildNodes); 78 | } 79 | 80 | /// <summary> 81 | /// 실제 파일 쓰기 작업을 기술한 메서드입니다. 82 | /// </summary> 83 | void CreateTask() 84 | { 85 | Thread.Sleep(ThreadProcessInterval); 86 | 87 | using (var writer = XmlWriter.Create(Path, WriterSettings)) 88 | { 89 | writer.WriteStartDocument(true); 90 | writer.WriteStartElement(RootElementName); 91 | 92 | foreach (var node in ChildNodes) 93 | { 94 | writer.WriteStartElement(node.Name); 95 | foreach (var p in node.Properties) writer.WriteAttributeString(p.Key, $"{p.Value}"); 96 | foreach (var v in node.DataList) writer.WriteElementString(v.Key, v.Value); 97 | writer.WriteEndElement(); 98 | } 99 | 100 | writer.WriteEndElement(); 101 | writer.WriteEndDocument(); 102 | } 103 | 104 | lock (this) Monitor.Pulse(this); 105 | } 106 | 107 | /// <summary> 108 | /// 실제 파일 읽기 작업을 기술한 메서드입니다. 109 | /// </summary> 110 | void ReadTask() 111 | { 112 | Thread.Sleep(ThreadProcessInterval); 113 | 114 | bool isRootNode = true; 115 | string childNodeName = null; 116 | var pKeys = new List<string>(); 117 | var pValues = new List<string>(); 118 | var vKeys = new List<string>(); 119 | var vValues = new List<string>(); 120 | Node childNode; 121 | 122 | if (ChildNodes == null) ChildNodes = new List<Node>(); 123 | else ChildNodes.Clear(); 124 | 125 | using (var reader = XmlReader.Create(Path)) 126 | { 127 | while (reader.Read()) 128 | { 129 | switch (reader.NodeType) 130 | { 131 | case XmlNodeType.Element: 132 | if (isRootNode) 133 | { 134 | isRootNode = false; 135 | RootElementName = reader.Name; 136 | } 137 | else 138 | { 139 | if (childNodeName == null) childNodeName = reader.Name; 140 | else 141 | { 142 | vKeys.Add(reader.Name); 143 | reader.Read(); 144 | vValues.Add(reader.Value); 145 | reader.Read(); 146 | } 147 | } 148 | break; 149 | case XmlNodeType.Attribute: 150 | if (childNodeName != null) { pKeys.Add(reader.Name); pValues.Add(reader.Value); } 151 | break; 152 | case XmlNodeType.EndElement: 153 | if (childNodeName != null) 154 | { 155 | childNode = new Node(childNodeName); 156 | for (int i = 0; i < pKeys.Count; i++) childNode.AddProperty(pKeys[i], pValues[i]); 157 | for (int i = 0; i < vKeys.Count; i++) childNode.AddData(vKeys[i], vValues[i]); 158 | ChildNodes.Add(childNode); 159 | 160 | pKeys.Clear(); 161 | pValues.Clear(); 162 | vKeys.Clear(); 163 | vValues.Clear(); 164 | childNodeName = null; 165 | } 166 | break; 167 | } 168 | } 169 | } 170 | 171 | lock (this) Monitor.Pulse(this); 172 | } 173 | 174 | /// <summary> 175 | /// XML 문서 객체 176 | /// </summary> 177 | public struct Document 178 | { 179 | /// <summary> 180 | /// XML 문서 최상단 요소의 태그명 181 | /// </summary> 182 | public string RootElementName { get; } 183 | 184 | /// <summary> 185 | /// 최상단 요소 내부의 자식 노드 목록 186 | /// </summary> 187 | public List<Node> ChildNodes { get; } 188 | 189 | /// <summary> 190 | /// XML 문서 객체를 생성합니다. 191 | /// </summary> 192 | /// <param name="rootElementName">XML 문서 최상단 요소의 태그명</param> 193 | /// <param name="childNodes">최상단 요소 내부의 자식 노드 목록</param> 194 | public Document(string rootElementName, List<Node> childNodes) 195 | { 196 | RootElementName = rootElementName; 197 | ChildNodes = childNodes; 198 | } 199 | } 200 | 201 | /// <summary> 202 | /// XML 노드 객체 203 | /// </summary> 204 | public struct Node 205 | { 206 | /// <summary> 207 | /// 노드의 이름 208 | /// </summary> 209 | public string Name { get; } 210 | 211 | /// <summary> 212 | /// 노드의 속성 목록 213 | /// </summary> 214 | public List<NodeData> Properties { get; } 215 | 216 | /// <summary> 217 | /// 노드의 데이터 목록<para/> 218 | /// 내부에 Key-Value Pair가 존재합니다. 219 | /// </summary> 220 | public List<NodeData> DataList { get; } 221 | 222 | /// <summary> 223 | /// XML 노드 객체를 생성합니다. 224 | /// </summary> 225 | /// <param name="name"></param> 226 | public Node(string name) 227 | { 228 | Name = name; 229 | Properties = new List<NodeData>(); 230 | DataList = new List<NodeData>(); 231 | } 232 | 233 | /// <summary> 234 | /// XML 노드에 속성을 삽입합니다. 235 | /// </summary> 236 | /// <param name="key">속성 Key</param> 237 | /// <param name="value">속성 Value</param> 238 | public void AddProperty(string key, bool value) { Properties.Add(new NodeData(key, $"{value}")); } 239 | 240 | /// <summary> 241 | /// XML 노드에 속성을 삽입합니다. 242 | /// </summary> 243 | /// <param name="key">속성 Key</param> 244 | /// <param name="value">속성 Value</param> 245 | public void AddProperty(string key, sbyte value) { Properties.Add(new NodeData(key, $"{value}")); } 246 | 247 | /// <summary> 248 | /// XML 노드에 속성을 삽입합니다. 249 | /// </summary> 250 | /// <param name="key">속성 Key</param> 251 | /// <param name="value">속성 Value</param> 252 | public void AddProperty(string key, byte value) { Properties.Add(new NodeData(key, $"{value}")); } 253 | 254 | /// <summary> 255 | /// XML 노드에 속성을 삽입합니다. 256 | /// </summary> 257 | /// <param name="key">속성 Key</param> 258 | /// <param name="value">속성 Value</param> 259 | public void AddProperty(string key, char value) { Properties.Add(new NodeData(key, $"{value}")); } 260 | 261 | /// <summary> 262 | /// XML 노드에 속성을 삽입합니다. 263 | /// </summary> 264 | /// <param name="key">속성 Key</param> 265 | /// <param name="value">속성 Value</param> 266 | public void AddProperty(string key, short value) { Properties.Add(new NodeData(key, $"{value}")); } 267 | 268 | /// <summary> 269 | /// XML 노드에 속성을 삽입합니다. 270 | /// </summary> 271 | /// <param name="key">속성 Key</param> 272 | /// <param name="value">속성 Value</param> 273 | public void AddProperty(string key, ushort value) { Properties.Add(new NodeData(key, $"{value}")); } 274 | 275 | /// <summary> 276 | /// XML 노드에 속성을 삽입합니다. 277 | /// </summary> 278 | /// <param name="key">속성 Key</param> 279 | /// <param name="value">속성 Value</param> 280 | public void AddProperty(string key, int value) { Properties.Add(new NodeData(key, $"{value}")); } 281 | 282 | /// <summary> 283 | /// XML 노드에 속성을 삽입합니다. 284 | /// </summary> 285 | /// <param name="key">속성 Key</param> 286 | /// <param name="value">속성 Value</param> 287 | public void AddProperty(string key, uint value) { Properties.Add(new NodeData(key, $"{value}")); } 288 | 289 | /// <summary> 290 | /// XML 노드에 속성을 삽입합니다. 291 | /// </summary> 292 | /// <param name="key">Key</param> 293 | /// <param name="value">Value</param> 294 | public void AddProperty(string key, long value) { Properties.Add(new NodeData(key, $"{value}")); } 295 | 296 | /// <summary> 297 | /// XML 노드에 속성을 삽입합니다. 298 | /// </summary> 299 | /// <param name="key">속성 Key</param> 300 | /// <param name="value">속성 Value</param> 301 | public void AddProperty(string key, ulong value) { Properties.Add(new NodeData(key, $"{value}")); } 302 | 303 | /// <summary> 304 | /// XML 노드에 속성을 삽입합니다. 305 | /// </summary> 306 | /// <param name="key">속성 Key</param> 307 | /// <param name="value">속성 Value</param> 308 | public void AddProperty(string key, float value) { Properties.Add(new NodeData(key, $"{value}")); } 309 | 310 | /// <summary> 311 | /// XML 노드에 속성을 삽입합니다. 312 | /// </summary> 313 | /// <param name="key">속성 Key</param> 314 | /// <param name="value">속성 Value</param> 315 | public void AddProperty(string key, double value) { Properties.Add(new NodeData(key, $"{value}")); } 316 | 317 | /// <summary> 318 | /// XML 노드에 속성을 삽입합니다. 319 | /// </summary> 320 | /// <param name="key">속성 Key</param> 321 | /// <param name="value">속성 Value</param> 322 | public void AddProperty(string key, decimal value) { Properties.Add(new NodeData(key, $"{value}")); } 323 | 324 | /// <summary> 325 | /// XML 노드에 속성을 삽입합니다. 326 | /// </summary> 327 | /// <param name="key">속성 Key</param> 328 | /// <param name="value">속성 Value</param> 329 | public void AddProperty(string key, string value) { Properties.Add(new NodeData(key, $"{value}")); } 330 | 331 | /// <summary> 332 | /// XML 노드에서 속성을 제거합니다. 333 | /// </summary> 334 | /// <param name="key">속성 Key</param> 335 | public void RemoveProperty(string key) 336 | { 337 | for (int i = 0; i < Properties.Count; i++) if (Properties[i].Key == key) { Properties.RemoveAt(i); break; } 338 | } 339 | 340 | /// <summary> 341 | /// XML 노드 속성의 값을 가져옵니다. 342 | /// </summary> 343 | /// <param name="key">속성 Key</param> 344 | /// <returns>속성 Value</returns> 345 | public string GetProperty(string key) 346 | { 347 | for (int i = 0; i < Properties.Count; i++) if (Properties[i].Key == key) return Properties[i].Value; 348 | return null; 349 | } 350 | 351 | /// <summary> 352 | /// XML 노드에 데이터를 삽입합니다. 353 | /// </summary> 354 | /// <param name="key">데이터 Key</param> 355 | /// <param name="value">데이터 Value</param> 356 | public void AddData(string key, bool value) { DataList.Add(new NodeData(key, $"{value}")); } 357 | 358 | /// <summary> 359 | /// XML 노드에 데이터를 삽입합니다. 360 | /// </summary> 361 | /// <param name="key">데이터 Key</param> 362 | /// <param name="value">데이터 Value</param> 363 | public void AddData(string key, sbyte value) { DataList.Add(new NodeData(key, $"{value}")); } 364 | 365 | /// <summary> 366 | /// XML 노드에 데이터를 삽입합니다. 367 | /// </summary> 368 | /// <param name="key">데이터 Key</param> 369 | /// <param name="value">데이터 Value</param> 370 | public void AddData(string key, byte value) { DataList.Add(new NodeData(key, $"{value}")); } 371 | 372 | /// <summary> 373 | /// XML 노드에 데이터를 삽입합니다. 374 | /// </summary> 375 | /// <param name="key">데이터 Key</param> 376 | /// <param name="value">데이터 Value</param> 377 | public void AddData(string key, char value) { DataList.Add(new NodeData(key, $"{value}")); } 378 | 379 | /// <summary> 380 | /// XML 노드에 데이터를 삽입합니다. 381 | /// </summary> 382 | /// <param name="key">데이터 Key</param> 383 | /// <param name="value">데이터 Value</param> 384 | public void AddData(string key, short value) { DataList.Add(new NodeData(key, $"{value}")); } 385 | 386 | /// <summary> 387 | /// XML 노드에 데이터를 삽입합니다. 388 | /// </summary> 389 | /// <param name="key">데이터 Key</param> 390 | /// <param name="value">데이터 Value</param> 391 | public void AddData(string key, ushort value) { DataList.Add(new NodeData(key, $"{value}")); } 392 | 393 | /// <summary> 394 | /// XML 노드에 데이터를 삽입합니다. 395 | /// </summary> 396 | /// <param name="key">데이터 Key</param> 397 | /// <param name="value">데이터 Value</param> 398 | public void AddData(string key, int value) { DataList.Add(new NodeData(key, $"{value}")); } 399 | 400 | /// <summary> 401 | /// XML 노드에 데이터를 삽입합니다. 402 | /// </summary> 403 | /// <param name="key">데이터 Key</param> 404 | /// <param name="value">데이터 Value</param> 405 | public void AddData(string key, uint value) { DataList.Add(new NodeData(key, $"{value}")); } 406 | 407 | /// <summary> 408 | /// XML 노드에 데이터를 삽입합니다. 409 | /// </summary> 410 | /// <param name="key">데이터 Key</param> 411 | /// <param name="value">데이터 Value</param> 412 | public void AddData(string key, long value) { DataList.Add(new NodeData(key, $"{value}")); } 413 | 414 | /// <summary> 415 | /// XML 노드에 데이터를 삽입합니다. 416 | /// </summary> 417 | /// <param name="key">데이터 Key</param> 418 | /// <param name="value">데이터 Value</param> 419 | public void AddData(string key, ulong value) { DataList.Add(new NodeData(key, $"{value}")); } 420 | 421 | /// <summary> 422 | /// XML 노드에 데이터를 삽입합니다. 423 | /// </summary> 424 | /// <param name="key">데이터 Key</param> 425 | /// <param name="value">데이터 Value</param> 426 | public void AddData(string key, float value) { DataList.Add(new NodeData(key, $"{value}")); } 427 | 428 | /// <summary> 429 | /// XML 노드에 데이터를 삽입합니다. 430 | /// </summary> 431 | /// <param name="key">데이터 Key</param> 432 | /// <param name="value">데이터 Value</param> 433 | public void AddData(string key, double value) { DataList.Add(new NodeData(key, $"{value}")); } 434 | 435 | /// <summary> 436 | /// XML 노드에 데이터를 삽입합니다. 437 | /// </summary> 438 | /// <param name="key">데이터 Key</param> 439 | /// <param name="value">데이터 Value</param> 440 | public void AddData(string key, decimal value) { DataList.Add(new NodeData(key, $"{value}")); } 441 | 442 | /// <summary> 443 | /// XML 노드에 데이터를 삽입합니다. 444 | /// </summary> 445 | /// <param name="key">데이터 Key</param> 446 | /// <param name="value">데이터 Value</param> 447 | public void AddData(string key, string value) { DataList.Add(new NodeData(key, $"{value}")); } 448 | 449 | /// <summary> 450 | /// XML 노드에서 데이터를 삭제합니다. 451 | /// </summary> 452 | /// <param name="key">데이터 Key</param> 453 | public void RemoveData(string key) 454 | { 455 | for (int i = 0; i < DataList.Count; i++) if (DataList[i].Key == key) { DataList.RemoveAt(i); break; } 456 | } 457 | 458 | /// <summary> 459 | /// XML 노드에서 데이터의 값을 가져옵니다. 460 | /// </summary> 461 | /// <param name="key">데이터 key</param> 462 | /// <returns>데이터 Value</returns> 463 | public string GetData(string key) 464 | { 465 | for (int i = 0; i < DataList.Count; i++) if (DataList[i].Key == key) return DataList[i].Value; 466 | return null; 467 | } 468 | } 469 | 470 | /// <summary> 471 | /// XML 노드의 데이터 객체 472 | /// </summary> 473 | public struct NodeData 474 | { 475 | /// <summary> 476 | /// XML 노드 데이터의 Key 477 | /// </summary> 478 | public string Key { get; set; } 479 | 480 | /// <summary> 481 | /// XML 노드 데이터의 Value 482 | /// </summary> 483 | public string Value { get; set; } 484 | 485 | /// <summary> 486 | /// XML 노드 데이터 객체를 생성합니다. 487 | /// </summary> 488 | /// <param name="key">XML 노드 데이터의 Key</param> 489 | /// <param name="value">XML 노드 데이터의 Value</param> 490 | public NodeData(string key, string value) 491 | { 492 | Key = key; 493 | Value = value; 494 | } 495 | } 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /KakaoTalkAPI/ClipboardManager.cs: -------------------------------------------------------------------------------- 1 | using Less.API.NetFramework.WindowsAPI; 2 | using System; 3 | using System.Drawing; 4 | using System.Runtime.InteropServices; 5 | 6 | // .Net Framework에서 기본 제공하는 Clipboard 클래스는 불안정하기 때문에, 전부 Native API로 처리해야 함. 7 | 8 | namespace Less.API.NetFramework.KakaoTalkAPI 9 | { 10 | internal sealed class ClipboardManager 11 | { 12 | public static bool HasDataToRestore = false; 13 | static uint Format; 14 | static object Data; 15 | static IntPtr MemoryHandle = IntPtr.Zero; 16 | 17 | /// <summary> 18 | /// 현재 클립보드에 저장되어 있는 데이터를 백업합니다. 클립보드 열기 요청 실패 시 ClipboardManager.CannotOpenException 예외가 발생합니다. 19 | /// </summary> 20 | public static void BackupData(IntPtr clipboardOwner) 21 | { 22 | Format = 0; 23 | 24 | bool isClipboardOpen = WinAPI.OpenClipboard(clipboardOwner); 25 | if (!isClipboardOpen) throw new CannotOpenException(); 26 | do { Format = WinAPI.EnumClipboardFormats(Format); } 27 | while (Format >= 0x200 || Format == 0); 28 | 29 | IntPtr pointer = WinAPI.GetClipboardData(Format); 30 | switch (Format) 31 | { 32 | case WinAPI.CF_TEXT: 33 | Data = Marshal.PtrToStringAnsi(pointer); 34 | MemoryHandle = Marshal.StringToHGlobalAnsi((string)Data); 35 | break; 36 | case WinAPI.CF_UNICODETEXT: 37 | Data = Marshal.PtrToStringUni(pointer); 38 | MemoryHandle = Marshal.StringToHGlobalUni((string)Data); 39 | break; 40 | case WinAPI.CF_BITMAP: 41 | Data = Image.FromHbitmap(pointer); 42 | MemoryHandle = ((Bitmap)Data).GetHbitmap(); 43 | break; 44 | } 45 | WinAPI.CloseClipboard(); 46 | 47 | HasDataToRestore = true; 48 | } 49 | 50 | /// <summary> 51 | /// 백업했던 클립보드 데이터를 복구합니다. 현재 텍스트와 이미지만 복구 기능을 지원하며, 클립보드 열기 요청 실패 시 ClipboardManager.CannotOpenException 예외가 발생합니다. 52 | /// </summary> 53 | public static void RestoreData(IntPtr clipboardOwner) 54 | { 55 | if (!HasDataToRestore) return; 56 | 57 | if (Format == WinAPI.CF_TEXT || Format == WinAPI.CF_UNICODETEXT) 58 | { 59 | bool isClipboardOpen = WinAPI.OpenClipboard(clipboardOwner); 60 | if (!isClipboardOpen) throw new CannotOpenException(); 61 | } 62 | 63 | switch (Format) 64 | { 65 | case WinAPI.CF_TEXT: 66 | WinAPI.SetClipboardData(Format, MemoryHandle); 67 | break; 68 | case WinAPI.CF_UNICODETEXT: 69 | WinAPI.SetClipboardData(Format, MemoryHandle); 70 | break; 71 | case WinAPI.CF_BITMAP: 72 | case WinAPI.CF_DIB: 73 | Format = WinAPI.CF_BITMAP; 74 | SetImage(MemoryHandle, clipboardOwner); 75 | (Data as Bitmap).Dispose(); 76 | break; 77 | } 78 | if (Format == WinAPI.CF_TEXT || Format == WinAPI.CF_UNICODETEXT) WinAPI.CloseClipboard(); 79 | 80 | WinAPI.DeleteObject(MemoryHandle); 81 | Data = null; 82 | MemoryHandle = IntPtr.Zero; 83 | HasDataToRestore = false; 84 | } 85 | 86 | /// <summary> 87 | /// 클립보드에서 텍스트를 가져옵니다. 클립보드 열기 요청 실패 시 ClipboardManager.CannotOpenException 예외가 발생하고, 만약 텍스트가 존재하지 않을 경우 null을 반환합니다. 88 | /// </summary> 89 | public static string GetText(IntPtr clipboardOwner) 90 | { 91 | string text = null; 92 | 93 | bool isClipboardOpen = WinAPI.OpenClipboard(clipboardOwner); 94 | if (!isClipboardOpen) throw new CannotOpenException(); 95 | IntPtr pointer = WinAPI.GetClipboardData(WinAPI.CF_UNICODETEXT); 96 | if (pointer == IntPtr.Zero) 97 | { 98 | pointer = WinAPI.GetClipboardData(WinAPI.CF_TEXT); 99 | if (pointer != IntPtr.Zero) text = Marshal.PtrToStringAnsi(pointer); 100 | } 101 | else text = Marshal.PtrToStringUni(pointer); 102 | WinAPI.CloseClipboard(); 103 | 104 | return text; 105 | } 106 | 107 | /// <summary> 108 | /// 클립보드에 텍스트를 저장합니다. 클립보드 열기 요청 실패 시 ClipboardManager.CannotOpenException 예외가 발생합니다. 109 | /// </summary> 110 | /// <param name="text">저장할 텍스트</param> 111 | public static void SetText(string text, IntPtr clipboardOwner) 112 | { 113 | bool isClipboardOpen = WinAPI.OpenClipboard(clipboardOwner); 114 | if (!isClipboardOpen) throw new CannotOpenException(); 115 | WinAPI.EmptyClipboard(); 116 | WinAPI.SetClipboardData(WinAPI.CF_TEXT, Marshal.StringToHGlobalAnsi(text)); 117 | WinAPI.SetClipboardData(WinAPI.CF_UNICODETEXT, Marshal.StringToHGlobalUni(text)); 118 | WinAPI.CloseClipboard(); 119 | } 120 | 121 | /// <summary> 122 | /// 클립보드에 이미지를 저장합니다. 클립보드 열기 요청 실패 시 ClipboardManager.CannotOpenException 예외가 발생합니다. 123 | /// 또한 이 메서드를 짧은 시간 간격을 두고 주기적으로 호출할 경우 ExternalException 및 ContextSwitchDeadLock 현상이 발생할 수 있습니다. 124 | /// 따라서 이 메서드를 반복문 내에서 사용할 때는 주의가 필요합니다. 125 | /// </summary> 126 | /// <param name="imagePath">저장할 이미지의 원본 파일 경로</param> 127 | public static void SetImage(string imagePath, IntPtr clipboardOwner) 128 | { 129 | using (Bitmap image = (Bitmap)Image.FromFile(imagePath)) _SetImage(image, clipboardOwner); 130 | } 131 | 132 | public static void SetImage(IntPtr hBitmap, IntPtr clipboardOwner) 133 | { 134 | using (Bitmap image = Image.FromHbitmap(hBitmap)) _SetImage(image, clipboardOwner); 135 | } 136 | 137 | private static void _SetImage(Bitmap image, IntPtr clipboardOwner) 138 | { 139 | Bitmap tempImage = new Bitmap(image.Width, image.Height); 140 | using (Graphics graphics = Graphics.FromImage(tempImage)) 141 | { 142 | IntPtr hScreenDC = WinAPI.GetWindowDC(IntPtr.Zero); // 기본적인 Device Context의 속성들을 카피하기 위한 작업 143 | IntPtr hDestDC = WinAPI.CreateCompatibleDC(hScreenDC); 144 | IntPtr hDestBitmap = WinAPI.CreateCompatibleBitmap(hScreenDC, image.Width, image.Height); // destDC와 destBitmap 모두 반드시 screenDC의 속성들을 기반으로 해야 함. 145 | IntPtr hPrevDestObject = WinAPI.SelectObject(hDestDC, hDestBitmap); 146 | 147 | IntPtr hSourceDC = graphics.GetHdc(); 148 | IntPtr hSourceBitmap = image.GetHbitmap(); 149 | IntPtr hPrevSourceObject = WinAPI.SelectObject(hSourceDC, hSourceBitmap); 150 | 151 | WinAPI.BitBlt(hDestDC, 0, 0, image.Width, image.Height, hSourceDC, 0, 0, WinAPI.SRCCOPY); 152 | 153 | WinAPI.DeleteObject(WinAPI.SelectObject(hSourceDC, hPrevSourceObject)); 154 | WinAPI.SelectObject(hDestDC, hPrevDestObject); // 리턴값 : hDestBitmap 155 | graphics.ReleaseHdc(hSourceDC); 156 | WinAPI.DeleteDC(hDestDC); 157 | 158 | bool isClipboardOpen = WinAPI.OpenClipboard(clipboardOwner); 159 | if (!isClipboardOpen) 160 | { 161 | WinAPI.DeleteObject(hDestBitmap); 162 | WinAPI.DeleteObject(hSourceDC); 163 | WinAPI.DeleteObject(hSourceBitmap); 164 | tempImage.Dispose(); 165 | throw new CannotOpenException(); 166 | } 167 | WinAPI.EmptyClipboard(); 168 | WinAPI.SetClipboardData(WinAPI.CF_BITMAP, hDestBitmap); 169 | WinAPI.CloseClipboard(); 170 | 171 | WinAPI.DeleteObject(hDestBitmap); 172 | WinAPI.DeleteObject(hSourceDC); 173 | WinAPI.DeleteObject(hSourceBitmap); 174 | } 175 | tempImage.Dispose(); 176 | } 177 | 178 | public class CannotOpenException : Exception 179 | { 180 | internal CannotOpenException() : base("클립보드가 다른 프로그램에 의해 이미 사용되고 있습니다.") { } 181 | } 182 | 183 | public class InvalidFormatRequestException : Exception 184 | { 185 | internal InvalidFormatRequestException() : base("잘못된 클립보드 포맷 요청입니다.") { } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /KakaoTalkAPI/KakaoTalkAPI.csproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> 4 | <PropertyGroup> 5 | <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> 6 | <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> 7 | <ProjectGuid>{BD0427C4-BA2A-48F8-8329-11574735F961}</ProjectGuid> 8 | <OutputType>Library</OutputType> 9 | <AppDesignerFolder>Properties</AppDesignerFolder> 10 | <RootNamespace>Less.API.NetFramework.KakaoTalkAPI</RootNamespace> 11 | <AssemblyName>KakaoTalkAPI</AssemblyName> 12 | <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> 13 | <FileAlignment>512</FileAlignment> 14 | <Deterministic>true</Deterministic> 15 | <TargetFrameworkProfile /> 16 | </PropertyGroup> 17 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 18 | <DebugSymbols>true</DebugSymbols> 19 | <DebugType>full</DebugType> 20 | <Optimize>false</Optimize> 21 | <OutputPath>bin\Debug\</OutputPath> 22 | <DefineConstants>DEBUG;TRACE</DefineConstants> 23 | <ErrorReport>prompt</ErrorReport> 24 | <WarningLevel>4</WarningLevel> 25 | </PropertyGroup> 26 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> 27 | <DebugType>pdbonly</DebugType> 28 | <Optimize>true</Optimize> 29 | <OutputPath>bin\Release\</OutputPath> 30 | <DefineConstants>TRACE</DefineConstants> 31 | <ErrorReport>prompt</ErrorReport> 32 | <WarningLevel>4</WarningLevel> 33 | </PropertyGroup> 34 | <ItemGroup> 35 | <Reference Include="System" /> 36 | <Reference Include="System.Core" /> 37 | <Reference Include="System.Drawing" /> 38 | <Reference Include="System.Xml.Linq" /> 39 | <Reference Include="System.Data.DataSetExtensions" /> 40 | <Reference Include="Microsoft.CSharp" /> 41 | <Reference Include="System.Data" /> 42 | <Reference Include="System.Net.Http" /> 43 | <Reference Include="System.Xml" /> 44 | </ItemGroup> 45 | <ItemGroup> 46 | <Compile Include="ClipboardManager.cs" /> 47 | <Compile Include="KakaoTalk.cs" /> 48 | <Compile Include="Properties\AssemblyInfo.cs" /> 49 | <Compile Include="WinAPI.cs" /> 50 | </ItemGroup> 51 | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> 52 | </Project> -------------------------------------------------------------------------------- /KakaoTalkAPI/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("KakaoTalkAPI")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("KakaoTalkAPI")] 13 | [assembly: AssemblyCopyright("Copyright © 2018 Less")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("bd0427c4-ba2a-48f8-8329-11574735f961")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.3.0")] 36 | [assembly: AssemblyFileVersion("1.0.3.0")] 37 | -------------------------------------------------------------------------------- /KakaoTalkAPI/WinAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace Less.API.NetFramework.WindowsAPI 7 | { 8 | /// <summary> 9 | /// Windows API의 메서드 목록을 담고 있는 클래스입니다. 10 | /// </summary> 11 | public sealed class WinAPI 12 | { 13 | // hWnd 관련 상수값 목록 14 | public const int GW_HWNDFIRST = 0; 15 | public const int GW_HWNDLAST = 1; 16 | public const int GW_HWNDNEXT = 2; 17 | public const int GW_HWNDPREV = 3; 18 | public const int GW_OWNER = 4; 19 | public const int GW_CHILD = 5; 20 | 21 | // Send / Post Message 문자열 처리 22 | public const int WM_SETTEXT = 0xC; 23 | public const int WM_GETTEXT = 0xD; 24 | public const int WM_GETTEXTLENGTH = 0xE; 25 | 26 | // Key Press 27 | public const int WM_KEYDOWN = 0x100; 28 | public const int WM_KEYUP = 0x101; 29 | 30 | // Command 31 | public const int WM_COMMAND = 0x0111; 32 | 33 | // Mouse Click 34 | public const int WM_LBUTTONDOWN = 0x201; 35 | public const int WM_LBUTTONUP = 0x202; 36 | public const int WM_LBUTTONDBLCLK = 0x203; 37 | 38 | public const int WM_RBUTTONDOWN = 0x204; 39 | public const int WM_RBUTTONUP = 0x205; 40 | public const int WM_RBUTTONDBLCLK = 0x206; 41 | 42 | public const int WM_MBUTTONDOWN = 0x207; 43 | public const int WM_MBUTTONUP = 0x208; 44 | public const int WM_MBUTTONDBLCLK = 0x209; 45 | 46 | public const int HTCLIENT = 1; 47 | public const int WM_SETCURSOR = 0x20; 48 | public const int WM_NCHITTEST = 0x84; 49 | public const int WM_MOUSEMOVE = 0x200; 50 | 51 | // GDI 52 | public const int SRCCOPY = 0xCC0020; 53 | 54 | // Clipboard 55 | public const int CF_TEXT = 1; 56 | public const int CF_BITMAP = 2; 57 | public const int CF_DIB = 8; 58 | public const int CF_UNICODETEXT = 13; 59 | 60 | // Hooking 61 | public const int WH_GETMESSAGE = 3; 62 | public const int WH_CALLWNDPROC = 4; 63 | public const int WH_KEYBOARD_LL = 13; 64 | 65 | // ShowWindow 66 | public const int SW_MINIMIZE = 6; 67 | public const int SW_RESTORE = 9; 68 | 69 | [DllImport("User32.dll")] 70 | public static extern IntPtr FindWindow(string className, string wndTitle); 71 | 72 | [DllImport("User32.dll")] 73 | public static extern IntPtr GetWindow(IntPtr hwnd, int uCmd); 74 | 75 | [DllImport("User32.dll")] 76 | public static extern bool ClientToScreen(IntPtr hwnd, ref Point lpPoint); 77 | 78 | [DllImport("User32.dll")] 79 | public static extern bool SetForegroundWindow(IntPtr hWnd); 80 | 81 | // 메시지 핸들링 82 | [DllImport("User32.dll")] 83 | public static extern int SendMessage(IntPtr hwnd, uint msg, int wParam, int lParam); 84 | 85 | [DllImport("User32.dll")] 86 | public static extern int PostMessage(IntPtr hwnd, uint msg, int wParam, int lParam); 87 | 88 | [DllImport("User32.dll", CharSet = CharSet.Ansi)] 89 | public static extern int SendMessageA(IntPtr hWnd, uint msg, int wParam, string lParam); 90 | 91 | [DllImport("User32.dll", CharSet = CharSet.Unicode)] 92 | public static extern int SendMessageW(IntPtr hWnd, uint msg, int wParam, string lParam); 93 | 94 | [DllImport("User32.dll", EntryPoint = "SendMessage")] 95 | public static extern int SendMessageGetTextLen(IntPtr hWnd, uint msg, int wParam, int lParam); 96 | 97 | [DllImport("User32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Unicode)] 98 | public static extern IntPtr SendMessageGetTextW(IntPtr hWnd, uint msg, int wParam, [Out] StringBuilder lParam); 99 | 100 | // GDI 함수 101 | [DllImport("User32.dll")] 102 | public static extern IntPtr GetWindowDC(IntPtr hWnd); 103 | 104 | [DllImport("Gdi32.dll")] 105 | public static extern IntPtr CreateCompatibleDC(IntPtr hDC); 106 | 107 | [DllImport("Gdi32.dll")] 108 | public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int width, int height); 109 | 110 | [DllImport("Gdi32.dll")] 111 | public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hGdiObject); 112 | 113 | [DllImport("Gdi32.dll")] 114 | public static extern bool DeleteObject(IntPtr hObject); 115 | 116 | [DllImport("Gdi32.dll")] 117 | public static extern bool BitBlt(IntPtr hDestDC, int destX, int destY, int width, int height, IntPtr hSourceDC, int sourceX, int sourceY, int rasterOperationType); 118 | 119 | [DllImport("Gdi32.dll")] 120 | public static extern bool DeleteDC(IntPtr hDC); 121 | 122 | // 클립보드 처리 123 | [DllImport("User32.dll")] 124 | public static extern bool OpenClipboard(IntPtr hNewOwner); 125 | 126 | [DllImport("User32.dll")] 127 | public static extern bool EmptyClipboard(); 128 | 129 | [DllImport("User32.dll")] 130 | public static extern IntPtr SetClipboardData(uint format, IntPtr hMemory); 131 | 132 | [DllImport("User32.dll")] 133 | public static extern IntPtr GetClipboardData(uint format); 134 | 135 | [DllImport("User32.dll")] 136 | public static extern uint EnumClipboardFormats(uint format); 137 | 138 | [DllImport("User32.dll")] 139 | public static extern bool CloseClipboard(); 140 | 141 | // 창 다루기 142 | [DllImport("User32.dll")] 143 | public static extern IntPtr GetForegroundWindow(); 144 | 145 | [DllImport("User32.dll")] 146 | public static extern int GetWindowText(IntPtr hWnd, StringBuilder buffer, int maxLength); 147 | 148 | [DllImport("User32.dll")] 149 | public static extern int GetClassName(IntPtr hWnd, StringBuilder buffer, int maxLength); 150 | 151 | [DllImport("User32.dll")] 152 | public static extern bool GetWindowInfo(IntPtr hWnd, ref WINDOWINFO windowInfo); 153 | 154 | [DllImport("User32.dll")] 155 | public static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); 156 | 157 | public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); 158 | 159 | [DllImport("User32.dll")] 160 | public static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect); 161 | 162 | [DllImport("User32.dll")] 163 | public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint); 164 | 165 | [DllImport("User32.dll")] 166 | public static extern bool IsIconic(IntPtr hWnd); 167 | 168 | [DllImport("User32.dll")] 169 | public static extern bool ShowWindow(IntPtr hWnd, int command); 170 | 171 | [DllImport("User32.dll")] 172 | public static extern bool BringWindowToTop(IntPtr hWnd); 173 | 174 | // 커스텀 API 목록 175 | 176 | // 커스텀 상수 177 | public enum Encoding { Ansi = 1, Unicode } 178 | 179 | public static string GetEditText(IntPtr hWnd) 180 | { 181 | int capacity = SendMessage(hWnd, WM_GETTEXTLENGTH, 0, 0); 182 | var buffer = new StringBuilder(capacity); 183 | SendMessageGetTextW(hWnd, WM_GETTEXT, capacity + 1, buffer); 184 | 185 | return buffer.ToString(); 186 | } 187 | 188 | public static void SetEditText(IntPtr hWnd, string text, Encoding encoding) 189 | { 190 | if (encoding == Encoding.Ansi) SendMessageA(hWnd, WM_SETTEXT, 0, text); 191 | else if (encoding == Encoding.Unicode) SendMessageW(hWnd, WM_SETTEXT, 0, text); 192 | } 193 | 194 | public static void PressKeyInBackground(IntPtr hWnd, int keyCode) 195 | { 196 | PostMessage(hWnd, WM_KEYDOWN, keyCode, 0x1); 197 | PostMessage(hWnd, WM_KEYUP, keyCode, (int)(0x100000000 - 0xC0000001)); 198 | } 199 | 200 | public static void ClickInBackground(IntPtr hWnd, MouseButton button, short x, short y) 201 | { 202 | int message1, message2; 203 | 204 | if (button == MouseButton.Left) 205 | { 206 | message1 = WM_LBUTTONDOWN; 207 | message2 = WM_LBUTTONUP; 208 | } 209 | else if (button == MouseButton.Right) 210 | { 211 | message1 = WM_RBUTTONDOWN; 212 | message2 = WM_RBUTTONUP; 213 | } 214 | else if (button == MouseButton.Middle) 215 | { 216 | message1 = WM_MBUTTONDOWN; 217 | message2 = WM_MBUTTONUP; 218 | } 219 | else throw new NoSuchButtonException(); 220 | 221 | PostMessage(hWnd, (uint)message1, 0, (y * 0x10000) | (x & 0xFFFF)); 222 | PostMessage(hWnd, (uint)message2, 0, (y * 0x10000) | (x & 0xFFFF)); 223 | } 224 | 225 | public static void DoubleClickInBackground(IntPtr hWnd, MouseButton button, short x, short y) 226 | { 227 | int message; 228 | 229 | if (button == MouseButton.Left) message = WM_LBUTTONDBLCLK; 230 | else if (button == MouseButton.Right) message = WM_RBUTTONDBLCLK; 231 | else if (button == MouseButton.Middle) message = WM_MBUTTONDBLCLK; 232 | else throw new NoSuchButtonException(); 233 | 234 | PostMessage(hWnd, (uint)message, 0, (y * 0x10000) | (x & 0xFFFF)); 235 | } 236 | 237 | public static IntPtr GetFirstHwndWithIdentifiers(string className, string caption) 238 | { 239 | IntPtr hWndNew = IntPtr.Zero; 240 | 241 | EnumWindows(delegate (IntPtr hWnd, IntPtr lParam) 242 | { 243 | if (className == null || GetClassName(hWnd) == className) 244 | { 245 | if (caption == null || GetWindowText(hWnd) == caption) 246 | { 247 | hWndNew = hWnd; 248 | return false; 249 | } 250 | } 251 | 252 | return true; 253 | }, IntPtr.Zero); 254 | 255 | return hWndNew; 256 | } 257 | 258 | public static List<IntPtr> GetHwndListWithIdentifiers(string className, string caption) 259 | { 260 | List<IntPtr> hWndList = new List<IntPtr>(); 261 | 262 | EnumWindows(delegate (IntPtr hWnd, IntPtr lParam) 263 | { 264 | if (className == null || GetClassName(hWnd) == className) 265 | { 266 | if (caption == null || GetWindowText(hWnd) == caption) hWndList.Add(hWnd); 267 | } 268 | 269 | return true; 270 | }, IntPtr.Zero); 271 | 272 | return hWndList; 273 | } 274 | 275 | public static string GetWindowInfo(IntPtr hWnd) 276 | { 277 | int resultBufferCapacity = 512; 278 | int tempBufferCapacity = 256; 279 | StringBuilder resultBuffer = new StringBuilder(resultBufferCapacity); 280 | 281 | resultBuffer.Append("hWnd : 0x" + hWnd.ToString("X") + "\n"); 282 | 283 | StringBuilder tempBuffer = new StringBuilder(tempBufferCapacity); 284 | int length = GetWindowText(hWnd, tempBuffer, tempBufferCapacity); 285 | resultBuffer.Append("Caption : " + tempBuffer.ToString() + "\n"); 286 | resultBuffer.Append("Caption Length : " + length + "\n"); 287 | 288 | tempBuffer.Clear(); 289 | length = GetClassName(hWnd, tempBuffer, tempBufferCapacity); 290 | resultBuffer.Append("Class : " + tempBuffer.ToString() + "\n"); 291 | resultBuffer.Append("Class Length : " + length + "\n"); 292 | resultBuffer.Append("\n"); 293 | 294 | var windowInfo = new WINDOWINFO(); 295 | windowInfo.cbSize = (uint)Marshal.SizeOf(windowInfo); 296 | GetWindowInfo(hWnd, ref windowInfo); 297 | 298 | resultBuffer.Append("rcWindow.Left : " + windowInfo.rcWindow.left + "\n"); 299 | resultBuffer.Append("rcWindow.Top : " + windowInfo.rcWindow.top + "\n"); 300 | resultBuffer.Append("rcWindow.Right : " + windowInfo.rcWindow.right + "\n"); 301 | resultBuffer.Append("rcWindow.Bottom : " + windowInfo.rcWindow.bottom + "\n"); 302 | resultBuffer.Append("\n"); 303 | 304 | resultBuffer.Append("rcClient.Left : " + windowInfo.rcClient.left + "\n"); 305 | resultBuffer.Append("rcClient.Top : " + windowInfo.rcClient.top + "\n"); 306 | resultBuffer.Append("rcClient.Right : " + windowInfo.rcClient.right + "\n"); 307 | resultBuffer.Append("rcClient.Bottom : " + windowInfo.rcClient.bottom + "\n"); 308 | resultBuffer.Append("\n"); 309 | 310 | return resultBuffer.ToString(); 311 | } 312 | 313 | public static RECT GetWindowRect(IntPtr hWnd) 314 | { 315 | var rect = new RECT(); 316 | GetWindowRect(hWnd, ref rect); 317 | 318 | return rect; 319 | } 320 | 321 | public static string GetClassName(IntPtr hWnd) 322 | { 323 | int capacity = 256; 324 | StringBuilder buffer = new StringBuilder(capacity); 325 | GetClassName(hWnd, buffer, capacity); 326 | 327 | return buffer.ToString(); 328 | } 329 | 330 | public static string GetWindowText(IntPtr hWnd) 331 | { 332 | int capacity = 256; 333 | StringBuilder buffer = new StringBuilder(capacity); 334 | GetWindowText(hWnd, buffer, capacity); 335 | 336 | return buffer.ToString(); 337 | } 338 | 339 | public static void ResizeWindow(IntPtr hWnd, int width, int height) 340 | { 341 | RECT rect = GetWindowRect(hWnd); 342 | MoveWindow(hWnd, rect.left, rect.top, width, height, true); 343 | } 344 | 345 | public static void MoveWindow(IntPtr hWnd, int x, int y) 346 | { 347 | RECT rect = GetWindowRect(hWnd); 348 | int width = rect.right - rect.left; 349 | int height = rect.bottom - rect.top; 350 | MoveWindow(hWnd, x, y, width, height, false); 351 | } 352 | 353 | public struct KeyCode 354 | { 355 | public static int VK_LBUTTON = 0x01; // Left Mouse Button 356 | public static int VK_RBUTTON = 0x02; // Right Mouse Button 357 | public static int VK_MBUTTON = 0x04; // Middle Mouse Button 358 | public static int VK_TAB = 0x09; 359 | public static int VK_RETURN = 0x0D; // == Enter Key 360 | public static int VK_ENTER = VK_RETURN; 361 | public static int VK_SHIFT = 0x10; 362 | public static int VK_CONTROL = 0x11; 363 | public static int VK_MENU = 0x12; // Alt Key 364 | public static int VK_CAPITAL = 0x14; // Caps Lock Key 365 | public static int VK_ESCAPE = 0x1B; // == ESC key 366 | public static int VK_ESC = VK_ESCAPE; 367 | public static int VK_LEFT = 0x25; 368 | public static int VK_UP = 0x26; 369 | public static int VK_RIGHT = 0x27; 370 | public static int VK_DOWN = 0x28; 371 | } 372 | 373 | public enum MouseButton { Left = 1, Right, Middle } 374 | 375 | public class NoSuchButtonException : Exception 376 | { 377 | internal NoSuchButtonException() : base("존재하지 않는 버튼 값입니다.") { } 378 | } 379 | } 380 | 381 | /// <summary> 382 | /// x,y 좌표값에 대한 정보를 갖는 구조체입니다. 383 | /// </summary> 384 | public struct Point 385 | { 386 | public int x; 387 | public int y; 388 | 389 | public Point(int x, int y) 390 | { 391 | this.x = x; 392 | this.y = y; 393 | } 394 | } 395 | 396 | /// <summary> 397 | /// 상,하,좌,우 좌표값에 대한 정보를 갖는 구조체입니다. 398 | /// </summary> 399 | public struct RECT 400 | { 401 | public int left; 402 | public int top; 403 | public int right; 404 | public int bottom; 405 | } 406 | 407 | /// <summary> 408 | /// 창에 대한 정보를 갖는 구조체입니다. 409 | /// </summary> 410 | public struct WINDOWINFO 411 | { 412 | public uint cbSize; 413 | public RECT rcWindow; 414 | public RECT rcClient; 415 | public uint dwStyle; 416 | public uint dwExStyle; 417 | public uint dwWindowStatus; 418 | public uint cxWindowBorders; 419 | public uint cyWindowBorders; 420 | public ushort atomWindowType; 421 | public ushort wCreatorVersion; 422 | } 423 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Non-Profit Open Software License 3.0 (NPOSL-3.0) 2 | 3 | This Non-Profit Open Software License ("Non-Profit OSL") version 3.0 (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: 4 | 5 | Licensed under the Non-Profit Open Software License version 3.0 6 | 7 | 1) **Grant of Copyright License.** Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 8 | 9 | a) to reproduce the Original Work in copies, either alone or as part of a collective work; 10 | 11 | b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 12 | 13 | c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Non-Profit Open Software License or as provided in section 17(d); 14 | 15 | d) to perform the Original Work publicly; and 16 | 17 | e) to display the Original Work publicly. 18 | 19 | 2) **Grant of Patent License.** Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 20 | 21 | 3) **Grant of Source Code License.** The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 22 | 23 | 4) **Exclusions From License Grant.** Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 24 | 25 | 5) **External Deployment.** The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 26 | 27 | 6) **Attribution Rights.** You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 28 | 29 | 7) **Warranty of Provenance and Disclaimer of Warranty.** The Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 30 | 31 | 8) **Limitation of Liability.** Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 32 | 33 | 9) **Acceptance and Termination.** If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 34 | 35 | 10) **Termination for Patent Action.** This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 36 | 37 | 11) **Jurisdiction, Venue and Governing Law.** Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 38 | 39 | 12) **Attorneys' Fees.** In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 40 | 41 | 13) **Miscellaneous.** If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 42 | 43 | 14) **Definition of "You" in This License.** "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 44 | 45 | 15) **Right to Use.** You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 46 | 47 | 16) **Modification of This License.** This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. 48 | 49 | 17) **Non-Profit Amendment.** The name of this amended version of the Open Software License ("OSL 3.0") is "Non-Profit Open Software License 3.0". The original OSL 3.0 license has been amended as follows: 50 | 51 | (a) Licensor represents and declares that it is a not-for-profit organization that derives no revenue whatsoever from the distribution of the Original Work or Derivative Works thereof, or from support or services relating thereto. 52 | 53 | (b) The first sentence of Section 7 ["Warranty of Provenance"] of OSL 3.0 has been stricken. For Original Works licensed under this Non-Profit OSL 3.0, LICENSOR OFFERS NO WARRANTIES WHATSOEVER. 54 | 55 | (c) In the first sentence of Section 8 ["Limitation of Liability"] of this Non-Profit OSL 3.0, the list of damages for which LIABILITY IS LIMITED now includes "direct" damages. 56 | 57 | (d) The proviso in Section 1(c) of this License now refers to this "Non-Profit Open Software License" rather than the "Open Software License". You may distribute or communicate the Original Work or Derivative Works thereof under this Non-Profit OSL 3.0 license only if You make the representation and declaration in paragraph (a) of this Section 17. Otherwise, You shall distribute or communicate the Original Work or Derivative Works thereof only under the OSL 3.0 license and You shall publish clear licensing notices so stating. Also by way of clarification, this License does not authorize You to distribute or communicate works under this Non-Profit OSL 3.0 if You received them under the original OSL 3.0 license. 58 | 59 | (e) Original Works licensed under this license shall reference "Non-Profit OSL 3.0" in licensing notices to distinguish them from works licensed under the original OSL 3.0 license. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Less.API.NetFramework.KakaoBotAPI 2 | An advanced application of the previous publish, "KakaoTalk API". <br/> 3 | -> 기존에 출시했던 "KakaoTalk API"의 응용 버전입니다. 4 | 5 | @ IMPORTANT : PLEASE MIGRATE TO VERSION 1.1.0 OR HIGHER. THIS PATCH INCLUDES COUPLE OF BUG FIXES AND MAJOR CHANGES. <br/> 6 | -> 1.1.0 이상 버전으로 migrate해주시기 바랍니다. 이 패치에는 다수의 버그 수정 및 중요한 변경 사항이 포함되어 있습니다. 7 | 8 | @ Author : Eric Kim 9 | 10 | @ Nickname : Less, Syusa 11 | 12 | @ Email : syusa5537@gmail.com 13 | 14 | @ ProductName : Less.API.NetFramework.KakaoBotAPI 15 | 16 | @ Version : 1.1.0 17 | 18 | @ License : The Non-Profit Open Software License v3.0 (NPOSL-3.0) (https://opensource.org/licenses/NPOSL-3.0) <br/> 19 | - -> 이 API에는 NPOSL-3.0 오픈소스 라이선스가 적용되며, 사용자는 절대 영리적 목적으로 이 API를 사용해서는 안 됩니다. 20 | 21 | @ Other Legal Responsibilities : <br/> 22 | - Developers using this automation API should never try to harm or damage servers of "Kakao Corp." by any kinds of approaches. <br/> 23 | - -> 이 자동화 API를 이용하는 개발자들은 절대 어떠한 방법으로도 카카오 회사의 서버에 피해를 입히려는 시도를 해서는 안 됩니다. <br/> 24 | - Developers using this automation API should never try to take any undesired actions which are opposite to the Kakao Terms of Service (http://www.kakao.com/policy/terms?type=ts). <br/> 25 | - -> 이 자동화 API를 이용하는 개발자들은 절대 카카오 서비스 약관 (http://www.kakao.com/policy/terms?type=ts) 에 반하는 바람직하지 않은 행동들을 취해서는 안 됩니다. 26 | 27 | # Version History 28 | @ 1.1.0 (2019-09-20, Lastest) <br/> 29 | - File paths have been generally modified. (파일 경로 대부분 변경) <br/> 30 | - Message and image sending strategy has been modified for QuizBot class. (QuizBot 클래스의 메시지 및 이미지 전송 전략 변경) <br/> 31 | - Fixed a problem of QuizBot not being ordinarily terminated when Stop method has been called while quiz is playing. (퀴즈 진행 도중 바로 봇 종료 메서드를 호출할 경우 정상적으로 종료되지 않던 문제 수정) <br/> 32 | - Added quiz data resources for SampleApplication project. (SampleApplication 프로젝트용 퀴즈 데이터 리소스 추가) <br/> 33 | - KakaoTalkAPI version update. (KakaoTalkAPI 버전 업데이트) <br/> 34 | - Other minor improvements applied. (그 밖에 소규모 개선 사항들 적용) <br/><br/> 35 | 36 | @ 1.0.2 (2019-08-26, Lastest) <br/> 37 | - Stability update. (안정성 업데이트) <br/><br/> 38 | 39 | @ 1.0.1 (2019-08-10) <br/> 40 | - Stability update. (안정성 업데이트) <br/> 41 | - Random quiz algorithm update. (랜덤 퀴즈 알고리즘 개선) <br/><br/> 42 | 43 | @ 1.0.0 (2019-08-08) <br/> 44 | - ChatBot and QuizBot class is now finished (ChatBot 및 QuizBot 클래스가 완성되었습니다.) <br/> 45 | - All Properties and Methods now have comments. (모든 속성과 메서드에 주석 처리 완료하였습니다.) <br/><br/> 46 | 47 | @ 0.1.0 (2019-07-23) <br/> 48 | - Initial publish of KakaoBot API. <br/> 49 | - -> 카카오봇 API 최초 공개. 50 | -------------------------------------------------------------------------------- /SampleApplication/App.config: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <configuration> 3 | <startup> 4 | <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> 5 | </startup> 6 | </configuration> 7 | -------------------------------------------------------------------------------- /SampleApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using Less.API.NetFramework.KakaoBotAPI.Bot; 2 | 3 | namespace SampleApplication 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | string roomName = "나의 채팅방"; // 여기에 채팅방 이름을 입력합니다. 10 | var roomType = ChatBot.TargetTypeOption.Group; // 여기에 채팅방의 유형을 입력합니다. (Self : 본인과의 1:1 채팅방, Friend : 친구와의 1:1 채팅방, Group : 단톡방) 11 | string botRunnerName = "나의 닉네임"; // 여기에 당신의 해당 채팅방에서의 닉네임을 입력합니다. 오픈채팅의 경우, 당신의 닉네임이 방마다 다를 수 있으니 실수하지 않도록 주의하세요. 12 | string identifier = "MyQuiz"; // 여기에 이 봇을 구분하기 위한 특별한 식별자를 입력합니다. 파일 이름 및 봇을 식별하기 위한 여러 장소에서 사용되는 값입니다. 13 | RunSampleChatBot(roomName, roomType, botRunnerName, identifier); 14 | // RunSampleQuizBot(roomName, roomType, botRunnerName, identifier); 15 | } 16 | 17 | static void RunSampleChatBot(string roomName, ChatBot.TargetTypeOption roomType, string botRunnerName, string identifier) 18 | { 19 | var bot = new SampleChatBot(roomName, roomType, botRunnerName, identifier); 20 | bot.Start(); 21 | } 22 | 23 | static void RunSampleQuizBot(string roomName, ChatBot.TargetTypeOption roomType, string botRunnerName, string identifier) 24 | { 25 | var bot = new SampleQuizBot(roomName, roomType, botRunnerName, identifier); 26 | bot.Start(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SampleApplication/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SampleApplication")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SampleApplication")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("aa6bf2c8-5537-4e32-89e7-d6a3608b1166")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.1.0")] 36 | [assembly: AssemblyFileVersion("1.0.1.0")] 37 | -------------------------------------------------------------------------------- /SampleApplication/SampleApplication.csproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> 4 | <PropertyGroup> 5 | <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> 6 | <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> 7 | <ProjectGuid>{AA6BF2C8-5537-4E32-89E7-D6A3608B1166}</ProjectGuid> 8 | <OutputType>Exe</OutputType> 9 | <RootNamespace>SampleApplication</RootNamespace> 10 | <AssemblyName>SampleApplication</AssemblyName> 11 | <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> 12 | <FileAlignment>512</FileAlignment> 13 | <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> 14 | <Deterministic>true</Deterministic> 15 | <TargetFrameworkProfile /> 16 | </PropertyGroup> 17 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 18 | <PlatformTarget>AnyCPU</PlatformTarget> 19 | <DebugSymbols>true</DebugSymbols> 20 | <DebugType>full</DebugType> 21 | <Optimize>false</Optimize> 22 | <OutputPath>bin\Debug\</OutputPath> 23 | <DefineConstants>DEBUG;TRACE</DefineConstants> 24 | <ErrorReport>prompt</ErrorReport> 25 | <WarningLevel>4</WarningLevel> 26 | </PropertyGroup> 27 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> 28 | <PlatformTarget>AnyCPU</PlatformTarget> 29 | <DebugType>pdbonly</DebugType> 30 | <Optimize>true</Optimize> 31 | <OutputPath>bin\Release\</OutputPath> 32 | <DefineConstants>TRACE</DefineConstants> 33 | <ErrorReport>prompt</ErrorReport> 34 | <WarningLevel>4</WarningLevel> 35 | </PropertyGroup> 36 | <ItemGroup> 37 | <Reference Include="System" /> 38 | <Reference Include="System.Core" /> 39 | <Reference Include="System.Xml.Linq" /> 40 | <Reference Include="System.Data.DataSetExtensions" /> 41 | <Reference Include="Microsoft.CSharp" /> 42 | <Reference Include="System.Data" /> 43 | <Reference Include="System.Net.Http" /> 44 | <Reference Include="System.Xml" /> 45 | </ItemGroup> 46 | <ItemGroup> 47 | <Compile Include="SampleChatBot.cs" /> 48 | <Compile Include="Program.cs" /> 49 | <Compile Include="Properties\AssemblyInfo.cs" /> 50 | <Compile Include="SampleQuizBot.cs" /> 51 | </ItemGroup> 52 | <ItemGroup> 53 | <None Include="App.config" /> 54 | </ItemGroup> 55 | <ItemGroup> 56 | <ProjectReference Include="..\KakaoBotAPI\KakaoBotAPI.csproj"> 57 | <Project>{564ea995-d572-4a10-a312-e0bc12e66424}</Project> 58 | <Name>KakaoBotAPI</Name> 59 | </ProjectReference> 60 | <ProjectReference Include="..\KakaoTalkAPI\KakaoTalkAPI.csproj"> 61 | <Project>{bd0427c4-ba2a-48f8-8329-11574735f961}</Project> 62 | <Name>KakaoTalkAPI</Name> 63 | </ProjectReference> 64 | </ItemGroup> 65 | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> 66 | </Project> -------------------------------------------------------------------------------- /SampleApplication/SampleChatBot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Less.API.NetFramework.KakaoBotAPI.Bot; 3 | 4 | namespace SampleApplication 5 | { 6 | class SampleChatBot : ChatBot 7 | { 8 | public SampleChatBot(string roomName, TargetTypeOption type, string botRunnerName, string identifier) : base(roomName, type, botRunnerName, identifier) {} 9 | 10 | protected override string GetDateChangeNotice(string content, DateTime sendTime) 11 | { 12 | string notice = content; 13 | Console.WriteLine(notice); 14 | return content; 15 | } 16 | 17 | protected override string GetStartNotice() 18 | { 19 | string notice = $"채팅봇이 시작되었습니다. 방 이름은 {RoomName}입니다."; 20 | Console.WriteLine(notice); 21 | return notice; 22 | } 23 | 24 | protected override string GetStopNotice() 25 | { 26 | string notice = $"채팅봇을 종료합니다."; 27 | Console.WriteLine(notice); 28 | return notice; 29 | } 30 | 31 | protected override string GetUserJoinNotice(string userName, DateTime sendTime) 32 | { 33 | string notice = $"{userName}님께서 들어오셨습니다."; 34 | Console.WriteLine(notice); 35 | return notice; 36 | } 37 | 38 | protected override string GetUserLeaveNotice(string userName, DateTime sendTime) 39 | { 40 | string notice = $"{userName}님께서 나가셨습니다."; 41 | Console.WriteLine(notice); 42 | return notice; 43 | } 44 | 45 | protected override void InitializeBotSettings() { 46 | // 여기에 초기화 문장들을 작성합니다. 47 | } 48 | 49 | protected override void ParseMessage(string userName, string content, DateTime sendTime) 50 | { 51 | Console.WriteLine($"유저 이름 : {userName}, 내용 : {content}, 전송 시각 : {sendTime.ToString()}"); 52 | 53 | string[] words = content.Split(' '); 54 | 55 | if (words[0].Equals("!종료")) this.Stop(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SampleApplication/SampleQuizBot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading; 4 | using Less.API.NetFramework.KakaoBotAPI.Bot; 5 | using Less.API.NetFramework.KakaoBotAPI.Model; 6 | 7 | namespace SampleApplication 8 | { 9 | class SampleQuizBot : QuizBot 10 | { 11 | const string Version = "v1.0.0"; 12 | const string AlertHeader = "[알림]"; 13 | 14 | const string RoomNameAlias = "퀴즈방"; 15 | 16 | public SampleQuizBot(string roomName, TargetTypeOption type, string botRunnerName, string identifier) : base(roomName, type, botRunnerName, identifier) {} 17 | 18 | protected override string GetDateChangeNotice(string content, DateTime sendTime) 19 | { 20 | string notice = content; 21 | Console.WriteLine(notice); 22 | return notice; 23 | } 24 | 25 | protected override string GetStartNotice() 26 | { 27 | string notice = $"{AlertHeader}퀴즈봇 {Version} 버전이 실행되었습니다.\n"; 28 | notice += "!명령어 를 입력하여 기능을 확인하세요."; 29 | Console.WriteLine(notice); 30 | return notice; 31 | } 32 | 33 | protected override string GetStopNotice() 34 | { 35 | string notice = $"{AlertHeader}퀴즈봇을 종료합니다."; 36 | Console.WriteLine(notice); 37 | return notice; 38 | } 39 | 40 | protected override string GetUserJoinNotice(string userName, DateTime sendTime) 41 | { 42 | string notice = $"{AlertHeader}반갑습니다, {userName}님. {Window.RoomName}에 오신 것을 환영합니다.\n"; 43 | Console.WriteLine(notice); 44 | return notice; 45 | } 46 | 47 | protected override string GetUserLeaveNotice(string userName, DateTime sendTime) 48 | { 49 | string notice = $"{AlertHeader}{userName} 님이 퇴장하셨습니다."; 50 | Console.WriteLine(notice); 51 | return notice; 52 | } 53 | 54 | protected override void InitializeBotSettings() 55 | { 56 | base.InitializeBotSettings(); // QuizBot 객체에서 퀴즈 목록 갱신 작업을 진행하므로, 반드시 base 메서드를 호출해주어야 합니다. 57 | 58 | // 여기에 초기화 문장들을 작성합니다. 59 | } 60 | 61 | protected override void ParseMessage(string userName, string content, DateTime sendTime) 62 | { 63 | Console.WriteLine($"유저 이름 : {userName}, 내용 : {content}, 전송 시각 : {sendTime.ToString()}"); 64 | 65 | QuizUser user = FindUserByNickname(userName); 66 | 67 | switch (content) 68 | { 69 | case "!명령어": OnRequestingCommands(); break; 70 | case "!정보": OnRequestingInfo(user); break; 71 | case "!퀴즈 실행": OnRequestingQuizStart(); break; 72 | case "!퀴즈 중지": OnRequestingQuizStop(); break; 73 | case "!종료": OnRequestingQuit(); break; 74 | } 75 | } 76 | 77 | private void OnRequestingCommands() 78 | { 79 | string message = $"[명령어]\n"; 80 | message += "!정보 : 유저 정보를 확인합니다.\n"; 81 | message += "!퀴즈 실행 : 퀴즈를 실행합니다.\n"; 82 | message += "!퀴즈 중지 : 퀴즈를 중지합니다.\n"; 83 | message += "!종료 : 퀴즈봇을 종료합니다."; 84 | 85 | SendMessage(message); 86 | Thread.Sleep(SendMessageInterval); 87 | } 88 | 89 | private void OnRequestingInfo(QuizUser user) 90 | { 91 | StringBuilder availableTitles = new StringBuilder(); 92 | foreach (Title title in user.AvailableTitles) availableTitles.Append($"{title.Name}, "); 93 | availableTitles.Remove(availableTitles.Length - 2, 2); 94 | 95 | string message = $"[유저 정보 - {RoomNameAlias}]\n"; 96 | message += $"닉네임 : {user.Nickname}\n"; 97 | message += $"레벨 : {user.Level}\n"; 98 | message += $"경험치 : {user.Experience}\n"; 99 | message += $"머니 : {user.Money}\n"; 100 | message += $"세대 : {user.Generation}세대\n"; 101 | message += $"타이틀 : {user.CurrentTitle.Name}"; 102 | 103 | SendMessage(message); 104 | Thread.Sleep(SendMessageInterval); 105 | } 106 | 107 | private void OnRequestingQuizStart() 108 | { 109 | SendMessage("속담 퀴즈를 시작합니다."); 110 | Thread.Sleep(SendMessageInterval); 111 | 112 | var quizType = Quiz.TypeOption.General; // 퀴즈의 유형 (일반 퀴즈) 113 | string[] subjects = new string[] { "속담" }; // 퀴즈 주제 목록 114 | int requestQuizCount = 5; // 요청할 퀴즈의 총 개수 115 | int minQuizCount = 3; // 최소 퀴즈 개수 (만약 데이터 파일에 넣어둔 문제 수가 이 값보다 작으면, 퀴즈가 실행되지 않음) 116 | int quizTimeLimit = 20; // 퀴즈 문제 하나당 풀이 제한시간 (초 단위) 117 | int bonusExperience = 10; // 퀴즈 정답 시 추가로 받는 경험치 118 | int bonusMoney = 50; // 퀴즈 정답 시 추가로 받는 머니 119 | int idleTimeLimit = 180; // 잠수일 경우, 퀴즈가 자동으로 중단되기까지 걸리는 시간 (초 단위) 120 | bool showSubject = true; // 퀴즈 출제 시 주제 표시 여부 121 | 122 | StartQuiz(quizType, subjects, requestQuizCount, minQuizCount, quizTimeLimit, bonusExperience, bonusMoney, idleTimeLimit, showSubject); 123 | } 124 | 125 | private void OnRequestingQuizStop() 126 | { 127 | SendMessage("퀴즈를 중지합니다."); 128 | Thread.Sleep(SendMessageInterval); 129 | 130 | StopQuiz(); 131 | } 132 | 133 | private void OnRequestingQuit() 134 | { 135 | SendMessage("종료를 요청하셨습니다."); 136 | Thread.Sleep(SendMessageInterval); 137 | this.Stop(); // == StopQuiz() + ChatBot.Stop() 138 | } 139 | 140 | protected override void UpdateUserProfile(QuizUser user, int bonusExperience, int bonusMoney) 141 | { 142 | user.Experience += bonusExperience; 143 | user.Money += bonusMoney; 144 | 145 | SaveUserData(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SampleApplication/bin/Debug/data/quiz/[General]Saying/data.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="yes"?> 2 | <list> 3 | <data> 4 | <question>가게 기둥에 □□.</question> 5 | <answer>입춘</answer> 6 | <regDate>2019-08-04 17:40:48</regDate> 7 | </data> 8 | <data> 9 | <question>□□ 구제는 나라도 못 한다.</question> 10 | <answer>가난</answer> 11 | <regDate>2019-08-04 17:40:48</regDate> 12 | </data> 13 | <data> 14 | <question>가난이 □보다 무섭다.</question> 15 | <answer>병</answer> 16 | <regDate>2019-08-04 17:40:48</regDate> 17 | </data> 18 | <data> 19 | <question>가난한 집 □□□ 돌아오듯.</question> 20 | <answer>제삿날</answer> 21 | <regDate>2019-08-04 17:40:48</regDate> 22 | </data> 23 | <data> 24 | <question>가는 날이 □□이다.</question> 25 | <answer>장날</answer> 26 | <regDate>2019-08-04 17:40:48</regDate> 27 | </data> 28 | <data> 29 | <question>가는 말이 □□□ 오는 말이 곱다.</question> 30 | <answer>고와야</answer> 31 | <regDate>2019-08-04 17:40:48</regDate> 32 | </data> 33 | <data> 34 | <question>가다가 □□하면 아니 감만 못하다.</question> 35 | <answer>중지</answer> 36 | <regDate>2019-08-04 17:40:48</regDate> 37 | </data> 38 | <data> 39 | <question>□□□에 옷 젖는 줄 모른다.</question> 40 | <answer>가랑비</answer> 41 | <regDate>2019-08-04 17:40:48</regDate> 42 | </data> 43 | <data> 44 | <question>□□□으로 눈 가리고 아웅하다.</question> 45 | <answer>가랑잎</answer> 46 | <regDate>2019-08-04 17:40:48</regDate> 47 | </data> 48 | <data> 49 | <question>가마 속의 □도 삶아야 먹는다.</question> 50 | <answer>콩</answer> 51 | <regDate>2019-08-04 17:40:48</regDate> 52 | </data> 53 | <data> 54 | <question>□□에 콩 나듯.</question> 55 | <answer>가뭄</answer> 56 | <regDate>2019-08-04 17:40:48</regDate> 57 | </data> 58 | <data> 59 | <question>가재는 □ 편이라.</question> 60 | <answer>게</answer> 61 | <regDate>2019-08-04 17:40:48</regDate> 62 | </data> 63 | <data> 64 | <question>가지 많은 나무 □□ 잘 날 없다.</question> 65 | <answer>바람</answer> 66 | <regDate>2019-08-04 17:40:48</regDate> 67 | </data> 68 | <data> 69 | <question>간에 가 붙고 □□에 가 붙는다.</question> 70 | <answer>쓸개</answer> 71 | <regDate>2019-08-04 17:40:48</regDate> 72 | </data> 73 | <data> 74 | <question>□도 모르고 싸다 한다.</question> 75 | <answer>값</answer> 76 | <regDate>2019-08-04 17:40:48</regDate> 77 | </data> 78 | <data> 79 | <question>갓 쓰고 □□□ 탄다.</question> 80 | <answer>자전거</answer> 81 | <regDate>2019-08-04 17:40:48</regDate> 82 | </data> 83 | <data> 84 | <question>같은 값이면 □□□□다.</question> 85 | <answer>다홍치마</answer> 86 | <regDate>2019-08-04 17:40:48</regDate> 87 | </data> 88 | <data> 89 | <question>□ 눈에는 똥만 보인다.</question> 90 | <answer>개</answer> 91 | <regDate>2019-08-04 17:40:48</regDate> 92 | </data> 93 | <data> 94 | <question>개 □□ 먹듯.</question> 95 | <answer>머루</answer> 96 | <regDate>2019-08-04 17:40:48</regDate> 97 | </data> 98 | <data> 99 | <question>개 □□ 것은 들에 가 짖는다.</question> 100 | <answer>못된</answer> 101 | <regDate>2019-08-04 17:40:48</regDate> 102 | </data> 103 | <data> 104 | <question>개 □□ 쇠듯한다.</question> 105 | <answer>보름</answer> 106 | <regDate>2019-08-04 17:40:48</regDate> 107 | </data> 108 | <data> 109 | <question>□□□ 낯짝에 물 퍼붓기.</question> 110 | <answer>개구리</answer> 111 | <regDate>2019-08-04 17:40:48</regDate> 112 | </data> 113 | <data> 114 | <question>개구리가 □□□ 적 생각 못한다.</question> 115 | <answer>올챙이</answer> 116 | <regDate>2019-08-04 17:40:48</regDate> 117 | </data> 118 | <data> 119 | <question>개꼬리 삼 년 묻어도 □□되지 않는다.</question> 120 | <answer>황모</answer> 121 | <regDate>2019-08-04 17:40:48</regDate> 122 | </data> 123 | <data> 124 | <question>□□□에 굴러도 이승이 좋다.</question> 125 | <answer>개똥밭</answer> 126 | <regDate>2019-08-04 17:40:48</regDate> 127 | </data> 128 | <data> 129 | <question>개발에 주석 □□.</question> 130 | <answer>편자</answer> 131 | <regDate>2019-08-04 17:40:48</regDate> 132 | </data> 133 | <data> 134 | <question>개밥에 □□□.</question> 135 | <answer>도토리</answer> 136 | <regDate>2019-08-04 17:40:48</regDate> 137 | </data> 138 | <data> 139 | <question>개싸움에 □ 끼얹는다.</question> 140 | <answer>물</answer> 141 | <regDate>2019-08-04 17:40:48</regDate> 142 | </data> 143 | <data> 144 | <question>거적문에 □□□ 단다.</question> 145 | <answer>돌쩌귀</answer> 146 | <regDate>2019-08-04 17:40:48</regDate> 147 | </data> 148 | <data> 149 | <question>겉 □□□ 속 다르다.</question> 150 | <answer>다르고</answer> 151 | <regDate>2019-08-04 17:40:48</regDate> 152 | </data> 153 | </list> -------------------------------------------------------------------------------- /SampleApplication/bin/Debug/data/quiz/[General]Saying/settings.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/SampleApplication/bin/Debug/data/quiz/[General]Saying/settings.ini -------------------------------------------------------------------------------- /SampleApplication/bin/Release/data/quiz/[General]Saying/data.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="yes"?> 2 | <list> 3 | <data> 4 | <question>가게 기둥에 □□.</question> 5 | <answer>입춘</answer> 6 | <regDate>2019-08-04 17:40:48</regDate> 7 | </data> 8 | <data> 9 | <question>□□ 구제는 나라도 못 한다.</question> 10 | <answer>가난</answer> 11 | <regDate>2019-08-04 17:40:48</regDate> 12 | </data> 13 | <data> 14 | <question>가난이 □보다 무섭다.</question> 15 | <answer>병</answer> 16 | <regDate>2019-08-04 17:40:48</regDate> 17 | </data> 18 | <data> 19 | <question>가난한 집 □□□ 돌아오듯.</question> 20 | <answer>제삿날</answer> 21 | <regDate>2019-08-04 17:40:48</regDate> 22 | </data> 23 | <data> 24 | <question>가는 날이 □□이다.</question> 25 | <answer>장날</answer> 26 | <regDate>2019-08-04 17:40:48</regDate> 27 | </data> 28 | <data> 29 | <question>가는 말이 □□□ 오는 말이 곱다.</question> 30 | <answer>고와야</answer> 31 | <regDate>2019-08-04 17:40:48</regDate> 32 | </data> 33 | <data> 34 | <question>가다가 □□하면 아니 감만 못하다.</question> 35 | <answer>중지</answer> 36 | <regDate>2019-08-04 17:40:48</regDate> 37 | </data> 38 | <data> 39 | <question>□□□에 옷 젖는 줄 모른다.</question> 40 | <answer>가랑비</answer> 41 | <regDate>2019-08-04 17:40:48</regDate> 42 | </data> 43 | <data> 44 | <question>□□□으로 눈 가리고 아웅하다.</question> 45 | <answer>가랑잎</answer> 46 | <regDate>2019-08-04 17:40:48</regDate> 47 | </data> 48 | <data> 49 | <question>가마 속의 □도 삶아야 먹는다.</question> 50 | <answer>콩</answer> 51 | <regDate>2019-08-04 17:40:48</regDate> 52 | </data> 53 | <data> 54 | <question>□□에 콩 나듯.</question> 55 | <answer>가뭄</answer> 56 | <regDate>2019-08-04 17:40:48</regDate> 57 | </data> 58 | <data> 59 | <question>가재는 □ 편이라.</question> 60 | <answer>게</answer> 61 | <regDate>2019-08-04 17:40:48</regDate> 62 | </data> 63 | <data> 64 | <question>가지 많은 나무 □□ 잘 날 없다.</question> 65 | <answer>바람</answer> 66 | <regDate>2019-08-04 17:40:48</regDate> 67 | </data> 68 | <data> 69 | <question>간에 가 붙고 □□에 가 붙는다.</question> 70 | <answer>쓸개</answer> 71 | <regDate>2019-08-04 17:40:48</regDate> 72 | </data> 73 | <data> 74 | <question>□도 모르고 싸다 한다.</question> 75 | <answer>값</answer> 76 | <regDate>2019-08-04 17:40:48</regDate> 77 | </data> 78 | <data> 79 | <question>갓 쓰고 □□□ 탄다.</question> 80 | <answer>자전거</answer> 81 | <regDate>2019-08-04 17:40:48</regDate> 82 | </data> 83 | <data> 84 | <question>같은 값이면 □□□□다.</question> 85 | <answer>다홍치마</answer> 86 | <regDate>2019-08-04 17:40:48</regDate> 87 | </data> 88 | <data> 89 | <question>□ 눈에는 똥만 보인다.</question> 90 | <answer>개</answer> 91 | <regDate>2019-08-04 17:40:48</regDate> 92 | </data> 93 | <data> 94 | <question>개 □□ 먹듯.</question> 95 | <answer>머루</answer> 96 | <regDate>2019-08-04 17:40:48</regDate> 97 | </data> 98 | <data> 99 | <question>개 □□ 것은 들에 가 짖는다.</question> 100 | <answer>못된</answer> 101 | <regDate>2019-08-04 17:40:48</regDate> 102 | </data> 103 | <data> 104 | <question>개 □□ 쇠듯한다.</question> 105 | <answer>보름</answer> 106 | <regDate>2019-08-04 17:40:48</regDate> 107 | </data> 108 | <data> 109 | <question>□□□ 낯짝에 물 퍼붓기.</question> 110 | <answer>개구리</answer> 111 | <regDate>2019-08-04 17:40:48</regDate> 112 | </data> 113 | <data> 114 | <question>개구리가 □□□ 적 생각 못한다.</question> 115 | <answer>올챙이</answer> 116 | <regDate>2019-08-04 17:40:48</regDate> 117 | </data> 118 | <data> 119 | <question>개꼬리 삼 년 묻어도 □□되지 않는다.</question> 120 | <answer>황모</answer> 121 | <regDate>2019-08-04 17:40:48</regDate> 122 | </data> 123 | <data> 124 | <question>□□□에 굴러도 이승이 좋다.</question> 125 | <answer>개똥밭</answer> 126 | <regDate>2019-08-04 17:40:48</regDate> 127 | </data> 128 | <data> 129 | <question>개발에 주석 □□.</question> 130 | <answer>편자</answer> 131 | <regDate>2019-08-04 17:40:48</regDate> 132 | </data> 133 | <data> 134 | <question>개밥에 □□□.</question> 135 | <answer>도토리</answer> 136 | <regDate>2019-08-04 17:40:48</regDate> 137 | </data> 138 | <data> 139 | <question>개싸움에 □ 끼얹는다.</question> 140 | <answer>물</answer> 141 | <regDate>2019-08-04 17:40:48</regDate> 142 | </data> 143 | <data> 144 | <question>거적문에 □□□ 단다.</question> 145 | <answer>돌쩌귀</answer> 146 | <regDate>2019-08-04 17:40:48</regDate> 147 | </data> 148 | <data> 149 | <question>겉 □□□ 속 다르다.</question> 150 | <answer>다르고</answer> 151 | <regDate>2019-08-04 17:40:48</regDate> 152 | </data> 153 | </list> -------------------------------------------------------------------------------- /SampleApplication/bin/Release/data/quiz/[General]Saying/settings.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/repo-list/Less.API.NetFramework.KakaoBotAPI/6c8e082a22db602e1b95c81d2a127f47806d11f0/SampleApplication/bin/Release/data/quiz/[General]Saying/settings.ini --------------------------------------------------------------------------------