├── .gitignore ├── LICENSE ├── NdgrClientSharp.Protocol ├── NdgrClientSharp.Protocol.csproj └── src │ └── generated │ ├── Atoms.g.cs │ ├── Message.g.cs │ ├── Moderator.g.cs │ ├── Origin.g.cs │ ├── Payload.g.cs │ └── State.g.cs ├── NdgrClientSharp.Test ├── GlobalUsings.cs ├── NdgrClientSharp.Test.csproj ├── NdgrLiveCommentFetcherSpec.cs ├── NdgrPastCommentFetcherSpec.cs └── NdgrProtobufStreamReaderSpec.cs ├── NdgrClientSharp.sln ├── NdgrClientSharp ├── NdgrApi │ ├── INdgrApiClient.cs │ └── NdgrApiClient.cs ├── NdgrClientSharp.csproj ├── NdgrLiveCommentFetcher.cs ├── NdgrPastCommentFetcher.cs ├── NdgrSnapshotFetcher.cs └── Utilities │ ├── NdgrProtobufStreamReader.cs │ ├── PooledArray.cs │ └── PooledBuffer.cs ├── README.md └── Sandbox ├── Sandbox.cs └── Sandbox.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | *.sln.docstates 5 | 6 | # Build results 7 | 8 | .idea/ 9 | [Dd]ebug/ 10 | [Rr]elease/ 11 | x64/ 12 | [Bb]in/ 13 | [Oo]bj/ 14 | 15 | # MSTest test Results 16 | [Tt]est[Rr]esult*/ 17 | [Bb]uild[Ll]og.* 18 | 19 | *_i.c 20 | *_p.c 21 | *_i.h 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.tmp_proj 36 | *.log 37 | *.vspscc 38 | *.vssscc 39 | .builds 40 | *.pidb 41 | *.log 42 | *.svclog 43 | *.scc 44 | 45 | # Visual C++ cache files 46 | ipch/ 47 | *.aps 48 | *.ncb 49 | *.opensdf 50 | *.sdf 51 | *.cachefile 52 | 53 | # Visual Studio profiler 54 | *.psess 55 | *.vsp 56 | *.vspx 57 | 58 | # Guidance Automation Toolkit 59 | *.gpState 60 | 61 | # ReSharper is a .NET coding add-in 62 | _ReSharper*/ 63 | *.[Rr]e[Ss]harper 64 | *.DotSettings.user 65 | 66 | # Click-Once directory 67 | publish/ 68 | 69 | # Publish Web Output 70 | *.Publish.xml 71 | *.pubxml 72 | *.azurePubxml 73 | 74 | # NuGet Packages Directory 75 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 76 | packages/ 77 | ## TODO: If the tool you use requires repositories.config, also uncomment the next line 78 | !packages/repositories.config 79 | 80 | # Windows Azure Build Output 81 | csx/ 82 | *.build.csdef 83 | 84 | # Windows Store app package directory 85 | AppPackages/ 86 | 87 | # Others 88 | sql/ 89 | *.Cache 90 | ClientBin/ 91 | [Ss]tyle[Cc]op.* 92 | ![Ss]tyle[Cc]op.targets 93 | ~$* 94 | *~ 95 | *.dbmdl 96 | *.[Pp]ublish.xml 97 | 98 | *.publishsettings 99 | 100 | # RIA/Silverlight projects 101 | Generated_Code/ 102 | 103 | # Backup & report files from converting an old project file to a newer 104 | # Visual Studio version. Backup files are not needed, because we have git ;-) 105 | _UpgradeReport_Files/ 106 | Backup*/ 107 | UpgradeLog*.XML 108 | UpgradeLog*.htm 109 | 110 | # SQL Server files 111 | App_Data/*.mdf 112 | App_Data/*.ldf 113 | 114 | # ========================= 115 | # Windows detritus 116 | # ========================= 117 | 118 | # Windows image file caches 119 | Thumbs.db 120 | ehthumbs.db 121 | 122 | # Folder config file 123 | Desktop.ini 124 | 125 | # Recycle Bin used on file shares 126 | $RECYCLE.BIN/ 127 | 128 | # Mac desktop service store files 129 | .DS_Store 130 | 131 | _NCrunch* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TORISOUP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NdgrClientSharp.Protocol/NdgrClientSharp.Protocol.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | NdgrClientSharp.Protocol 6 | netstandard2.1;netstandard2.0 7 | 8 8 | 0.1.7 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /NdgrClientSharp.Protocol/src/generated/Message.g.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by the protocol buffer compiler. DO NOT EDIT! 3 | // source: dwango/nicolive/chat/data/message.proto 4 | // 5 | #pragma warning disable 1591, 0612, 3021, 8981 6 | #region Designer generated code 7 | 8 | using pb = global::Google.Protobuf; 9 | using pbc = global::Google.Protobuf.Collections; 10 | using pbr = global::Google.Protobuf.Reflection; 11 | using scg = global::System.Collections.Generic; 12 | namespace Dwango.Nicolive.Chat.Data { 13 | 14 | /// Holder for reflection information generated from dwango/nicolive/chat/data/message.proto 15 | public static partial class MessageReflection { 16 | 17 | #region Descriptor 18 | /// File descriptor for dwango/nicolive/chat/data/message.proto 19 | public static pbr::FileDescriptor Descriptor { 20 | get { return descriptor; } 21 | } 22 | private static pbr::FileDescriptor descriptor; 23 | 24 | static MessageReflection() { 25 | byte[] descriptorData = global::System.Convert.FromBase64String( 26 | string.Concat( 27 | "Cidkd2FuZ28vbmljb2xpdmUvY2hhdC9kYXRhL21lc3NhZ2UucHJvdG8SGWR3", 28 | "YW5nby5uaWNvbGl2ZS5jaGF0LmRhdGEaJWR3YW5nby9uaWNvbGl2ZS9jaGF0", 29 | "L2RhdGEvYXRvbXMucHJvdG8aL2R3YW5nby9uaWNvbGl2ZS9jaGF0L2RhdGEv", 30 | "YXRvbXMvbW9kZXJhdG9yLnByb3RvIt4ECg9OaWNvbGl2ZU1lc3NhZ2USLwoE", 31 | "Y2hhdBgBIAEoCzIfLmR3YW5nby5uaWNvbGl2ZS5jaGF0LmRhdGEuQ2hhdEgA", 32 | "EkwKE3NpbXBsZV9ub3RpZmljYXRpb24YByABKAsyLS5kd2FuZ28ubmljb2xp", 33 | "dmUuY2hhdC5kYXRhLlNpbXBsZU5vdGlmaWNhdGlvbkgAEi8KBGdpZnQYCCAB", 34 | "KAsyHy5kd2FuZ28ubmljb2xpdmUuY2hhdC5kYXRhLkdpZnRIABIzCgZuaWNv", 35 | "YWQYCSABKAsyIS5kd2FuZ28ubmljb2xpdmUuY2hhdC5kYXRhLk5pY29hZEgA", 36 | "EjwKC2dhbWVfdXBkYXRlGA0gASgLMiUuZHdhbmdvLm5pY29saXZlLmNoYXQu", 37 | "ZGF0YS5HYW1lVXBkYXRlSAASPAoLdGFnX3VwZGF0ZWQYESABKAsyJS5kd2Fu", 38 | "Z28ubmljb2xpdmUuY2hhdC5kYXRhLlRhZ1VwZGF0ZWRIABJOChFtb2RlcmF0", 39 | "b3JfdXBkYXRlZBgSIAEoCzIxLmR3YW5nby5uaWNvbGl2ZS5jaGF0LmRhdGEu", 40 | "YXRvbXMuTW9kZXJhdG9yVXBkYXRlZEgAEkQKDHNzbmdfdXBkYXRlZBgTIAEo", 41 | "CzIsLmR3YW5nby5uaWNvbGl2ZS5jaGF0LmRhdGEuYXRvbXMuU1NOR1VwZGF0", 42 | "ZWRIABI6Cg9vdmVyZmxvd2VkX2NoYXQYFCABKAsyHy5kd2FuZ28ubmljb2xp", 43 | "dmUuY2hhdC5kYXRhLkNoYXRIAEIGCgRkYXRhSgQIAhAHSgQIChANSgQIDhAR", 44 | "YgZwcm90bzM=")); 45 | descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, 46 | new pbr::FileDescriptor[] { global::Dwango.Nicolive.Chat.Data.AtomsReflection.Descriptor, global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorReflection.Descriptor, }, 47 | new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { 48 | new pbr::GeneratedClrTypeInfo(typeof(global::Dwango.Nicolive.Chat.Data.NicoliveMessage), global::Dwango.Nicolive.Chat.Data.NicoliveMessage.Parser, new[]{ "Chat", "SimpleNotification", "Gift", "Nicoad", "GameUpdate", "TagUpdated", "ModeratorUpdated", "SsngUpdated", "OverflowedChat" }, new[]{ "Data" }, null, null, null) 49 | })); 50 | } 51 | #endregion 52 | 53 | } 54 | #region Messages 55 | [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] 56 | public sealed partial class NicoliveMessage : pb::IMessage 57 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 58 | , pb::IBufferMessage 59 | #endif 60 | { 61 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new NicoliveMessage()); 62 | private pb::UnknownFieldSet _unknownFields; 63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 64 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 65 | public static pb::MessageParser Parser { get { return _parser; } } 66 | 67 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 68 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 69 | public static pbr::MessageDescriptor Descriptor { 70 | get { return global::Dwango.Nicolive.Chat.Data.MessageReflection.Descriptor.MessageTypes[0]; } 71 | } 72 | 73 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 74 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 75 | pbr::MessageDescriptor pb::IMessage.Descriptor { 76 | get { return Descriptor; } 77 | } 78 | 79 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 80 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 81 | public NicoliveMessage() { 82 | OnConstruction(); 83 | } 84 | 85 | partial void OnConstruction(); 86 | 87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 88 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 89 | public NicoliveMessage(NicoliveMessage other) : this() { 90 | switch (other.DataCase) { 91 | case DataOneofCase.Chat: 92 | Chat = other.Chat.Clone(); 93 | break; 94 | case DataOneofCase.SimpleNotification: 95 | SimpleNotification = other.SimpleNotification.Clone(); 96 | break; 97 | case DataOneofCase.Gift: 98 | Gift = other.Gift.Clone(); 99 | break; 100 | case DataOneofCase.Nicoad: 101 | Nicoad = other.Nicoad.Clone(); 102 | break; 103 | case DataOneofCase.GameUpdate: 104 | GameUpdate = other.GameUpdate.Clone(); 105 | break; 106 | case DataOneofCase.TagUpdated: 107 | TagUpdated = other.TagUpdated.Clone(); 108 | break; 109 | case DataOneofCase.ModeratorUpdated: 110 | ModeratorUpdated = other.ModeratorUpdated.Clone(); 111 | break; 112 | case DataOneofCase.SsngUpdated: 113 | SsngUpdated = other.SsngUpdated.Clone(); 114 | break; 115 | case DataOneofCase.OverflowedChat: 116 | OverflowedChat = other.OverflowedChat.Clone(); 117 | break; 118 | } 119 | 120 | _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); 121 | } 122 | 123 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 124 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 125 | public NicoliveMessage Clone() { 126 | return new NicoliveMessage(this); 127 | } 128 | 129 | /// Field number for the "chat" field. 130 | public const int ChatFieldNumber = 1; 131 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 132 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 133 | public global::Dwango.Nicolive.Chat.Data.Chat Chat { 134 | get { return dataCase_ == DataOneofCase.Chat ? (global::Dwango.Nicolive.Chat.Data.Chat) data_ : null; } 135 | set { 136 | data_ = value; 137 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.Chat; 138 | } 139 | } 140 | 141 | /// Field number for the "simple_notification" field. 142 | public const int SimpleNotificationFieldNumber = 7; 143 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 144 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 145 | public global::Dwango.Nicolive.Chat.Data.SimpleNotification SimpleNotification { 146 | get { return dataCase_ == DataOneofCase.SimpleNotification ? (global::Dwango.Nicolive.Chat.Data.SimpleNotification) data_ : null; } 147 | set { 148 | data_ = value; 149 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.SimpleNotification; 150 | } 151 | } 152 | 153 | /// Field number for the "gift" field. 154 | public const int GiftFieldNumber = 8; 155 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 156 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 157 | public global::Dwango.Nicolive.Chat.Data.Gift Gift { 158 | get { return dataCase_ == DataOneofCase.Gift ? (global::Dwango.Nicolive.Chat.Data.Gift) data_ : null; } 159 | set { 160 | data_ = value; 161 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.Gift; 162 | } 163 | } 164 | 165 | /// Field number for the "nicoad" field. 166 | public const int NicoadFieldNumber = 9; 167 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 168 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 169 | public global::Dwango.Nicolive.Chat.Data.Nicoad Nicoad { 170 | get { return dataCase_ == DataOneofCase.Nicoad ? (global::Dwango.Nicolive.Chat.Data.Nicoad) data_ : null; } 171 | set { 172 | data_ = value; 173 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.Nicoad; 174 | } 175 | } 176 | 177 | /// Field number for the "game_update" field. 178 | public const int GameUpdateFieldNumber = 13; 179 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 180 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 181 | public global::Dwango.Nicolive.Chat.Data.GameUpdate GameUpdate { 182 | get { return dataCase_ == DataOneofCase.GameUpdate ? (global::Dwango.Nicolive.Chat.Data.GameUpdate) data_ : null; } 183 | set { 184 | data_ = value; 185 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.GameUpdate; 186 | } 187 | } 188 | 189 | /// Field number for the "tag_updated" field. 190 | public const int TagUpdatedFieldNumber = 17; 191 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 192 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 193 | public global::Dwango.Nicolive.Chat.Data.TagUpdated TagUpdated { 194 | get { return dataCase_ == DataOneofCase.TagUpdated ? (global::Dwango.Nicolive.Chat.Data.TagUpdated) data_ : null; } 195 | set { 196 | data_ = value; 197 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.TagUpdated; 198 | } 199 | } 200 | 201 | /// Field number for the "moderator_updated" field. 202 | public const int ModeratorUpdatedFieldNumber = 18; 203 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 204 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 205 | public global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated ModeratorUpdated { 206 | get { return dataCase_ == DataOneofCase.ModeratorUpdated ? (global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated) data_ : null; } 207 | set { 208 | data_ = value; 209 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.ModeratorUpdated; 210 | } 211 | } 212 | 213 | /// Field number for the "ssng_updated" field. 214 | public const int SsngUpdatedFieldNumber = 19; 215 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 216 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 217 | public global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated SsngUpdated { 218 | get { return dataCase_ == DataOneofCase.SsngUpdated ? (global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated) data_ : null; } 219 | set { 220 | data_ = value; 221 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.SsngUpdated; 222 | } 223 | } 224 | 225 | /// Field number for the "overflowed_chat" field. 226 | public const int OverflowedChatFieldNumber = 20; 227 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 228 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 229 | public global::Dwango.Nicolive.Chat.Data.Chat OverflowedChat { 230 | get { return dataCase_ == DataOneofCase.OverflowedChat ? (global::Dwango.Nicolive.Chat.Data.Chat) data_ : null; } 231 | set { 232 | data_ = value; 233 | dataCase_ = value == null ? DataOneofCase.None : DataOneofCase.OverflowedChat; 234 | } 235 | } 236 | 237 | private object data_; 238 | /// Enum of possible cases for the "data" oneof. 239 | public enum DataOneofCase { 240 | None = 0, 241 | Chat = 1, 242 | SimpleNotification = 7, 243 | Gift = 8, 244 | Nicoad = 9, 245 | GameUpdate = 13, 246 | TagUpdated = 17, 247 | ModeratorUpdated = 18, 248 | SsngUpdated = 19, 249 | OverflowedChat = 20, 250 | } 251 | private DataOneofCase dataCase_ = DataOneofCase.None; 252 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 253 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 254 | public DataOneofCase DataCase { 255 | get { return dataCase_; } 256 | } 257 | 258 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 259 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 260 | public void ClearData() { 261 | dataCase_ = DataOneofCase.None; 262 | data_ = null; 263 | } 264 | 265 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 266 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 267 | public override bool Equals(object other) { 268 | return Equals(other as NicoliveMessage); 269 | } 270 | 271 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 272 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 273 | public bool Equals(NicoliveMessage other) { 274 | if (ReferenceEquals(other, null)) { 275 | return false; 276 | } 277 | if (ReferenceEquals(other, this)) { 278 | return true; 279 | } 280 | if (!object.Equals(Chat, other.Chat)) return false; 281 | if (!object.Equals(SimpleNotification, other.SimpleNotification)) return false; 282 | if (!object.Equals(Gift, other.Gift)) return false; 283 | if (!object.Equals(Nicoad, other.Nicoad)) return false; 284 | if (!object.Equals(GameUpdate, other.GameUpdate)) return false; 285 | if (!object.Equals(TagUpdated, other.TagUpdated)) return false; 286 | if (!object.Equals(ModeratorUpdated, other.ModeratorUpdated)) return false; 287 | if (!object.Equals(SsngUpdated, other.SsngUpdated)) return false; 288 | if (!object.Equals(OverflowedChat, other.OverflowedChat)) return false; 289 | if (DataCase != other.DataCase) return false; 290 | return Equals(_unknownFields, other._unknownFields); 291 | } 292 | 293 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 294 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 295 | public override int GetHashCode() { 296 | int hash = 1; 297 | if (dataCase_ == DataOneofCase.Chat) hash ^= Chat.GetHashCode(); 298 | if (dataCase_ == DataOneofCase.SimpleNotification) hash ^= SimpleNotification.GetHashCode(); 299 | if (dataCase_ == DataOneofCase.Gift) hash ^= Gift.GetHashCode(); 300 | if (dataCase_ == DataOneofCase.Nicoad) hash ^= Nicoad.GetHashCode(); 301 | if (dataCase_ == DataOneofCase.GameUpdate) hash ^= GameUpdate.GetHashCode(); 302 | if (dataCase_ == DataOneofCase.TagUpdated) hash ^= TagUpdated.GetHashCode(); 303 | if (dataCase_ == DataOneofCase.ModeratorUpdated) hash ^= ModeratorUpdated.GetHashCode(); 304 | if (dataCase_ == DataOneofCase.SsngUpdated) hash ^= SsngUpdated.GetHashCode(); 305 | if (dataCase_ == DataOneofCase.OverflowedChat) hash ^= OverflowedChat.GetHashCode(); 306 | hash ^= (int) dataCase_; 307 | if (_unknownFields != null) { 308 | hash ^= _unknownFields.GetHashCode(); 309 | } 310 | return hash; 311 | } 312 | 313 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 314 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 315 | public override string ToString() { 316 | return pb::JsonFormatter.ToDiagnosticString(this); 317 | } 318 | 319 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 320 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 321 | public void WriteTo(pb::CodedOutputStream output) { 322 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 323 | output.WriteRawMessage(this); 324 | #else 325 | if (dataCase_ == DataOneofCase.Chat) { 326 | output.WriteRawTag(10); 327 | output.WriteMessage(Chat); 328 | } 329 | if (dataCase_ == DataOneofCase.SimpleNotification) { 330 | output.WriteRawTag(58); 331 | output.WriteMessage(SimpleNotification); 332 | } 333 | if (dataCase_ == DataOneofCase.Gift) { 334 | output.WriteRawTag(66); 335 | output.WriteMessage(Gift); 336 | } 337 | if (dataCase_ == DataOneofCase.Nicoad) { 338 | output.WriteRawTag(74); 339 | output.WriteMessage(Nicoad); 340 | } 341 | if (dataCase_ == DataOneofCase.GameUpdate) { 342 | output.WriteRawTag(106); 343 | output.WriteMessage(GameUpdate); 344 | } 345 | if (dataCase_ == DataOneofCase.TagUpdated) { 346 | output.WriteRawTag(138, 1); 347 | output.WriteMessage(TagUpdated); 348 | } 349 | if (dataCase_ == DataOneofCase.ModeratorUpdated) { 350 | output.WriteRawTag(146, 1); 351 | output.WriteMessage(ModeratorUpdated); 352 | } 353 | if (dataCase_ == DataOneofCase.SsngUpdated) { 354 | output.WriteRawTag(154, 1); 355 | output.WriteMessage(SsngUpdated); 356 | } 357 | if (dataCase_ == DataOneofCase.OverflowedChat) { 358 | output.WriteRawTag(162, 1); 359 | output.WriteMessage(OverflowedChat); 360 | } 361 | if (_unknownFields != null) { 362 | _unknownFields.WriteTo(output); 363 | } 364 | #endif 365 | } 366 | 367 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 368 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 369 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 370 | void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { 371 | if (dataCase_ == DataOneofCase.Chat) { 372 | output.WriteRawTag(10); 373 | output.WriteMessage(Chat); 374 | } 375 | if (dataCase_ == DataOneofCase.SimpleNotification) { 376 | output.WriteRawTag(58); 377 | output.WriteMessage(SimpleNotification); 378 | } 379 | if (dataCase_ == DataOneofCase.Gift) { 380 | output.WriteRawTag(66); 381 | output.WriteMessage(Gift); 382 | } 383 | if (dataCase_ == DataOneofCase.Nicoad) { 384 | output.WriteRawTag(74); 385 | output.WriteMessage(Nicoad); 386 | } 387 | if (dataCase_ == DataOneofCase.GameUpdate) { 388 | output.WriteRawTag(106); 389 | output.WriteMessage(GameUpdate); 390 | } 391 | if (dataCase_ == DataOneofCase.TagUpdated) { 392 | output.WriteRawTag(138, 1); 393 | output.WriteMessage(TagUpdated); 394 | } 395 | if (dataCase_ == DataOneofCase.ModeratorUpdated) { 396 | output.WriteRawTag(146, 1); 397 | output.WriteMessage(ModeratorUpdated); 398 | } 399 | if (dataCase_ == DataOneofCase.SsngUpdated) { 400 | output.WriteRawTag(154, 1); 401 | output.WriteMessage(SsngUpdated); 402 | } 403 | if (dataCase_ == DataOneofCase.OverflowedChat) { 404 | output.WriteRawTag(162, 1); 405 | output.WriteMessage(OverflowedChat); 406 | } 407 | if (_unknownFields != null) { 408 | _unknownFields.WriteTo(ref output); 409 | } 410 | } 411 | #endif 412 | 413 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 414 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 415 | public int CalculateSize() { 416 | int size = 0; 417 | if (dataCase_ == DataOneofCase.Chat) { 418 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Chat); 419 | } 420 | if (dataCase_ == DataOneofCase.SimpleNotification) { 421 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(SimpleNotification); 422 | } 423 | if (dataCase_ == DataOneofCase.Gift) { 424 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Gift); 425 | } 426 | if (dataCase_ == DataOneofCase.Nicoad) { 427 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Nicoad); 428 | } 429 | if (dataCase_ == DataOneofCase.GameUpdate) { 430 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(GameUpdate); 431 | } 432 | if (dataCase_ == DataOneofCase.TagUpdated) { 433 | size += 2 + pb::CodedOutputStream.ComputeMessageSize(TagUpdated); 434 | } 435 | if (dataCase_ == DataOneofCase.ModeratorUpdated) { 436 | size += 2 + pb::CodedOutputStream.ComputeMessageSize(ModeratorUpdated); 437 | } 438 | if (dataCase_ == DataOneofCase.SsngUpdated) { 439 | size += 2 + pb::CodedOutputStream.ComputeMessageSize(SsngUpdated); 440 | } 441 | if (dataCase_ == DataOneofCase.OverflowedChat) { 442 | size += 2 + pb::CodedOutputStream.ComputeMessageSize(OverflowedChat); 443 | } 444 | if (_unknownFields != null) { 445 | size += _unknownFields.CalculateSize(); 446 | } 447 | return size; 448 | } 449 | 450 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 451 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 452 | public void MergeFrom(NicoliveMessage other) { 453 | if (other == null) { 454 | return; 455 | } 456 | switch (other.DataCase) { 457 | case DataOneofCase.Chat: 458 | if (Chat == null) { 459 | Chat = new global::Dwango.Nicolive.Chat.Data.Chat(); 460 | } 461 | Chat.MergeFrom(other.Chat); 462 | break; 463 | case DataOneofCase.SimpleNotification: 464 | if (SimpleNotification == null) { 465 | SimpleNotification = new global::Dwango.Nicolive.Chat.Data.SimpleNotification(); 466 | } 467 | SimpleNotification.MergeFrom(other.SimpleNotification); 468 | break; 469 | case DataOneofCase.Gift: 470 | if (Gift == null) { 471 | Gift = new global::Dwango.Nicolive.Chat.Data.Gift(); 472 | } 473 | Gift.MergeFrom(other.Gift); 474 | break; 475 | case DataOneofCase.Nicoad: 476 | if (Nicoad == null) { 477 | Nicoad = new global::Dwango.Nicolive.Chat.Data.Nicoad(); 478 | } 479 | Nicoad.MergeFrom(other.Nicoad); 480 | break; 481 | case DataOneofCase.GameUpdate: 482 | if (GameUpdate == null) { 483 | GameUpdate = new global::Dwango.Nicolive.Chat.Data.GameUpdate(); 484 | } 485 | GameUpdate.MergeFrom(other.GameUpdate); 486 | break; 487 | case DataOneofCase.TagUpdated: 488 | if (TagUpdated == null) { 489 | TagUpdated = new global::Dwango.Nicolive.Chat.Data.TagUpdated(); 490 | } 491 | TagUpdated.MergeFrom(other.TagUpdated); 492 | break; 493 | case DataOneofCase.ModeratorUpdated: 494 | if (ModeratorUpdated == null) { 495 | ModeratorUpdated = new global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated(); 496 | } 497 | ModeratorUpdated.MergeFrom(other.ModeratorUpdated); 498 | break; 499 | case DataOneofCase.SsngUpdated: 500 | if (SsngUpdated == null) { 501 | SsngUpdated = new global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated(); 502 | } 503 | SsngUpdated.MergeFrom(other.SsngUpdated); 504 | break; 505 | case DataOneofCase.OverflowedChat: 506 | if (OverflowedChat == null) { 507 | OverflowedChat = new global::Dwango.Nicolive.Chat.Data.Chat(); 508 | } 509 | OverflowedChat.MergeFrom(other.OverflowedChat); 510 | break; 511 | } 512 | 513 | _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); 514 | } 515 | 516 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 517 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 518 | public void MergeFrom(pb::CodedInputStream input) { 519 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 520 | input.ReadRawMessage(this); 521 | #else 522 | uint tag; 523 | while ((tag = input.ReadTag()) != 0) { 524 | if ((tag & 7) == 4) { 525 | // Abort on any end group tag. 526 | return; 527 | } 528 | switch(tag) { 529 | default: 530 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); 531 | break; 532 | case 10: { 533 | global::Dwango.Nicolive.Chat.Data.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.Chat(); 534 | if (dataCase_ == DataOneofCase.Chat) { 535 | subBuilder.MergeFrom(Chat); 536 | } 537 | input.ReadMessage(subBuilder); 538 | Chat = subBuilder; 539 | break; 540 | } 541 | case 58: { 542 | global::Dwango.Nicolive.Chat.Data.SimpleNotification subBuilder = new global::Dwango.Nicolive.Chat.Data.SimpleNotification(); 543 | if (dataCase_ == DataOneofCase.SimpleNotification) { 544 | subBuilder.MergeFrom(SimpleNotification); 545 | } 546 | input.ReadMessage(subBuilder); 547 | SimpleNotification = subBuilder; 548 | break; 549 | } 550 | case 66: { 551 | global::Dwango.Nicolive.Chat.Data.Gift subBuilder = new global::Dwango.Nicolive.Chat.Data.Gift(); 552 | if (dataCase_ == DataOneofCase.Gift) { 553 | subBuilder.MergeFrom(Gift); 554 | } 555 | input.ReadMessage(subBuilder); 556 | Gift = subBuilder; 557 | break; 558 | } 559 | case 74: { 560 | global::Dwango.Nicolive.Chat.Data.Nicoad subBuilder = new global::Dwango.Nicolive.Chat.Data.Nicoad(); 561 | if (dataCase_ == DataOneofCase.Nicoad) { 562 | subBuilder.MergeFrom(Nicoad); 563 | } 564 | input.ReadMessage(subBuilder); 565 | Nicoad = subBuilder; 566 | break; 567 | } 568 | case 106: { 569 | global::Dwango.Nicolive.Chat.Data.GameUpdate subBuilder = new global::Dwango.Nicolive.Chat.Data.GameUpdate(); 570 | if (dataCase_ == DataOneofCase.GameUpdate) { 571 | subBuilder.MergeFrom(GameUpdate); 572 | } 573 | input.ReadMessage(subBuilder); 574 | GameUpdate = subBuilder; 575 | break; 576 | } 577 | case 138: { 578 | global::Dwango.Nicolive.Chat.Data.TagUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.TagUpdated(); 579 | if (dataCase_ == DataOneofCase.TagUpdated) { 580 | subBuilder.MergeFrom(TagUpdated); 581 | } 582 | input.ReadMessage(subBuilder); 583 | TagUpdated = subBuilder; 584 | break; 585 | } 586 | case 146: { 587 | global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated(); 588 | if (dataCase_ == DataOneofCase.ModeratorUpdated) { 589 | subBuilder.MergeFrom(ModeratorUpdated); 590 | } 591 | input.ReadMessage(subBuilder); 592 | ModeratorUpdated = subBuilder; 593 | break; 594 | } 595 | case 154: { 596 | global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated(); 597 | if (dataCase_ == DataOneofCase.SsngUpdated) { 598 | subBuilder.MergeFrom(SsngUpdated); 599 | } 600 | input.ReadMessage(subBuilder); 601 | SsngUpdated = subBuilder; 602 | break; 603 | } 604 | case 162: { 605 | global::Dwango.Nicolive.Chat.Data.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.Chat(); 606 | if (dataCase_ == DataOneofCase.OverflowedChat) { 607 | subBuilder.MergeFrom(OverflowedChat); 608 | } 609 | input.ReadMessage(subBuilder); 610 | OverflowedChat = subBuilder; 611 | break; 612 | } 613 | } 614 | } 615 | #endif 616 | } 617 | 618 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 619 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 620 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 621 | void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { 622 | uint tag; 623 | while ((tag = input.ReadTag()) != 0) { 624 | if ((tag & 7) == 4) { 625 | // Abort on any end group tag. 626 | return; 627 | } 628 | switch(tag) { 629 | default: 630 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); 631 | break; 632 | case 10: { 633 | global::Dwango.Nicolive.Chat.Data.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.Chat(); 634 | if (dataCase_ == DataOneofCase.Chat) { 635 | subBuilder.MergeFrom(Chat); 636 | } 637 | input.ReadMessage(subBuilder); 638 | Chat = subBuilder; 639 | break; 640 | } 641 | case 58: { 642 | global::Dwango.Nicolive.Chat.Data.SimpleNotification subBuilder = new global::Dwango.Nicolive.Chat.Data.SimpleNotification(); 643 | if (dataCase_ == DataOneofCase.SimpleNotification) { 644 | subBuilder.MergeFrom(SimpleNotification); 645 | } 646 | input.ReadMessage(subBuilder); 647 | SimpleNotification = subBuilder; 648 | break; 649 | } 650 | case 66: { 651 | global::Dwango.Nicolive.Chat.Data.Gift subBuilder = new global::Dwango.Nicolive.Chat.Data.Gift(); 652 | if (dataCase_ == DataOneofCase.Gift) { 653 | subBuilder.MergeFrom(Gift); 654 | } 655 | input.ReadMessage(subBuilder); 656 | Gift = subBuilder; 657 | break; 658 | } 659 | case 74: { 660 | global::Dwango.Nicolive.Chat.Data.Nicoad subBuilder = new global::Dwango.Nicolive.Chat.Data.Nicoad(); 661 | if (dataCase_ == DataOneofCase.Nicoad) { 662 | subBuilder.MergeFrom(Nicoad); 663 | } 664 | input.ReadMessage(subBuilder); 665 | Nicoad = subBuilder; 666 | break; 667 | } 668 | case 106: { 669 | global::Dwango.Nicolive.Chat.Data.GameUpdate subBuilder = new global::Dwango.Nicolive.Chat.Data.GameUpdate(); 670 | if (dataCase_ == DataOneofCase.GameUpdate) { 671 | subBuilder.MergeFrom(GameUpdate); 672 | } 673 | input.ReadMessage(subBuilder); 674 | GameUpdate = subBuilder; 675 | break; 676 | } 677 | case 138: { 678 | global::Dwango.Nicolive.Chat.Data.TagUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.TagUpdated(); 679 | if (dataCase_ == DataOneofCase.TagUpdated) { 680 | subBuilder.MergeFrom(TagUpdated); 681 | } 682 | input.ReadMessage(subBuilder); 683 | TagUpdated = subBuilder; 684 | break; 685 | } 686 | case 146: { 687 | global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorUpdated(); 688 | if (dataCase_ == DataOneofCase.ModeratorUpdated) { 689 | subBuilder.MergeFrom(ModeratorUpdated); 690 | } 691 | input.ReadMessage(subBuilder); 692 | ModeratorUpdated = subBuilder; 693 | break; 694 | } 695 | case 154: { 696 | global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated subBuilder = new global::Dwango.Nicolive.Chat.Data.Atoms.SSNGUpdated(); 697 | if (dataCase_ == DataOneofCase.SsngUpdated) { 698 | subBuilder.MergeFrom(SsngUpdated); 699 | } 700 | input.ReadMessage(subBuilder); 701 | SsngUpdated = subBuilder; 702 | break; 703 | } 704 | case 162: { 705 | global::Dwango.Nicolive.Chat.Data.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.Chat(); 706 | if (dataCase_ == DataOneofCase.OverflowedChat) { 707 | subBuilder.MergeFrom(OverflowedChat); 708 | } 709 | input.ReadMessage(subBuilder); 710 | OverflowedChat = subBuilder; 711 | break; 712 | } 713 | } 714 | } 715 | } 716 | #endif 717 | 718 | } 719 | 720 | #endregion 721 | 722 | } 723 | 724 | #endregion Designer generated code 725 | -------------------------------------------------------------------------------- /NdgrClientSharp.Protocol/src/generated/Origin.g.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by the protocol buffer compiler. DO NOT EDIT! 3 | // source: dwango/nicolive/chat/data/origin.proto 4 | // 5 | #pragma warning disable 1591, 0612, 3021, 8981 6 | #region Designer generated code 7 | 8 | using pb = global::Google.Protobuf; 9 | using pbc = global::Google.Protobuf.Collections; 10 | using pbr = global::Google.Protobuf.Reflection; 11 | using scg = global::System.Collections.Generic; 12 | namespace Dwango.Nicolive.Chat.Data { 13 | 14 | /// Holder for reflection information generated from dwango/nicolive/chat/data/origin.proto 15 | public static partial class OriginReflection { 16 | 17 | #region Descriptor 18 | /// File descriptor for dwango/nicolive/chat/data/origin.proto 19 | public static pbr::FileDescriptor Descriptor { 20 | get { return descriptor; } 21 | } 22 | private static pbr::FileDescriptor descriptor; 23 | 24 | static OriginReflection() { 25 | byte[] descriptorData = global::System.Convert.FromBase64String( 26 | string.Concat( 27 | "CiZkd2FuZ28vbmljb2xpdmUvY2hhdC9kYXRhL29yaWdpbi5wcm90bxIZZHdh", 28 | "bmdvLm5pY29saXZlLmNoYXQuZGF0YSJzCg5OaWNvbGl2ZU9yaWdpbhI+CgRj", 29 | "aGF0GAEgASgLMi4uZHdhbmdvLm5pY29saXZlLmNoYXQuZGF0YS5OaWNvbGl2", 30 | "ZU9yaWdpbi5DaGF0SAAaFwoEQ2hhdBIPCgdsaXZlX2lkGAEgASgDQggKBm9y", 31 | "aWdpbmIGcHJvdG8z")); 32 | descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, 33 | new pbr::FileDescriptor[] { }, 34 | new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { 35 | new pbr::GeneratedClrTypeInfo(typeof(global::Dwango.Nicolive.Chat.Data.NicoliveOrigin), global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Parser, new[]{ "Chat" }, new[]{ "Origin" }, null, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat), global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat.Parser, new[]{ "LiveId" }, null, null, null, null)}) 36 | })); 37 | } 38 | #endregion 39 | 40 | } 41 | #region Messages 42 | [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] 43 | public sealed partial class NicoliveOrigin : pb::IMessage 44 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 45 | , pb::IBufferMessage 46 | #endif 47 | { 48 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new NicoliveOrigin()); 49 | private pb::UnknownFieldSet _unknownFields; 50 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 51 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 52 | public static pb::MessageParser Parser { get { return _parser; } } 53 | 54 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 55 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 56 | public static pbr::MessageDescriptor Descriptor { 57 | get { return global::Dwango.Nicolive.Chat.Data.OriginReflection.Descriptor.MessageTypes[0]; } 58 | } 59 | 60 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 61 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 62 | pbr::MessageDescriptor pb::IMessage.Descriptor { 63 | get { return Descriptor; } 64 | } 65 | 66 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 67 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 68 | public NicoliveOrigin() { 69 | OnConstruction(); 70 | } 71 | 72 | partial void OnConstruction(); 73 | 74 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 75 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 76 | public NicoliveOrigin(NicoliveOrigin other) : this() { 77 | switch (other.OriginCase) { 78 | case OriginOneofCase.Chat: 79 | Chat = other.Chat.Clone(); 80 | break; 81 | } 82 | 83 | _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); 84 | } 85 | 86 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 87 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 88 | public NicoliveOrigin Clone() { 89 | return new NicoliveOrigin(this); 90 | } 91 | 92 | /// Field number for the "chat" field. 93 | public const int ChatFieldNumber = 1; 94 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 95 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 96 | public global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat Chat { 97 | get { return originCase_ == OriginOneofCase.Chat ? (global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat) origin_ : null; } 98 | set { 99 | origin_ = value; 100 | originCase_ = value == null ? OriginOneofCase.None : OriginOneofCase.Chat; 101 | } 102 | } 103 | 104 | private object origin_; 105 | /// Enum of possible cases for the "origin" oneof. 106 | public enum OriginOneofCase { 107 | None = 0, 108 | Chat = 1, 109 | } 110 | private OriginOneofCase originCase_ = OriginOneofCase.None; 111 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 112 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 113 | public OriginOneofCase OriginCase { 114 | get { return originCase_; } 115 | } 116 | 117 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 118 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 119 | public void ClearOrigin() { 120 | originCase_ = OriginOneofCase.None; 121 | origin_ = null; 122 | } 123 | 124 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 125 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 126 | public override bool Equals(object other) { 127 | return Equals(other as NicoliveOrigin); 128 | } 129 | 130 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 131 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 132 | public bool Equals(NicoliveOrigin other) { 133 | if (ReferenceEquals(other, null)) { 134 | return false; 135 | } 136 | if (ReferenceEquals(other, this)) { 137 | return true; 138 | } 139 | if (!object.Equals(Chat, other.Chat)) return false; 140 | if (OriginCase != other.OriginCase) return false; 141 | return Equals(_unknownFields, other._unknownFields); 142 | } 143 | 144 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 145 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 146 | public override int GetHashCode() { 147 | int hash = 1; 148 | if (originCase_ == OriginOneofCase.Chat) hash ^= Chat.GetHashCode(); 149 | hash ^= (int) originCase_; 150 | if (_unknownFields != null) { 151 | hash ^= _unknownFields.GetHashCode(); 152 | } 153 | return hash; 154 | } 155 | 156 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 157 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 158 | public override string ToString() { 159 | return pb::JsonFormatter.ToDiagnosticString(this); 160 | } 161 | 162 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 163 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 164 | public void WriteTo(pb::CodedOutputStream output) { 165 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 166 | output.WriteRawMessage(this); 167 | #else 168 | if (originCase_ == OriginOneofCase.Chat) { 169 | output.WriteRawTag(10); 170 | output.WriteMessage(Chat); 171 | } 172 | if (_unknownFields != null) { 173 | _unknownFields.WriteTo(output); 174 | } 175 | #endif 176 | } 177 | 178 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 179 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 180 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 181 | void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { 182 | if (originCase_ == OriginOneofCase.Chat) { 183 | output.WriteRawTag(10); 184 | output.WriteMessage(Chat); 185 | } 186 | if (_unknownFields != null) { 187 | _unknownFields.WriteTo(ref output); 188 | } 189 | } 190 | #endif 191 | 192 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 193 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 194 | public int CalculateSize() { 195 | int size = 0; 196 | if (originCase_ == OriginOneofCase.Chat) { 197 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Chat); 198 | } 199 | if (_unknownFields != null) { 200 | size += _unknownFields.CalculateSize(); 201 | } 202 | return size; 203 | } 204 | 205 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 206 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 207 | public void MergeFrom(NicoliveOrigin other) { 208 | if (other == null) { 209 | return; 210 | } 211 | switch (other.OriginCase) { 212 | case OriginOneofCase.Chat: 213 | if (Chat == null) { 214 | Chat = new global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat(); 215 | } 216 | Chat.MergeFrom(other.Chat); 217 | break; 218 | } 219 | 220 | _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); 221 | } 222 | 223 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 224 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 225 | public void MergeFrom(pb::CodedInputStream input) { 226 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 227 | input.ReadRawMessage(this); 228 | #else 229 | uint tag; 230 | while ((tag = input.ReadTag()) != 0) { 231 | if ((tag & 7) == 4) { 232 | // Abort on any end group tag. 233 | return; 234 | } 235 | switch(tag) { 236 | default: 237 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); 238 | break; 239 | case 10: { 240 | global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat(); 241 | if (originCase_ == OriginOneofCase.Chat) { 242 | subBuilder.MergeFrom(Chat); 243 | } 244 | input.ReadMessage(subBuilder); 245 | Chat = subBuilder; 246 | break; 247 | } 248 | } 249 | } 250 | #endif 251 | } 252 | 253 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 254 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 255 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 256 | void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { 257 | uint tag; 258 | while ((tag = input.ReadTag()) != 0) { 259 | if ((tag & 7) == 4) { 260 | // Abort on any end group tag. 261 | return; 262 | } 263 | switch(tag) { 264 | default: 265 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); 266 | break; 267 | case 10: { 268 | global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat subBuilder = new global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Types.Chat(); 269 | if (originCase_ == OriginOneofCase.Chat) { 270 | subBuilder.MergeFrom(Chat); 271 | } 272 | input.ReadMessage(subBuilder); 273 | Chat = subBuilder; 274 | break; 275 | } 276 | } 277 | } 278 | } 279 | #endif 280 | 281 | #region Nested types 282 | /// Container for nested types declared in the NicoliveOrigin message type. 283 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 284 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 285 | public static partial class Types { 286 | [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] 287 | public sealed partial class Chat : pb::IMessage 288 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 289 | , pb::IBufferMessage 290 | #endif 291 | { 292 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Chat()); 293 | private pb::UnknownFieldSet _unknownFields; 294 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 295 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 296 | public static pb::MessageParser Parser { get { return _parser; } } 297 | 298 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 299 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 300 | public static pbr::MessageDescriptor Descriptor { 301 | get { return global::Dwango.Nicolive.Chat.Data.NicoliveOrigin.Descriptor.NestedTypes[0]; } 302 | } 303 | 304 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 305 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 306 | pbr::MessageDescriptor pb::IMessage.Descriptor { 307 | get { return Descriptor; } 308 | } 309 | 310 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 311 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 312 | public Chat() { 313 | OnConstruction(); 314 | } 315 | 316 | partial void OnConstruction(); 317 | 318 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 319 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 320 | public Chat(Chat other) : this() { 321 | liveId_ = other.liveId_; 322 | _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); 323 | } 324 | 325 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 326 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 327 | public Chat Clone() { 328 | return new Chat(this); 329 | } 330 | 331 | /// Field number for the "live_id" field. 332 | public const int LiveIdFieldNumber = 1; 333 | private long liveId_; 334 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 335 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 336 | public long LiveId { 337 | get { return liveId_; } 338 | set { 339 | liveId_ = value; 340 | } 341 | } 342 | 343 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 344 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 345 | public override bool Equals(object other) { 346 | return Equals(other as Chat); 347 | } 348 | 349 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 350 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 351 | public bool Equals(Chat other) { 352 | if (ReferenceEquals(other, null)) { 353 | return false; 354 | } 355 | if (ReferenceEquals(other, this)) { 356 | return true; 357 | } 358 | if (LiveId != other.LiveId) return false; 359 | return Equals(_unknownFields, other._unknownFields); 360 | } 361 | 362 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 363 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 364 | public override int GetHashCode() { 365 | int hash = 1; 366 | if (LiveId != 0L) hash ^= LiveId.GetHashCode(); 367 | if (_unknownFields != null) { 368 | hash ^= _unknownFields.GetHashCode(); 369 | } 370 | return hash; 371 | } 372 | 373 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 374 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 375 | public override string ToString() { 376 | return pb::JsonFormatter.ToDiagnosticString(this); 377 | } 378 | 379 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 380 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 381 | public void WriteTo(pb::CodedOutputStream output) { 382 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 383 | output.WriteRawMessage(this); 384 | #else 385 | if (LiveId != 0L) { 386 | output.WriteRawTag(8); 387 | output.WriteInt64(LiveId); 388 | } 389 | if (_unknownFields != null) { 390 | _unknownFields.WriteTo(output); 391 | } 392 | #endif 393 | } 394 | 395 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 396 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 397 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 398 | void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { 399 | if (LiveId != 0L) { 400 | output.WriteRawTag(8); 401 | output.WriteInt64(LiveId); 402 | } 403 | if (_unknownFields != null) { 404 | _unknownFields.WriteTo(ref output); 405 | } 406 | } 407 | #endif 408 | 409 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 410 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 411 | public int CalculateSize() { 412 | int size = 0; 413 | if (LiveId != 0L) { 414 | size += 1 + pb::CodedOutputStream.ComputeInt64Size(LiveId); 415 | } 416 | if (_unknownFields != null) { 417 | size += _unknownFields.CalculateSize(); 418 | } 419 | return size; 420 | } 421 | 422 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 423 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 424 | public void MergeFrom(Chat other) { 425 | if (other == null) { 426 | return; 427 | } 428 | if (other.LiveId != 0L) { 429 | LiveId = other.LiveId; 430 | } 431 | _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); 432 | } 433 | 434 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 435 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 436 | public void MergeFrom(pb::CodedInputStream input) { 437 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 438 | input.ReadRawMessage(this); 439 | #else 440 | uint tag; 441 | while ((tag = input.ReadTag()) != 0) { 442 | if ((tag & 7) == 4) { 443 | // Abort on any end group tag. 444 | return; 445 | } 446 | switch(tag) { 447 | default: 448 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); 449 | break; 450 | case 8: { 451 | LiveId = input.ReadInt64(); 452 | break; 453 | } 454 | } 455 | } 456 | #endif 457 | } 458 | 459 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 460 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 461 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 462 | void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { 463 | uint tag; 464 | while ((tag = input.ReadTag()) != 0) { 465 | if ((tag & 7) == 4) { 466 | // Abort on any end group tag. 467 | return; 468 | } 469 | switch(tag) { 470 | default: 471 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); 472 | break; 473 | case 8: { 474 | LiveId = input.ReadInt64(); 475 | break; 476 | } 477 | } 478 | } 479 | } 480 | #endif 481 | 482 | } 483 | 484 | } 485 | #endregion 486 | 487 | } 488 | 489 | #endregion 490 | 491 | } 492 | 493 | #endregion Designer generated code 494 | -------------------------------------------------------------------------------- /NdgrClientSharp.Protocol/src/generated/State.g.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by the protocol buffer compiler. DO NOT EDIT! 3 | // source: dwango/nicolive/chat/data/state.proto 4 | // 5 | #pragma warning disable 1591, 0612, 3021, 8981 6 | #region Designer generated code 7 | 8 | using pb = global::Google.Protobuf; 9 | using pbc = global::Google.Protobuf.Collections; 10 | using pbr = global::Google.Protobuf.Reflection; 11 | using scg = global::System.Collections.Generic; 12 | namespace Dwango.Nicolive.Chat.Data { 13 | 14 | /// Holder for reflection information generated from dwango/nicolive/chat/data/state.proto 15 | public static partial class StateReflection { 16 | 17 | #region Descriptor 18 | /// File descriptor for dwango/nicolive/chat/data/state.proto 19 | public static pbr::FileDescriptor Descriptor { 20 | get { return descriptor; } 21 | } 22 | private static pbr::FileDescriptor descriptor; 23 | 24 | static StateReflection() { 25 | byte[] descriptorData = global::System.Convert.FromBase64String( 26 | string.Concat( 27 | "CiVkd2FuZ28vbmljb2xpdmUvY2hhdC9kYXRhL3N0YXRlLnByb3RvEhlkd2Fu", 28 | "Z28ubmljb2xpdmUuY2hhdC5kYXRhGiVkd2FuZ28vbmljb2xpdmUvY2hhdC9k", 29 | "YXRhL2F0b21zLnByb3RvGi9kd2FuZ28vbmljb2xpdmUvY2hhdC9kYXRhL2F0", 30 | "b21zL21vZGVyYXRvci5wcm90byLaBgoNTmljb2xpdmVTdGF0ZRI+CgpzdGF0", 31 | "aXN0aWNzGAEgASgLMiUuZHdhbmdvLm5pY29saXZlLmNoYXQuZGF0YS5TdGF0", 32 | "aXN0aWNzSACIAQESOAoHZW5xdWV0ZRgCIAEoCzIiLmR3YW5nby5uaWNvbGl2", 33 | "ZS5jaGF0LmRhdGEuRW5xdWV0ZUgBiAEBEj0KCm1vdmVfb3JkZXIYAyABKAsy", 34 | "JC5kd2FuZ28ubmljb2xpdmUuY2hhdC5kYXRhLk1vdmVPcmRlckgCiAEBEjgK", 35 | "B21hcnF1ZWUYBCABKAsyIi5kd2FuZ28ubmljb2xpdmUuY2hhdC5kYXRhLk1h", 36 | "cnF1ZWVIA4gBARJBCgxjb21tZW50X2xvY2sYBSABKAsyJi5kd2FuZ28ubmlj", 37 | "b2xpdmUuY2hhdC5kYXRhLkNvbW1lbnRMb2NrSASIAQESQQoMY29tbWVudF9t", 38 | "b2RlGAYgASgLMiYuZHdhbmdvLm5pY29saXZlLmNoYXQuZGF0YS5Db21tZW50", 39 | "TW9kZUgFiAEBEj8KC3RyaWFsX3BhbmVsGAcgASgLMiUuZHdhbmdvLm5pY29s", 40 | "aXZlLmNoYXQuZGF0YS5UcmlhbFBhbmVsSAaIAQESQQoMZmluZ2VyX3ByaW50", 41 | "GAggASgLMiYuZHdhbmdvLm5pY29saXZlLmNoYXQuZGF0YS5GaW5nZXJQcmlu", 42 | "dEgHiAEBEkUKDnByb2dyYW1fc3RhdHVzGAkgASgLMiguZHdhbmdvLm5pY29s", 43 | "aXZlLmNoYXQuZGF0YS5Qcm9ncmFtU3RhdHVzSAiIAQESXQoXbW9kZXJhdGlv", 44 | "bl9hbm5vdW5jZW1lbnQYCiABKAsyNy5kd2FuZ28ubmljb2xpdmUuY2hhdC5k", 45 | "YXRhLmF0b21zLk1vZGVyYXRpb25Bbm5vdW5jZW1lbnRICYgBAUINCgtfc3Rh", 46 | "dGlzdGljc0IKCghfZW5xdWV0ZUINCgtfbW92ZV9vcmRlckIKCghfbWFycXVl", 47 | "ZUIPCg1fY29tbWVudF9sb2NrQg8KDV9jb21tZW50X21vZGVCDgoMX3RyaWFs", 48 | "X3BhbmVsQg8KDV9maW5nZXJfcHJpbnRCEQoPX3Byb2dyYW1fc3RhdHVzQhoK", 49 | "GF9tb2RlcmF0aW9uX2Fubm91bmNlbWVudGIGcHJvdG8z")); 50 | descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, 51 | new pbr::FileDescriptor[] { global::Dwango.Nicolive.Chat.Data.AtomsReflection.Descriptor, global::Dwango.Nicolive.Chat.Data.Atoms.ModeratorReflection.Descriptor, }, 52 | new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { 53 | new pbr::GeneratedClrTypeInfo(typeof(global::Dwango.Nicolive.Chat.Data.NicoliveState), global::Dwango.Nicolive.Chat.Data.NicoliveState.Parser, new[]{ "Statistics", "Enquete", "MoveOrder", "Marquee", "CommentLock", "CommentMode", "TrialPanel", "FingerPrint", "ProgramStatus", "ModerationAnnouncement" }, new[]{ "Statistics", "Enquete", "MoveOrder", "Marquee", "CommentLock", "CommentMode", "TrialPanel", "FingerPrint", "ProgramStatus", "ModerationAnnouncement" }, null, null, null) 54 | })); 55 | } 56 | #endregion 57 | 58 | } 59 | #region Messages 60 | [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] 61 | public sealed partial class NicoliveState : pb::IMessage 62 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 63 | , pb::IBufferMessage 64 | #endif 65 | { 66 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new NicoliveState()); 67 | private pb::UnknownFieldSet _unknownFields; 68 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 69 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 70 | public static pb::MessageParser Parser { get { return _parser; } } 71 | 72 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 73 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 74 | public static pbr::MessageDescriptor Descriptor { 75 | get { return global::Dwango.Nicolive.Chat.Data.StateReflection.Descriptor.MessageTypes[0]; } 76 | } 77 | 78 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 79 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 80 | pbr::MessageDescriptor pb::IMessage.Descriptor { 81 | get { return Descriptor; } 82 | } 83 | 84 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 85 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 86 | public NicoliveState() { 87 | OnConstruction(); 88 | } 89 | 90 | partial void OnConstruction(); 91 | 92 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 93 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 94 | public NicoliveState(NicoliveState other) : this() { 95 | statistics_ = other.statistics_ != null ? other.statistics_.Clone() : null; 96 | enquete_ = other.enquete_ != null ? other.enquete_.Clone() : null; 97 | moveOrder_ = other.moveOrder_ != null ? other.moveOrder_.Clone() : null; 98 | marquee_ = other.marquee_ != null ? other.marquee_.Clone() : null; 99 | commentLock_ = other.commentLock_ != null ? other.commentLock_.Clone() : null; 100 | commentMode_ = other.commentMode_ != null ? other.commentMode_.Clone() : null; 101 | trialPanel_ = other.trialPanel_ != null ? other.trialPanel_.Clone() : null; 102 | fingerPrint_ = other.fingerPrint_ != null ? other.fingerPrint_.Clone() : null; 103 | programStatus_ = other.programStatus_ != null ? other.programStatus_.Clone() : null; 104 | moderationAnnouncement_ = other.moderationAnnouncement_ != null ? other.moderationAnnouncement_.Clone() : null; 105 | _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); 106 | } 107 | 108 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 109 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 110 | public NicoliveState Clone() { 111 | return new NicoliveState(this); 112 | } 113 | 114 | /// Field number for the "statistics" field. 115 | public const int StatisticsFieldNumber = 1; 116 | private global::Dwango.Nicolive.Chat.Data.Statistics statistics_; 117 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 118 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 119 | public global::Dwango.Nicolive.Chat.Data.Statistics Statistics { 120 | get { return statistics_; } 121 | set { 122 | statistics_ = value; 123 | } 124 | } 125 | 126 | /// Field number for the "enquete" field. 127 | public const int EnqueteFieldNumber = 2; 128 | private global::Dwango.Nicolive.Chat.Data.Enquete enquete_; 129 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 130 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 131 | public global::Dwango.Nicolive.Chat.Data.Enquete Enquete { 132 | get { return enquete_; } 133 | set { 134 | enquete_ = value; 135 | } 136 | } 137 | 138 | /// Field number for the "move_order" field. 139 | public const int MoveOrderFieldNumber = 3; 140 | private global::Dwango.Nicolive.Chat.Data.MoveOrder moveOrder_; 141 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 142 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 143 | public global::Dwango.Nicolive.Chat.Data.MoveOrder MoveOrder { 144 | get { return moveOrder_; } 145 | set { 146 | moveOrder_ = value; 147 | } 148 | } 149 | 150 | /// Field number for the "marquee" field. 151 | public const int MarqueeFieldNumber = 4; 152 | private global::Dwango.Nicolive.Chat.Data.Marquee marquee_; 153 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 154 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 155 | public global::Dwango.Nicolive.Chat.Data.Marquee Marquee { 156 | get { return marquee_; } 157 | set { 158 | marquee_ = value; 159 | } 160 | } 161 | 162 | /// Field number for the "comment_lock" field. 163 | public const int CommentLockFieldNumber = 5; 164 | private global::Dwango.Nicolive.Chat.Data.CommentLock commentLock_; 165 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 166 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 167 | public global::Dwango.Nicolive.Chat.Data.CommentLock CommentLock { 168 | get { return commentLock_; } 169 | set { 170 | commentLock_ = value; 171 | } 172 | } 173 | 174 | /// Field number for the "comment_mode" field. 175 | public const int CommentModeFieldNumber = 6; 176 | private global::Dwango.Nicolive.Chat.Data.CommentMode commentMode_; 177 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 178 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 179 | public global::Dwango.Nicolive.Chat.Data.CommentMode CommentMode { 180 | get { return commentMode_; } 181 | set { 182 | commentMode_ = value; 183 | } 184 | } 185 | 186 | /// Field number for the "trial_panel" field. 187 | public const int TrialPanelFieldNumber = 7; 188 | private global::Dwango.Nicolive.Chat.Data.TrialPanel trialPanel_; 189 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 190 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 191 | public global::Dwango.Nicolive.Chat.Data.TrialPanel TrialPanel { 192 | get { return trialPanel_; } 193 | set { 194 | trialPanel_ = value; 195 | } 196 | } 197 | 198 | /// Field number for the "finger_print" field. 199 | public const int FingerPrintFieldNumber = 8; 200 | private global::Dwango.Nicolive.Chat.Data.FingerPrint fingerPrint_; 201 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 202 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 203 | public global::Dwango.Nicolive.Chat.Data.FingerPrint FingerPrint { 204 | get { return fingerPrint_; } 205 | set { 206 | fingerPrint_ = value; 207 | } 208 | } 209 | 210 | /// Field number for the "program_status" field. 211 | public const int ProgramStatusFieldNumber = 9; 212 | private global::Dwango.Nicolive.Chat.Data.ProgramStatus programStatus_; 213 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 214 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 215 | public global::Dwango.Nicolive.Chat.Data.ProgramStatus ProgramStatus { 216 | get { return programStatus_; } 217 | set { 218 | programStatus_ = value; 219 | } 220 | } 221 | 222 | /// Field number for the "moderation_announcement" field. 223 | public const int ModerationAnnouncementFieldNumber = 10; 224 | private global::Dwango.Nicolive.Chat.Data.Atoms.ModerationAnnouncement moderationAnnouncement_; 225 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 226 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 227 | public global::Dwango.Nicolive.Chat.Data.Atoms.ModerationAnnouncement ModerationAnnouncement { 228 | get { return moderationAnnouncement_; } 229 | set { 230 | moderationAnnouncement_ = value; 231 | } 232 | } 233 | 234 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 235 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 236 | public override bool Equals(object other) { 237 | return Equals(other as NicoliveState); 238 | } 239 | 240 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 241 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 242 | public bool Equals(NicoliveState other) { 243 | if (ReferenceEquals(other, null)) { 244 | return false; 245 | } 246 | if (ReferenceEquals(other, this)) { 247 | return true; 248 | } 249 | if (!object.Equals(Statistics, other.Statistics)) return false; 250 | if (!object.Equals(Enquete, other.Enquete)) return false; 251 | if (!object.Equals(MoveOrder, other.MoveOrder)) return false; 252 | if (!object.Equals(Marquee, other.Marquee)) return false; 253 | if (!object.Equals(CommentLock, other.CommentLock)) return false; 254 | if (!object.Equals(CommentMode, other.CommentMode)) return false; 255 | if (!object.Equals(TrialPanel, other.TrialPanel)) return false; 256 | if (!object.Equals(FingerPrint, other.FingerPrint)) return false; 257 | if (!object.Equals(ProgramStatus, other.ProgramStatus)) return false; 258 | if (!object.Equals(ModerationAnnouncement, other.ModerationAnnouncement)) return false; 259 | return Equals(_unknownFields, other._unknownFields); 260 | } 261 | 262 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 263 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 264 | public override int GetHashCode() { 265 | int hash = 1; 266 | if (statistics_ != null) hash ^= Statistics.GetHashCode(); 267 | if (enquete_ != null) hash ^= Enquete.GetHashCode(); 268 | if (moveOrder_ != null) hash ^= MoveOrder.GetHashCode(); 269 | if (marquee_ != null) hash ^= Marquee.GetHashCode(); 270 | if (commentLock_ != null) hash ^= CommentLock.GetHashCode(); 271 | if (commentMode_ != null) hash ^= CommentMode.GetHashCode(); 272 | if (trialPanel_ != null) hash ^= TrialPanel.GetHashCode(); 273 | if (fingerPrint_ != null) hash ^= FingerPrint.GetHashCode(); 274 | if (programStatus_ != null) hash ^= ProgramStatus.GetHashCode(); 275 | if (moderationAnnouncement_ != null) hash ^= ModerationAnnouncement.GetHashCode(); 276 | if (_unknownFields != null) { 277 | hash ^= _unknownFields.GetHashCode(); 278 | } 279 | return hash; 280 | } 281 | 282 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 283 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 284 | public override string ToString() { 285 | return pb::JsonFormatter.ToDiagnosticString(this); 286 | } 287 | 288 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 289 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 290 | public void WriteTo(pb::CodedOutputStream output) { 291 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 292 | output.WriteRawMessage(this); 293 | #else 294 | if (statistics_ != null) { 295 | output.WriteRawTag(10); 296 | output.WriteMessage(Statistics); 297 | } 298 | if (enquete_ != null) { 299 | output.WriteRawTag(18); 300 | output.WriteMessage(Enquete); 301 | } 302 | if (moveOrder_ != null) { 303 | output.WriteRawTag(26); 304 | output.WriteMessage(MoveOrder); 305 | } 306 | if (marquee_ != null) { 307 | output.WriteRawTag(34); 308 | output.WriteMessage(Marquee); 309 | } 310 | if (commentLock_ != null) { 311 | output.WriteRawTag(42); 312 | output.WriteMessage(CommentLock); 313 | } 314 | if (commentMode_ != null) { 315 | output.WriteRawTag(50); 316 | output.WriteMessage(CommentMode); 317 | } 318 | if (trialPanel_ != null) { 319 | output.WriteRawTag(58); 320 | output.WriteMessage(TrialPanel); 321 | } 322 | if (fingerPrint_ != null) { 323 | output.WriteRawTag(66); 324 | output.WriteMessage(FingerPrint); 325 | } 326 | if (programStatus_ != null) { 327 | output.WriteRawTag(74); 328 | output.WriteMessage(ProgramStatus); 329 | } 330 | if (moderationAnnouncement_ != null) { 331 | output.WriteRawTag(82); 332 | output.WriteMessage(ModerationAnnouncement); 333 | } 334 | if (_unknownFields != null) { 335 | _unknownFields.WriteTo(output); 336 | } 337 | #endif 338 | } 339 | 340 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 341 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 342 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 343 | void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { 344 | if (statistics_ != null) { 345 | output.WriteRawTag(10); 346 | output.WriteMessage(Statistics); 347 | } 348 | if (enquete_ != null) { 349 | output.WriteRawTag(18); 350 | output.WriteMessage(Enquete); 351 | } 352 | if (moveOrder_ != null) { 353 | output.WriteRawTag(26); 354 | output.WriteMessage(MoveOrder); 355 | } 356 | if (marquee_ != null) { 357 | output.WriteRawTag(34); 358 | output.WriteMessage(Marquee); 359 | } 360 | if (commentLock_ != null) { 361 | output.WriteRawTag(42); 362 | output.WriteMessage(CommentLock); 363 | } 364 | if (commentMode_ != null) { 365 | output.WriteRawTag(50); 366 | output.WriteMessage(CommentMode); 367 | } 368 | if (trialPanel_ != null) { 369 | output.WriteRawTag(58); 370 | output.WriteMessage(TrialPanel); 371 | } 372 | if (fingerPrint_ != null) { 373 | output.WriteRawTag(66); 374 | output.WriteMessage(FingerPrint); 375 | } 376 | if (programStatus_ != null) { 377 | output.WriteRawTag(74); 378 | output.WriteMessage(ProgramStatus); 379 | } 380 | if (moderationAnnouncement_ != null) { 381 | output.WriteRawTag(82); 382 | output.WriteMessage(ModerationAnnouncement); 383 | } 384 | if (_unknownFields != null) { 385 | _unknownFields.WriteTo(ref output); 386 | } 387 | } 388 | #endif 389 | 390 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 391 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 392 | public int CalculateSize() { 393 | int size = 0; 394 | if (statistics_ != null) { 395 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Statistics); 396 | } 397 | if (enquete_ != null) { 398 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Enquete); 399 | } 400 | if (moveOrder_ != null) { 401 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(MoveOrder); 402 | } 403 | if (marquee_ != null) { 404 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(Marquee); 405 | } 406 | if (commentLock_ != null) { 407 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(CommentLock); 408 | } 409 | if (commentMode_ != null) { 410 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(CommentMode); 411 | } 412 | if (trialPanel_ != null) { 413 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(TrialPanel); 414 | } 415 | if (fingerPrint_ != null) { 416 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(FingerPrint); 417 | } 418 | if (programStatus_ != null) { 419 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(ProgramStatus); 420 | } 421 | if (moderationAnnouncement_ != null) { 422 | size += 1 + pb::CodedOutputStream.ComputeMessageSize(ModerationAnnouncement); 423 | } 424 | if (_unknownFields != null) { 425 | size += _unknownFields.CalculateSize(); 426 | } 427 | return size; 428 | } 429 | 430 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 431 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 432 | public void MergeFrom(NicoliveState other) { 433 | if (other == null) { 434 | return; 435 | } 436 | if (other.statistics_ != null) { 437 | if (statistics_ == null) { 438 | Statistics = new global::Dwango.Nicolive.Chat.Data.Statistics(); 439 | } 440 | Statistics.MergeFrom(other.Statistics); 441 | } 442 | if (other.enquete_ != null) { 443 | if (enquete_ == null) { 444 | Enquete = new global::Dwango.Nicolive.Chat.Data.Enquete(); 445 | } 446 | Enquete.MergeFrom(other.Enquete); 447 | } 448 | if (other.moveOrder_ != null) { 449 | if (moveOrder_ == null) { 450 | MoveOrder = new global::Dwango.Nicolive.Chat.Data.MoveOrder(); 451 | } 452 | MoveOrder.MergeFrom(other.MoveOrder); 453 | } 454 | if (other.marquee_ != null) { 455 | if (marquee_ == null) { 456 | Marquee = new global::Dwango.Nicolive.Chat.Data.Marquee(); 457 | } 458 | Marquee.MergeFrom(other.Marquee); 459 | } 460 | if (other.commentLock_ != null) { 461 | if (commentLock_ == null) { 462 | CommentLock = new global::Dwango.Nicolive.Chat.Data.CommentLock(); 463 | } 464 | CommentLock.MergeFrom(other.CommentLock); 465 | } 466 | if (other.commentMode_ != null) { 467 | if (commentMode_ == null) { 468 | CommentMode = new global::Dwango.Nicolive.Chat.Data.CommentMode(); 469 | } 470 | CommentMode.MergeFrom(other.CommentMode); 471 | } 472 | if (other.trialPanel_ != null) { 473 | if (trialPanel_ == null) { 474 | TrialPanel = new global::Dwango.Nicolive.Chat.Data.TrialPanel(); 475 | } 476 | TrialPanel.MergeFrom(other.TrialPanel); 477 | } 478 | if (other.fingerPrint_ != null) { 479 | if (fingerPrint_ == null) { 480 | FingerPrint = new global::Dwango.Nicolive.Chat.Data.FingerPrint(); 481 | } 482 | FingerPrint.MergeFrom(other.FingerPrint); 483 | } 484 | if (other.programStatus_ != null) { 485 | if (programStatus_ == null) { 486 | ProgramStatus = new global::Dwango.Nicolive.Chat.Data.ProgramStatus(); 487 | } 488 | ProgramStatus.MergeFrom(other.ProgramStatus); 489 | } 490 | if (other.moderationAnnouncement_ != null) { 491 | if (moderationAnnouncement_ == null) { 492 | ModerationAnnouncement = new global::Dwango.Nicolive.Chat.Data.Atoms.ModerationAnnouncement(); 493 | } 494 | ModerationAnnouncement.MergeFrom(other.ModerationAnnouncement); 495 | } 496 | _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); 497 | } 498 | 499 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 500 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 501 | public void MergeFrom(pb::CodedInputStream input) { 502 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 503 | input.ReadRawMessage(this); 504 | #else 505 | uint tag; 506 | while ((tag = input.ReadTag()) != 0) { 507 | if ((tag & 7) == 4) { 508 | // Abort on any end group tag. 509 | return; 510 | } 511 | switch(tag) { 512 | default: 513 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); 514 | break; 515 | case 10: { 516 | if (statistics_ == null) { 517 | Statistics = new global::Dwango.Nicolive.Chat.Data.Statistics(); 518 | } 519 | input.ReadMessage(Statistics); 520 | break; 521 | } 522 | case 18: { 523 | if (enquete_ == null) { 524 | Enquete = new global::Dwango.Nicolive.Chat.Data.Enquete(); 525 | } 526 | input.ReadMessage(Enquete); 527 | break; 528 | } 529 | case 26: { 530 | if (moveOrder_ == null) { 531 | MoveOrder = new global::Dwango.Nicolive.Chat.Data.MoveOrder(); 532 | } 533 | input.ReadMessage(MoveOrder); 534 | break; 535 | } 536 | case 34: { 537 | if (marquee_ == null) { 538 | Marquee = new global::Dwango.Nicolive.Chat.Data.Marquee(); 539 | } 540 | input.ReadMessage(Marquee); 541 | break; 542 | } 543 | case 42: { 544 | if (commentLock_ == null) { 545 | CommentLock = new global::Dwango.Nicolive.Chat.Data.CommentLock(); 546 | } 547 | input.ReadMessage(CommentLock); 548 | break; 549 | } 550 | case 50: { 551 | if (commentMode_ == null) { 552 | CommentMode = new global::Dwango.Nicolive.Chat.Data.CommentMode(); 553 | } 554 | input.ReadMessage(CommentMode); 555 | break; 556 | } 557 | case 58: { 558 | if (trialPanel_ == null) { 559 | TrialPanel = new global::Dwango.Nicolive.Chat.Data.TrialPanel(); 560 | } 561 | input.ReadMessage(TrialPanel); 562 | break; 563 | } 564 | case 66: { 565 | if (fingerPrint_ == null) { 566 | FingerPrint = new global::Dwango.Nicolive.Chat.Data.FingerPrint(); 567 | } 568 | input.ReadMessage(FingerPrint); 569 | break; 570 | } 571 | case 74: { 572 | if (programStatus_ == null) { 573 | ProgramStatus = new global::Dwango.Nicolive.Chat.Data.ProgramStatus(); 574 | } 575 | input.ReadMessage(ProgramStatus); 576 | break; 577 | } 578 | case 82: { 579 | if (moderationAnnouncement_ == null) { 580 | ModerationAnnouncement = new global::Dwango.Nicolive.Chat.Data.Atoms.ModerationAnnouncement(); 581 | } 582 | input.ReadMessage(ModerationAnnouncement); 583 | break; 584 | } 585 | } 586 | } 587 | #endif 588 | } 589 | 590 | #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE 591 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 592 | [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] 593 | void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { 594 | uint tag; 595 | while ((tag = input.ReadTag()) != 0) { 596 | if ((tag & 7) == 4) { 597 | // Abort on any end group tag. 598 | return; 599 | } 600 | switch(tag) { 601 | default: 602 | _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); 603 | break; 604 | case 10: { 605 | if (statistics_ == null) { 606 | Statistics = new global::Dwango.Nicolive.Chat.Data.Statistics(); 607 | } 608 | input.ReadMessage(Statistics); 609 | break; 610 | } 611 | case 18: { 612 | if (enquete_ == null) { 613 | Enquete = new global::Dwango.Nicolive.Chat.Data.Enquete(); 614 | } 615 | input.ReadMessage(Enquete); 616 | break; 617 | } 618 | case 26: { 619 | if (moveOrder_ == null) { 620 | MoveOrder = new global::Dwango.Nicolive.Chat.Data.MoveOrder(); 621 | } 622 | input.ReadMessage(MoveOrder); 623 | break; 624 | } 625 | case 34: { 626 | if (marquee_ == null) { 627 | Marquee = new global::Dwango.Nicolive.Chat.Data.Marquee(); 628 | } 629 | input.ReadMessage(Marquee); 630 | break; 631 | } 632 | case 42: { 633 | if (commentLock_ == null) { 634 | CommentLock = new global::Dwango.Nicolive.Chat.Data.CommentLock(); 635 | } 636 | input.ReadMessage(CommentLock); 637 | break; 638 | } 639 | case 50: { 640 | if (commentMode_ == null) { 641 | CommentMode = new global::Dwango.Nicolive.Chat.Data.CommentMode(); 642 | } 643 | input.ReadMessage(CommentMode); 644 | break; 645 | } 646 | case 58: { 647 | if (trialPanel_ == null) { 648 | TrialPanel = new global::Dwango.Nicolive.Chat.Data.TrialPanel(); 649 | } 650 | input.ReadMessage(TrialPanel); 651 | break; 652 | } 653 | case 66: { 654 | if (fingerPrint_ == null) { 655 | FingerPrint = new global::Dwango.Nicolive.Chat.Data.FingerPrint(); 656 | } 657 | input.ReadMessage(FingerPrint); 658 | break; 659 | } 660 | case 74: { 661 | if (programStatus_ == null) { 662 | ProgramStatus = new global::Dwango.Nicolive.Chat.Data.ProgramStatus(); 663 | } 664 | input.ReadMessage(ProgramStatus); 665 | break; 666 | } 667 | case 82: { 668 | if (moderationAnnouncement_ == null) { 669 | ModerationAnnouncement = new global::Dwango.Nicolive.Chat.Data.Atoms.ModerationAnnouncement(); 670 | } 671 | input.ReadMessage(ModerationAnnouncement); 672 | break; 673 | } 674 | } 675 | } 676 | } 677 | #endif 678 | 679 | } 680 | 681 | #endregion 682 | 683 | } 684 | 685 | #endregion Designer generated code 686 | -------------------------------------------------------------------------------- /NdgrClientSharp.Test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /NdgrClientSharp.Test/NdgrClientSharp.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /NdgrClientSharp.Test/NdgrLiveCommentFetcherSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Dwango.Nicolive.Chat.Data; 3 | using Dwango.Nicolive.Chat.Service.Edge; 4 | using Google.Protobuf.WellKnownTypes; 5 | using Moq; 6 | using NdgrClientSharp.NdgrApi; 7 | using R3; 8 | 9 | namespace NdgrClientSharp.Test; 10 | 11 | public sealed class NdgrLiveCommentFetcherSpec 12 | { 13 | [Test, Timeout(5000)] 14 | public async Task メッセージを受信しきったらDisconnectする() 15 | { 16 | var apiClientMock = new Mock(); 17 | 18 | apiClientMock 19 | .Setup(x => 20 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 21 | .Returns(new ValueTask( 22 | new ChunkedEntry.Types.ReadyForNext 23 | { 24 | At = 0 25 | } 26 | )); 27 | 28 | apiClientMock 29 | .Setup(x => 30 | x.FetchChunkedMessagesAsync("segment_1", It.IsAny())) 31 | .Returns(CreateChunkedMessagesAsync(TimeSpan.FromMilliseconds(100), "1", "2", "3")); 32 | 33 | apiClientMock 34 | .Setup(x => 35 | x.FetchChunkedMessagesAsync("segment_2", It.IsAny())) 36 | .Returns(CreateChunkedMessagesAsync(TimeSpan.FromMilliseconds(100), "4", "5", "6")); 37 | 38 | apiClientMock 39 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny())) 40 | .Returns(new[] 41 | { 42 | new ChunkedEntry() 43 | { 44 | Segment = new MessageSegment() 45 | { 46 | Uri = "segment_1", 47 | From = new Timestamp() 48 | { 49 | Seconds = 0 // 時間としてはめちゃくちゃだが処理は進むからOK 50 | } 51 | } 52 | }, 53 | // 1回目はNextがある 54 | new ChunkedEntry() 55 | { 56 | Next = new ChunkedEntry.Types.ReadyForNext() 57 | { 58 | At = 1 59 | } 60 | } 61 | }.ToAsyncEnumerable()); 62 | 63 | apiClientMock 64 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 1, It.IsAny())) 65 | .Returns(new[] 66 | { 67 | // 2回目はNextがない 68 | new ChunkedEntry() 69 | { 70 | Segment = new MessageSegment() 71 | { 72 | Uri = "segment_2", 73 | From = new Timestamp() 74 | { 75 | Seconds = 0 // 時間としてはめちゃくちゃだが処理は進むからOK 76 | } 77 | } 78 | } 79 | }.ToAsyncEnumerable()); 80 | 81 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 82 | 83 | // 結果の保持用 84 | var list = fetcher.OnMessageReceived.Select(x => x.Message.Chat.Content).ToLiveList(); 85 | var statusList = fetcher.ConnectionStatus.ToLiveList(); 86 | 87 | // 取得開始 88 | fetcher.Connect("test"); 89 | 90 | // Disconnectされるまで待つ 91 | await fetcher.ConnectionStatus 92 | .Where(x => x == ConnectionState.Disconnected) 93 | .FirstAsync(); 94 | 95 | Assert.Multiple(() => 96 | { 97 | // Segmentの時間が適当なのでメッセージは意図した順番ではこない 98 | // ただしここでは全メッセージが受信できていることさえ見れれば良い 99 | CollectionAssert.AreEqual(new[] { "1", "2", "3", "4", "5", "6" }, list.Order()); 100 | 101 | // 切断まで進んでいるはず 102 | CollectionAssert.AreEqual( 103 | new[] 104 | { 105 | ConnectionState.Disconnected, 106 | ConnectionState.Connecting, 107 | ConnectionState.Connected, 108 | ConnectionState.Disconnected, 109 | }, statusList 110 | ); 111 | }); 112 | } 113 | 114 | 115 | [Test] 116 | public void 最初の取得時にServiceUnavailableのときは上限までリトライする() 117 | { 118 | var apiClientMock = new Mock(); 119 | 120 | var ex = new NdgrApiClientHttpException(HttpStatusCode.ServiceUnavailable); 121 | 122 | // 呼び出されても例外をなげる 123 | apiClientMock 124 | .Setup(x => 125 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 126 | .Throws(ex); 127 | 128 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 129 | 130 | // 最大5回までリトライ(なので最初の一回と合わせて計6回通信する) 131 | fetcher.MaxRetryCount = 5; 132 | fetcher.RetryInterval = TimeSpan.FromMilliseconds(0); 133 | 134 | // 結果の保持用 135 | var list = fetcher.OnMessageReceived.Materialize().ToLiveList(); 136 | 137 | // 取得開始 138 | fetcher.Connect(""); 139 | Assert.Multiple(() => 140 | { 141 | // OnErrorResumeが6回発火しているはず 142 | Assert.That(list.Count, Is.EqualTo(6)); 143 | Assert.That(list.All(x => x.Kind == NotificationKind.OnErrorResume), Is.True); 144 | 145 | // 最終的に切断している 146 | Assert.That(fetcher.ConnectionStatus.CurrentValue, Is.EqualTo(ConnectionState.Disconnected)); 147 | }); 148 | } 149 | 150 | [Test, Timeout(1000)] 151 | public void 最初の取得時にメッセージ破損はリトライする() 152 | { 153 | var apiClientMock = new Mock(); 154 | 155 | var ex = new NdgrApiClientByteReadException("Failed to read varint from the stream."); 156 | 157 | // 呼び出されても例外をなげる 158 | apiClientMock 159 | .Setup(x => 160 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 161 | .Throws(ex); 162 | 163 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 164 | fetcher.MaxRetryCount = 5; 165 | fetcher.RetryInterval = TimeSpan.FromMilliseconds(0); 166 | 167 | // 結果の保持用 168 | var list = fetcher.OnMessageReceived.Materialize().ToLiveList(); 169 | 170 | // 取得開始 171 | fetcher.Connect(""); 172 | Assert.Multiple(() => 173 | { 174 | // OnErrorResumeが発火している 175 | Assert.That(list.Count, Is.GreaterThan(1)); 176 | Assert.That(list.All(x => x.Kind == NotificationKind.OnErrorResume), Is.True); 177 | 178 | // 最終的に切断している 179 | Assert.That(fetcher.ConnectionStatus.CurrentValue, Is.EqualTo(ConnectionState.Disconnected)); 180 | }); 181 | } 182 | 183 | [Test] 184 | public void 最初の取得時にその他のエラー時はリトライせずに諦める() 185 | { 186 | var apiClientMock = new Mock(); 187 | 188 | var ex = new NdgrApiClientHttpException(HttpStatusCode.NotFound); 189 | 190 | // 呼び出されても例外をなげる 191 | apiClientMock 192 | .Setup(x => 193 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 194 | .Throws(ex); 195 | 196 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 197 | fetcher.MaxRetryCount = 5; 198 | fetcher.RetryInterval = TimeSpan.FromMilliseconds(0); 199 | 200 | // 結果の保持用 201 | var list = fetcher.OnMessageReceived.Materialize().ToLiveList(); 202 | 203 | // 取得開始 204 | fetcher.Connect(""); 205 | Assert.Multiple(() => 206 | { 207 | // OnErrorResumeが1回発火しているはず 208 | Assert.That(list.Count, Is.EqualTo(1)); 209 | Assert.That(list.All(x => x.Kind == NotificationKind.OnErrorResume), Is.True); 210 | 211 | // 最終的に切断している 212 | Assert.That(fetcher.ConnectionStatus.CurrentValue, Is.EqualTo(ConnectionState.Disconnected)); 213 | }); 214 | } 215 | 216 | [Test] 217 | public void Viewを連続して取得中にServiceUnavailableが出た場合はRecconectする() 218 | { 219 | var apiClientMock = new Mock(); 220 | 221 | var ex = new NdgrApiClientHttpException(HttpStatusCode.ServiceUnavailable); 222 | 223 | var count = 0; 224 | 225 | 226 | apiClientMock 227 | .Setup(x => 228 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 229 | .Returns(new ValueTask( 230 | new ChunkedEntry.Types.ReadyForNext 231 | { 232 | At = 0 233 | } 234 | )); 235 | 236 | // 最初のViewAtの取得で例外 237 | apiClientMock 238 | .SetupSequence(x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny())) 239 | .Throws(ex) 240 | .Returns(Array.Empty().ToAsyncEnumerable); 241 | 242 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 243 | 244 | // 結果の保持用 245 | var list = fetcher.OnMessageReceived.Materialize().ToLiveList(); 246 | var statusList = fetcher.ConnectionStatus.ToLiveList(); 247 | 248 | // 取得開始 249 | fetcher.Connect("test"); 250 | 251 | Assert.Multiple(() => 252 | { 253 | // OnErrorResumeが1回発火しているはず 254 | Assert.That(list.Count, Is.EqualTo(1)); 255 | Assert.That(list.All(x => x.Kind == NotificationKind.OnErrorResume), Is.True); 256 | 257 | // それぞれ呼び出されているはず 258 | apiClientMock 259 | .Verify( 260 | x => x.FetchViewAtNowAsync(It.IsAny(), It.IsAny()), 261 | Times.Exactly(2) 262 | ); 263 | 264 | apiClientMock 265 | .Verify( 266 | x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny()), 267 | Times.Exactly(2) 268 | ); 269 | 270 | 271 | CollectionAssert.AreEqual( 272 | new[] 273 | { 274 | // 1回目の接続 275 | ConnectionState.Disconnected, 276 | ConnectionState.Connecting, 277 | ConnectionState.Connected, 278 | // 再接続 279 | ConnectionState.Disconnected, 280 | ConnectionState.Connecting, 281 | ConnectionState.Connected, 282 | ConnectionState.Disconnected 283 | }, statusList 284 | ); 285 | }); 286 | } 287 | 288 | 289 | [Test] 290 | public void Viewを連続して取得中にバイトが破損した出た場合はRecconectする() 291 | { 292 | var apiClientMock = new Mock(); 293 | 294 | var ex = new NdgrApiClientByteReadException("Failed to read varint from the stream."); 295 | 296 | apiClientMock 297 | .Setup(x => 298 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 299 | .Returns(new ValueTask( 300 | new ChunkedEntry.Types.ReadyForNext 301 | { 302 | At = 0 303 | } 304 | )); 305 | 306 | // 最初のViewAtの取得で例外 307 | apiClientMock 308 | .SetupSequence(x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny())) 309 | .Throws(ex) 310 | .Returns(Array.Empty().ToAsyncEnumerable); 311 | 312 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 313 | 314 | // 結果の保持用 315 | var list = fetcher.OnMessageReceived.Materialize().ToLiveList(); 316 | var statusList = fetcher.ConnectionStatus.ToLiveList(); 317 | 318 | // 取得開始 319 | fetcher.Connect("test"); 320 | 321 | Assert.Multiple(() => 322 | { 323 | // OnErrorResumeが1回発火しているはず 324 | Assert.That(list.Count, Is.EqualTo(1)); 325 | Assert.That(list.All(x => x.Kind == NotificationKind.OnErrorResume), Is.True); 326 | 327 | // それぞれ呼び出されているはず 328 | apiClientMock 329 | .Verify( 330 | x => x.FetchViewAtNowAsync(It.IsAny(), It.IsAny()), 331 | Times.Exactly(2) 332 | ); 333 | 334 | apiClientMock 335 | .Verify( 336 | x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny()), 337 | Times.Exactly(2) 338 | ); 339 | 340 | 341 | CollectionAssert.AreEqual( 342 | new[] 343 | { 344 | // 1回目の接続 345 | ConnectionState.Disconnected, 346 | ConnectionState.Connecting, 347 | ConnectionState.Connected, 348 | // 再接続 349 | ConnectionState.Disconnected, 350 | ConnectionState.Connecting, 351 | ConnectionState.Connected, 352 | ConnectionState.Disconnected 353 | }, statusList 354 | ); 355 | }); 356 | } 357 | 358 | [Test] 359 | public void 受信中のSegmentが正しく反映される() 360 | { 361 | var apiClientMock = new Mock(); 362 | 363 | apiClientMock 364 | .Setup(x => 365 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 366 | .Returns(new ValueTask( 367 | new ChunkedEntry.Types.ReadyForNext 368 | { 369 | At = 0 370 | } 371 | )); 372 | 373 | apiClientMock 374 | .Setup(x => 375 | x.FetchChunkedMessagesAsync("segment_1", It.IsAny())) 376 | .Returns(CreateChunkedMessagesAsync(TimeSpan.FromMilliseconds(100), "1", "2", "3")); 377 | 378 | apiClientMock 379 | .Setup(x => 380 | x.FetchChunkedMessagesAsync("segment_2", It.IsAny())) 381 | .Returns(CreateChunkedMessagesAsync(TimeSpan.FromMilliseconds(100), "4", "5", "6")); 382 | 383 | 384 | apiClientMock 385 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny())) 386 | .Returns(new[] 387 | { 388 | new ChunkedEntry() 389 | { 390 | Segment = new MessageSegment() 391 | { 392 | Uri = "segment_1", 393 | From = new Timestamp() 394 | { 395 | Seconds = 0 // 時間としてはめちゃくちゃだが処理は進むからOK 396 | } 397 | } 398 | }, 399 | new ChunkedEntry() 400 | { 401 | Next = new ChunkedEntry.Types.ReadyForNext() 402 | { 403 | At = 1 404 | } 405 | } 406 | }.ToAsyncEnumerable()); 407 | 408 | 409 | async IAsyncEnumerable Create_1(Task waitTask) 410 | { 411 | await waitTask; 412 | yield return new ChunkedEntry() 413 | { 414 | Segment = new MessageSegment() 415 | { 416 | Uri = "segment_1", 417 | From = new Timestamp() 418 | { 419 | Seconds = 0 420 | } 421 | } 422 | }; 423 | 424 | yield return new ChunkedEntry() 425 | { 426 | Segment = new MessageSegment() 427 | { 428 | Uri = "segment_2", 429 | From = new Timestamp() 430 | { 431 | Seconds = 0 432 | } 433 | } 434 | }; 435 | 436 | yield return new ChunkedEntry() 437 | { 438 | Next = new ChunkedEntry.Types.ReadyForNext() 439 | { 440 | At = 2 441 | } 442 | }; 443 | } 444 | 445 | async IAsyncEnumerable Create_2(Task waitTask) 446 | { 447 | await waitTask; 448 | yield return new ChunkedEntry() 449 | { 450 | Segment = new MessageSegment() 451 | { 452 | Uri = "segment_2", 453 | From = new Timestamp() 454 | { 455 | Seconds = 0 456 | } 457 | } 458 | }; 459 | } 460 | 461 | var tcs1 = new TaskCompletionSource(); 462 | var tcs2 = new TaskCompletionSource(); 463 | apiClientMock 464 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 1, It.IsAny())) 465 | .Returns(Create_1(tcs1.Task)); 466 | 467 | apiClientMock 468 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 2, It.IsAny())) 469 | .Returns(Create_2(tcs2.Task)); 470 | 471 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 472 | 473 | // 取得開始 474 | fetcher.Connect("test"); 475 | 476 | // 最初はSegment1の取得まで進んでいるはず 477 | CollectionAssert.AreEqual(fetcher.CurrentReceivingSegments, new[] { "segment_1" }); 478 | 479 | // 次に進んでSegment1,2の取得まで進んでいるはず 480 | tcs1.SetResult(); 481 | 482 | CollectionAssert.AreEqual(fetcher.CurrentReceivingSegments, new[] { "segment_1", "segment_2" }); 483 | 484 | // 最後に進んでSegment2のみの取得に進んでいるはず 485 | tcs2.SetResult(); 486 | 487 | CollectionAssert.AreEqual(fetcher.CurrentReceivingSegments, new[] { "segment_2" }); 488 | } 489 | 490 | [Test] 491 | public void Reconnect時に重複したコメントは受信しない() 492 | { 493 | var apiClientMock = new Mock(); 494 | 495 | apiClientMock 496 | .Setup(x => 497 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 498 | .Returns(new ValueTask( 499 | new ChunkedEntry.Types.ReadyForNext 500 | { 501 | At = 0 502 | } 503 | )); 504 | 505 | apiClientMock 506 | .Setup(x => 507 | x.FetchChunkedMessagesAsync("segment_1", It.IsAny())) 508 | .Returns(CreateChunkedMessagesAsync(TimeSpan.Zero, "1", "2", "3")); 509 | 510 | async IAsyncEnumerable CreateSegment(Task waitTask) 511 | { 512 | yield return new ChunkedEntry() 513 | { 514 | Segment = new MessageSegment() 515 | { 516 | Uri = "segment_1", 517 | From = new Timestamp() 518 | { 519 | Seconds = 0 520 | } 521 | } 522 | }; 523 | 524 | await waitTask; 525 | 526 | yield return new ChunkedEntry() 527 | { 528 | Next = new ChunkedEntry.Types.ReadyForNext() 529 | { 530 | At = 2 531 | } 532 | }; 533 | } 534 | 535 | var tcs = new TaskCompletionSource(); 536 | apiClientMock 537 | .Setup(x => x.FetchViewAtAsync(It.IsAny(), 0, It.IsAny())) 538 | .Returns(CreateSegment(tcs.Task)); 539 | 540 | var fetcher = new NdgrLiveCommentFetcher(apiClientMock.Object); 541 | 542 | // 結果の保持用 543 | var list = fetcher.OnMessageReceived.Select(x => x.Message.Chat.Content).ToLiveList(); 544 | 545 | // 取得開始 546 | fetcher.Connect("test"); 547 | 548 | // コメントの受信ができている 549 | CollectionAssert.AreEqual(new[] { "1", "2", "3" }, list.Order()); 550 | 551 | // 再接続する 552 | fetcher.Reconnect(); 553 | tcs.SetResult(); 554 | 555 | // 重複したコメントは受信されない 556 | CollectionAssert.AreEqual(new[] { "1", "2", "3" }, list.Order()); 557 | } 558 | 559 | 560 | private async IAsyncEnumerable CreateChunkedMessagesAsync(TimeSpan delayTime, params string[] texts) 561 | { 562 | foreach (var text in texts) 563 | { 564 | await Task.Delay(delayTime); 565 | yield return new ChunkedMessage() 566 | { 567 | Message = new NicoliveMessage() 568 | { 569 | Chat = new Chat() 570 | { 571 | Content = text 572 | } 573 | }, 574 | Meta = new ChunkedMessage.Types.Meta() 575 | { 576 | Id = text 577 | } 578 | }; 579 | } 580 | } 581 | } -------------------------------------------------------------------------------- /NdgrClientSharp.Test/NdgrPastCommentFetcherSpec.cs: -------------------------------------------------------------------------------- 1 | using Dwango.Nicolive.Chat.Data; 2 | using Dwango.Nicolive.Chat.Service.Edge; 3 | using Google.Protobuf.WellKnownTypes; 4 | using Moq; 5 | using NdgrClientSharp.NdgrApi; 6 | using R3; 7 | using ReadyForNext = Dwango.Nicolive.Chat.Service.Edge.ChunkedEntry.Types.ReadyForNext; 8 | 9 | namespace NdgrClientSharp.Test; 10 | 11 | public sealed class NdgrPastCommentFetcherSpec 12 | { 13 | [Test, Timeout(1000)] 14 | public async Task 新しい方のPreviousからコメントが取得される() 15 | { 16 | var apiClientMock = new Mock(); 17 | 18 | apiClientMock 19 | .Setup(x => 20 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 21 | .Returns(new ValueTask( 22 | new ReadyForNext 23 | { 24 | At = 10 25 | } 26 | )); 27 | 28 | // 2つのPreviousが返される 29 | apiClientMock 30 | .Setup(x => 31 | x.FetchViewAtAsync(It.IsAny(), 10, It.IsAny())) 32 | .Returns(new[] 33 | { 34 | new ChunkedEntry() 35 | { 36 | Previous = new MessageSegment() 37 | { 38 | From = new Timestamp() { Seconds = 2 }, 39 | Until = new Timestamp() { Seconds = 3 }, 40 | Uri = "first" 41 | }, 42 | }, 43 | new ChunkedEntry() 44 | { 45 | Previous = new MessageSegment() 46 | { 47 | From = new Timestamp() { Seconds = 1 }, 48 | Until = new Timestamp() { Seconds = 2 }, 49 | Uri = "seconds" 50 | }, 51 | } 52 | }.ToAsyncEnumerable() 53 | ); 54 | 55 | apiClientMock.Setup(x => 56 | x.FetchChunkedMessagesAsync("first", It.IsAny())) 57 | .Returns(CreateChunkedMessages("one").ToAsyncEnumerable); 58 | apiClientMock.Setup(x => 59 | x.FetchChunkedMessagesAsync("seconds", It.IsAny())) 60 | .Returns(CreateChunkedMessages("two").ToAsyncEnumerable); 61 | 62 | var pastCommentFetcher = new NdgrPastCommentFetcher(apiClientMock.Object); 63 | 64 | // 取得実行 65 | var list = await pastCommentFetcher.FetchPastComments("", 1).ToListAsync(); 66 | 67 | // 時刻が新しいほうが呼び出されているはず 68 | apiClientMock.Verify(x => 69 | x.FetchChunkedMessagesAsync("first", It.IsAny()), Times.Once); 70 | apiClientMock.Verify(x => 71 | x.FetchChunkedMessagesAsync("seconds", It.IsAny()), Times.Never); 72 | 73 | CollectionAssert.AreEqual(CreateChunkedMessages("one"), list); 74 | } 75 | 76 | [Test, Timeout(1000)] 77 | public async Task 指定した個数に達するまでコメントを連続取得する() 78 | { 79 | var apiClientMock = new Mock(); 80 | 81 | apiClientMock 82 | .Setup(x => 83 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 84 | .Returns(new ValueTask( 85 | new ReadyForNext 86 | { 87 | At = 10 88 | } 89 | )); 90 | 91 | // PreviousとBackwardを返す 92 | apiClientMock 93 | .Setup(x => 94 | x.FetchViewAtAsync(It.IsAny(), 10, It.IsAny())) 95 | .Returns(new[] 96 | { 97 | new ChunkedEntry() 98 | { 99 | Backward = new BackwardSegment() 100 | { 101 | Until = new Timestamp() { Seconds = 2 }, 102 | Segment = new PackedSegment.Types.Next() 103 | { 104 | Uri = "segment_0" 105 | } 106 | }, 107 | }, 108 | new ChunkedEntry() 109 | { 110 | Previous = new MessageSegment() 111 | { 112 | From = new Timestamp() { Seconds = 2 }, 113 | Until = new Timestamp() { Seconds = 3 }, 114 | Uri = "first" 115 | }, 116 | } 117 | }.ToAsyncEnumerable() 118 | ); 119 | 120 | // Previousがコメントを1個返す 121 | apiClientMock.Setup(x => 122 | x.FetchChunkedMessagesAsync("first", It.IsAny())) 123 | .Returns(CreateChunkedMessages("0").ToAsyncEnumerable); 124 | 125 | // セットアップ 126 | SetupBackwardSegment(apiClientMock, "segment_0", "segment_1", CreateChunkedMessages("1", "2")); 127 | SetupBackwardSegment(apiClientMock, "segment_1", "segment_2", CreateChunkedMessages("3", "4")); 128 | SetupBackwardSegment(apiClientMock, "segment_2", "segment_3", CreateChunkedMessages("5", "6")); 129 | 130 | 131 | var pastCommentFetcher = new NdgrPastCommentFetcher(apiClientMock.Object); 132 | 133 | // 4個を目標に取得 134 | var list = await pastCommentFetcher.FetchPastComments("", 4).ToListAsync(); 135 | 136 | // 実際は5個取得されているはず 137 | Assert.That(list.Count, Is.EqualTo(5)); 138 | 139 | // コメントも期待通り取得されている 140 | var resultChat = list.Select(x => x.Message.Chat.Content).ToArray(); 141 | CollectionAssert.AreEqual(new[] { "0", "1", "2", "3", "4" }, resultChat); 142 | 143 | // 呼び出し数の確認 144 | apiClientMock.Verify(x => 145 | x.FetchChunkedMessagesAsync("first", It.IsAny()), Times.Once); 146 | apiClientMock.Verify(x => 147 | x.FetchPackedSegmentAsync("segment_0", It.IsAny()), Times.Once); 148 | apiClientMock.Verify(x => 149 | x.FetchPackedSegmentAsync("segment_1", It.IsAny()), Times.Once); 150 | // ここまでは達してない 151 | apiClientMock.Verify(x => 152 | x.FetchPackedSegmentAsync("segment_2", It.IsAny()), Times.Never); 153 | } 154 | 155 | [Test, Timeout(1000)] 156 | public async Task 個数を指定しない場合は最後まで連続取得する() 157 | { 158 | var apiClientMock = new Mock(); 159 | 160 | apiClientMock 161 | .Setup(x => 162 | x.FetchViewAtNowAsync(It.IsAny(), It.IsAny())) 163 | .Returns(new ValueTask( 164 | new ReadyForNext 165 | { 166 | At = 10 167 | } 168 | )); 169 | 170 | // PreviousとBackwardを返す 171 | apiClientMock 172 | .Setup(x => 173 | x.FetchViewAtAsync(It.IsAny(), 10, It.IsAny())) 174 | .Returns(new[] 175 | { 176 | new ChunkedEntry() 177 | { 178 | Backward = new BackwardSegment() 179 | { 180 | Until = new Timestamp() { Seconds = 2 }, 181 | Segment = new PackedSegment.Types.Next() 182 | { 183 | Uri = "segment_0" 184 | } 185 | }, 186 | }, 187 | new ChunkedEntry() 188 | { 189 | Previous = new MessageSegment() 190 | { 191 | From = new Timestamp() { Seconds = 2 }, 192 | Until = new Timestamp() { Seconds = 3 }, 193 | Uri = "first" 194 | }, 195 | } 196 | }.ToAsyncEnumerable() 197 | ); 198 | 199 | // Previousがコメントを1個返す 200 | apiClientMock.Setup(x => 201 | x.FetchChunkedMessagesAsync("first", It.IsAny())) 202 | .Returns(CreateChunkedMessages("0").ToAsyncEnumerable); 203 | 204 | // セットアップ 205 | SetupBackwardSegment(apiClientMock, "segment_0", "segment_1", CreateChunkedMessages("1", "2")); 206 | SetupBackwardSegment(apiClientMock, "segment_1", "segment_2", CreateChunkedMessages("3", "4")); 207 | SetupBackwardSegment(apiClientMock, "segment_2", null, CreateChunkedMessages("5", "6")); 208 | 209 | var pastCommentFetcher = new NdgrPastCommentFetcher(apiClientMock.Object); 210 | 211 | // 上限無しで取得 212 | var list = await pastCommentFetcher.FetchPastComments("").ToListAsync(); 213 | 214 | // 7個取得されているはず 215 | Assert.That(list.Count, Is.EqualTo(7)); 216 | 217 | // コメントも期待通り取得されている 218 | var resultChat = list.Select(x => x.Message.Chat.Content).ToArray(); 219 | CollectionAssert.AreEqual(new[] { "0", "1", "2", "3", "4", "5", "6" }, resultChat); 220 | 221 | // 呼び出し数の確認 222 | apiClientMock.Verify(x => 223 | x.FetchChunkedMessagesAsync("first", It.IsAny()), Times.Once); 224 | apiClientMock.Verify(x => 225 | x.FetchPackedSegmentAsync("segment_0", It.IsAny()), Times.Once); 226 | apiClientMock.Verify(x => 227 | x.FetchPackedSegmentAsync("segment_1", It.IsAny()), Times.Once); 228 | apiClientMock.Verify(x => 229 | x.FetchPackedSegmentAsync("segment_2", It.IsAny()), Times.Once); 230 | } 231 | 232 | 233 | private ChunkedMessage[] CreateChunkedMessages(params string[] texts) 234 | { 235 | return texts.Select(x => new ChunkedMessage() 236 | { 237 | Message = new NicoliveMessage() 238 | { 239 | Chat = new Chat() 240 | { 241 | Content = x 242 | } 243 | } 244 | }) 245 | .ToArray(); 246 | } 247 | 248 | private void SetupBackwardSegment(Mock apiClientMock, 249 | string currentUir, 250 | string? nextUri, 251 | ChunkedMessage[] messages) 252 | { 253 | PackedSegment packedSegment; 254 | if (nextUri == null) 255 | { 256 | packedSegment = new PackedSegment() 257 | { 258 | Messages = { messages }, 259 | }; 260 | } 261 | else 262 | { 263 | packedSegment = new PackedSegment() 264 | { 265 | Messages = { messages }, 266 | Next = new PackedSegment.Types.Next() 267 | { 268 | Uri = nextUri 269 | } 270 | }; 271 | } 272 | 273 | apiClientMock 274 | .Setup(x => 275 | x.FetchPackedSegmentAsync(currentUir, It.IsAny())) 276 | .Returns(new ValueTask(packedSegment)); 277 | } 278 | } -------------------------------------------------------------------------------- /NdgrClientSharp.Test/NdgrProtobufStreamReaderSpec.cs: -------------------------------------------------------------------------------- 1 | using NdgrClientSharp.Utilities; 2 | 3 | namespace NdgrClientSharp.Test; 4 | 5 | public class NdgrProtobufStreamReaderSpec 6 | { 7 | private byte[] EncodeVarint(ulong value) 8 | { 9 | using var ms = new MemoryStream(); 10 | while (value > 0x7F) 11 | { 12 | ms.WriteByte((byte)((value & 0x7F) | 0x80)); 13 | value >>= 7; 14 | } 15 | 16 | ms.WriteByte((byte)value); 17 | return ms.ToArray(); 18 | } 19 | 20 | [TestCase(0u)] 21 | [TestCase(1u)] 22 | [TestCase(127u)] 23 | [TestCase(128u)] 24 | [TestCase(300u)] 25 | [TestCase(uint.MaxValue)] 26 | public void ReadVarint_uintの範囲内を正しく読める(uint expected) 27 | { 28 | var reader = new NdgrProtobufStreamReader(); 29 | 30 | // encodeして戻す 31 | var encoded = EncodeVarint(expected); 32 | 33 | reader.AddNewChunk(encoded); 34 | var result = reader.ReadVarint(); 35 | 36 | Assert.IsNotNull(result); 37 | Assert.That(result.Value.result, Is.EqualTo(expected)); 38 | } 39 | 40 | [TestCase(((ulong)123456 << 32) + 0u, 0u)] 41 | [TestCase(((ulong)123456 << 32) + 1u, 1u)] 42 | [TestCase(((ulong)123456 << 32) + 127u, 127u)] 43 | [TestCase(((ulong)123456 << 32) + 128u, 128u)] 44 | [TestCase(((ulong)123456 << 32) + 300u, 300u)] 45 | [TestCase(((ulong)123456 << 32) + uint.MaxValue, uint.MaxValue)] 46 | public void ReadVarint_ulongの場合はuintの範囲のみ読み取られる(ulong input, uint expected) 47 | { 48 | var reader = new NdgrProtobufStreamReader(); 49 | 50 | // encodeして戻す 51 | var encoded = EncodeVarint(input); 52 | 53 | reader.AddNewChunk(encoded); 54 | var result = reader.ReadVarint(); 55 | 56 | Assert.IsNotNull(result); 57 | Assert.That(result.Value.result, Is.EqualTo(expected)); 58 | } 59 | 60 | [Test] 61 | public void ReadVarint_不十分なVarintがある場合はnullを返す() 62 | { 63 | var reader = new NdgrProtobufStreamReader(); 64 | reader.AddNewChunk([0x80]); // 0x80は不完全なVarint 65 | 66 | var result = reader.ReadVarint(); 67 | 68 | Assert.IsNull(result); 69 | } 70 | 71 | [Test] 72 | public void ReadVarint_64bitを超えるVarintがある場合は例外() 73 | { 74 | var reader = new NdgrProtobufStreamReader(); 75 | reader.AddNewChunk([ 76 | 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 // over 64bit 77 | ]); 78 | 79 | Assert.Throws(() => reader.ReadVarint()); 80 | } 81 | 82 | [Test] 83 | public void UnshiftChunk_Varintが不十分なうちはfalseを返す() 84 | { 85 | var reader = new NdgrProtobufStreamReader(); 86 | reader.AddNewChunk([0x80]); // 0x80は不完全なVarint 87 | 88 | var (isValid, _) = reader.UnshiftChunk(new byte[1]); 89 | 90 | Assert.IsFalse(isValid); 91 | } 92 | 93 | [Test] 94 | public void UnshiftChunk_データが揃っていない場合はfalse() 95 | { 96 | var reader = new NdgrProtobufStreamReader(); 97 | reader.AddNewChunk(EncodeVarint(123)); // varint分しかない 98 | 99 | var (isValid, _) = reader.UnshiftChunk(new byte[1024]); 100 | 101 | Assert.IsFalse(isValid); 102 | } 103 | 104 | [Test] 105 | public void UnshiftChunk_データが揃っている場合は正常に読み取る() 106 | { 107 | var reader = new NdgrProtobufStreamReader(); 108 | reader.AddNewChunk(EncodeVarint(3)); 109 | 110 | var expected = new byte[] { 1, 2, 3 }; 111 | reader.AddNewChunk(expected); 112 | 113 | var resultBuffer = new byte[1024]; 114 | var (isValid, size) = reader.UnshiftChunk(resultBuffer); 115 | 116 | Assert.IsTrue(isValid); 117 | Assert.That(size, Is.EqualTo(expected.Length)); 118 | Assert.That(resultBuffer[..size], Is.EqualTo(expected)); 119 | } 120 | 121 | [Test] 122 | public void UnshiftChunk_データが大きすぎる場合は例外() 123 | { 124 | var reader = new NdgrProtobufStreamReader(); 125 | reader.AddNewChunk(EncodeVarint(1024)); 126 | 127 | var expected = new byte[1024]; 128 | reader.AddNewChunk(expected); 129 | 130 | var resultBuffer = new byte[100]; // 100バイトしか収容できない 131 | Assert.Throws(() => reader.UnshiftChunk(resultBuffer)); 132 | } 133 | 134 | [Test] 135 | public void UnshiftChunk_データが足りない場合は追加することで読み取りができる() 136 | { 137 | var reader = new NdgrProtobufStreamReader(); 138 | var resultBuffer = new byte[2048]; 139 | 140 | // varintを2byte用意 141 | var varints = EncodeVarint(1024); 142 | 143 | var payload = new byte[1024]; 144 | for (int i = 0; i < payload.Length; i++) 145 | { 146 | payload[i] = (byte)i; 147 | } 148 | 149 | // varintを一部書き込み 150 | reader.AddNewChunk(varints.Take(1).ToArray()); 151 | var result = reader.UnshiftChunk(resultBuffer); 152 | 153 | // まだ足りない 154 | Assert.IsFalse(result.isValid); 155 | 156 | // 残りを書き込み 157 | reader.AddNewChunk(varints.Skip(1).ToArray()); 158 | result = reader.UnshiftChunk(resultBuffer); 159 | 160 | // varintは揃ったが、payloadはまだ足りない 161 | Assert.IsFalse(result.isValid); 162 | 163 | // ------ 164 | 165 | // payloadを部分的に書き込む 166 | reader.AddNewChunk(payload.Take(512).ToArray()); 167 | 168 | // まだ足りない 169 | result = reader.UnshiftChunk(resultBuffer); 170 | Assert.IsFalse(result.isValid); 171 | 172 | // ---- 173 | 174 | // payloadを残りを書き込む 175 | reader.AddNewChunk(payload.Skip(512).ToArray()); 176 | result = reader.UnshiftChunk(resultBuffer); 177 | 178 | // すべて揃った 179 | Assert.IsTrue(result.isValid); 180 | Assert.That(result.messageSize, Is.EqualTo(payload.Length)); 181 | Assert.That(resultBuffer[..result.messageSize], Is.EqualTo(payload)); 182 | } 183 | } -------------------------------------------------------------------------------- /NdgrClientSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NdgrClientSharp", "NdgrClientSharp\NdgrClientSharp.csproj", "{3F9650EF-A7FC-4B8F-A970-7C623F5D5CDF}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NdgrClientSharp.Protocol", "NdgrClientSharp.Protocol\NdgrClientSharp.Protocol.csproj", "{6EE27585-D2B6-4409-B4E4-A0F723F6B0D9}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "Sandbox\Sandbox.csproj", "{13DD8DFB-FFC7-4A99-982C-B3905DDCE741}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NdgrClientSharp.Test", "NdgrClientSharp.Test\NdgrClientSharp.Test.csproj", "{8555AA7B-746B-4F3C-B391-4FD54965535C}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {3F9650EF-A7FC-4B8F-A970-7C623F5D5CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {3F9650EF-A7FC-4B8F-A970-7C623F5D5CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {3F9650EF-A7FC-4B8F-A970-7C623F5D5CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {3F9650EF-A7FC-4B8F-A970-7C623F5D5CDF}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {6EE27585-D2B6-4409-B4E4-A0F723F6B0D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {6EE27585-D2B6-4409-B4E4-A0F723F6B0D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {6EE27585-D2B6-4409-B4E4-A0F723F6B0D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {6EE27585-D2B6-4409-B4E4-A0F723F6B0D9}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {13DD8DFB-FFC7-4A99-982C-B3905DDCE741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {13DD8DFB-FFC7-4A99-982C-B3905DDCE741}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {13DD8DFB-FFC7-4A99-982C-B3905DDCE741}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {8555AA7B-746B-4F3C-B391-4FD54965535C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8555AA7B-746B-4F3C-B391-4FD54965535C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8555AA7B-746B-4F3C-B391-4FD54965535C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrApi/INdgrApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Dwango.Nicolive.Chat.Service.Edge; 6 | using ReadyForNext = Dwango.Nicolive.Chat.Service.Edge.ChunkedEntry.Types.ReadyForNext; 7 | 8 | namespace NdgrClientSharp.NdgrApi 9 | { 10 | public interface INdgrApiClient : IDisposable 11 | { 12 | /// 13 | /// GET /api/view/v4/:view?at=now 14 | /// 15 | ValueTask FetchViewAtNowAsync(string viewApiUri, 16 | CancellationToken token = default); 17 | 18 | /// 19 | /// GET /api/view/v4/:view?at=unixtime 20 | /// 指定時刻付近のコメント取得のための情報を取得する 21 | /// backward,previous,segment,nextが非同期的に返ってくる 22 | /// 23 | IAsyncEnumerable FetchViewAtAsync(string viewApiUri, 24 | long unixTime, 25 | CancellationToken token = default); 26 | 27 | /// 28 | /// ChunkedMessage(コメント)を取得する 29 | /// Segment, Previous, Snapshot APIが対応 30 | /// 31 | IAsyncEnumerable FetchChunkedMessagesAsync( 32 | string apiUri, 33 | CancellationToken token = default); 34 | 35 | /// 36 | /// PackedSegmentを取得する 37 | /// 38 | ValueTask FetchPackedSegmentAsync(string uri, CancellationToken token = default); 39 | } 40 | 41 | 42 | public abstract class NdgrApiClientException : Exception 43 | { 44 | protected NdgrApiClientException(string message) : base(message) 45 | { 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrApi/NdgrApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Dwango.Nicolive.Chat.Service.Edge; 11 | using NdgrClientSharp.Utilities; 12 | using ReadyForNext = Dwango.Nicolive.Chat.Service.Edge.ChunkedEntry.Types.ReadyForNext; 13 | 14 | namespace NdgrClientSharp.NdgrApi 15 | { 16 | public class NdgrApiClient : INdgrApiClient 17 | { 18 | private readonly HttpClient _httpClient; 19 | private readonly bool _needDisposeHttpClient; 20 | private readonly CancellationTokenSource _mainCts = new CancellationTokenSource(); 21 | 22 | public NdgrApiClient() 23 | { 24 | _httpClient = new HttpClient(); 25 | _httpClient.DefaultRequestHeaders.Add("User-Agent", "ndgr-client-sharp"); 26 | _needDisposeHttpClient = true; 27 | } 28 | 29 | /// 30 | /// HttpClientを指定して初期化する 31 | /// このHttpClientのDisposeはNdgrApiClientでは行わない 32 | /// 33 | public NdgrApiClient(HttpClient httpClient) 34 | { 35 | _httpClient = httpClient; 36 | _needDisposeHttpClient = false; 37 | } 38 | 39 | private static string TrimQuery(string uri) 40 | { 41 | // URIからクエリパラメータを除外 42 | return uri.Split('?')[0]; 43 | } 44 | 45 | 46 | #region view api 47 | 48 | /// 49 | /// コメント取得の起点 50 | /// GET /api/view/v4/:view?at=now を実行して、次のコメント取得のための情報を取得する 51 | /// クエリパラメータはTrimしてから実行するため含まれていても問題ない 52 | /// 53 | public async ValueTask FetchViewAtNowAsync(string viewApiUri, CancellationToken token = default) 54 | { 55 | var ct = CreateLinkedToken(token); 56 | 57 | // URIからクエリパラメータを除外 58 | var uri = TrimQuery(viewApiUri); 59 | 60 | var response = await _httpClient.GetAsync( 61 | $"{uri}?at=now", 62 | HttpCompletionOption.ResponseHeadersRead, ct); 63 | 64 | if (!response.IsSuccessStatusCode) 65 | { 66 | throw new NdgrApiClientHttpException(response.StatusCode); 67 | } 68 | 69 | await foreach (var chunk in ReadProtoBuffBytesAsync(await response.Content.ReadAsStreamAsync()) 70 | .WithCancellation(ct)) 71 | { 72 | var message = ParseChunkedEntry(chunk); 73 | 74 | // at=nowの場合はほぼ現在時刻のunixtimeが返ってくる 75 | return message.Next; 76 | } 77 | 78 | throw new NdgrApiClientByteReadException("Failed to read bytes from stream."); 79 | } 80 | 81 | private static ChunkedEntry ParseChunkedEntry(PooledBuffer message) 82 | { 83 | try 84 | { 85 | return ChunkedEntry.Parser.ParseFrom(message.Span); 86 | } 87 | finally 88 | { 89 | message.Dispose(); 90 | } 91 | } 92 | 93 | 94 | /// 95 | /// 指定時刻付近のコメント取得のための情報を取得する 96 | /// backward,previous,segment,nextが非同期的に返ってくる 97 | /// 98 | public async IAsyncEnumerable FetchViewAtAsync(string viewApiUri, 99 | long unixTime, 100 | [EnumeratorCancellation] CancellationToken token = default) 101 | { 102 | var ct = CreateLinkedToken(token); 103 | 104 | var uri = TrimQuery(viewApiUri); 105 | var response = 106 | await _httpClient.GetAsync( 107 | $"{uri}?at={unixTime}", 108 | HttpCompletionOption.ResponseHeadersRead, ct); 109 | 110 | if (!response.IsSuccessStatusCode) 111 | { 112 | throw new NdgrApiClientHttpException(response.StatusCode); 113 | } 114 | 115 | var stream = await response.Content.ReadAsStreamAsync(); 116 | await foreach (var chunk in ReadProtoBuffBytesAsync(stream, ct)) 117 | { 118 | var message = ParseChunkedEntry(chunk); 119 | yield return message; 120 | } 121 | } 122 | 123 | #endregion 124 | 125 | 126 | /// 127 | /// ChunkedMessage(コメント)を取得する 128 | /// Segment, Previous APIが対応 129 | /// 130 | public async IAsyncEnumerable FetchChunkedMessagesAsync( 131 | string apiUri, 132 | [EnumeratorCancellation] CancellationToken token = default) 133 | { 134 | var ct = CreateLinkedToken(token); 135 | 136 | using var response = await _httpClient.GetAsync(apiUri, HttpCompletionOption.ResponseHeadersRead, ct); 137 | 138 | if (!response.IsSuccessStatusCode) 139 | { 140 | throw new NdgrApiClientHttpException(response.StatusCode); 141 | } 142 | 143 | await foreach (var chunk in ReadProtoBuffBytesAsync(await response.Content.ReadAsStreamAsync(), ct)) 144 | { 145 | ct.ThrowIfCancellationRequested(); 146 | ChunkedMessage message; 147 | try 148 | { 149 | message = ParseChunkedMessage(chunk); 150 | } 151 | catch 152 | { 153 | // ignore... 154 | continue; 155 | } 156 | 157 | yield return message; 158 | } 159 | } 160 | 161 | private static ChunkedMessage ParseChunkedMessage(PooledBuffer message) 162 | { 163 | try 164 | { 165 | return ChunkedMessage.Parser.ParseFrom(message.Span); 166 | } 167 | finally 168 | { 169 | message.Dispose(); 170 | } 171 | } 172 | 173 | 174 | /// 175 | /// PackedSegmentを取得する 176 | /// 177 | public async ValueTask FetchPackedSegmentAsync( 178 | string uri, 179 | CancellationToken token = default) 180 | { 181 | var ct = CreateLinkedToken(token); 182 | var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, ct); 183 | response.EnsureSuccessStatusCode(); 184 | var message = PackedSegment.Parser.ParseFrom(await response.Content.ReadAsStreamAsync()); 185 | return message; 186 | } 187 | 188 | #region read bytes utils 189 | 190 | /// 191 | /// Streamから非同期的に生のバイト配列を読み取る 192 | /// 193 | private static async IAsyncEnumerable ReadRawBytesAsync( 194 | Stream stream, 195 | [EnumeratorCancellation] CancellationToken ct = default) 196 | { 197 | var pool = ArrayPool.Shared; 198 | 199 | while (true) 200 | { 201 | var buffer = pool.Rent(2048); 202 | int read; 203 | try 204 | { 205 | read = await stream.ReadAsync(buffer, 0, buffer.Length, ct); 206 | } 207 | catch 208 | { 209 | pool.Return(buffer); 210 | throw; 211 | } 212 | 213 | if (read == 0) 214 | { 215 | pool.Return(buffer); 216 | yield break; 217 | } 218 | 219 | yield return new PooledBuffer(buffer, read, pool); 220 | } 221 | } 222 | 223 | /// 224 | /// Streamを「ProtoBuffのメッセージとして解釈可能なバイト配列単位」で読み取る 225 | /// 226 | private static async IAsyncEnumerable ReadProtoBuffBytesAsync( 227 | Stream stream, 228 | [EnumeratorCancellation] CancellationToken ct = default) 229 | { 230 | using var reader = new NdgrProtobufStreamReader(); 231 | await foreach (var chunk in ReadRawBytesAsync(stream).WithCancellation(ct)) 232 | { 233 | try 234 | { 235 | ct.ThrowIfCancellationRequested(); 236 | reader.AddNewChunk(chunk.Array, chunk.Length); 237 | } 238 | finally 239 | { 240 | chunk.Dispose(); 241 | } 242 | 243 | var pool = ArrayPool.Shared; 244 | while (true) 245 | { 246 | int size; 247 | var buffer = pool.Rent(1024); 248 | try 249 | { 250 | bool isValid; 251 | (isValid, size) = reader.UnshiftChunk(buffer); 252 | if (!isValid) 253 | { 254 | break; 255 | } 256 | } 257 | catch (NdgrProtobufStreamReaderException) 258 | { 259 | pool.Return(buffer); 260 | throw new NdgrApiClientByteReadException("Failed to read varint from the stream."); 261 | } 262 | 263 | yield return new PooledBuffer(buffer, size, pool); 264 | } 265 | } 266 | } 267 | 268 | #endregion 269 | 270 | private CancellationToken CreateLinkedToken(CancellationToken ct) 271 | { 272 | if (!ct.CanBeCanceled) return _mainCts.Token; 273 | return CancellationTokenSource.CreateLinkedTokenSource(_mainCts.Token, ct).Token; 274 | } 275 | 276 | public void Dispose() 277 | { 278 | if (_needDisposeHttpClient) 279 | { 280 | _httpClient.Dispose(); 281 | } 282 | 283 | _mainCts.Cancel(); 284 | _mainCts.Dispose(); 285 | } 286 | } 287 | 288 | 289 | public sealed class NdgrApiClientByteReadException : NdgrApiClientException 290 | { 291 | public NdgrApiClientByteReadException(string message) : base(message) 292 | { 293 | } 294 | } 295 | 296 | 297 | public sealed class NdgrApiClientHttpException : Exception 298 | { 299 | public HttpStatusCode HttpStatusCode { get; } 300 | 301 | public NdgrApiClientHttpException(HttpStatusCode httpStatusCode) 302 | { 303 | HttpStatusCode = httpStatusCode; 304 | } 305 | 306 | public override string ToString() 307 | { 308 | return $"{base.ToString()}, {nameof(HttpStatusCode)}: {HttpStatusCode}"; 309 | } 310 | } 311 | } -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrClientSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | 0.1.3 6 | 8 7 | netstandard2.0;netstandard2.1 8 | 0.1.7 9 | 10 | 11 | 12 | 13 | All 14 | 15 | 16 | <_Parameter1>NdgrClientSharp.Test 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrLiveCommentFetcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Dwango.Nicolive.Chat.Data; 8 | using Dwango.Nicolive.Chat.Service.Edge; 9 | using NdgrClientSharp.NdgrApi; 10 | using R3; 11 | 12 | namespace NdgrClientSharp 13 | { 14 | /// 15 | /// ニコ生の生放送中のコメントを取得する 16 | /// 17 | public sealed class NdgrLiveCommentFetcher : IDisposable 18 | { 19 | /// 20 | /// ・受信したコメント情報が発信される 21 | /// ・実行中に発生したエラーはOnErrorResumeとして発行 22 | /// ・DisconnectしてもOnCompletedは発行されない 23 | /// ・DisposeするとOnCompletedが発行される 24 | /// 25 | public Observable OnMessageReceived => _messageSubject; 26 | 27 | /// 28 | /// 番組が終了したことを通知する 29 | /// すでに終了済みの番組については発火しない 30 | /// 31 | public Observable OnProgramEnded { get; } 32 | 33 | /// 34 | /// 接続状態 35 | /// 36 | public ReadOnlyReactiveProperty ConnectionStatus => _connectionStatus; 37 | 38 | /// 39 | /// コメント取得エラー発生時の最大リトライ回数 40 | /// 0の場合はリトライしない 41 | /// 42 | public int MaxRetryCount { get; set; } = 5; 43 | 44 | /// 45 | /// コメント取得エラー発生時のリトライ間隔 46 | /// 47 | public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(2); 48 | 49 | /// 50 | /// 現在受信中のSegment一覧 51 | /// 52 | public IEnumerable CurrentReceivingSegments 53 | { 54 | get 55 | { 56 | lock (_receivedMessages) 57 | { 58 | return _receivedMessages.Keys.ToArray(); 59 | } 60 | } 61 | } 62 | 63 | private readonly INdgrApiClient _ndgrApiClient; 64 | private readonly object _gate = new object(); 65 | private readonly Subject _messageSubject = new Subject(); 66 | private readonly bool _needDisposeNdgrApiClient; 67 | 68 | private readonly SynchronizedReactiveProperty 69 | _connectionStatus = new SynchronizedReactiveProperty(ConnectionState.Disconnected); 70 | 71 | private CancellationTokenSource? _mainCts; 72 | private bool _isDisposed; 73 | private string _latestViewApiUri = string.Empty; 74 | private uint _fetchingSegmentCount = 0; 75 | 76 | private readonly Dictionary> _receivedMessages = 77 | new Dictionary>(); 78 | 79 | public NdgrLiveCommentFetcher(INdgrApiClient? ndgrApiClient = null) 80 | { 81 | if (ndgrApiClient == null) 82 | { 83 | _ndgrApiClient = new NdgrApiClient(); 84 | _needDisposeNdgrApiClient = true; 85 | } 86 | else 87 | { 88 | _ndgrApiClient = ndgrApiClient; 89 | _needDisposeNdgrApiClient = false; 90 | } 91 | 92 | OnProgramEnded = _messageSubject 93 | .Where(x => x.State?.ProgramStatus?.State is ProgramStatus.Types.State.Ended) 94 | .AsUnitObservable() 95 | .Share(); 96 | } 97 | 98 | 99 | /// 100 | /// ViewAPIに接続してコメント取得を開始する 101 | /// 接続中またはすでに接続済みの場合は何もしない 102 | /// 103 | public void Connect(string viewApiUri) 104 | { 105 | lock (_gate) 106 | { 107 | if (_isDisposed) throw new ObjectDisposedException(nameof(NdgrLiveCommentFetcher)); 108 | if (ConnectionStatus.CurrentValue != ConnectionState.Disconnected) 109 | { 110 | return; 111 | } 112 | 113 | _mainCts = new CancellationTokenSource(); 114 | _connectionStatus.Value = ConnectionState.Connecting; 115 | _latestViewApiUri = viewApiUri; 116 | _fetchingSegmentCount = 0; 117 | 118 | // 取得処理開始 119 | Forget(FetchStartAsync(viewApiUri, _mainCts.Token)); 120 | } 121 | } 122 | 123 | /// 124 | /// コメント取得を停止する 125 | /// 126 | public void Disconnect() 127 | { 128 | lock (_gate) 129 | { 130 | if (_isDisposed) return; 131 | 132 | _mainCts?.Cancel(); 133 | _mainCts?.Dispose(); 134 | _mainCts = null; 135 | _connectionStatus.Value = ConnectionState.Disconnected; 136 | _fetchingSegmentCount = 0; 137 | } 138 | } 139 | 140 | 141 | /// 142 | /// 最後に接続していたViewAPIに再接続する 143 | /// すでに接続済みの場合は切断してから再接続する 144 | /// 145 | public void Reconnect() 146 | { 147 | lock (_gate) 148 | { 149 | if (_isDisposed) throw new ObjectDisposedException(nameof(NdgrLiveCommentFetcher)); 150 | if (ConnectionStatus.CurrentValue == ConnectionState.Connected) 151 | { 152 | Disconnect(); 153 | } 154 | 155 | if (_latestViewApiUri == string.Empty) 156 | { 157 | throw new InvalidOperationException("No latest ViewAPI URI. Cannot reconnect."); 158 | } 159 | 160 | Connect(_latestViewApiUri); 161 | } 162 | } 163 | 164 | 165 | public void Dispose() 166 | { 167 | lock (_gate) 168 | { 169 | if (_isDisposed) return; 170 | _isDisposed = true; 171 | _receivedMessages.Clear(); 172 | 173 | Disconnect(); 174 | 175 | if (_needDisposeNdgrApiClient) 176 | { 177 | _ndgrApiClient.Dispose(); 178 | } 179 | 180 | _messageSubject.Dispose(); 181 | _connectionStatus.Dispose(); 182 | } 183 | } 184 | 185 | #region AsyncMainAction 186 | 187 | private async ValueTask FetchStartAsync(string viewApiUri, CancellationToken ct) 188 | { 189 | var tryCount = 0; 190 | 191 | do 192 | { 193 | try 194 | { 195 | // 次の取得するべきViewのunixtimeを取得 196 | var next = await _ndgrApiClient.FetchViewAtNowAsync(viewApiUri, ct); 197 | 198 | if (_connectionStatus.Value == ConnectionState.Connecting) 199 | { 200 | // 取得成功したら接続済にする 201 | _connectionStatus.Value = ConnectionState.Connected; 202 | } 203 | 204 | // コメント取得のためのSegmentおよびNextの取得処理 205 | Forget(FetchLoopAsync(viewApiUri, next.At, ct)); 206 | return; 207 | } 208 | catch (OperationCanceledException) 209 | { 210 | // nothing 211 | return; 212 | } 213 | catch (WebException w) when (w.Status == WebExceptionStatus.RequestCanceled) 214 | { 215 | // nothing 216 | return; 217 | } 218 | catch (NdgrApiClientHttpException ex) 219 | { 220 | lock (_gate) 221 | { 222 | _messageSubject.OnErrorResume(ex); 223 | } 224 | 225 | // 503だったときはちょっとまってリトライしてみる 226 | if (ex.HttpStatusCode == HttpStatusCode.ServiceUnavailable) 227 | { 228 | await Task.Delay(RetryInterval, ct); 229 | } 230 | else 231 | { 232 | // 503以外のエラーはリトライしない 233 | Disconnect(); 234 | return; 235 | } 236 | } 237 | catch (NdgrApiClientByteReadException ex) 238 | { 239 | lock (_gate) 240 | { 241 | _messageSubject.OnErrorResume(ex); 242 | } 243 | 244 | // 受信バイトがおかしい場合はリトライ 245 | await Task.Delay(RetryInterval, ct); 246 | } 247 | catch (Exception ex) 248 | { 249 | lock (_gate) 250 | { 251 | if (!_messageSubject.IsDisposed) 252 | { 253 | _messageSubject.OnErrorResume(ex); 254 | } 255 | } 256 | 257 | Disconnect(); 258 | return; 259 | } 260 | } while (++tryCount <= MaxRetryCount); 261 | 262 | // リトライしてもダメだったらDisconnect 263 | Disconnect(); 264 | } 265 | 266 | 267 | /// 268 | /// コメント取得のためのSegmentおよびNextの取得処理 269 | /// 270 | private async ValueTask FetchLoopAsync(string viewApiUri, long unixTime, CancellationToken ct) 271 | { 272 | var targetTime = unixTime; 273 | try 274 | { 275 | while (!ct.IsCancellationRequested) 276 | { 277 | var hasNext = false; 278 | var chunks = _ndgrApiClient.FetchViewAtAsync(viewApiUri, targetTime, ct); 279 | var receivedSegments = new HashSet(); 280 | 281 | // SegmentおよびNextの監視 282 | await foreach (var chunk in chunks) 283 | { 284 | switch (chunk.EntryCase) 285 | { 286 | case ChunkedEntry.EntryOneofCase.Segment: 287 | // Segment(コメント本文を返すAPI情報) 288 | var segment = chunk.Segment; 289 | receivedSegments.Add(segment.Uri); 290 | Forget(FetchSegmentAsync(segment, ct)); 291 | 292 | break; 293 | case ChunkedEntry.EntryOneofCase.Next: 294 | 295 | // 次のView取得情報 296 | targetTime = chunk.Next.At; 297 | hasNext = true; 298 | break; 299 | 300 | // 未使用 301 | case ChunkedEntry.EntryOneofCase.None: 302 | case ChunkedEntry.EntryOneofCase.Backward: 303 | case ChunkedEntry.EntryOneofCase.Previous: 304 | break; 305 | 306 | default: 307 | throw new ArgumentOutOfRangeException(); 308 | } 309 | } 310 | 311 | ct.ThrowIfCancellationRequested(); 312 | 313 | // ReceivedSegmentsに含まれないものはDictionaryから削除 314 | lock (_receivedMessages) 315 | { 316 | foreach (var key in _receivedMessages.Keys.ToArray()) 317 | { 318 | if (!receivedSegments.Contains(key)) 319 | { 320 | _receivedMessages[key]?.Clear(); 321 | _receivedMessages.Remove(key); 322 | } 323 | } 324 | } 325 | 326 | if (!hasNext) 327 | { 328 | // Nextがない場合はSegmentの全受信を待ってから終了 329 | // だが実際はNextは無限に存在し続けるぽいからここが実行されることはないはず 330 | while (_fetchingSegmentCount > 0) 331 | { 332 | // 適当に終わりそうな時間だけ待つ 333 | await Task.Delay(TimeSpan.FromSeconds(3), ct); 334 | } 335 | 336 | Disconnect(); 337 | } 338 | } 339 | } 340 | catch (OperationCanceledException) 341 | { 342 | // nothing 343 | } 344 | catch (WebException w) when (w.Status == WebExceptionStatus.RequestCanceled) 345 | { 346 | // nothing 347 | } 348 | catch (NdgrApiClientHttpException e) 349 | { 350 | lock (_gate) 351 | { 352 | if (!_messageSubject.IsDisposed) 353 | { 354 | _messageSubject.OnErrorResume(e); 355 | } 356 | 357 | switch (e.HttpStatusCode) 358 | { 359 | case HttpStatusCode.ServiceUnavailable: 360 | // 503の場合はリトライできる可能性あり 361 | Reconnect(); 362 | break; 363 | default: 364 | // リカバリ不可能な場合は動作停止 365 | Disconnect(); 366 | break; 367 | } 368 | } 369 | } 370 | catch (NdgrApiClientByteReadException e) 371 | { 372 | lock (_gate) 373 | { 374 | if (!_messageSubject.IsDisposed) 375 | { 376 | _messageSubject.OnErrorResume(e); 377 | } 378 | 379 | // バイト列の破損の可能性があるのでリトライ 380 | Reconnect(); 381 | } 382 | } 383 | catch (Exception e) 384 | { 385 | // エラーを発行してDisconnect 386 | lock (_gate) 387 | { 388 | if (!_messageSubject.IsDisposed) 389 | { 390 | _messageSubject.OnErrorResume(e); 391 | } 392 | } 393 | 394 | Disconnect(); 395 | } 396 | } 397 | 398 | private async ValueTask FetchSegmentAsync(MessageSegment segment, CancellationToken ct = default) 399 | { 400 | var tryCount = 0; 401 | 402 | while (++tryCount <= 2) 403 | { 404 | try 405 | { 406 | _fetchingSegmentCount++; 407 | 408 | HashSet hashSet; 409 | 410 | lock (_receivedMessages) 411 | { 412 | if (!_receivedMessages.TryGetValue(segment.Uri, out var set)) 413 | { 414 | hashSet = new HashSet(); 415 | _receivedMessages.Add(segment.Uri, hashSet); 416 | } 417 | else 418 | { 419 | hashSet = set; 420 | } 421 | } 422 | 423 | var uri = segment.Uri; 424 | 425 | await foreach (var chunkedMessage in _ndgrApiClient.FetchChunkedMessagesAsync(uri, ct)) 426 | { 427 | lock (_gate) 428 | { 429 | var meta = chunkedMessage.Meta; 430 | if (meta != null) 431 | { 432 | // Metaが存在する場合は重複チェック 433 | if (hashSet.Add(chunkedMessage.Meta.Id)) 434 | { 435 | _messageSubject.OnNext(chunkedMessage); 436 | } 437 | } 438 | else 439 | { 440 | // Metaが存在しない場合は素通し 441 | _messageSubject.OnNext(chunkedMessage); 442 | } 443 | } 444 | } 445 | 446 | return; 447 | } 448 | catch (OperationCanceledException) 449 | { 450 | throw; 451 | } 452 | catch (NdgrApiClientHttpException e) 453 | { 454 | lock (_gate) 455 | { 456 | if (!_messageSubject.IsDisposed) 457 | { 458 | _messageSubject.OnErrorResume(e); 459 | } 460 | } 461 | 462 | await Task.Delay(RetryInterval, ct); 463 | } 464 | catch (NdgrApiClientByteReadException e) 465 | { 466 | lock (_gate) 467 | { 468 | if (!_messageSubject.IsDisposed) 469 | { 470 | _messageSubject.OnErrorResume(e); 471 | } 472 | } 473 | 474 | // Segmentのバイト列が破損している可能性があるので少し待ってリトライ 475 | await Task.Delay(RetryInterval, ct); 476 | } 477 | catch (WebException w) when (w.Status == WebExceptionStatus.RequestCanceled) 478 | { 479 | return; 480 | } 481 | catch (Exception e) 482 | { 483 | lock (_gate) 484 | { 485 | if (!_messageSubject.IsDisposed) 486 | { 487 | _messageSubject.OnErrorResume(e); 488 | } 489 | } 490 | 491 | // その他のエラーはリトライしない 492 | return; 493 | } 494 | finally 495 | { 496 | _fetchingSegmentCount--; 497 | } 498 | } 499 | } 500 | 501 | 502 | private async void Forget(ValueTask task) 503 | { 504 | try 505 | { 506 | await task; 507 | } 508 | catch (OperationCanceledException) 509 | { 510 | // ignore 511 | } 512 | catch (WebException w) when (w.Status == WebExceptionStatus.RequestCanceled) 513 | { 514 | // nothing 515 | } 516 | catch (Exception e) 517 | { 518 | lock (_gate) 519 | { 520 | if (!_messageSubject.IsDisposed) 521 | { 522 | _messageSubject.OnErrorResume(e); 523 | } 524 | } 525 | } 526 | } 527 | 528 | #endregion 529 | } 530 | 531 | public enum ConnectionState 532 | { 533 | Disconnected, 534 | Connecting, 535 | Connected 536 | } 537 | } -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrPastCommentFetcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using Dwango.Nicolive.Chat.Service.Edge; 6 | using NdgrClientSharp.NdgrApi; 7 | using R3; 8 | 9 | namespace NdgrClientSharp 10 | { 11 | /// 12 | /// 過去のコメントを取得する 13 | /// 14 | public sealed class NdgrPastCommentFetcher : IDisposable 15 | { 16 | private readonly INdgrApiClient _ndgrApiClient; 17 | private readonly bool _needDisposeNdgrApiClient; 18 | private bool _isDisposed; 19 | private readonly object _gate = new object(); 20 | private readonly CancellationTokenSource _mainCts = new CancellationTokenSource(); 21 | 22 | public NdgrPastCommentFetcher(INdgrApiClient? ndgrApiClient = null) 23 | { 24 | if (ndgrApiClient == null) 25 | { 26 | _ndgrApiClient = new NdgrApiClient(); 27 | _needDisposeNdgrApiClient = true; 28 | } 29 | else 30 | { 31 | _ndgrApiClient = ndgrApiClient; 32 | _needDisposeNdgrApiClient = false; 33 | } 34 | } 35 | 36 | /// 37 | /// 過去のコメントを取得する 38 | /// 39 | /// ViewURI 40 | /// 41 | /// 取得数の目安。コメントはまとまった単位で取得するため、この数値を超える数のうちの最小回数まで取得する。 42 | /// nullを指定した場合はすべてのコメントを取得する。 43 | /// 44 | public Observable FetchPastComments(string viewApiUri, int? inaccurateLimit = 100) 45 | { 46 | lock (_gate) 47 | { 48 | if (_isDisposed) throw new ObjectDisposedException(nameof(NdgrPastCommentFetcher)); 49 | } 50 | 51 | return Observable.Create(async (o, ct) => 52 | { 53 | var token = CancellationTokenSource.CreateLinkedTokenSource(_mainCts.Token, ct).Token; 54 | 55 | try 56 | { 57 | var sendCount = 0; 58 | 59 | var now = await _ndgrApiClient.FetchViewAtNowAsync(viewApiUri, token); 60 | var previous = new List(); 61 | BackwardSegment? backward = null; 62 | 63 | await foreach (var chunkedEntry in _ndgrApiClient.FetchViewAtAsync(viewApiUri, now.At, token)) 64 | { 65 | if (chunkedEntry.EntryCase == ChunkedEntry.EntryOneofCase.Previous) 66 | { 67 | previous.Add(chunkedEntry.Previous); 68 | } 69 | else if (chunkedEntry.EntryCase == ChunkedEntry.EntryOneofCase.Backward) 70 | { 71 | backward = chunkedEntry.Backward; 72 | } 73 | else if (chunkedEntry.EntryCase == ChunkedEntry.EntryOneofCase.Segment) 74 | { 75 | // 通常のSegmentが来た時点で止める 76 | break; 77 | } 78 | } 79 | 80 | 81 | // PreviousSegmentの新しい側から取得 82 | foreach (var p in previous.OrderByDescending(x => x.Until)) 83 | { 84 | await foreach (var chunkedMessage in _ndgrApiClient.FetchChunkedMessagesAsync(p.Uri, token)) 85 | { 86 | o.OnNext(chunkedMessage); 87 | sendCount++; 88 | } 89 | 90 | if (sendCount >= inaccurateLimit) 91 | { 92 | o.OnCompleted(); 93 | return; 94 | } 95 | } 96 | 97 | if (backward != null) 98 | { 99 | var uri = backward.Segment.Uri; 100 | // Previousよりさらに過去のコメントの取得 101 | while (!token.IsCancellationRequested) 102 | { 103 | var packedSegment = await _ndgrApiClient.FetchPackedSegmentAsync(uri, token); 104 | foreach (var message in packedSegment.Messages) 105 | { 106 | o.OnNext(message); 107 | sendCount++; 108 | } 109 | 110 | if (sendCount >= inaccurateLimit) 111 | { 112 | o.OnCompleted(); 113 | return; 114 | } 115 | 116 | if (packedSegment.Next != null) 117 | { 118 | uri = packedSegment.Next.Uri; 119 | continue; 120 | } 121 | 122 | break; 123 | } 124 | } 125 | 126 | o.OnCompleted(); 127 | } 128 | catch (Exception ex) 129 | { 130 | if (ex is OperationCanceledException) 131 | { 132 | o.OnCompleted(); 133 | } 134 | else 135 | { 136 | o.OnCompleted(ex); 137 | } 138 | } 139 | }); 140 | } 141 | 142 | 143 | public void Dispose() 144 | { 145 | lock (_gate) 146 | { 147 | if (_isDisposed) return; 148 | _isDisposed = true; 149 | _mainCts.Cancel(); 150 | _mainCts.Dispose(); 151 | 152 | if (_needDisposeNdgrApiClient) 153 | { 154 | _ndgrApiClient.Dispose(); 155 | } 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /NdgrClientSharp/NdgrSnapshotFetcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using Dwango.Nicolive.Chat.Service.Edge; 6 | using NdgrClientSharp.NdgrApi; 7 | 8 | namespace NdgrClientSharp 9 | { 10 | public sealed class NdgrSnapshotFetcher : IDisposable 11 | { 12 | private readonly INdgrApiClient _ndgrApiClient; 13 | private readonly bool _needDisposeNdgrApiClient; 14 | private bool _isDisposed; 15 | private readonly object _gate = new object(); 16 | private readonly CancellationTokenSource _mainCts = new CancellationTokenSource(); 17 | 18 | public NdgrSnapshotFetcher(INdgrApiClient? ndgrApiClient = null) 19 | { 20 | if (ndgrApiClient == null) 21 | { 22 | _ndgrApiClient = new NdgrApiClient(); 23 | _needDisposeNdgrApiClient = true; 24 | } 25 | else 26 | { 27 | _ndgrApiClient = ndgrApiClient; 28 | _needDisposeNdgrApiClient = false; 29 | } 30 | } 31 | 32 | /// 33 | /// 現在のスナップショットを取得する 34 | /// 35 | public async IAsyncEnumerable FetchCurrentSnapshotAsync( 36 | string viewUri, 37 | [EnumeratorCancellation] CancellationToken token =default) 38 | { 39 | lock (_gate) 40 | { 41 | if (_isDisposed) throw new ObjectDisposedException(nameof(NdgrSnapshotFetcher)); 42 | } 43 | 44 | var ct = CancellationTokenSource.CreateLinkedTokenSource(_mainCts.Token, token).Token; 45 | 46 | var now = await _ndgrApiClient.FetchViewAtNowAsync(viewUri, ct); 47 | 48 | await foreach (var ce in _ndgrApiClient.FetchViewAtAsync(viewUri, now.At, ct)) 49 | { 50 | if (ce.EntryCase != ChunkedEntry.EntryOneofCase.Backward) continue; 51 | 52 | var backward = ce.Backward; 53 | await foreach (var s in _ndgrApiClient.FetchChunkedMessagesAsync(backward.Snapshot.Uri, ct)) 54 | { 55 | yield return s; 56 | } 57 | } 58 | } 59 | 60 | public void Dispose() 61 | { 62 | lock (_gate) 63 | { 64 | if (_isDisposed) return; 65 | _isDisposed = true; 66 | 67 | _mainCts.Cancel(); 68 | _mainCts.Dispose(); 69 | 70 | if (_needDisposeNdgrApiClient) 71 | { 72 | _ndgrApiClient.Dispose(); 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /NdgrClientSharp/Utilities/NdgrProtobufStreamReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | 5 | namespace NdgrClientSharp.Utilities 6 | { 7 | internal sealed class NdgrProtobufStreamReader : IDisposable 8 | { 9 | private readonly MemoryStream _bufferStream = new MemoryStream(); 10 | 11 | public void AddNewChunk(byte[] chunk, int? length = null) 12 | { 13 | // 末尾に書き込む 14 | _bufferStream.Position = _bufferStream.Length; 15 | _bufferStream.Write(chunk, 0, length ?? chunk.Length); 16 | } 17 | 18 | /// 19 | /// Varintの読み取り 20 | /// バイト列の先頭をVarint(Protocol Buffersの1データサイズの長さ)として読み取る 21 | /// 22 | /// 参考 23 | /// https://protobuf.dev/programming-guides/encoding/ 24 | /// https://github.com/protocolbuffers/protobuf/blob/384fabf35a9b5d1f1502edaf9a139b1f51551a01/csharp/src/Google.Protobuf/ParsingPrimitives.cs#L720-L73 25 | /// This implementation is inspired by the Protocol Buffers library by Google. 26 | /// 27 | /// 28 | /// 29 | internal (int shift, uint result)? ReadVarint() 30 | { 31 | var result = 0; 32 | var offset = 0; 33 | var shift = 0; 34 | 35 | _bufferStream.Position = 0; 36 | 37 | for (; offset < 32; offset += 7) 38 | { 39 | int b = _bufferStream.ReadByte(); 40 | if (b == -1) 41 | { 42 | return null; 43 | } 44 | 45 | shift++; 46 | result |= (b & 0x7f) << offset; 47 | if ((b & 0x80) == 0) 48 | { 49 | return (shift, (uint)result); 50 | } 51 | } 52 | 53 | for (; offset < 64; offset += 7) 54 | { 55 | var b = _bufferStream.ReadByte(); 56 | if (b == -1) 57 | { 58 | return null; 59 | } 60 | 61 | shift++; 62 | 63 | if ((b & 0x80) == 0) 64 | { 65 | return (shift, (uint)result); 66 | } 67 | } 68 | 69 | // Varintの読み取りに失敗した 70 | throw new NdgrProtobufStreamReaderException("Failed to read varint from the stream."); 71 | } 72 | 73 | /// 74 | /// Bufferの先頭からVarintを読み取り、その値に基づいてバッファからメッセージを取り出す 75 | /// falseが返却された場合は、まだ十分なデータを受信仕切っていないことを示す 76 | /// 77 | /// 戻り値がtrueの場合はProtoBuffのバイナリ列が格納されている 78 | /// 正常に読み取れたか、読み取れた場合はバイトサイズも返す 79 | /// 80 | public (bool isValid, int messageSize) UnshiftChunk(byte[] messageBuffer) 81 | { 82 | var readVarint = ReadVarint(); 83 | if (readVarint == null) return (false, 0); 84 | 85 | var (offset, varint) = readVarint.Value; 86 | 87 | // offset : Varint分のバイト数 88 | // varint : 実際のProtoBuffのメッセージサイズ 89 | // offset + varint : この長さ分のバイト列がProtoBuffとして解釈するために必要 90 | if (offset + varint > _bufferStream.Length) 91 | { 92 | // まだ十分なデータを受信していない 93 | return (false, 0); 94 | } 95 | 96 | if (offset + varint > messageBuffer.Length) 97 | { 98 | // messageBufferのサイズが足りない 99 | throw new NdgrProtobufStreamReaderException("Message size is too large."); 100 | } 101 | 102 | 103 | // Read the message bytes directly into the array 104 | _bufferStream.Position = offset; 105 | _bufferStream.Read(messageBuffer, 0, (int)varint); 106 | 107 | // Shift the buffer content 108 | var remainingLength = (int)(_bufferStream.Length - offset - varint); 109 | using var rentedBuffer = new PooledArray(remainingLength); 110 | 111 | _bufferStream.Position = offset + varint; 112 | _bufferStream.Read(rentedBuffer.Array, 0, remainingLength); 113 | 114 | // Reset and refill the stream with the remaining data 115 | _bufferStream.SetLength(0); 116 | _bufferStream.Write(rentedBuffer.Array, 0, remainingLength); 117 | 118 | return (true, (int)varint); 119 | } 120 | 121 | public void Dispose() 122 | { 123 | _bufferStream.Dispose(); 124 | } 125 | } 126 | 127 | public class NdgrProtobufStreamReaderException : Exception 128 | { 129 | public NdgrProtobufStreamReaderException(string message) : base(message) 130 | { 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /NdgrClientSharp/Utilities/PooledArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Threading; 4 | 5 | namespace NdgrClientSharp.Utilities 6 | { 7 | public struct PooledArray : IDisposable 8 | { 9 | public T[] Array => _array ?? throw new ObjectDisposedException(nameof(PooledArray)); 10 | private readonly ArrayPool _pool; 11 | private readonly int _length; 12 | 13 | private T[]? _array; 14 | private int _disposed; 15 | 16 | public int Length => _length; 17 | public Span Span => Array.AsSpan(0, _length); 18 | 19 | public PooledArray(int length, ArrayPool? pool = null) 20 | { 21 | _pool = pool ?? ArrayPool.Shared; 22 | _array = _pool.Rent(length); 23 | _length = length; 24 | _disposed = 0; 25 | } 26 | 27 | public void Dispose() 28 | { 29 | if (Interlocked.Exchange(ref _disposed, 1) == 0) 30 | { 31 | if (_array != null) 32 | { 33 | _pool.Return(_array); 34 | _array = null; 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /NdgrClientSharp/Utilities/PooledBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Threading; 4 | 5 | namespace NdgrClientSharp.Utilities 6 | { 7 | public struct PooledBuffer : IDisposable 8 | { 9 | public byte[] Array { get; } 10 | public int Length { get; } 11 | private readonly ArrayPool _pool; 12 | private int _disposed; 13 | 14 | public PooledBuffer(byte[] array, int length, ArrayPool pool) 15 | { 16 | Array = array; 17 | Length = length; 18 | _pool = pool; 19 | _disposed = 0; 20 | } 21 | 22 | public ReadOnlySpan Span => Array.AsSpan(0, Length); 23 | 24 | public void Dispose() 25 | { 26 | if (Interlocked.Exchange(ref _disposed, 1) == 0) 27 | { 28 | _pool.Return(Array); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NdgrClientSharp 2 | 3 | ニコニコ生放送の新コメントサーバー「NDGR(のどぐろ)」用のC#クライアントです。 4 | 次の機能を使うことができます。 5 | 6 | * 各APIと通信できるSimpleなクライアント 7 | * `/api/view/v4/:view?at=now` 8 | * `/api/view/v4/:view?at=:at` 9 | * `/data/segment/v4/:segment` 10 | * `/data/backward/v4/:backward` 11 | * `/data/snapshot/v4/:snapshot` 12 | * 通信レイヤーをラップしたEasyなクライアント 13 | * 生放送のリアルタイムなコメント受信 14 | * 生放送の過去のコメント取得 15 | * 生放送の現在の状態(スナップショット)の取得 16 | 17 | ## 動作環境 18 | 19 | `C# 8.0`以上 & `.NET Standard 2.0`以上向けです。 20 | 21 | また、動作には次のライブラリが必要です。 22 | 23 | * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/) 24 | * [R3](https://www.nuget.org/packages/R3) 25 | 26 | ## プロジェクト構成 27 | 28 | * `NdgrClientSharp` : クライアント本体 29 | * `NdgrClientSharp.Protocol` : 通信及びクライアントが利用するProtoBuffから生成されたデータ構造置き場 30 | * `NdgrClientSharp.Test` : テスト 31 | * `Sandbox` : 実験用のコード置き場 32 | 33 | ## ProtocolBufferの定義について 34 | 35 | 本ライブラリが使用するProtoBuffの定義は[N Airが定義するprotoファイル(v2024.801.103927)](https://github.com/n-air-app/nicolive-comment-protobuf/releases/tag/v2024.801.103927)を元にしています。 36 | 37 | 38 | # 使用方法 39 | 40 | ## 導入 41 | 42 | Releaseよりzipでダウンロードして、次のdllをプロジェクトに入れて下さい。 43 | 44 | * `NdgrClientSharp.dll` 45 | * `NdgrClientSharp.Protocol.dll` 46 | 47 | また次のライブラリも導入している必要があります。 48 | 49 | * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/) 50 | * [R3](https://www.nuget.org/packages/R3) 51 | 52 | 加えてUnityで動作させる場合は[R3.Unity](https://github.com/Cysharp/R3?tab=readme-ov-file#unity)も導入してください。 53 | 54 | ### Unityで使用する場合 55 | 56 | Unityで使用する場合はHttpHandlerを[YetAnotherHttpHandler](https://github.com/Cysharp/YetAnotherHttpHandler)に差し替えて使用してください。 57 | 58 | ```cs 59 | var handler = new YetAnotherHttpHandler(); // here 60 | var httpClient = new HttpClient(handler); 61 | 62 | var ndgrApiClient = new NdgrApiClient(httpClient); 63 | var ndgrLiveCommentFetcher = new NdgrLiveCommentFetcher(ndgrApiClient); 64 | ``` 65 | 66 | ## 使い方 67 | 68 | ### NdgrLiveCommentFetcher 69 | 70 | ニコニコ生放送の放送中のコメントをリアルタイムに取得するクライアントです。 71 | 72 | `OnMessageReceived`から`Observable`としてコメントを取得できます。 73 | 74 | ```cs 75 | // 生放送コメント取得用のクライアントを生成 76 | var liveCommentFetcher = new NdgrLiveCommentFetcher(); 77 | 78 | // コメントの受信準備 79 | liveCommentFetcher 80 | .OnMessageReceived 81 | .Subscribe(chukedMessage => 82 | { 83 | switch (chukedMessage.PayloadCase) 84 | { 85 | case ChunkedMessage.PayloadOneofCase.Message: 86 | // コメントやギフトの情報などはMessage 87 | Console.WriteLine(chukedMessage.Message); 88 | break; 89 | case ChunkedMessage.PayloadOneofCase.State: 90 | // 番組他状態の変更などはStateから取得可能 91 | Console.WriteLine(chukedMessage.State); 92 | break; 93 | 94 | default: 95 | break; 96 | } 97 | }); 98 | 99 | // コメントの受信開始 100 | liveCommentFetcher.Connect(viewApiUri); 101 | 102 | // --- 103 | 104 | // コメントの受信停止 105 | liveCommentFetcher.Disconnect(); 106 | 107 | // リソースの解放 108 | liveCommentFetcher.Dispose(); 109 | ``` 110 | 111 | #### 補足1:メッセージの発行スレッド 112 | 113 | `ConfigureAwait(true)`として動作します。そのため`Connect()`したときの同じスレッドでメッセージ発行される**はず**です。 114 | 115 | ただもしスレッドを確実に指定したい場合、たとえばUnityなどでは`ObserveOnMainThread`を保険として挟んでおくのはありかもしれません。 116 | 117 | ```cs 118 | // Unityで使う場合 119 | // R3.UnityのObserveOnMainThreadを使うことで 120 | // 確実にUnityメインスレッドで受信できる 121 | liveCommentFetcher 122 | .OnMessageReceived 123 | // これ 124 | .ObserveOnMainThread() 125 | .Subscribe(chukedMessage => 126 | { 127 | switch (chukedMessage.PayloadCase) 128 | { 129 | case ChunkedMessage.PayloadOneofCase.Message: 130 | // コメントやギフトの情報などはMessage 131 | Console.WriteLine(chukedMessage.Message); 132 | break; 133 | case ChunkedMessage.PayloadOneofCase.State: 134 | // 番組他状態の変更などはStateから取得可能 135 | Console.WriteLine(chukedMessage.State); 136 | break; 137 | 138 | default: 139 | break; 140 | } 141 | }); 142 | ``` 143 | 144 | 145 | #### 補足2:NdgrApiClientの指定 146 | 147 | `NdgrApiClient` を生成してコンストラクタで指定することができます。 148 | `HttpClient`の挙動をカスタマイズしたい場合は、カスタマイズした`HttpClient`で`NdgrApiClient`を初期化し、それを`NdgrLiveCommentFetcher`に渡してください。 149 | 150 | ```cs 151 | var httpClient = new HttpClient(); 152 | var negrApiClient = new NdgrApiClient(httpClient); 153 | 154 | // NdgrApiClientを指定することが可能 155 | var ndgrLiveCommentFetcher = new NdgrLiveCommentFetcher(negrApiClient); 156 | 157 | // Disposeは手動で 158 | ndgrLiveCommentFetcher.Dispose(); 159 | negrApiClient.Dispose(); 160 | httpClient.Dispose(); 161 | ``` 162 | 163 | #### 補足3:発生したエラーの検知 164 | 165 | エラーは`Observable`の`OnErrorResume`として通知されます。 166 | 167 | ```cs 168 | liveCommentFetcher 169 | .OnMessageReceived 170 | .Subscribe( 171 | onNext: chukedMessage => Console.WriteLine(chukedMessage), 172 | onErrorResume: ex => Console.WriteLine(ex), 173 | onCompleted: result => Console.WriteLine(result)); 174 | ``` 175 | 176 | R3のObservableについては詳しくは[こちらの記事](https://qiita.com/toRisouP/items/e7be5a5a43058556db8f)などを参照。 177 | 178 | #### 補足4: エラー発生時の再接続 179 | 180 | `NdgrLiveCommentFetcher`は通信時、ステータスコード「`503 Service Unavailable`」が返ってきた場合のみ自動で再接続を試みます。 181 | 再接続時のリトライ数やバックオフタイムはプロパティから設定可能です。 182 | 183 | ```cs 184 | var liveCommentFetcher = new NdgrLiveCommentFetcher(); 185 | 186 | liveCommentFetcher.MaxRetryCount = 5; 187 | liveCommentFetcher.RetryInterval = TimeSpan.FromSeconds(5); 188 | ``` 189 | 190 | #### 補足5: 接続状態の確認 191 | 192 | クライアントの接続状況は`ConnectionStatus`より`ReadOnlyReactiveProperty`として取得可能です。 193 | 194 | 195 | ```cs 196 | // 接続状態の通知 197 | liveCommentFetcher.ConnectionStatus 198 | .Subscribe(state => 199 | { 200 | switch (state) 201 | { 202 | case ConnectionState.Disconnected: 203 | break; 204 | case ConnectionState.Connecting: 205 | break; 206 | case ConnectionState.Connected: 207 | break; 208 | default: 209 | break; 210 | } 211 | }); 212 | 213 | // 同期的に接続状態を取得 214 | Console.WriteLine(liveCommentFetcher.ConnectionStatus.CurrentValue); 215 | ``` 216 | 217 | #### 補足6: 番組終了時の挙動 218 | 219 | 放送中の番組に対して`Connect()`していた場合、**番組が終了しても自動でDisconnectはしません。** 220 | もし番組が終了を検知して`Disconnect()`したい場合は`OnProgramEnded`を購読して発行されるイベントを利用してください。 221 | 222 | ```cs 223 | using var ndgrLiveCommentFetcher = new NdgrLiveCommentFetcher(); 224 | ndgrLiveCommentFetcher.Connect(viewApiUri); 225 | 226 | // 番組が終了時にイベントが発行される 227 | // それのイベントを受けてDisconnectする 228 | ndgrLiveCommentFetcher 229 | .OnProgramEnded 230 | .Subscribe(_ => ndgrLiveCommentFetcher.Disconnect()); 231 | ``` 232 | 233 | 234 | なお、すでに終了済みの番組に対して`Connect()`をした場合、`OnProgramEnded`は発行されません。 235 | 番組状態を別のAPIで取得し、放送中であることを確認してから`Connect()`してください。 236 | 237 | ### NdgrPastCommentFetcher 238 | 239 | 放送中番組の過去のコメント(`ChunckedMessage`)やタイムシフトからコメントを取得するクライントです。 240 | 現在時刻から遡って指定件数くらいのコメントを`Observable`として取得できます。 241 | 242 | 243 | ```cs 244 | var pastCommentFetcher = new NdgrPastCommentFetcher(); 245 | 246 | // 最低100件取得する 247 | pastCommentFetcher 248 | .FetchPastComments(viewApiUri, 100) 249 | .Subscribe(x => Console.WriteLine(x.Message)); 250 | 251 | pastCommentFetcher.Dispose(); 252 | ``` 253 | 254 | `FetchPastComments()`の引数としてコメントの件数が指定できます。ただしこの引数は**最低でもこの件数を取得する**という挙動をします(コメント取得はある程度まとまった単位で実行されるため、指定した個数ピッタリ取得することができない。) 255 | 256 | またこの引数にnullを指定した場合は過去のコメントをすべて取得します。 257 | 258 | 259 | #### 補足:コメントの順序 260 | 261 | 件数を指定した場合、現在時刻から遡る方向で指定件数分はコメントを取得します。 262 | ただし、`Observable`から発行されるメッセージの順序は時刻順であることを**保証しません**。 263 | 264 | 順序が重要な場合は受信後に自身でソートしてください。 265 | 266 | ```cs 267 | using var pastCommentFetcher = new NdgrPastCommentFetcher(); 268 | 269 | // 最低100件取得する 270 | var list = await pastCommentFetcher 271 | .FetchPastComments(viewApiUri, 100) 272 | .ToListAsync(); 273 | 274 | // 取得したコメントを並び替えて表示 275 | foreach (var c in list 276 | .Where(x => x.Meta?.At != null) 277 | .OrderBy(x => x.Meta.At)) 278 | { 279 | Console.WriteLine(c); 280 | } 281 | ``` 282 | 283 | ### NdgrSnapshotFetcher 284 | 285 | 生放送の現在の状態(Snapshot)を取得するクライアントです。 286 | 287 | ```cs 288 | using var ndgrSnapshotFetcher = new NdgrSnapshotFetcher(); 289 | 290 | // 現在の状態(運営コメントの設定やアンケートの状態)を取得する 291 | await foreach (var chunkedMessage in ndgrSnapshotFetcher.FetchCurrentSnapshotAsync(viewApiUri)) 292 | { 293 | Console.WriteLine(chunkedMessage); 294 | } 295 | ``` 296 | 297 | 298 | ### NdgrApiClient 299 | 300 | **「NdgrApiClient」は上級者向けのクライアントです。** 301 | より簡単にコメント受信だけがしたい場合は**NdgrLiveCommentFetcher**か**NdgrPastCommentFetcher**を使って下さい。 302 | 303 | NDGRと通信するためのAPIクライアントです。NDGRのAPIとの通信をほぼラップせずに提供します。 304 | 結果はすべてProtoBuffから自動生成されたデータ構造をそのまま返します。 305 | 306 | また`NdgrLiveCommentFetcher`/`NdgrPastCommentFetcher`/`NdgrSnapshotFetcher`が内部的に依存するクライアントでもあります。 307 | 308 | 自身でNDGRとの通信を細かく制御したい場合に使用してください。 309 | 310 | ```cs 311 | // 初期化 312 | using (var ndgrApiClient = new NdgrApiClient()) 313 | { 314 | // /api/view/v4/:view?at=now の取得 315 | var next = await ndgrApiClient.FetchViewAtNowAsync(viewApiUri); 316 | 317 | // /api/view/v4/:view?at=unixtime の取得 318 | await foreach (var chunkedEntry in ndgrApiClient.FetchViewAtAsync(viewApiUri, next.At)) 319 | { 320 | switch (chunkedEntry.EntryCase) 321 | { 322 | case ChunkedEntry.EntryOneofCase.None: 323 | break; 324 | case ChunkedEntry.EntryOneofCase.Backward: 325 | break; 326 | case ChunkedEntry.EntryOneofCase.Previous: 327 | break; 328 | case ChunkedEntry.EntryOneofCase.Segment: 329 | break; 330 | case ChunkedEntry.EntryOneofCase.Next: 331 | break; 332 | default: 333 | throw new ArgumentOutOfRangeException(); 334 | } 335 | } 336 | } 337 | ``` 338 | 339 | また通信の挙動をより細かく制御したい場合はコンストラクタに`HttpClient`を渡すことができます。 340 | ただし**コンストラクタでHttpClientを指定した場合、このHttpClientのDisposeは自身で管理してください** 341 | 342 | ```cs 343 | // HttpClientの作成 344 | var httpClient = new HttpClient(new HttpClientHandler()); 345 | 346 | // UAを設定したり 347 | httpClient.DefaultRequestHeaders.Add("User-Agent", "my-user-agent"); 348 | 349 | // ndgrApiClientの作成 350 | var ndgrApiClient = new NdgrApiClient(httpClient); 351 | 352 | // NdgrApiClientをDisposeしただけではHttpClientはDisposeされない 353 | // 自身でHttpClientのDisposeを行う必要あり 354 | ndgrApiClient.Dispose(); 355 | httpClient.Dispose(); 356 | ``` 357 | 358 | #### 補足: 例外 359 | 360 | 次の例外が定義されており、`NdgrApiClient`の動作に応じて例外が発行されます。 361 | 362 | ```cs 363 | // 基底クラス 364 | public abstract class NdgrApiClientException : Exception 365 | { 366 | protected NdgrApiClientException(string message) : base(message) 367 | { 368 | } 369 | } 370 | 371 | // バイトの読み出し、ProtocolBuffer周りで例外が発生した場合に発行 372 | public sealed class NdgrApiClientByteReadException : NdgrApiClientException 373 | { 374 | public NdgrApiClientByteReadException(string message) : base(message) 375 | { 376 | } 377 | } 378 | 379 | // 通信に失敗した場合に発行 380 | public sealed class NdgrApiClientHttpException : Exception 381 | { 382 | public HttpStatusCode HttpStatusCode { get; } 383 | 384 | public NdgrApiClientHttpException(HttpStatusCode httpStatusCode) 385 | { 386 | HttpStatusCode = httpStatusCode; 387 | } 388 | 389 | public override string ToString() 390 | { 391 | return $"{base.ToString()}, {nameof(HttpStatusCode)}: {HttpStatusCode}"; 392 | } 393 | } 394 | ``` 395 | 396 | 397 | # ライセンスについて 398 | 399 | MITライセンスです。 400 | 401 | # 権利表記 402 | 403 | ### R3 404 | 405 | MIT License 406 | 407 | Copyright (c) 2024 Cysharp, Inc. 408 | 409 | Permission is hereby granted, free of charge, to any person obtaining a copy 410 | of this software and associated documentation files (the "Software"), to deal 411 | in the Software without restriction, including without limitation the rights 412 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 413 | copies of the Software, and to permit persons to whom the Software is 414 | furnished to do so, subject to the following conditions: 415 | 416 | The above copyright notice and this permission notice shall be included in all 417 | copies or substantial portions of the Software. 418 | 419 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 420 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 421 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 422 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 423 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 424 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 425 | SOFTWARE. 426 | 427 | 428 | ### Protocol Buffers 429 | 430 | ``` 431 | Copyright 2008 Google Inc. All rights reserved. 432 | 433 | Redistribution and use in source and binary forms, with or without 434 | modification, are permitted provided that the following conditions are 435 | met: 436 | 437 | * Redistributions of source code must retain the above copyright 438 | notice, this list of conditions and the following disclaimer. 439 | * Redistributions in binary form must reproduce the above 440 | copyright notice, this list of conditions and the following disclaimer 441 | in the documentation and/or other materials provided with the 442 | distribution. 443 | * Neither the name of Google Inc. nor the names of its 444 | contributors may be used to endorse or promote products derived from 445 | this software without specific prior written permission. 446 | 447 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 448 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 449 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 450 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 451 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 452 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 453 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 454 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 455 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 456 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 457 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 458 | 459 | Code generated by the Protocol Buffer compiler is owned by the owner 460 | of the input file used when generating it. This code is not 461 | standalone and requires a support library to be linked with it. This 462 | support library is itself covered by the above license. 463 | ``` 464 | -------------------------------------------------------------------------------- /Sandbox/Sandbox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Dwango.Nicolive.Chat.Data; 7 | using Dwango.Nicolive.Chat.Service.Edge; 8 | using NdgrClientSharp; 9 | using NdgrClientSharp.NdgrApi; 10 | using R3; 11 | 12 | var viewApiUri = ""; 13 | using var live = new NdgrLiveCommentFetcher(); 14 | 15 | 16 | live.ConnectionStatus.Subscribe(x => Console.WriteLine(x)); 17 | live.OnMessageReceived.Subscribe(x => Console.WriteLine(x)); 18 | live.Connect(viewApiUri); 19 | live.OnProgramEnded.Subscribe(_ => live.Disconnect()); 20 | 21 | 22 | Console.ReadLine(); -------------------------------------------------------------------------------- /Sandbox/Sandbox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | Exe 6 | netcoreapp3.1 7 | 12 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------