forked from tfenster/PlannerExAndImport
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPlanner.cs
More file actions
233 lines (203 loc) · 9.97 KB
/
Planner.cs
File metadata and controls
233 lines (203 loc) · 9.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using PlannerExAndImport.JSON;
namespace PlannerExAndImport
{
// connects to the MS Graph API (https://graph.microsoft.com) and ex- and imports groups, plans, tasks, details etc.
public class Planner
{
// URLs and settings for the Graph connection
private const string GRAPH_ENDPOINT = "https://graph.microsoft.com";
private const string PLANNER_SUB = "/beta/planner/";
private const string GROUPS_SUB = "/beta/groups/";
private const string RESOURCE_ID = GRAPH_ENDPOINT;
public static string CLIENT_ID = "";
public static string TENANT = "";
// export a plan and optionally output it as json
public static Plan Export(bool output = true)
{
Plan plan = SelectPlan();
using (var httpClient = PreparePlannerClient())
{
// get all buckets, tasks and task details
var buckets = GraphResponse<BucketResponse>.Get("plans/" + plan.Id + "/buckets", httpClient).Result.Buckets;
var tasks = GraphResponse<TaskResponse>.Get("plans/" + plan.Id + "/tasks", httpClient).Result.Tasks;
foreach (var task in tasks)
{
task.TaskDetail = GraphResponse<TaskDetailResponse>.Get("tasks/" + task.Id + "/details", httpClient).Result;
}
// put tasks in buckets so that the plan object has all data hierarchically
foreach (var bucket in buckets)
{
bucket.Tasks = tasks.Where(t => t.BucketId == bucket.Id).ToArray();
}
plan.Buckets = buckets;
}
if (output)
Console.WriteLine(Serialize.ToJson(plan));
return plan;
}
// export a plan and import everything into a new plan
public static void Import()
{
Console.WriteLine("Step 1: Select the plan to export");
Plan exportedPlan = Export(false);
Console.WriteLine("Step 2: Select the plan in which you want to import");
Plan targetPlan = SelectPlan();
bool addAssignments = Program.GetInput("Do you want to import the assignments (y/n)? This might send email notifications to the assignees. ") == "y";
using (var httpClient = PreparePlannerClient())
{
// buckets and tasks are always added at the beginning, therefore reversing the order when importing, otherwise e.g. the
// last exported bucket would become the first bucket in the imported plan
exportedPlan.Buckets = exportedPlan.Buckets.Reverse().ToArray();
// create buckets and tasks and then set details for the created tasks (can't be done in one step)
foreach (Bucket bucket in exportedPlan.Buckets)
{
bucket.PlanId = targetPlan.Id;
// reset all order hints as the exported values don't work
bucket.OrderHint = " !";
var newBucket = GraphResponse<Bucket>.Post("buckets", httpClient, bucket).Result;
bucket.Tasks = bucket.Tasks.Reverse().ToArray();
foreach (PlannerTask task in bucket.Tasks)
{
task.PlanId = targetPlan.Id;
task.BucketId = newBucket.Id;
task.OrderHint = " !";
// assignments contain the users assigned to a task
if (addAssignments)
{
foreach (Assignment assignment in task.Assignments.Values)
{
assignment.OrderHint = " !";
}
}
else
{
task.Assignments = new Dictionary<string, Assignment>();
}
var newTask = GraphResponse<PlannerTask>.Post("tasks", httpClient, task).Result;
// remember new task id for next loop
task.Id = newTask.Id;
}
// if we are too quick the created tasks are not available yet
Thread.Sleep(2 * 1000);
foreach (PlannerTask task in bucket.Tasks)
{
var newTaskDetailsResponse = GraphResponse<TaskDetailResponse>.Get("tasks/" + task.Id + "/details", httpClient).Result;
foreach (var checklist in task.TaskDetail.Checklist.Values)
{
checklist.OrderHint = " !";
}
foreach (var reference in task.TaskDetail.References.Values)
{
// same as order hint
reference.PreviewPriority = " !";
}
var updatedTaskDetailsResponse = GraphResponse<TaskDetailResponse>.Patch("tasks/" + task.Id + "/details", httpClient, task.TaskDetail, newTaskDetailsResponse.OdataEtag).Result;
}
}
}
Console.WriteLine("Import is done");
}
public static void ForgetCredentials()
{
AuthenticationContext ctx = new AuthenticationContext("https://login.microsoftonline.com/common");
ctx.TokenCache.Clear();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Vergessen.");
}
// allows the user to search for a group, select the right one and then select the right plan
// idea: if only one group matches or only one plan is in the group, that could be preselected
private static Plan SelectPlan()
{
using (var httpClient = PrepareGroupsClient())
{
bool foundGroup = false;
while (!foundGroup)
{
string groupSearch = Program.GetInput("Please enter the start of the name of the group containing your plan: ");
var groups = GraphResponse<GroupResponse>.Get("?$filter=groupTypes/any(c:c+eq+'Unified') and startswith(displayName, '" + groupSearch + "')", httpClient).Result.Groups;
if (groups.Length == 0)
{
Console.WriteLine("Found no matching group");
}
else
{
foundGroup = true;
Console.WriteLine("Select group:");
for (int i = 0; i < groups.Length; i++)
{
Console.WriteLine("(" + i + ") " + groups[i].DisplayName);
}
string selectedGroupS = Program.GetInput("Which group do you want to use: ");
int selectedGroup = -1;
if (int.TryParse(selectedGroupS, out selectedGroup))
{
var plans = GraphResponse<PlanResponse>.Get(groups[selectedGroup].Id + "/planner/plans", httpClient).Result.Plans;
Console.WriteLine("Select plan:");
for (int i = 0; i < plans.Length; i++)
{
Console.WriteLine("(" + i + ") " + plans[i].Title);
}
string selectedPlanS = Program.GetInput("Which plan do you want to use: ");
int selectedPlan = -1;
if (int.TryParse(selectedPlanS, out selectedPlan))
{
return plans[selectedPlan];
}
}
}
}
}
throw new Exception("Please select a plan");
}
private static HttpClient PreparePlannerClient()
{
return PrepareClient(PLANNER_SUB);
}
private static HttpClient PrepareGroupsClient()
{
return PrepareClient(GROUPS_SUB);
}
private static HttpClient PrepareClient(string sub)
{
var token = GetToken().Result;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
httpClient.BaseAddress = new Uri(GRAPH_ENDPOINT + sub);
return httpClient;
}
// see https://github.com/Azure-Samples/active-directory-dotnet-deviceprofile
private static async Task<AuthenticationResult> GetToken()
{
AuthenticationContext ctx = new AuthenticationContext("https://login.microsoftonline.com/" + TENANT, true, new EncryptedFileCache());
AuthenticationResult result = null;
try
{
result = await ctx.AcquireTokenSilentAsync(RESOURCE_ID, CLIENT_ID);
}
catch (AdalSilentTokenAcquisitionException)
{
result = await GetTokenViaCode(ctx);
}
return result;
}
static async Task<AuthenticationResult> GetTokenViaCode(AuthenticationContext ctx)
{
AuthenticationResult result = null;
DeviceCodeResult codeResult = await ctx.AcquireDeviceCodeAsync(RESOURCE_ID, CLIENT_ID);
Console.ResetColor();
Console.WriteLine("You need to sign in.");
Console.WriteLine("Message: " + codeResult.Message + "\n");
result = await ctx.AcquireTokenByDeviceCodeAsync(codeResult);
return result;
}
}
}