├── .gitignore
├── .travis.yml
├── README.md
├── TwitchLib.Extension.Core
├── Authentication
│ ├── Events
│ │ └── TokenValidatedContext.cs
│ ├── TwitchExtensionAuthDefaults.cs
│ ├── TwitchExtensionAuthExtensions.cs
│ ├── TwitchExtensionAuthHandler.cs
│ └── TwitchExtensionAuthOptions.cs
├── ExtensionsManager
│ └── ExtensionManagerExtensions.cs
└── TwitchLib.Extension.Core.csproj
├── TwitchLib.Extension.sln
└── TwitchLib.Extension
├── Events
└── SecretRotatedEventArgs.cs
├── Exceptions
├── BadParameterException.cs
├── BadRequestException.cs
├── BadResourceException.cs
├── BadScopeException.cs
└── InvalidCredentialException.cs
├── Extension
├── ExtensionBase.cs
├── ExtensionConfiguration.cs
├── ExtensionManager.cs
├── Extensions.cs
├── ExtensionsConfiguration.cs
├── RotatedSecretExtension.cs
└── StaticSecretExtension.cs
├── Models
├── CreateSecretRequest.cs
├── ExtensionPubSubRequest.cs
├── LiveChannel.cs
├── LiveChannels.cs
├── Secret.cs
├── Secrets.cs
├── SetExtensionBroadcasterOAuthReceiptRequest.cs
├── SetExtensionRequiredConfigurationRequest.cs
└── TwitchLibJsonSerializer.cs
└── TwitchLib.Extension.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
400 | # Builds
401 | build/
402 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: csharp
2 | mono: none
3 | dotnet: 2.0.0
4 | dist: trusty
5 | script:
6 | - dotnet restore
7 | - dotnet build /home/travis/build/TwitchLib/TwitchLib.Extension/TwitchLib.Extension/TwitchLib.Extension.csproj --framework netstandard2.0
8 | - dotnet build /home/travis/build/TwitchLib/TwitchLib.Extension/TwitchLib.Extension.Core/TwitchLib.Extension.Core.csproj
9 |
10 | addons:
11 | apt:
12 | sources:
13 | - sourceline: 'deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main'
14 | key_url: 'https://packages.microsoft.com/keys/microsoft.asc'
15 | packages:
16 | - dotnet-hostfxr-1.0.1
17 | - dotnet-sharedframework-microsoft.netcore.app-1.0.5
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## About
6 | TwitchLib is a powerful C# library that allows for interaction with various Twitch services. Currently supported services are: chat and whisper, API's (v3, v5, helix, undocumented, and third party), and PubSub event system. Below are the descriptions of the core components that make up TwitchLib.
7 |
8 | * **TwitchClient**: Handles chat and whisper Twitch services. Complete with a suite of events that fire for virtually every piece of data received from Twitch. Helper methods also exist for replying to whispers or fetching moderator lists.
9 | * **TwitchAPI**: Complete coverage of v3, v5, and Helix endpoints. The API is now a singleton class. This class allows fetching all publically accessable data as well as modify Twitch services like profiles and streams.
10 | * **TwitchPubSub**: Supports all documented Twitch PubSub topics as well as a few undocumented ones.
11 |
12 | ## Implementing
13 | Below are basic examples of how to utilize TwitchLib.Extension. These are C# examples, but this library can also be used in Visual Basic.
14 | This is meant to be utilized as part of a web based frontend, with that in mind the following examples are based on new Web applications built in Full framework 4.5.2 and above or dotnet core 2.0 and above.
15 |
16 |
17 | **NOTE**: This documentation is currently not final, support for the Extension Library wil be done via the Twitch API - Discord server
18 |
19 | ### Basics - full framework or dotnet core
20 | ```csharp
21 | using TwitchLib.Extension;
22 |
23 | //There are currently two types of extension which Extend the abstract ExtensionBase class
24 | //StaticSecretExtension & RotatedSecretExtension. StaticSecretExtension does not rotate the serect
25 | //away from the intial secret you set. RotatedSecretExtension rotates the secret based on the time interval you set
26 | //Twitch recommends a secret is reotated every 12 hours. More Extension types will be created in the future
27 | StaticSecretExtension staticExtension = new StaticSecretExtension(new ExtensionConfiguration
28 | {
29 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}",
30 | OwnerId = "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}",
31 | VersionNumber = "{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1
32 | StartingSecret = "{{YOUR_EXTENSION_SECRET}}"
33 |
34 | });
35 |
36 | RotatedSecretExtension rotatedExtension = new RotatedSecretExtension(new ExtensionConfiguration
37 | {
38 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}",
39 | OwnerId = "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}",
40 | VersionNumber = "{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1
41 | StartingSecret = "{{YOUR_EXTENSION_SECRET}}"
42 |
43 | }, 720);
44 |
45 | //Verify a JWT
46 | string jwt = Request.Headers["x-extension-jwt"];
47 | ClaimsPrincipal user = extension.Verify(jwt, out var validTokenOverlay);
48 | if (user == null) throw new Exception("Not valid");
49 |
50 | //Creates a new secret and returns a list of the current available secrets
51 | //It's not recommended to use this method outside of an Extension instance.
52 | //It's recommended that a SecretHandler is created/used, we have created two for you
53 | //StaticSecretHandler or RotatedSecretHandler you can also implement the abstract SecretHandler class
54 | var secrets = await extension.CreateExtensionSecretAsync();
55 |
56 | //Gets the current secret from the Extensions secret handler
57 | var currentSecret = extension.GetCurrentSecret();
58 |
59 | //Gets the current list of secrets for the Extension from twitch
60 | secrets = await extension.GetExtensionSecretAsync();
61 |
62 | //FOR EMERGENCY USE ONLY - deletes all extension secrets from twitch, your extension will no longer work
63 | var complete = await extension.RevokeExtensionSecretAsync();
64 |
65 |
66 | var channels = await extension.GetLiveChannelsWithExtensionActivatedAsync(null);
67 |
68 | var channelId = "{{BROADCASTER_CHANNEL_ID}}"; //the channel id can be received from the current verified user principal;
69 | //var channelId = user.Claims.FirstOrDefault(y => y.Type == "channel_id").Value
70 |
71 |
72 | //Within your extension version management -> Extension Capabilities -> Required Configurations
73 | //When a braodcaster has "setup" the extension to your liking and it requires no further mandatory config
74 | //send the channelId and the string set in you version settings to signify this to twitch
75 | complete = await extension.SetExtensionRequiredConfigurationAsync(channelId, "{{WHATEVER_STRING_YOU_SET_IN_VERSION_MANAGEMENT}}");
76 |
77 |
78 | //Within your extension version management -> Extension Capabilities -> Required Broadcaster Abilities
79 | //An OAuth process may occur, set your scopes in this section under Version Management
80 | //The broadcaster will go through the OAuth process set your Oauth callback in
81 | //Extension -> Settings-> General -> OAuth Redirect URI
82 | //Then follow the standard/twitch procedure to get an access_token
83 | //when complete pass true or when failed pass false using the method below
84 | complete = await extension.SetExtensionBroadcasterOAuthReceiptAsync(channelId, false);
85 |
86 |
87 | //You can send pubsub messages to your extension
88 | //see the twitch example for receiving these message
89 | //the below method send the message
90 | //It is up to you to determine permisions to send messages, the EBS or the current user can send messages
91 | //check https://dev.twitch.tv/docs/extensions/reference#jwt-schema (pubsub_perms) for more info
92 | //if a jwt is not passed it creates one based on the EBS sending the message
93 | complete = await extension.SendExtensionPubSubMessageAsync(channelId, new TwitchLib.Extension.Models.ExtensionPubSubRequest {
94 | Content_Type = "application/json",
95 | Targets = [ "broadcast"],
96 | Message = "{\"foo\":\"bar\"}"
97 | });
98 |
99 |
100 |
101 | ```
102 |
103 |
104 |
105 | ### .Net Core
106 | There is currently additional support for .Net core.
107 |
108 | #### Startup.cs
109 | ```csharp
110 | using TwitchLib.Extension;
111 | using TwitchLib.Extension.Core.Authentication;
112 | using TwitchLib.Extension.Core.ExtensionsManager;
113 |
114 | public void ConfigureServices(IServiceCollection services)
115 | {
116 | .....
117 | services.AddAuthentication(options =>
118 | {
119 | /*Options removed for space*/
120 | })
121 | .AddTwitchExtensionAuth();//<---- this is the bit you're after
122 |
123 | services.AddAuthorization(options =>
124 | {
125 | options.AddPolicy("{{CREATE_A_POLICY_NAME}}",
126 | policy => policy.RequireClaim("extension_id",
127 | "{{INSERT_YOUR_EXTENSION_CLIENTID}}",
128 | "{{INSERT_YOUR_EXTENSION_CLIENTID2}}",
129 | )
130 | );
131 | });
132 |
133 | //Add the extension manager functionality
134 | services.AddTwitchExtensionManager();
135 | .....
136 | }
137 |
138 |
139 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
140 | {
141 | .....
142 | //This allows for multiple extensions.
143 | app.UseTwitchExtensionManager(serviceProvider, new Dictionary
144 | {
145 | {
146 | "{{INSERT_YOUR_EXTENSION_CLIENTID}}",
147 | new StaticSecretExtension( new ExtensionConfiguration {
148 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID}}",
149 | OwnerId= "{{THE_TWITCH_ID_OF_EXTENSION_OWNER}}",
150 | VersionNumber ="{{VERSION_NUMBER_YOU_ARE_USING}}",//e.g. 0.0.1
151 | SecretHandler = new StaticSecretHandler("{{YOUR_EXTENSION_SECRET}}")
152 | })
153 | },
154 | {
155 | "{{INSERT_YOUR_EXTENSION_CLIENTID2}}",
156 | new StaticSecretExtension( new ExtensionConfiguration {
157 | Id = "{{INSERT_YOUR_EXTENSION_CLIENTID2}}",
158 | OwnerId= "{{THE_TWITCH_ID_OF_EXTENSION_OWNER2}}",
159 | VersionNumber ="{{VERSION_NUMBER_YOU_ARE_USING2}}",//e.g. 0.0.1
160 | SecretHandler = new StaticSecretHandler("{{YOUR_EXTENSION_SECRET2}}")
161 | })
162 | }
163 | });
164 |
165 |
166 |
167 | .....
168 | }
169 | ```
170 | #### Example Controller
171 | ```csharp
172 | [Produces("application/json")]
173 | [Route("api/[Controller]")]
174 | [Authorize(AuthenticationSchemes = "TwitchExtensionAuth", Policy ="{{ENTER_THE_POLICY_USED_AT_STARTUP}}")]
175 | public class ExampleController : Controller
176 | {
177 | private readonly ExtensionManager _extensionManager;
178 |
179 | public ExampleController(ExtensionManager extensionManager)
180 | {
181 | _extensionManager = extensionManager;
182 | }
183 |
184 | [HttpGet]
185 | [Route("[Action]")]
186 | public async Task Puzzle()
187 | {
188 | //as you added the AddTwitchExtensionAuth in Startup and Authorize attribute on the controller the user is Authenticated and Authorized for you
189 | var user = User;
190 |
191 | //User Id - if user is not null but user_id is the user has not given permision to share their user_id
192 | user.Claims.FirstOrDefault(y => y.Type == "user_id")
193 |
194 | //Channel Id
195 | user.Claims.FirstOrDefault(y => y.Type == "channel_id")
196 |
197 | //Extension Id - This is added at Authorization and is the client id of the extension you were authorized against (if you have multiple extensions)
198 | user.Claims.FirstOrDefault(y => y.Type == "extension_id")
199 |
200 | //Other Claims - "exp", "opaque_user_id", "role", "pubsub_perms"
201 |
202 |
203 | var extensionId = user.Claims.FirstOrDefault(y => y.Type == "extension_id").Value;
204 | //call an extension method
205 | _extensionManager.GetExtension(extensionId).GetCurrentSecret(); //All Extension API methods available
206 | }
207 | }
208 |
209 |
210 |
211 | ```
212 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/Authentication/Events/TokenValidatedContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.IdentityModel.Tokens;
4 |
5 | namespace TwitchLib.Extension.Core.Authentication.Events
6 | {
7 | public class TokenValidatedContext : ResultContext
8 | {
9 | public TokenValidatedContext(
10 | HttpContext context,
11 | AuthenticationScheme scheme,
12 | TwitchExtensionAuthOptions options)
13 | : base(context, scheme, options) {}
14 |
15 | public SecurityToken SecurityToken { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthDefaults.cs:
--------------------------------------------------------------------------------
1 | namespace TwitchLib.Extension.Core.Authentication
2 | {
3 | public static class TwitchExtensionAuthDefaults
4 | {
5 | public const string AuthenticationScheme = "TwitchExtensionAuth";
6 |
7 | public static readonly string DisplayName = "TwitchExtensionAuth";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using System;
3 |
4 | namespace TwitchLib.Extension.Core.Authentication
5 | {
6 | public static class TwitchExtensionAuthExtensions
7 | {
8 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder)
9 | => builder.AddTwitchExtensionAuth(options => { });
10 |
11 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, Action configureOptions)
12 | => builder.AddTwitchExtensionAuth(TwitchExtensionAuthDefaults.AuthenticationScheme, configureOptions);
13 |
14 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions)
15 | => builder.AddTwitchExtensionAuth(authenticationScheme, TwitchExtensionAuthDefaults.DisplayName, configureOptions);
16 |
17 | public static AuthenticationBuilder AddTwitchExtensionAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions)
18 | => builder.AddScheme(authenticationScheme, displayName, configureOptions);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.Extensions.Options;
4 | using TwitchLib.Extension.Core.Authentication.Events;
5 | using System.Text.Encodings.Web;
6 | using System.Threading.Tasks;
7 |
8 | namespace TwitchLib.Extension.Core.Authentication
9 | {
10 | public class TwitchExtensionAuthHandler : AuthenticationHandler
11 | {
12 | private readonly ExtensionManager _extensionManager;
13 |
14 | public TwitchExtensionAuthHandler(IOptionsMonitor options,
15 | ILoggerFactory logger,
16 | UrlEncoder encoder,
17 | ISystemClock clock,
18 | ExtensionManager extensionManager) : base(options, logger, encoder, clock)
19 | {
20 | _extensionManager = extensionManager;
21 | }
22 |
23 | protected override async Task HandleAuthenticateAsync()
24 | {
25 | string authorization = Request.Headers["x-extension-jwt"];
26 |
27 | if (!string.IsNullOrEmpty(authorization))
28 | {
29 | try
30 | {
31 | var user = _extensionManager.Verify(authorization, out var validTokenOverlay);
32 |
33 | if(user == null )
34 | return AuthenticateResult.NoResult();
35 |
36 | var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
37 | {
38 | Principal = user,
39 | SecurityToken = validTokenOverlay
40 | };
41 |
42 | tokenValidatedContext.Success();
43 | return tokenValidatedContext.Result;
44 | }
45 | catch
46 | {
47 | return AuthenticateResult.NoResult();
48 | }
49 | }
50 |
51 | return AuthenticateResult.NoResult();
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/Authentication/TwitchExtensionAuthOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 |
3 | namespace TwitchLib.Extension.Core.Authentication
4 | {
5 | public class TwitchExtensionAuthOptions : AuthenticationSchemeOptions
6 | {
7 | public TwitchExtensionAuthOptions()
8 | {
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/ExtensionsManager/ExtensionManagerExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace TwitchLib.Extension.Core.ExtensionsManager
8 | {
9 | public static class ExtensionManagerExtensions
10 | {
11 | public static IServiceCollection AddTwitchExtensionManager(this IServiceCollection services)
12 | {
13 | if (!services.Any(x => x.ServiceType == typeof(Extensions)))
14 | {
15 | services.AddSingleton();
16 | }
17 |
18 | if (!services.Any(x => x.ServiceType == typeof(ExtensionManager)))
19 | {
20 | services.AddSingleton();
21 | }
22 | return services;
23 | }
24 |
25 | public static IApplicationBuilder UseTwitchExtensionManager(this IApplicationBuilder app, IServiceProvider serviceProvider, IDictionary configuredExtensions)
26 | {
27 | var extensions = serviceProvider.GetService();
28 |
29 | extensions.Extension = configuredExtensions;
30 |
31 | return app;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.Core/TwitchLib.Extension.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp2.1
4 | TwitchLib.Extension.Core
5 | 1.3.0
6 | Extension component of TwitchLib. This component expands the base Extension Library to include a dot net standard (core) Authentication Middleware and Extension Manager
7 | true
8 | luckyNoS7evin,thetestgame
9 | swiftyspiffy (cole)
10 | https://colejelinek.com/dev/twitchlib.png
11 | https://github.com/TwitchLib/TwitchLib.Extension
12 | https://opensource.org/licenses/MIT
13 | Copyright 2022
14 | Version 1 of the Extension library
15 | https://github.com/TwitchLib/TwitchLib.Extension
16 | Git
17 | twitch extension api c# csharp net core 2.1 authorization extension manager
18 | en-US
19 | 2.2.0.0
20 | 2.2.0.0
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/TwitchLib.Extension.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27130.2027
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Extension", "TwitchLib.Extension\TwitchLib.Extension.csproj", "{3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Extension.Core", "TwitchLib.Extension.Core\TwitchLib.Extension.Core.csproj", "{5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {3707BBF6-ED3D-4044-8110-6C2C5CBEFB56}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {5ED18BCE-3A8B-4C7D-8A9A-4FB25BD241F3}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {8E06B51D-BBC5-431C-A0FE-1B967CA5AB44}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Events/SecretRotatedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Events
4 | {
5 | public class SecretRotatedEventArgs : EventArgs
6 | {
7 | public string NewSecret { get; set; }
8 | public DateTime Expires { get; set; }
9 |
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Exceptions/BadParameterException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Exceptions
4 | {
5 | /// Exception representing an invalid resource
6 | public class BadParameterException : Exception
7 | {
8 | /// Exception constructor
9 | public BadParameterException(string badParamData)
10 | : base(badParamData)
11 | {
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Exceptions/BadRequestException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Exceptions
4 | {
5 | /// Exception representing a request that doesn't have a clientid attached.
6 | public class BadRequestException : Exception
7 | {
8 | /// Exception constructor
9 | public BadRequestException(string apiData)
10 | : base(apiData)
11 | {
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Exceptions/BadResourceException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Exceptions
4 | {
5 | /// Exception representing an invalid resource
6 | public class BadResourceException : Exception
7 | {
8 | /// Exception constructor
9 | public BadResourceException(string apiData)
10 | : base(apiData)
11 | {
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/TwitchLib.Extension/Exceptions/BadScopeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Exceptions
4 | {
5 | /// Exception representing a provided scope was not permitted.
6 | public class BadScopeException : Exception
7 | {
8 | /// Exception constructor
9 | public BadScopeException(string data)
10 | : base(data)
11 | {
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/TwitchLib.Extension/Exceptions/InvalidCredentialException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace TwitchLib.Extension.Exceptions
4 | {
5 | /// Exception representing a detection that sent credentials were invalid.
6 | public class InvalidCredentialException : Exception
7 | {
8 | /// Exception constructor
9 | public InvalidCredentialException(string data)
10 | : base(data)
11 | {
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/ExtensionBase.cs:
--------------------------------------------------------------------------------
1 | using JWT;
2 | using JWT.Algorithms;
3 | using JWT.Serializers;
4 | using Microsoft.IdentityModel.Tokens;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.IO;
9 | using System.Linq;
10 | using System.Net;
11 | using System.Security.Claims;
12 | using System.Threading.Tasks;
13 | using TwitchLib.Extension.Exceptions;
14 | using TwitchLib.Extension.Models;
15 |
16 | namespace TwitchLib.Extension
17 | {
18 | public abstract class ExtensionBase
19 | {
20 | private const string _extensionUrl = "https://api.twitch.tv/helix/extensions/{0}";
21 | private readonly ExtensionConfiguration _config;
22 | protected IEnumerable Secrets { get; set; }
23 |
24 | public string CurrentSecret { get => Secrets.ToList().OrderByDescending(x => x.Expires).First().Content; }
25 |
26 | public ExtensionBase(ExtensionConfiguration config)
27 | {
28 | _config = config;
29 | Secrets = new List { new Secret(config.StartingSecret, DateTime.Now, DateTime.Now.AddYears(100)) };
30 | }
31 |
32 | ///
33 | /// Creates a new secret for a specified extension. Also rotates any current secrets out of service, with enough
34 | /// time for extension clients to gracefully switch over to the new secret. The delay period,
35 | /// between the generation of the new secret and its use by Twitch, is specified by a required parameter, activation_delay_secs.
36 | /// The default delay is 300 (5 minutes); if a value less than this is specified, Twitch uses 300.
37 | ///
38 | /// Use this function only when you are ready to install the new secret it returns.
39 | ///
40 | /// How long Twitch should wait before using your new secret and rolling it out to users
41 | /// Data object containing all of current extension secrets that are valid and haven't expired
42 | public virtual async Task CreateExtensionSecretAsync(int activationDelaySeconds = 300) =>
43 | await CreateExtensionSecretAsync(CurrentSecret, _config.Id, _config.OwnerId, activationDelaySeconds);
44 |
45 | ///
46 | /// Retrieves a specified extension’s secret data: a version and an array of secret objects.
47 | /// Each secret object returned contains a base64-encoded secret, a UTC timestamp when the secret becomes active,
48 | /// and a timestamp when the secret expires.
49 | ///
50 | /// Data object containing all of current extension secrets that are valid and haven't expired
51 | public virtual async Task GetExtensionSecretAsync() =>
52 | await GetExtensionSecretAsync(CurrentSecret, _config.Id, _config.OwnerId);
53 |
54 | ///
55 | /// Deletes all secrets associated with a specified extension.
56 | ///
57 | /// This immediately breaks all clients until both a new Create Extension Secret is executed
58 | /// and the clients manually refresh themselves.
59 | ///
60 | /// Use this only if a secret is compromised and must be removed immediately from circulation.
61 | ///
62 | /// true if secrets were successfully revoked
63 | public virtual async Task RevokeExtensionSecretAsync()
64 | {
65 | return await RevokeExtensionSecretAsync(
66 | CurrentSecret,
67 | _config.Id,
68 | _config.OwnerId);
69 | }
70 |
71 | ///
72 | /// Returns one page of live channels that have installed and activated a specified extension.
73 | ///
74 | /// A channel that just went live may take a few minutes to appear in this list, and a channel may continue to
75 | /// appear on this list for a few minutes after it stops broadcasting.
76 | ///
77 | ///
78 | /// List of channels that are live with the extension installed
79 | public virtual async Task GetLiveChannelsWithExtensionActivatedAsync(string cursor = null)
80 | {
81 | return await GetLiveChannelsWithExtensionActivatedAsync(
82 | CurrentSecret,
83 | _config.Id,
84 | _config.OwnerId,
85 | cursor);
86 | }
87 |
88 | ///
89 | /// Enable activation of a specified extension, after any required broadcaster configuration is correct.
90 | /// This is for extensions that require broadcaster configuration before activation.
91 | ///
92 | /// The Twitch channel ID we are setting the specified value for
93 | ///
94 | /// true if requiredConfiguration was set successfully
95 | public virtual async Task SetExtensionRequiredConfigurationAsync(string channelId, string requiredConfiguration)
96 | {
97 | return await SetExtensionRequiredConfigurationAsync(
98 | CurrentSecret,
99 | _config.Id,
100 | _config.VersionNumber,
101 | _config.OwnerId,
102 | channelId,
103 | requiredConfiguration);
104 | }
105 |
106 | ///
107 | /// Indicates whether the broadcaster allowed the permissions your extension requested,
108 | /// through a required permissions_received parameter. The endpoint URL includes the channel ID
109 | /// of the page where the extension is iframe embedded.
110 | ///
111 | /// The Twitch channel ID we are setting the specified value for
112 | ///
113 | /// true if permissionsReceived was set successfully
114 | public virtual async Task SetExtensionBroadcasterOAuthReceiptAsync(string channelId, bool permissionsReceived)
115 | {
116 | return await SetExtensionBroadcasterOAuthReceiptAsync(
117 | CurrentSecret,
118 | _config.Id,
119 | _config.VersionNumber,
120 | _config.OwnerId,
121 | channelId,
122 | permissionsReceived);
123 | }
124 |
125 | ///
126 | /// Twitch provides a publish-subscribe system for your EBS (Extension Back-end Service) to communicate
127 | /// with both the broadcaster and viewers. Calling this endpoint forwards your message using the same
128 | /// mechanism as the send() function in the JavaScript helper API.
129 | ///
130 | /// The Twitch channel ID we are sending the message for
131 | ///
132 | /// Optional JWT of user, this JWT should only be those passed by twitch in the x-extension-jwt header
133 | /// true if PubSub message successfully sent
134 | public virtual async Task SendExtensionPubSubMessageAsync(string channelId, Models.ExtensionPubSubRequest message, string jwt = null)
135 | {
136 | return await SendExtensionPubSubMessageAsync(
137 | CurrentSecret,
138 | _config.Id,
139 | _config.OwnerId,
140 | channelId,
141 | message,
142 | jwt);
143 | }
144 |
145 | protected async Task CreateExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId, int activationDelaySeconds = 300)
146 | {
147 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
148 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
149 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
150 | if (activationDelaySeconds < 300) throw new BadParameterException("The activation delay in seconds is not allowed to be less than 300");
151 |
152 | var url = $"jwt/secrets?extension_id={extensionId}";
153 | var request = new CreateSecretRequest { Activation_Delay_Secs = activationDelaySeconds };
154 | return ExtensionSecretsData.FromJson((await RequestAsync(extensionSecret, url, "POST", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Value);
155 | }
156 |
157 | protected async Task GetExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId)
158 | {
159 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
160 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
161 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
162 |
163 | var url = $"jwt/secrets?extension_id={extensionId}";
164 | return ExtensionSecretsData.FromJson((await RequestAsync(extensionSecret, url, "GET", extensionOwnerId, extensionId).ConfigureAwait(false)).Value);
165 | }
166 |
167 | protected async Task RevokeExtensionSecretAsync(string extensionSecret, string extensionId, string extensionOwnerId)
168 | {
169 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
170 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
171 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
172 |
173 | var url = $"jwt/secrets?extension_id={extensionId}";
174 | return (await RequestAsync(extensionSecret, url, "DELETE", extensionOwnerId, extensionId).ConfigureAwait(false)).Key == 204;
175 | }
176 |
177 | protected async Task GetLiveChannelsWithExtensionActivatedAsync(string extensionSecret, string extensionId,string extensionOwnerId, string cursor = null)
178 | {
179 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
180 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
181 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
182 |
183 | var url = $"/live?extension_id{extensionId}";
184 | if (!string.IsNullOrWhiteSpace(cursor))
185 | url += $"?after={cursor}";
186 | return LiveChannels.FromJson((await RequestAsync(extensionSecret, url, "GET", extensionOwnerId, extensionId).ConfigureAwait(false)).Value);
187 | }
188 |
189 | protected async Task SetExtensionRequiredConfigurationAsync(string extensionSecret, string extensionId, string extensionVersion, string extensionOwnerId, string channelId, string requiredConfiguration)
190 | {
191 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
192 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
193 | if (string.IsNullOrWhiteSpace(extensionVersion)) throw new BadParameterException("The extension version is not valid. It is not allowed to be null, empty or filled with whitespaces.");
194 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
195 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
196 | if (string.IsNullOrEmpty(requiredConfiguration)) throw new BadParameterException("The required configuration is not valid. It is not allowed to be null or empty.");
197 |
198 | var url = $"required_configuration?broadcaster_id={channelId}";
199 | var request = new SetExtensionRequiredConfigurationRequest
200 | {
201 | RequiredConfiguration = requiredConfiguration,
202 | ExtensionId = extensionId,
203 | ExtensionVersion = extensionVersion,
204 | };
205 |
206 | return (await RequestAsync(extensionSecret, url, "PUT", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Key == 204;
207 | }
208 |
209 | protected async Task SetExtensionBroadcasterOAuthReceiptAsync(string extensionSecret, string extensionId, string extensionVersion, string extensionOwnerId, string channelId, bool permissionsReceived)
210 | {
211 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
212 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
213 | if (string.IsNullOrWhiteSpace(extensionVersion)) throw new BadParameterException("The extension version is not valid. It is not allowed to be null, empty or filled with whitespaces.");
214 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
215 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
216 |
217 | var url = $"{extensionId}/{extensionVersion}/oauth_receipt?channel_id={channelId}";
218 | var request = new SetExtensionBroadcasterOAuthReceiptRequest { Permissions_Received = permissionsReceived };
219 | return (await RequestAsync(extensionSecret, url, "PUT", extensionOwnerId, extensionId, request.ToJson()).ConfigureAwait(false)).Key == 204;
220 | }
221 |
222 | protected async Task SendExtensionPubSubMessageAsync(string extensionSecret, string extensionId, string extensionOwnerId, string channelId, Models.ExtensionPubSubRequest message, string jwt =null)
223 | {
224 | if (string.IsNullOrWhiteSpace(extensionSecret)) throw new BadParameterException("The extension secret is not valid. It is not allowed to be null, empty or filled with whitespaces.");
225 | if (string.IsNullOrWhiteSpace(extensionId)) throw new BadParameterException("The extension id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
226 | if (string.IsNullOrWhiteSpace(extensionOwnerId)) throw new BadParameterException("The extension owner id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
227 | if (string.IsNullOrWhiteSpace(channelId)) throw new BadParameterException("The channel id is not valid. It is not allowed to be null, empty or filled with whitespaces.");
228 |
229 | if (string.IsNullOrEmpty(jwt)) jwt = Sign(extensionSecret, extensionOwnerId, 10, channelId);
230 | return (await RequestAsync(extensionSecret, "pubsub", "POST", extensionOwnerId, extensionId, message.ToJson(), jwt).ConfigureAwait(false)).Key == 204;
231 | }
232 |
233 | private async Task> RequestAsync(string secret, string url, string method, string userId, string clientId, object payload=null, string jwt = null)
234 | {
235 | var request = WebRequest.CreateHttp(string.Format(_extensionUrl, url));
236 |
237 | request.Headers["Client-Id"] = clientId;
238 | request.Method = method;
239 | request.ContentType = "application/json";
240 | var token = jwt ?? Sign(secret, userId, 10);
241 | request.Headers["Authorization"] = $"Bearer {token}";
242 |
243 | if (payload != null)
244 | using (var writer = new StreamWriter(await request.GetRequestStreamAsync()))
245 | writer.Write(payload);
246 | try
247 | {
248 | var response = (HttpWebResponse)request.GetResponse();
249 | using (var reader = new StreamReader(response.GetResponseStream()))
250 | {
251 | string data = reader.ReadToEnd();
252 | return new KeyValuePair((int)response.StatusCode, data);
253 | }
254 | }
255 | catch (WebException ex) { HandleWebException(ex); }
256 |
257 | return new KeyValuePair(0, null);
258 | }
259 |
260 |
261 | private void HandleWebException(WebException e)
262 | {
263 | HttpWebResponse errorResp = e.Response as HttpWebResponse;
264 | if (errorResp == null)
265 | throw e;
266 | switch (errorResp.StatusCode)
267 | {
268 | case HttpStatusCode.BadRequest:
269 | throw new BadRequestException("Your request failed because your ClientID was invalid/not set.");
270 | case HttpStatusCode.Unauthorized:
271 | throw new BadScopeException("Your request was blocked due to bad credentials (do you have the right scope for your access token?).");
272 | case HttpStatusCode.NotFound:
273 | throw new BadResourceException("The resource you tried to access was not valid.");
274 | default:
275 | throw e;
276 | }
277 | }
278 |
279 |
280 | #region JWTSignAndVerify
281 | public ClaimsPrincipal Verify(string jwt, out SecurityToken validTokenOverlay)
282 | {
283 | ClaimsPrincipal user = null;
284 | validTokenOverlay = null;
285 | foreach (var secret in Secrets.ToList().OrderByDescending(x => x.Expires).Where(x => x.Expires > DateTime.Now))
286 | {
287 | user = VerifyWithSecret(jwt, secret.Content, out validTokenOverlay);
288 | if (user != null)
289 | {
290 | ((ClaimsIdentity)user.Identity).AddClaim(new Claim("extension_id", _config.Id, ClaimValueTypes.String));
291 | break;
292 | }
293 | }
294 | return user;
295 | }
296 |
297 | private ClaimsPrincipal VerifyWithSecret(string jwt, string secret, out SecurityToken validTokenOverlay)
298 | {
299 | var validationParameters = new TokenValidationParameters
300 | {
301 | IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(secret)),
302 | ValidateAudience = false,
303 | ValidateLifetime = false,
304 | ValidateIssuer = false,
305 | ValidateIssuerSigningKey = true
306 | };
307 |
308 | var handler = new JwtSecurityTokenHandler();
309 |
310 | try
311 | {
312 | return handler.ValidateToken(jwt, validationParameters, out validTokenOverlay);
313 | }
314 | catch
315 | {
316 | validTokenOverlay = null;
317 | return null;
318 | }
319 | }
320 |
321 | private string Sign(string secret, string userId, int expirySeconds)
322 | {
323 | var payload = new Dictionary
324 | {
325 | { "exp", (GetEpoch() + expirySeconds) },
326 | { "user_id", userId },
327 | { "role", "external" }
328 | };
329 |
330 | IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
331 | IJsonSerializer serializer = new JsonNetSerializer();
332 | IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
333 | IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
334 |
335 | var token = encoder.Encode(payload, Convert.FromBase64String(secret));
336 | return token;
337 | }
338 |
339 | class perms
340 | {
341 | public string[] send;
342 | }
343 | private string Sign(string secret, string userId, int expirySeconds, string channelId)
344 | {
345 | var perms = new perms { send = new string[] { "*" } };
346 |
347 | var payload = new Dictionary
348 | {
349 | { "exp", (GetEpoch() + expirySeconds) },
350 | { "user_id", userId },
351 | { "role", "external" },
352 | { "channel_id", channelId },
353 | { "pubsub_perms", perms }
354 | };
355 |
356 | IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
357 | IJsonSerializer serializer = new JsonNetSerializer();
358 | IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
359 | IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
360 |
361 | var token = encoder.Encode(payload, Convert.FromBase64String(secret));
362 | return token;
363 | }
364 |
365 | private int GetEpoch()
366 | {
367 | TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1);
368 | int secondsSinceEpoch = (int)t.TotalSeconds;
369 | return secondsSinceEpoch;
370 | }
371 |
372 | #endregion
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/ExtensionConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace TwitchLib.Extension
2 | {
3 | public class ExtensionConfiguration
4 | {
5 | public string Id { get; set; }
6 | public string OwnerId { get; set; }
7 | public string VersionNumber { get; set; }
8 | public string StartingSecret { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/ExtensionManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IdentityModel.Tokens;
2 | using System.Security.Claims;
3 |
4 | namespace TwitchLib.Extension
5 | {
6 | public class ExtensionManager
7 | {
8 | private Extensions _extensions;
9 | public ExtensionManager(Extensions extensions)
10 | {
11 | _extensions = extensions;
12 | }
13 |
14 | public ClaimsPrincipal Verify(string jwt, out SecurityToken validTokenOverlay)
15 | {
16 | ClaimsPrincipal user = null;
17 | validTokenOverlay = null;
18 |
19 | foreach (var extension in _extensions.Extension.Values)
20 | {
21 | user = extension.Verify(jwt, out validTokenOverlay);
22 | if (user != null)
23 | {
24 | break;
25 | }
26 | }
27 |
28 | return user;
29 | }
30 |
31 | public ExtensionBase GetExtension(string extensionId)
32 | {
33 | return _extensions.Extension[extensionId];
34 | }
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace TwitchLib.Extension
4 | {
5 | public class Extensions
6 | {
7 | public IDictionary Extension { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/ExtensionsConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace TwitchLib.Extension
4 | {
5 | public class ExtensionsConfiguration
6 | {
7 | public IEnumerable Extensions { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/RotatedSecretExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using TwitchLib.Extension.Events;
5 |
6 | namespace TwitchLib.Extension
7 | {
8 | public class RotatedSecretExtension : ExtensionBase
9 | {
10 | private readonly int _interval;
11 | public event EventHandler SecretRotated;
12 |
13 | public RotatedSecretExtension(ExtensionConfiguration config, int rotationIntervalMinutes = 720) : base(config)
14 | {
15 | _interval = rotationIntervalMinutes * 1000 * 60;
16 | var secretsData = GetExtensionSecretAsync().GetAwaiter().GetResult();
17 | if (secretsData != null)
18 | Secrets = secretsData.Data.SelectMany(x => x.Secrets).ToList();
19 |
20 | Task.Run(async () =>
21 | {
22 | while (true)
23 | {
24 | await Task.Delay(_interval);
25 |
26 | if (SecretRotated != null)
27 | {
28 | secretsData = await CreateExtensionSecretAsync();
29 | if (secretsData == null) return;
30 |
31 | Secrets = secretsData.Data.SelectMany(x => x.Secrets).ToList();
32 | var currentSecret = Secrets.ToList().OrderByDescending(x => x.Expires).First();
33 | OnSecretRotated(new SecretRotatedEventArgs
34 | {
35 | NewSecret = currentSecret.Content,
36 | Expires = currentSecret.Expires
37 | });
38 | }
39 | }
40 | });
41 | }
42 |
43 | protected virtual void OnSecretRotated(SecretRotatedEventArgs e)
44 | {
45 | SecretRotated?.Invoke(this, e);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Extension/StaticSecretExtension.cs:
--------------------------------------------------------------------------------
1 | namespace TwitchLib.Extension
2 | {
3 | public class StaticSecretExtension : ExtensionBase
4 | {
5 | public StaticSecretExtension(ExtensionConfiguration config) : base(config)
6 | { }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/CreateSecretRequest.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public class CreateSecretRequest
6 | {
7 | [JsonProperty(PropertyName = "activation_delay_secs")]
8 | public int Activation_Delay_Secs { get; internal set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/ExtensionPubSubRequest.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public class ExtensionPubSubRequest
6 | {
7 | [JsonProperty(PropertyName = "message")]
8 | public object Message { get; set; }
9 | [JsonProperty(PropertyName = "target")]
10 | public string[] Targets { get; set; }
11 | [JsonProperty(PropertyName = "broadcaster_id")]
12 | public string BroadcasterId { get; set; } = null;
13 | [JsonProperty(PropertyName = "is_global_broadcast")]
14 | public bool IsGlobalBroadcast => string.IsNullOrEmpty(this.BroadcasterId);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/LiveChannel.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public class LiveChannel
6 | {
7 | [JsonProperty(PropertyName = "broadcaster_id")]
8 | public string BroadcasterId { get; protected set; }
9 | [JsonProperty(PropertyName = "broadcaster_name")]
10 | public string BroadcasterName { get; protected set; }
11 | [JsonProperty(PropertyName = "game_name")]
12 | public string GameName { get; protected set; }
13 | [JsonProperty(PropertyName = "game_id")]
14 | public string GameId { get; protected set; }
15 | [JsonProperty(PropertyName = "title")]
16 | public string Title { get; protected set; }
17 | [JsonProperty(PropertyName = "view_count")]
18 | public int View_Count { get; protected set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/LiveChannels.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public partial class LiveChannels
6 | {
7 | [JsonProperty(PropertyName = "channels")]
8 | public LiveChannel[] Channels { get; protected set; }
9 | [JsonProperty(PropertyName = "cursor")]
10 | public string Cursor { get; protected set; }
11 | }
12 |
13 | public partial class LiveChannels
14 | {
15 | public static LiveChannels FromJson(string json) => JsonConvert.DeserializeObject(json, TwitchLibJsonSerializer.Settings);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/Secret.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 |
4 | namespace TwitchLib.Extension.Models
5 | {
6 | public class Secret
7 | {
8 | public Secret()
9 | { }
10 |
11 | public Secret(string content, DateTime active, DateTime expires)
12 | {
13 | Content = content;
14 | Active = active;
15 | Expires = expires;
16 | }
17 |
18 | [JsonProperty(PropertyName = "active_at")]
19 | public DateTime Active { get; protected set; }
20 | [JsonProperty(PropertyName = "content")]
21 | public string Content { get; protected set; }
22 | [JsonProperty(PropertyName = "expires_at")]
23 | public DateTime Expires { get; protected set; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/Secrets.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public partial class ExtensionSecrets
6 | {
7 | [JsonProperty(PropertyName = "format_version")]
8 | public string Format_Version { get; protected set; }
9 | [JsonProperty(PropertyName = "secrets")]
10 | public Secret[] Secrets { get; protected set; }
11 | }
12 |
13 | public partial class ExtensionSecretsData
14 | {
15 | [JsonProperty(PropertyName = "data")]
16 | public ExtensionSecrets[] Data { get; protected set; }
17 | }
18 |
19 | public partial class ExtensionSecretsData
20 | {
21 | public static ExtensionSecretsData FromJson(string json) => JsonConvert.DeserializeObject(json, TwitchLibJsonSerializer.Settings);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/SetExtensionBroadcasterOAuthReceiptRequest.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public class SetExtensionBroadcasterOAuthReceiptRequest
6 | {
7 | [JsonProperty(PropertyName = "permissions_received")]
8 | public bool Permissions_Received { get; internal set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/SetExtensionRequiredConfigurationRequest.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace TwitchLib.Extension.Models
4 | {
5 | public class SetExtensionRequiredConfigurationRequest
6 | {
7 | [JsonProperty(PropertyName = "required_configuration")]
8 | public string RequiredConfiguration { get; internal set; }
9 | [JsonProperty(PropertyName = "extension_id")]
10 | public string ExtensionId { get; internal set; }
11 | [JsonProperty(PropertyName = "extension_version")]
12 | public string ExtensionVersion { get; internal set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/Models/TwitchLibJsonSerializer.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Serialization;
3 |
4 | namespace TwitchLib.Extension.Models
5 | {
6 | internal static class TwitchLibJsonSerializer
7 | {
8 | public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
9 | {
10 | ContractResolver = new LowercaseContractResolver(),
11 | NullValueHandling = NullValueHandling.Ignore
12 | };
13 |
14 | public static string ToJson(this T self) => JsonConvert.SerializeObject(self, Formatting.Indented, Settings);
15 |
16 | public class LowercaseContractResolver : DefaultContractResolver
17 | {
18 | protected override string ResolvePropertyName(string propertyName)
19 | {
20 | return propertyName.ToLower();
21 | }
22 | }
23 |
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/TwitchLib.Extension/TwitchLib.Extension.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp2.1;net46
4 | TwitchLib.Extension
5 | 1.3.0
6 | Extension component of TwitchLib. This component allows you to access Twitch's Extension Api system
7 | true
8 | luckyNoS7evin,thetestgame
9 | swiftyspiffy (cole)
10 | https://colejelinek.com/dev/twitchlib.png
11 | https://github.com/TwitchLib/TwitchLib.Extension
12 | https://opensource.org/licenses/MIT
13 | Copyright 2022
14 | Version 1 of the Extension library
15 | https://github.com/TwitchLib/TwitchLib.Extension
16 | Git
17 | twitch extension api c# csharp net core 2.1
18 | en-US
19 | 2.2.0.0
20 | 2.2.0.0
21 | ../build/TwitchLib.Extension
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | NET46
31 |
32 |
33 | NETSTANDARD
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------