I'm working on my own ChatBot for YouTube in Winforms and C#. It already works on Twitch and I'm trying to replicate the functionality for Youtube using the C# API. I can download the Chat Messages no problem, but creating a chat message is causing me a headache as I'm getting a 403, Insufficient Permission error. The full error message is
Google.Apis.Requests.RequestError
Insufficient Permission [403]
Errors [
Message[Insufficient Permission] Location[ - ] Reason[insufficientPermissions] Domain[global]
]
After some searching I've tried most things that I can find and have still come up empty as to what exactly is causing this. I get it's a permission issue and I obviously need to set something but I can't figure out what. My code is below and definitely works for reading data... but i don't know why it won't work for writing.
public class YouTubeDataWrapper
{
private YouTubeService youTubeService;
private string liveChatId;
private bool updatingChat;
private int prevResultCount;
public List<YouTubeMessage> Messages { get; private set; }
public bool Connected { get; private set; }
public bool ChatUpdated { get; set; }
//public Authorisation Authorisation { get; set; }
//public AccessToken AccessToken { get; set; }
public YouTubeDataWrapper()
{
this.Messages = new List<YouTubeMessage>();
}
public async void Connect()
{
Stream stream = new FileStream("client_secrets.json", FileMode.Open);
UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.Load(stream).Secrets, new[] { YouTubeService.Scope.YoutubeForceSsl }, "user", CancellationToken.None, new FileDataStore(this.GetType().ToString()));
stream.Close();
stream.Dispose();
this.youTubeService = new YouTubeService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = this.GetType().ToString()
});
var res = this.youTubeService.LiveBroadcasts.List("id,snippet,contentDetails,status");
res.BroadcastType = LiveBroadcastsResource.ListRequest.BroadcastTypeEnum.Persistent;
res.Mine = true;
//res.BroadcastStatus = LiveBroadcastsResource.ListRequest.BroadcastStatusEnum.Active;
var resListResponse = await res.ExecuteAsync();
IEnumerator<LiveBroadcast> ie = resListResponse.Items.GetEnumerator();
while (ie.MoveNext() && string.IsNullOrEmpty(this.liveChatId))
{
LiveBroadcast livebroadcast = ie.Current;
string id = livebroadcast.Snippet.LiveChatId;
if (!string.IsNullOrEmpty(id))
{
this.liveChatId = id;
this.Connected = true;
}
bool? def = livebroadcast.Snippet.IsDefaultBroadcast;
string title = livebroadcast.Snippet.Title;
LiveBroadcastStatus status = livebroadcast.Status;
}
}
public async void UpdateChat()
{
if (!this.updatingChat)
{
if (!string.IsNullOrEmpty(this.liveChatId) && this.Connected)
{
this.updatingChat = true;
var livechat = this.youTubeService.LiveChatMessages.List(this.liveChatId, "id,snippet,authorDetails");
var livechatResponse = await livechat.ExecuteAsync();
PageInfo pageInfo = livechatResponse.PageInfo;
this.ChatUpdated = false;
if (pageInfo.TotalResults.HasValue)
{
if (!this.prevResultCount.Equals(pageInfo.TotalResults.Value))
{
this.prevResultCount = pageInfo.TotalResults.Value;
this.ChatUpdated = true;
}
}
if (this.ChatUpdated)
{
this.Messages = new List<YouTubeMessage>();
foreach (var livemessage in livechatResponse.Items)
{
string id = livemessage.Id;
string displayName = livemessage.AuthorDetails.DisplayName;
string message = livemessage.Snippet.DisplayMessage;
YouTubeMessage msg = new YouTubeMessage(id, displayName, message);
string line = string.Format("{0}: {1}", displayName, message);
if (!this.Messages.Contains(msg))
{
this.Messages.Add(msg);
}
}
}
this.updatingChat = false;
}
}
}
public async void SendMessage(string message)
{
LiveChatMessage liveMessage = new LiveChatMessage();
liveMessage.Snippet = new LiveChatMessageSnippet() { LiveChatId = this.liveChatId, Type = "textMessageEvent", TextMessageDetails = new LiveChatTextMessageDetails() { MessageText = message } };
var insert = this.youTubeService.LiveChatMessages.Insert(liveMessage, "snippet");
var response = await insert.ExecuteAsync();
if (response != null)
{
}
}
}
The main code in question is the Send Message method. I've tried changing the Scope of the UserCredentials to everything I can try to no avail. Any ideas?
From the YouTube Data API - Error, your error 403 or insufficientPermissions is error in the OAuth 2.0 token provided for the request specifies scopes that are insufficient for accessing the requested data.
So make sure you use proper Scope in your application. Here is the example of scope that is needed in your application.
https://www.googleapis.com/auth/youtube.force-ssl
https://www.googleapis.com/auth/youtube
For more information about this error 403, you can check this related SO question.
Turns out that revoking the access and then re-doing it fixed the issue. The error message was not very helpful.
Related
I'm trying to create an online meeting from ASP.NET Core Web API, but I'm getting this "generalException" error.
AuthenticationProvider for configuring auth for the API call:
Code:
public class GraphAuthenticationProvider : IAuthenticationProvider
{
public const string GRAPH_URI = "https://graph.microsoft.com/v1.0/";
/// <summary>
/// Default constructor
/// </summary>
/// <param name="configuration"></param>
public GraphAuthenticationProvider(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; set; }
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{Configuration["AzureAd:TenantId"]}");
ClientCredential credentials = new ClientCredential(Configuration["AzureAd:ClientId"], Configuration["AzureAd:ClientSecret"]);
AuthenticationResult authResult = await authContext.AcquireTokenAsync(GRAPH_URI, credentials);
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
request.Headers.Add("Content-type", "application/json");
}
}
GraphProvider for making the actual API request:
public class MicrosoftGraphProvider : IGraphProvider
{
private IGraphServiceClient _graph;
public MicrosoftGraphProvider(IGraphServiceClient graph)
{
_graph = graph;
}
public async Task<CreateMeetingResult> CreateMeetingAsync(OnlineMeeting onlineMeeting)
{
// Initialize error message
var errorMessage = default(string);
// Initialize meeting result
var meetingResult = default(OnlineMeeting);
try
{
// Try creating the meeting
meetingResult = await _graph.Me.OnlineMeetings
.Request()
.AddAsync(onlineMeeting);
}
catch (Exception ex)
{
// Set the error message
errorMessage = ex.Message;
}
// Return the result
return new CreateMeetingResult
{
ErrorPhrase = errorMessage,
MeetingResult = meetingResult
};
}
}
AuthenticationProvider, GraphServiceClient, and GraphProvider transient instances in StartUp.cs:
services.AddTransient<IAuthenticationProvider, GraphAuthenticationProvider>(provider =>
new GraphAuthenticationProvider(provider.GetService<IConfiguration>()));
// Add transient instance of the graph service client
services.AddTransient<IGraphServiceClient, GraphServiceClient>(provider =>
new GraphServiceClient(provider.GetService<IAuthenticationProvider>()));
// Add transient instance of the graph provider
services.AddTransient<IGraphProvider, MicrosoftGraphProvider>(provider =>
new MicrosoftGraphProvider(provider.GetService<IGraphServiceClient>()));
Setting OnlineMeeting data and invoking CreatingMeeting:
var onlineMeeting = new OnlineMeeting
{
Subject = meetingDetails.Subject,
AllowedPresenters = OnlineMeetingPresenters.Organizer,
IsEntryExitAnnounced = true,
Participants = new MeetingParticipants
{
Attendees = new List<MeetingParticipantInfo>
{
new MeetingParticipantInfo
{
Role = OnlineMeetingRole.Attendee,
Identity = new IdentitySet
{
Application = new Identity
{
Id = Guid.NewGuid().ToString(),
DisplayName = "Attendee1"
}
}
},
new MeetingParticipantInfo
{
Role = OnlineMeetingRole.Presenter,
Identity = new IdentitySet
{
Application = new Identity
{
Id = Guid.NewGuid().ToString(),
DisplayName = "Attendee2"
}
}
},
new MeetingParticipantInfo
{
Role = OnlineMeetingRole.Presenter,
Identity = new IdentitySet
{
Application = new Identity
{
Id = Guid.NewGuid().ToString(),
DisplayName = "Attendee3"
}
}
}
},
Organizer = new MeetingParticipantInfo
{
Role = OnlineMeetingRole.Presenter,
Identity = new IdentitySet
{
Application = new Identity
{
Id = Guid.NewGuid().ToString(),
DisplayName = Framework.Construction.Configuration["OnlineMeeting:Organiser:DisplayName"]
}
}
}
},
EndDateTime = DateTimeOffset.Now.AddHours(1),
StartDateTime = DateTimeOffset.Now.AddHours(2)
};
// Fire create meeting
var meetingResult = await _graphProvider.CreateMeetingAsync(onlineMeeting);
ApiResponse:
{
"response": null,
"successful": false,
"errorMessage": "Code: generalException\r\nMessage: An error occurred sending the request.\r\n"
}
I have created an application in azure app service and added all necessary permissions as well.
What is it that I'm not doing correctly?
If you require more info, please ask me.
Thanks in advance.
I'm not sure where the error happened, but there are some problems in your code.
As I said in the comment, the resource should be https://graph.microsoft.com/. https://graph.microsoft.com/v1.0/ is the URL of Microsoft Graph API. Please see this article.
Here are some examples of Microsoft web-hosted resources:
Microsoft Graph: https://graph.microsoft.com
Microsoft 365 Mail API: https://outlook.office.com
Azure Key Vault: https://vault.azure.net
Try to replace Me with Users[<user-id>].
You use ClientCredential to authorize, which means you are using the Client credential flows. The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, instead of impersonating a user, to authenticate when calling another web service. So you could not call Microsoft Graph API with .Me which is used for the signed-in user.
The bot I'm developing is a replacement for a contact form for potential clients that want to be contacted by a company, so the user inputs have to be saved in a database. I have successfully connected a Cosmos DB to my bot which collect the state data when the bot is used. I have a dialog stack with one dialog per user input (Name, email and the message the user want to leave).
I can't find any helpful documentation on how to save conversation history for bots written in C#. Can anyone help me out? I'm still a beginner in Bot Framework and C#.
Here is my global.asax file:
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
var uri = new Uri(ConfigurationManager.AppSettings["DocumentDbUrl"]);
var key = ConfigurationManager.AppSettings["DocumentDbKey"];
var store = new DocumentDbBotDataStore(uri, key);
Conversation.UpdateContainer(
builder =>
{
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
builder.Register(c => new CachingBotDataStore(store, CachingBotDataStoreConsistencyPolicy.ETagBasedConsistency))
.As<IBotDataStore<BotData>>()
.AsSelf()
.InstancePerLifetimeScope();
});
}
}
Here is my NameDialog to collect the user's name: (the other dialogs are almost identical to this)
[Serializable]
public class NameDialog : IDialog<string>
{
private int attempts = 3;
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("What's your name?");
context.Wait(this.MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if ((message.Text != null) && (message.Text.Trim().Length > 0))
{
context.Done(message.Text);
}
else
{
--attempts;
if (attempts > 0)
{
await context.PostAsync("I couldn't understand, can you try again?");
context.Wait(this.MessageReceivedAsync);
}
else
{
context.Fail(new TooManyAttemptsException("This is not a valid input"));
}
}
}
}
I submitted a couple of comments asking for clarification in what you're looking for, but figured I may as well just provide an all-encompassing answer.
Use V4
If your bot is new, just use V4 of BotBuilder/BotFramework. It's easier, there's more features, and better support. I'll provide answers for both, anyway.
Saving Custom Data in V4
References:
Write directly to Storage-Cosmos
For custom storage where you specify the User Id:
// Create Cosmos Storage
private static readonly CosmosDbStorage _myStorage = new CosmosDbStorage(new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBCollectionName,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
});
// Write
var userData = new { Name = "xyz", Email = "xyz#email.com", Message = "my message" };
var changes = Dictionary<string, object>();
{
changes.Add("UserId", userData);
};
await _myStorage.WriteAsync(changes, cancellationToken);
// Read
var userDataFromStorage = await _myStorage.read(["UserId"]);
For User Data where the bot handles the Id:
See Basic Bot Sample.
Key parts:
Define the Greeting State
public class GreetingState
{
public string Name { get; set; }
public string City { get; set; }
}
Instantiate a State Accessor
private readonly IStatePropertyAccessor<GreetingState> _greetingStateAccessor;
[...]
_greetingStateAccessor = _userState.CreateProperty<GreetingState>(nameof(GreetingState));
[...]
Dialogs.Add(new GreetingDialog(_greetingStateAccessor));
Save UserState at the end of OnTurnAsync:
await _userState.SaveChangesAsync(turnContext);
Greeting Dialog to Get and Set User Data
var greetingState = await UserProfileAccessor.GetAsync(stepContext.Context, () => null);
[...]
greetingState.Name = char.ToUpper(lowerCaseName[0]) + lowerCaseName.Substring(1);
await UserProfileAccessor.SetAsync(stepContext.Context, greetingState);
Saving Full Conversation History in V4
References:
Conversation History Sample
Transcript Storage Docs
Just read the docs and look at the sample for this one. Too much code to copy/paste.
Saving Custom Data in V3
References:
Manage custom data storage
BotState Class Reference
Using Azure Table Storage with Cosmos
Sample showing how to store UserData
I'll copy/paste the code from this good answer to a similar question on StackOverflow, for posterity:
public class WebChatController : Controller
{
public ActionResult Index()
{
var connectionString = ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString;
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference("BotStore");
string userId = Guid.NewGuid().ToString();
TableQuery<BotDataRow> query = new TableQuery<BotDataRow>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, userId));
var dataRow = table.ExecuteQuery(query).FirstOrDefault();
if(dataRow != null)
{
dataRow.Data = Newtonsoft.Json.JsonConvert.SerializeObject(new
{
UserName = "This user's name",
Email = "whatever#email.com",
GraphAccessToken = "token",
TokenExpiryTime = DateTime.Now.AddHours(1)
});
dataRow.Timestamp = DateTimeOffset.UtcNow;
table.Execute(TableOperation.Replace(dataRow));
}
else
{
var row = new BotDataRow(userId, "userData");
row.Data = Newtonsoft.Json.JsonConvert.SerializeObject(new
{
UserName = "This user's name",
Email = "whatever#email.com",
GraphAccessToken = "token",
TokenExpiryTime = DateTime.Now.AddHours(1)
});
row.Timestamp = DateTimeOffset.UtcNow;
table.Execute(TableOperation.Insert(row));
}
var vm = new WebChatModel();
vm.UserId = userId;
return View(vm);
}
public class BotDataRow : TableEntity
{
public BotDataRow(string partitionKey, string rowKey)
{
this.PartitionKey = partitionKey;
this.RowKey = rowKey;
}
public BotDataRow() { }
public bool IsCompressed { get; set; }
public string Data { get; set; }
}
}
Saving User Data:
See State API Bot Sample
Saving Full Conversation History in V3
References:
Blog post for saving conversation history to a SQL Server
Sample that uses Middleware to log all activity
Intercept messages docs
Basically, you want to first capture all activity using IActivityLogger, like in the sample just above:
Create DebugActivityLogger
public class DebugActivityLogger : IActivityLogger
{
public async Task LogAsync(IActivity activity)
{
Debug.WriteLine($"From:{activity.From.Id} - To:{activity.Recipient.Id} - Message:{activity.AsMessageActivity()?.Text}");
// Add code to save in whatever format you'd like using "Saving Custom Data in V3" section
}
}
Add the following to Global.asax.cs:
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
var builder = new ContainerBuilder();
builder.RegisterType<DebugActivityLogger>().AsImplementedInterfaces().InstancePerDependency();
builder.Update(Conversation.Container);
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
I'm trying to get metadata's information about all of a Google Drive.
This is my code :
static void Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Green;
var clientId = "MyID";
var secret = "MySecret";
var service = GoogleDriveFileListDirectoryStructure.AuthenticateOauth(clientId, secret, "AppliUserAutor2");
var allFiles = GoogleDriveFileListDirectoryStructure.ListAll(service, new GoogleDriveFileListDirectoryStructure.FilesListOptionalParms { Q = "('root' in parents)", PageSize = 1000 });
GoogleDriveFileListDirectoryStructure.PrettyPrint(service, allFiles, "");
Console.ReadLine();
}
internal class GoogleDriveFileListDirectoryStructure
{
public static DriveService AuthenticateOauth(string clientId, string clientSecret, string userName)
{
try
{
if (string.IsNullOrEmpty(clientId))
throw new ArgumentNullException("clientId");
if (string.IsNullOrEmpty(clientSecret))
throw new ArgumentNullException("clientSecret");
if (string.IsNullOrEmpty(userName))
throw new ArgumentNullException("userName");
// These are the scopes of permissions you need. It is best to request only what you need and not all of them
string[] scopes = new string[] { DriveService.Scope.DriveReadonly }; //View the files in your Google Drive
var credPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
credPath = System.IO.Path.Combine(credPath, ".credentials/apiName");
// Requesting Authentication or loading previously stored authentication for userName
var credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets { ClientId = clientId, ClientSecret = clientSecret }
, scopes
, userName
, System.Threading.CancellationToken.None
, new FileDataStore(credPath, true)).Result;
// Returning the SheetsService
return new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Drive Oauth2 Authentication Sample"
});
}
catch (Exception ex)
{
Console.WriteLine("Create Oauth2 account DriveService failed" + ex.Message);
throw new Exception("CreateServiceAccountDriveFailed", ex);
}
}
public class FilesListOptionalParms
{
/// The source of files to list.
public string Corpus { get; set; }
/// A comma-separated list of sort keys. Valid keys are 'createdTime', 'folder', 'modifiedByMeTime', 'modifiedTime', 'name', 'quotaBytesUsed', 'recency', 'sharedWithMeTime', 'starred', and 'viewedByMeTime'. Each key sorts ascending by default, but may be reversed with the 'desc' modifier. Example usage: ?orderBy=folder,modifiedTime desc,name. Please note that there is a current limitation for users with approximately one million files in which the requested sort order is ignored.
public string OrderBy { get; set; }
/// The maximum number of files to return per page.
public int PageSize { get; set; }
/// The token for continuing a previous list request on the next page. This should be set to the value of 'nextPageToken' from the previous response.
public string PageToken { get; set; }
/// A query for filtering the file results. See the "Search for Files" guide for supported syntax.
public string Q { get; set; }
/// A comma-separated list of spaces to query within the corpus. Supported values are 'drive', 'appDataFolder' and 'photos'.
public string Spaces { get; set; }
}
public static Google.Apis.Drive.v3.Data.FileList ListAll(DriveService service, FilesListOptionalParms optional = null)
{
try
{
// Initial validation.
if (service == null)
throw new ArgumentNullException("service");
// Building the initial request.
var request = service.Files.List();
request.PageSize = 100;
//request.Fields = "nextPageToken, files(id,name,size,description,createdTime,webViewLink)";
// Applying optional parameters to the request.
request = (FilesResource.ListRequest)SampleHelpers.ApplyOptionalParms(request, optional);
var pageStreamer = new Google.Apis.Requests.PageStreamer<Google.Apis.Drive.v3.Data.File, FilesResource.ListRequest, Google.Apis.Drive.v3.Data.FileList, string>(
(req, token) => request.PageToken = token,
response => response.NextPageToken,
response => response.Files);
var allFiles = new FileList();
allFiles.Files = new List<File>();
foreach (var result in pageStreamer.Fetch(request))
{
allFiles.Files.Add(result);
}
return allFiles;
}
catch (Exception Ex)
{
throw new Exception("Request Files.List failed.", Ex);
}
}
When I try to ask more information with this request :
request.Fields = "nextPageToken, files(id,name,size,description,createdTime,webViewLink)";
I have only 29 results (allFiles) with all the needed information but in fact I have 1699 file.
When I don't specify any fields request I can list all the file but I only have ID and Name File.
You have an API (NOT modifiable) which you want to consume, this API receives some parameters which if they are NOT properly validated, the API throws a message of the error, it is precisely this message that I want to capture, for example here in the image I pass an erroneous password and want to show that message to the user.
For this, create a class called Response, which is responsible for managing the different calls made to the API
Response.cs:
public class Response
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
public object Result { get; set; }
[JsonProperty(PropertyName = "userMessage")]
public string userMessage { get; set; }
}
in my LoginViewModel I call the method that this API consumes, which is implemented in a class called ApiService.cs:
ApiService.cs:
public async Task<Response> GetLogin(
string urlAPILogin, string KeyLogin, string Rut, string Password)
{
try
{
var client = new HttpClient();
client.BaseAddress = new Uri(urlAPILogin);
string url = string.Format("login/index/?k=" + KeyLogin + "&rut=" + Rut + "&password=" + Password);
var response = await client.GetAsync(url);
var result = await response.Content.ReadAsStringAsync();
var model = JsonConvert.DeserializeObject<Response>(result);
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = response.StatusCode.ToString(),
Result = model,
};
}
return new Response
{
IsSuccess = true,
Message = "Ok",
Result = model,
};
}
catch (Exception ex)
{
return new Response
{
IsSuccess = false,
Message = ex.Message,
};
}
}
Now it is in my ViewModel (LoginViewModel) where I want to paint that message! and I try to capture it in the following way:
var response = await apiService.GetLogin(
urlAPILogin,
KeyLogin,
Rut,
Password);
if (string.IsNullOrEmpty(response.userMessage))
{
IsRunning = false;
IsEnabled = true;
await dialogService.ShowMessage(
"Error",
response.userMessage);
Password = null;
return;
}
but I'm not getting the expected response (he paints the blank message to me !!!)
being that the object if it brings the message !!
any help for me?
what am I doing wrong?
In your LoginViewModel method where you are awaiting the response, the variable with the userMessage you want is located a layer deeper.
Currently:
// userMessage is null in your *locals* section
await dialogService.ShowMessage("Error", response.userMessage);
Should be:
// This is where the wanted value is: userMessage is the desired message
await dialogService.ShowMessage("Error", response.Result.userMessage);
You will need to cast your response.Result as a Response since your Result variable is an object, but this should fix your problem.
Should be (casted):
// This is where the wanted value is: userMessage is the desired message
await dialogService.ShowMessage("Error", (response.Result as Response)?.userMessage);
I'm trying to follow the documentation for analytics reporting, but I'm getting pretty confused between the documentation for Oauth2 (which is in C# but I can't get to work), and the documentation for the analytics reporting API is very sparse (it has examples in Java and Python, but not C#). I've been reading over the following documentation:
https://developers.google.com/analytics/devguides/reporting/core/v4/
https://developers.google.com/analytics/devguides/reporting/core/v4/
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth
Analytics Reporting API V4 Client Library for .NET
I've put together some code based on the Stack Overflow answer and the Google Developers documentation on Oauth2 for Asp.NET MVC. here is my code:
class GoogleAnalyticsUtil
{
public List<AnalyticsPage> GetReport(DateTime startDate)
{
List<AnalyticsPage> report = new List<AnalyticsPage>();
try
{
var credentialTask = GetCredential();
credentialTask.Wait();
var credential = credentialTask.Result;
using (var svc = new AnalyticsReportingService(
new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "Google Analytics API Console"
}))
{
var dateRange = new DateRange
{
StartDate = startDate.ToString("yyyy-MM-dd"),
EndDate = DateTime.Now.ToString("yyyy-MM-dd")
};
var sessions = new Metric
{
Expression = "ga:sessions",
Alias = "Sessions"
};
var date = new Dimension { Name = "ga:date" };
var reportRequest = new ReportRequest
{
DateRanges = new List<DateRange> { dateRange },
Dimensions = new List<Dimension> { date },
Metrics = new List<Metric> { sessions },
ViewId = "<<view id>>"
};
var getReportsRequest = new GetReportsRequest
{
ReportRequests = new List<ReportRequest> { reportRequest }
};
var batchRequest = svc.Reports.BatchGet(getReportsRequest);
var response = batchRequest.Execute();
foreach (var x in response.Reports.First().Data.Rows)
{
report.Add(new AnalyticsPage()
{
Path = x.Dimensions.First(),
Views = x.Metrics.First().Values
});
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
return report;
}
static async Task<UserCredential> GetCredential()
{
var clientSecretPath = HttpRuntime.AppDomainAppPath + "client_secret.json";
var credPath = HttpRuntime.AppDomainAppPath + "credentials/GoogleAnalyticsApiConsole/";
UserCredential credential;
using (var stream = new FileStream(clientSecretPath,
FileMode.Open, FileAccess.Read))
{
var secrets = GoogleClientSecrets.Load(stream).Secrets;
credential = await dsAuthorizationBroker.AuthorizeAsync(
secrets,
new[] {AnalyticsReportingService.Scope.Analytics},
"<<username>>",
CancellationToken.None,
new FileDataStore(credPath, true));
return credential;
}
}
}
class AnalyticsPage
{
public string Path { get; set; }
public IList<string> Views { get; set; }
public string ViewString { get; set; }
}
public class dsAuthorizationBroker : GoogleWebAuthorizationBroker
{
public static string RedirectUri = "https://<<my website>>/AuthCallback/IndexAsync";
public new static async Task<UserCredential> AuthorizeAsync(
ClientSecrets clientSecrets,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore = null)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets,
};
return await AuthorizeAsyncCore(initializer, scopes, user,
taskCancellationToken, dataStore).ConfigureAwait(false);
}
private static async Task<UserCredential> AuthorizeAsyncCore(
GoogleAuthorizationCodeFlow.Initializer initializer,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore)
{
initializer.Scopes = scopes;
initializer.DataStore = dataStore ?? new FileDataStore(Folder);
var flow = new dsAuthorizationCodeFlow(initializer);
return await new AuthorizationCodeInstalledApp(flow,
new LocalServerCodeReceiver())
.AuthorizeAsync(user, taskCancellationToken).ConfigureAwait(false);
}
}
public class dsAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public dsAuthorizationCodeFlow(Initializer initializer)
: base(initializer) { }
public override AuthorizationCodeRequestUrl
CreateAuthorizationCodeRequest(string redirectUri)
{
return base.CreateAuthorizationCodeRequest(dsAuthorizationBroker.RedirectUri);
}
}
namespace MySite.Web.Controllers.WebAPI
{
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
[System.Web.Mvc.HttpGet]
public async Task IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var service = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "PMA"
});
// YOUR CODE SHOULD BE HERE..
// SAMPLE CODE:
//var list = await service.Files.List().ExecuteAsync();
}
Response.Redirect(result.RedirectUri);
}
}
}
The authorization in GetCredential() is failing, with the error
`Failed to launch browser with "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&client_id=224691372209-3ljoils93ufa13mgk2ahilvniqa30f2p.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fmysite.com%2FAuthCallback%2FIndexAsync&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fanalytics" for authorization. See inner exception for details.
When I try to go directly to mysite.com/AuthCallback/IndexAsync, I get a 404, so maybe that's the problem? But I'm not sure what's wrong with the controller, or why that's causing the authorization to fail. I'm using a Web Application Auth2.0 client ID, but I also tried using type 'Other' which doesn't take RedirectUrls in the parameters, but that didn't work either. Can I get it to authorize without redirecting anywhere? I need credential.result from the authorization to continue with the rest of my code, or is there another way in C# that I should be getting data from the Google Analytics Reporting API?