From e8908059c72f9c8c1e561628cff0be8bd20055f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Deme?= Date: Thu, 11 May 2023 05:54:39 +0200 Subject: [PATCH] initial commit --- .gitignore | 9 + README.md | 21 +- sources/Contenda/.gitignore | 219 ++++++++++++++++++ .../Contenda.Sdk/Auth/APIKeyAuthProvider.cs | 103 ++++++++ .../Contenda.Sdk/Auth/IAuthProvider.cs | 31 +++ sources/Contenda/Contenda.Sdk/Constants.cs | 34 +++ .../Contenda/Contenda.Sdk/Contenda.Sdk.csproj | 26 +++ sources/Contenda/Contenda.Sdk/ContendaAPI.cs | 169 ++++++++++++++ .../Exceptions/AuthenticationException.cs | 20 ++ .../Models/Request/SubmitJobV2.cs | 17 ++ .../Contenda.Sdk/Models/Request/TokenV2.cs | 13 ++ .../Models/Result/JobStatusResult.cs | 52 +++++ .../Contenda.Sdk/Models/Result/TokenResult.cs | 13 ++ .../Contenda.Sdk/Models/Result/UsageLimits.cs | 41 ++++ .../Models/VideoToBlogJobSubType.cs | 17 ++ .../Contenda.Sdk/PoorMansHttpClientFactory.cs | 26 +++ .../Contenda.SdkDemo/Contenda.SdkDemo.csproj | 14 ++ sources/Contenda/Contenda.SdkDemo/Program.cs | 51 ++++ sources/Contenda/Contenda.sln | 31 +++ sources/Contenda/Contenda.sln.DotSettings | 4 + 20 files changed, 909 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 sources/Contenda/.gitignore create mode 100644 sources/Contenda/Contenda.Sdk/Auth/APIKeyAuthProvider.cs create mode 100644 sources/Contenda/Contenda.Sdk/Auth/IAuthProvider.cs create mode 100644 sources/Contenda/Contenda.Sdk/Constants.cs create mode 100644 sources/Contenda/Contenda.Sdk/Contenda.Sdk.csproj create mode 100644 sources/Contenda/Contenda.Sdk/ContendaAPI.cs create mode 100644 sources/Contenda/Contenda.Sdk/Exceptions/AuthenticationException.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/Request/SubmitJobV2.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/Request/TokenV2.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/Result/JobStatusResult.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/Result/TokenResult.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/Result/UsageLimits.cs create mode 100644 sources/Contenda/Contenda.Sdk/Models/VideoToBlogJobSubType.cs create mode 100644 sources/Contenda/Contenda.Sdk/PoorMansHttpClientFactory.cs create mode 100644 sources/Contenda/Contenda.SdkDemo/Contenda.SdkDemo.csproj create mode 100644 sources/Contenda/Contenda.SdkDemo/Program.cs create mode 100644 sources/Contenda/Contenda.sln create mode 100644 sources/Contenda/Contenda.sln.DotSettings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..566fad7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Windows ignores +Thumbs.db +Thumbs.db.meta + +# Mac ignores +*.DS_Store +*.directory + +# Add your custom repo-wide rules below: \ No newline at end of file diff --git a/README.md b/README.md index de2273e..cf1ea3c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ -# contenda-dotnet-sdk -Contenda .NET SDK +# Contenda .NET SDK + +**Contenda** is the content catalyst for developer advocates. + +A .NET Standard 2.0 library to help you use Contenda's APIs in your .NET projects! + +![](https://img.shields.io/badge/platform-any-green.svg?longCache=true&style=flat-square) ![](https://img.shields.io/badge/nuget-yes-green.svg?longCache=true&style=flat-square) ![](https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square) + +## Usage + +Refer to the `Contenda.SdkDemo` project to see example code using this SDK. + +Make sure to call `Authenticate` before calling any APIs that require authentication - presently this includes all of them, except for the service health endpoint. + +## Need more help? + +💡 Read more about Contenda's API here https://prod.contenda.io/docs. + +💬 Find us on twitter [@ContendaCo](https://twitter.com/ContendaCo) or join our public [Discord](https://discord.gg/bYda4pQz2v). \ No newline at end of file diff --git a/sources/Contenda/.gitignore b/sources/Contenda/.gitignore new file mode 100644 index 0000000..08a421c --- /dev/null +++ b/sources/Contenda/.gitignore @@ -0,0 +1,219 @@ +# Created by https://www.gitignore.io/api/visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +# x64/ +# x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.obj +*.meta +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +# *.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions \ No newline at end of file diff --git a/sources/Contenda/Contenda.Sdk/Auth/APIKeyAuthProvider.cs b/sources/Contenda/Contenda.Sdk/Auth/APIKeyAuthProvider.cs new file mode 100644 index 0000000..6371191 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Auth/APIKeyAuthProvider.cs @@ -0,0 +1,103 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Contenda.Sdk.Exceptions; +using Contenda.Sdk.Models.Request; +using Contenda.Sdk.Models.Result; +using Newtonsoft.Json; + +namespace Contenda.Sdk.Auth +{ + /// + /// API Key Authentication Provider + /// + public sealed class APIKeyAuthProvider : IAuthProvider + { + private readonly string _email; + private readonly string _apiKey; + + private readonly DateTime _invalidDate = new(1970, 1, 1); + + private string? _apiToken; + + /// + /// Constructor for an API key auth provider + /// + /// user's email + /// api key + public APIKeyAuthProvider(string email, string apiKey) + { + _email = email; + _apiKey = apiKey; + } + + private DateTime ValidUntil() + { + if (_apiToken == null) return _invalidDate; + try + { + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(_apiToken); + return token.ValidTo; + } + catch + { + return _invalidDate; + } + } + + /// + public async Task TryAuthenticate(string apiBaseUri) + { + if (EnsureValid()) return true; + + var client = PoorMansHttpClientFactory.Instance.Client; + + var uri = $"{apiBaseUri}{Constants.Version2Prefix}{Constants.IdentityV2.Token}"; + var body = new TokenV2 + { + api_key = _apiKey, + email = _email + }; + var bodyString = JsonConvert.SerializeObject(body); + + try + { + var response = await client.PostAsync(new Uri(uri), new StringContent(bodyString, Encoding.UTF8, Constants.JsonMimeType)).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var tokenResponse = await response.Content.ReadAsStringAsync(); + var tokenResult = JsonConvert.DeserializeObject(tokenResponse); + _apiToken = tokenResult!.access_token; + return true; + } + catch + { + return false; + } + } + + private bool EnsureValid() => DateTime.Now < ValidUntil(); + + /// + public void ModifyHeadersCallback(HttpClient httpClient) + { + if (!EnsureValid()) throw new AuthenticationException("You have to be authenticated to call APIs with authentication."); + // nothing to do here + } + + /// + public string ModifyQueryCallback(string currentQuery) + { + if (!EnsureValid()) throw new AuthenticationException("You have to be authenticated to call APIs with authentication."); + + var nvc = HttpUtility.ParseQueryString((new Uri(currentQuery)).Query); + var paramChar = nvc.Count == 0 ? "?" : "&"; + return $"{currentQuery}{paramChar}token={HttpUtility.UrlEncode(_apiToken)}"; + } + + + } +} diff --git a/sources/Contenda/Contenda.Sdk/Auth/IAuthProvider.cs b/sources/Contenda/Contenda.Sdk/Auth/IAuthProvider.cs new file mode 100644 index 0000000..728d24a --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Auth/IAuthProvider.cs @@ -0,0 +1,31 @@ +using System.Net.Http; +using System.Threading.Tasks; + +namespace Contenda.Sdk.Auth +{ + /// + /// Authentication provider interface + /// + public interface IAuthProvider + { + /// + /// Modify headers callback + /// + /// the HttpClient with the headers to modify + void ModifyHeadersCallback(HttpClient httpClient); + + /// + /// Modify query callback + /// + /// the query to modify + /// modified query + string ModifyQueryCallback(string currentQuery); + + /// + /// Try authenticate + /// + /// API Base URI + /// true if successfully authenticated, false otherwise + Task TryAuthenticate(string apiBaseUri); + } +} diff --git a/sources/Contenda/Contenda.Sdk/Constants.cs b/sources/Contenda/Contenda.Sdk/Constants.cs new file mode 100644 index 0000000..fd5e4f7 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Constants.cs @@ -0,0 +1,34 @@ +namespace Contenda.Sdk +{ + internal static class Constants + { + internal const string BaseApiUri = "https://prod.contenda.io/"; + + internal const string Version2Prefix = "api/v2/"; + + internal const string Health = "health"; + + internal const string ClientUserAgent = "Contenda .NET SDK"; + + internal const string JsonMimeType = "application/json"; + + internal static class IdentityV2 + { + internal const string Token = "identity/token"; + } + + internal static class JobsV2 + { + internal const string Status = "jobs/status/"; + + internal const string SubmitVideoToBlog = "jobs/video-to-blog"; + + internal const string UsageLimits = "jobs/usage-limits"; + } + + internal static class ContentV2 + { + internal const string BlogMarkdown = "content/blog/{0}/markdown"; + } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Contenda.Sdk.csproj b/sources/Contenda/Contenda.Sdk/Contenda.Sdk.csproj new file mode 100644 index 0000000..0ed01cf --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Contenda.Sdk.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + latest + enable + 0.1.0.0 + 0.1.0.0 + Contenda .NET SDK + tomzorz + Contenda + Contenda .NET SDK + api, sdk, contenda + True + True + True + https://github.com/Contenda-Team/contenda-dotnet-sdk + + + + + + + + + diff --git a/sources/Contenda/Contenda.Sdk/ContendaAPI.cs b/sources/Contenda/Contenda.Sdk/ContendaAPI.cs new file mode 100644 index 0000000..d33b844 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/ContendaAPI.cs @@ -0,0 +1,169 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Contenda.Sdk.Auth; +using Contenda.Sdk.Models; +using Contenda.Sdk.Models.Request; +using Contenda.Sdk.Models.Result; +using Newtonsoft.Json; + +namespace Contenda.Sdk +{ + /// + /// The main Contenda API class + /// + public sealed class ContendaAPI + { + private readonly IAuthProvider _authProvider; + private readonly string _apiBaseUri; + + /// + /// Create a Contenda API class + /// + /// the authentication provider + /// API Base URI override, must contain scheme prefix and end in a /, e.g. "https://example.com/" + public ContendaAPI(IAuthProvider authProvider, string? apiBaseUriOverride = null) + { + _authProvider = authProvider; + _apiBaseUri = apiBaseUriOverride ?? Constants.BaseApiUri; + } + + /// + /// Test service health + /// + /// true if healthy, false otherwise + public async Task ServiceHealth() + { + var client = PoorMansHttpClientFactory.Instance.Client; + var uri = $"{_apiBaseUri}{Constants.Health}"; + try + { + var result = await client.GetAsync(uri).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + return true; + } + catch + { + return false; + } + } + + /// + /// Authenticate using the auth provider + /// + /// true if successful, false otherwise + public async Task Authenticate() + { + return await _authProvider.TryAuthenticate(_apiBaseUri).ConfigureAwait(false); + } + + /// + /// Get usage limits for the authenticated account + /// + /// + public async Task GetUsageLimits() + { + var client = PoorMansHttpClientFactory.Instance.Client; + var uri = $"{_apiBaseUri}{Constants.Version2Prefix}{Constants.JobsV2.UsageLimits}"; + + _authProvider.ModifyHeadersCallback(client); + uri = _authProvider.ModifyQueryCallback(uri); + + var result = await client.GetAsync(uri).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + + return JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + /// + /// Get a job's status + /// + /// job ID, e.g. uncertainty-rk-family-am-reflections-jo + /// job status + public async Task GetJobStatus(string jobId) + { + var client = PoorMansHttpClientFactory.Instance.Client; + var uri = $"{_apiBaseUri}{Constants.Version2Prefix}{Constants.JobsV2.Status}{HttpUtility.UrlEncode(jobId)}"; + + _authProvider.ModifyHeadersCallback(client); + uri = _authProvider.ModifyQueryCallback(uri); + + var result = await client.GetAsync(uri).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + + return JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + /// + /// Submit a new video to blog job + /// + /// source ID + /// blog type + /// optional, add a status update webhook url + /// optional, override the default email to send a status updates to + /// + /// + /// These are the supported source_id types - substitute the $values for your media: + /// + /// + /// "youtube $id" YouTube videos, $id for a YouTube video ID, e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ becomes "youtube dQw4w9WgXcQ" + /// + /// + /// "twitch $id" Twitch vods, $id for a Twitch vod ID, e.g. https://www.twitch.tv/videos/1079879708 becomes "twitch 1079879708" + /// + /// + /// "facebook $channel $id" Facebook videos, $channel for a Facebook page ID and $id for a Facebook video ID on that page, e.g. https://www.facebook.com/PersonOfInterestTV/videos/1827475693951431 becomes "facebook PersonOfInterestTV 1827475693951431" + /// + /// + /// "mux $id" Mux videos, $id for a Mux video ID, e.g. https://stream.mux.com/uNbxnGLKJ00yfbijDO8COxTOyVKT01xpxW.m3u8 becomes "mux uNbxnGLKJ00yfbijDO8COxTOyVKT01xpxW" + /// + /// + /// "url $url" Raw URL links, $url for a fully qualified URL that would download a media, e.g. "url https://download.blender.org/demo/movies/BBB/bbb_sunflower_1080p_60fps_normal.mp4" + /// + /// + /// + public async Task SubmitVideoToBlogJob(string sourceId, VideoToBlogJobSubType subType, string? statusUpdateWebhookUrl = null, string? statusUpdateEmail = null) + { + var client = PoorMansHttpClientFactory.Instance.Client; + var uri = $"{_apiBaseUri}{Constants.Version2Prefix}{Constants.JobsV2.SubmitVideoToBlog}"; + + _authProvider.ModifyHeadersCallback(client); + uri = _authProvider.ModifyQueryCallback(uri); + + var content = new StringContent(JsonConvert.SerializeObject(new SubmitJobV2 + { + source_id = sourceId, + status_update_email = statusUpdateEmail, + status_update_webhook_url = statusUpdateWebhookUrl, + type = subType == VideoToBlogJobSubType.Presentation ? "presentation" : "tutorial" + }), Encoding.UTF8, Constants.JsonMimeType); + + var result = await client.PostAsync(uri, content).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + + return JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + /// + /// Get blog as markdown + /// + /// blog ID + /// markdown blog + public async Task GetBlogAsMarkdown(string blogId) + { + var client = PoorMansHttpClientFactory.Instance.Client; + var uri = $"{_apiBaseUri}{Constants.Version2Prefix}{Constants.ContentV2.BlogMarkdown}"; + + uri = string.Format(uri, blogId); + + _authProvider.ModifyHeadersCallback(client); + uri = _authProvider.ModifyQueryCallback(uri); + + var result = await client.GetAsync(uri).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + + return await result.Content.ReadAsStringAsync().ConfigureAwait(false); + } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Exceptions/AuthenticationException.cs b/sources/Contenda/Contenda.Sdk/Exceptions/AuthenticationException.cs new file mode 100644 index 0000000..138b625 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Exceptions/AuthenticationException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Contenda.Sdk.Exceptions +{ + /// + /// Authentication Exception + /// + public sealed class AuthenticationException : Exception + { + /// + /// Exception reason + /// + public string Reason { get; } + + internal AuthenticationException(string reason) + { + Reason = reason; + } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/Request/SubmitJobV2.cs b/sources/Contenda/Contenda.Sdk/Models/Request/SubmitJobV2.cs new file mode 100644 index 0000000..b00d62e --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/Request/SubmitJobV2.cs @@ -0,0 +1,17 @@ +// ReSharper disable InconsistentNaming + +using JetBrains.Annotations; + +namespace Contenda.Sdk.Models.Request +{ + internal class SubmitJobV2 + { + public string? source_id { [UsedImplicitly] get; set; } + + public string? status_update_webhook_url { [UsedImplicitly] get; set; } + + public string? type { [UsedImplicitly] get; set; } + + public string? status_update_email { [UsedImplicitly] get; set; } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/Request/TokenV2.cs b/sources/Contenda/Contenda.Sdk/Models/Request/TokenV2.cs new file mode 100644 index 0000000..56b4660 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/Request/TokenV2.cs @@ -0,0 +1,13 @@ +// ReSharper disable InconsistentNaming + +using JetBrains.Annotations; + +namespace Contenda.Sdk.Models.Request +{ + internal class TokenV2 + { + public string? email { [UsedImplicitly] get; set; } + + public string? api_key { [UsedImplicitly] get; set; } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/Result/JobStatusResult.cs b/sources/Contenda/Contenda.Sdk/Models/Result/JobStatusResult.cs new file mode 100644 index 0000000..676fc62 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/Result/JobStatusResult.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; + +namespace Contenda.Sdk.Models.Result +{ + /// + /// Job result + /// + public sealed class JobResult + { + /// + /// Job result type + /// + [JsonProperty("type")] + public string? Type { get; set; } + + /// + /// Result document ID + /// + [JsonProperty("result_document_id")] + public string? ResultDocumentId { get; set; } + } + + /// + /// Job status result response model + /// + public sealed class JobStatusResult + { + /// + /// Job status message + /// + [JsonProperty("message")] + public string? Message { get; set; } + + /// + /// Job status + /// + [JsonProperty("status")] + public string? Status { get; set; } + + /// + /// Job ID + /// + [JsonProperty("job_id")] + public string? JobId { get; set; } + + /// + /// Job result, only exists if job state is successful + /// + [JsonProperty("result")] + public JobResult? Result { get; set; } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/Result/TokenResult.cs b/sources/Contenda/Contenda.Sdk/Models/Result/TokenResult.cs new file mode 100644 index 0000000..3788ff4 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/Result/TokenResult.cs @@ -0,0 +1,13 @@ +// ReSharper disable InconsistentNaming + +using JetBrains.Annotations; + +namespace Contenda.Sdk.Models.Result +{ + internal class TokenResult + { + [UsedImplicitly] public string? valid_until { get; set; } + + public string? access_token { get; [UsedImplicitly] set; } + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/Result/UsageLimits.cs b/sources/Contenda/Contenda.Sdk/Models/Result/UsageLimits.cs new file mode 100644 index 0000000..36944f0 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/Result/UsageLimits.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; + +namespace Contenda.Sdk.Models.Result +{ + /// + /// Usage Limits response model + /// + public sealed class UsageLimits + { + /// + /// If the current account is unlimited + /// + [JsonProperty("is_unlimited")] + public bool IsUnlimited { get; set; } + + /// + /// Measurement period + /// + [JsonProperty("period")] + public string? Period { get; set; } + + /// + /// Maximum media minutes allowed to process for the current measurement period + /// + [JsonProperty("limit")] + public double? Limit { get; set; } + + /// + /// Media minutes used in the current measurement period + /// + [JsonProperty("current")] + public double? Current { get; set; } + + /// + /// Human friendly description of the current limits + /// + [JsonProperty("friendly_message")] + public string? FriendlyMessage { get; set; } + + } +} diff --git a/sources/Contenda/Contenda.Sdk/Models/VideoToBlogJobSubType.cs b/sources/Contenda/Contenda.Sdk/Models/VideoToBlogJobSubType.cs new file mode 100644 index 0000000..0596108 --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/Models/VideoToBlogJobSubType.cs @@ -0,0 +1,17 @@ +namespace Contenda.Sdk.Models +{ + /// + /// Video to Blog job subtype + /// + public enum VideoToBlogJobSubType + { + /// + /// Presentation blog + /// + Presentation, + /// + /// Tutorial blog + /// + Tutorial + } +} diff --git a/sources/Contenda/Contenda.Sdk/PoorMansHttpClientFactory.cs b/sources/Contenda/Contenda.Sdk/PoorMansHttpClientFactory.cs new file mode 100644 index 0000000..184e7ad --- /dev/null +++ b/sources/Contenda/Contenda.Sdk/PoorMansHttpClientFactory.cs @@ -0,0 +1,26 @@ +using System.Net.Http; +using System.Reflection; + +namespace Contenda.Sdk +{ + /// + /// Poor man's HttpClient factory, to ensure we only use one in the lifetime of the API + /// + internal class PoorMansHttpClientFactory + { + private PoorMansHttpClientFactory() + { + Client = new HttpClient(); + + Client.DefaultRequestHeaders.TryAddWithoutValidation( + "User-Agent", + $"{Constants.ClientUserAgent} {Assembly.GetExecutingAssembly().GetName().Version}"); + } + + public HttpClient Client { get; } + + private static PoorMansHttpClientFactory? _instance; + + public static PoorMansHttpClientFactory Instance => _instance ??= new PoorMansHttpClientFactory(); + } +} diff --git a/sources/Contenda/Contenda.SdkDemo/Contenda.SdkDemo.csproj b/sources/Contenda/Contenda.SdkDemo/Contenda.SdkDemo.csproj new file mode 100644 index 0000000..a849acd --- /dev/null +++ b/sources/Contenda/Contenda.SdkDemo/Contenda.SdkDemo.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/sources/Contenda/Contenda.SdkDemo/Program.cs b/sources/Contenda/Contenda.SdkDemo/Program.cs new file mode 100644 index 0000000..6cee30c --- /dev/null +++ b/sources/Contenda/Contenda.SdkDemo/Program.cs @@ -0,0 +1,51 @@ +using Contenda.Sdk; +using Contenda.Sdk.Auth; +using Contenda.Sdk.Models; + +var authProvider = new APIKeyAuthProvider("YOUR EMAIL HERE", "YOUR API KEY HERE"); +var api = new ContendaAPI(authProvider); + +var isApiHealthy = await api.ServiceHealth(); + +Console.WriteLine($"Is api healthy: {isApiHealthy}"); + +var authSuccess = await api.Authenticate(); + +Console.WriteLine($"Successfully authenticated: {authSuccess}"); + +var usageLimits = await api.GetUsageLimits(); + +Console.WriteLine(usageLimits!.FriendlyMessage); + +var newJob = await api.SubmitVideoToBlogJob("youtube dQw4w9WgXcQ", VideoToBlogJobSubType.Presentation); + +var jobId = newJob!.JobId; + +Console.WriteLine($"Successfully submitted job: {jobId}"); + +var jobStatus = await api.GetJobStatus(jobId!); + +while (jobStatus!.Status != "succeeded" && jobStatus.Status != "failed") +{ + // poll for job status + + jobStatus = await api.GetJobStatus(jobId!); + + Console.WriteLine("Waiting for job to complete..."); + + await Task.Delay(10*1000); +} + +Console.WriteLine($"{jobStatus.JobId} {jobStatus.Message}"); + +if (jobStatus.Status == "failed") +{ + Console.WriteLine("Job failed!"); + return; +} + +var blogId = jobStatus.Result!.ResultDocumentId; + +Console.WriteLine(await api.GetBlogAsMarkdown(blogId!)); + +Console.ReadKey(); \ No newline at end of file diff --git a/sources/Contenda/Contenda.sln b/sources/Contenda/Contenda.sln new file mode 100644 index 0000000..7ffdb2d --- /dev/null +++ b/sources/Contenda/Contenda.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contenda.Sdk", "Contenda.Sdk\Contenda.Sdk.csproj", "{93AB3B49-CB79-4181-BCE7-C9D032F96D78}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contenda.SdkDemo", "Contenda.SdkDemo\Contenda.SdkDemo.csproj", "{5A2A42AA-D720-42A0-9FE7-A4BA9ADCAB80}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {93AB3B49-CB79-4181-BCE7-C9D032F96D78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93AB3B49-CB79-4181-BCE7-C9D032F96D78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93AB3B49-CB79-4181-BCE7-C9D032F96D78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93AB3B49-CB79-4181-BCE7-C9D032F96D78}.Release|Any CPU.Build.0 = Release|Any CPU + {5A2A42AA-D720-42A0-9FE7-A4BA9ADCAB80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A2A42AA-D720-42A0-9FE7-A4BA9ADCAB80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A2A42AA-D720-42A0-9FE7-A4BA9ADCAB80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A2A42AA-D720-42A0-9FE7-A4BA9ADCAB80}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6FA63A3A-9B90-441D-9926-C30161C79A09} + EndGlobalSection +EndGlobal diff --git a/sources/Contenda/Contenda.sln.DotSettings b/sources/Contenda/Contenda.sln.DotSettings new file mode 100644 index 0000000..33d4edf --- /dev/null +++ b/sources/Contenda/Contenda.sln.DotSettings @@ -0,0 +1,4 @@ + + API + True + True \ No newline at end of file