-
Notifications
You must be signed in to change notification settings - Fork 62
/
DDBApi.js
277 lines (243 loc) · 11.4 KB
/
DDBApi.js
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/** DDBApi.js - DndBeyond Api endpoints */
const DEFAULT_AVTT_ENCOUNTER_DATA = {
"name": "AboveVTT",
"flavorText": "This encounter is maintained by AboveVTT",
"description": "If you delete this encounter, a new one will be created the next time you DM a game. If you edit this encounter, your changes may be lost. AboveVTT automatically deletes encounters that it had previously created."
};
class DDBApi {
static async #refreshToken() {
if (Date.now() < MYCOBALT_TOKEN_EXPIRATION) {
return MYCOBALT_TOKEN;
}
const url = `https://auth-service.dndbeyond.com/v1/cobalt-token`;
const config = { method: 'POST', credentials: 'include' };
console.log("DDBApi is refreshing auth token");
const request = await fetch(url, config).then(DDBApi.lookForErrors);
const response = await request.json();
MYCOBALT_TOKEN = response.token;
MYCOBALT_TOKEN_EXPIRATION = Date.now() + (response.ttl * 1000) - 10000;
return response.token;
}
static async lookForErrors(response) {
if (response.status < 400) {
return response;
}
if(response.status == 410){
showError(new Error(`DDB 410 Error`), `<b>Try clearing <div style="backdrop-filter: brightness(0.8);padding: 0px 3px;display: inline-block;border-radius: 5px;">${navigator.userAgent.indexOf("Firefox") != -1 ? `temporary cached files and pages` : `cached images and files`}</div> and restarting the browser.</b>`, `<br/><b>As long as you do <span style='color: #900;'>not</span> clear <div style="backdrop-filter: brightness(0.8);padding: 0px 3px;display: inline-block;border-radius: 5px;">cookies and other site data</div> this should not remove any AboveVTT data.`);
}
else{
// We have an error so let's try to parse it
console.debug("DDBApi.lookForErrors", response);
const responseJson = await response.json()
.catch(parsingError => console.error("DDBApi.lookForErrors Failed to parse json", response, parsingError));
const type = responseJson?.type || `Unknown Error ${response.status}`;
const messages = responseJson?.errors?.message?.join("; ") || "";
console.error(`DDB API Error: ${type} ${messages}`);
if(type == 'EncounterLimitException'){
alert("Encounter limit reached. AboveVTT needs 1 encounter slot free to join as DM. If you are on a free DDB account you are limited to 8 encounter slots. Please try deleting an encounter.")
}
showError(new Error(`DDB API Error: ${type} ${messages}`));
}
}
static async fetchJsonWithToken(url, extraConfig = {}) {
const token = await DDBApi.#refreshToken();
const config = {...extraConfig,
credentials: 'omit',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
}
}
const request = await fetch(url, config).then(DDBApi.lookForErrors)
return await request.json();
}
static async fetchJsonWithCredentials(url, extraConfig = {}) {
console.debug("DDBApi.fetchJsonWithCredentials url", url)
const request = await fetch(url, {...extraConfig, credentials: 'include'}).then(DDBApi.lookForErrors);
console.debug("DDBApi.fetchJsonWithCredentials request", request);
const response = await request.json();
console.debug("DDBApi.fetchJsonWithCredentials response", response);
return response;
}
static async postJsonWithToken(url, body) {
const config = {
method: 'POST',
body: JSON.stringify(body)
}
return await DDBApi.fetchJsonWithToken(url, config);
}
static async deleteWithToken(url) {
const token = await DDBApi.#refreshToken();
const config = {
method: 'DELETE',
credentials: 'include',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}
// Explicitly not calling `lookForErrors` here because we don't actually care if this succeeds.
// We're just trying to clean up anything that we can
return await fetch(url, config);
}
static async fetchCharacter(id) {
if (typeof id !== "string" || id.length <= 1) {
throw new Error(`Invalid id: ${id}`);
}
const url = `https://character-service.dndbeyond.com/character/v5/character/${id}`;
const response = await DDBApi.fetchJsonWithToken(url);
console.debug("DDBApi.fetchCharacter response", response);
return response.data;
}
static async fetchEncounter(id) {
if (typeof id !== "string" || id.length <= 1) {
throw new Error(`Invalid id: ${id}`);
}
const url = `https://encounter-service.dndbeyond.com/v1/encounters/${id}`;
const response = await DDBApi.fetchJsonWithCredentials(url);
console.debug("DDBApi.fetchEncounter response", response);
return response.data;
}
static async fetchAllEncounters() {
console.log(`DDBApi.fetchAllEncounters starting`);
const url = `https://encounter-service.dndbeyond.com/v1/encounters`;
// make the first request to get pagination info
console.log(`DDBApi.fetchAllEncounters attempting to fetch page 1`);
const firstPage = await DDBApi.fetchJsonWithToken(`${url}?page=1`);
let encounters = firstPage.data;
const numberOfPages = firstPage.pagination.pages;
if (isNaN(numberOfPages)) {
throw new Error(`Unexpected Pagination Data: ${JSON.stringify(firstPage.pagination)}`);
} else {
console.log(`DDBApi.fetchAllEncounters attempting to fetch pages 2 through ${numberOfPages}`);
}
for (let i = 2; i <= numberOfPages; i++) {
const response = await DDBApi.fetchJsonWithToken(`${url}?page=${i}`)
console.debug(`DDBApi.fetchAllEncounters page ${i} response: `, response);
encounters = encounters.concat(response.data);
console.log(`DDBApi.fetchAllEncounters successfully fetched page ${i}`);
}
return encounters;
}
static async deleteAboveVttEncounters(encounters) {
console.log("DDBApi.deleteAboveVttEncounters starting");
// make sure we don't delete the encounter that we're actively on
const avttId = is_encounters_page() ? window.location.pathname.split("/").pop() : undefined;
const avttEncounters = encounters.filter(e => e.id !== avttId && e.name === DEFAULT_AVTT_ENCOUNTER_DATA.name);
console.debug(`DDBApi.deleteAboveVttEncounters avttId: ${avttId}, avttEncounters:`, avttEncounters);
const failedEncounters = JSON.parse(localStorage.getItem('avttFailedDelete'));
let newFailed = (failedEncounters != null && failedEncounters != undefined && Array.isArray(failedEncounters)) ? failedEncounters : [];
for (const encounter of avttEncounters) {
if(newFailed.includes(encounter.id))
continue;
console.log("DDBApi.deleteAboveVttEncounters attempting to delete encounter with id:", encounter.id);
const response = await DDBApi.deleteWithToken(`https://encounter-service.dndbeyond.com/v1/encounters/${encounter.id}`);
console.log("DDBApi.deleteAboveVttEncounters delete encounter response:", response.status);
if(response.status == 401){
newFailed.push(encounter.id)
try{
localStorage.setItem('avttFailedDelete', JSON.stringify(newFailed));
}
catch(e){
console.warn('localStorage avttFailedDelete Failed', e)
}
}
}
}
static async createAboveVttEncounter(campaignId = find_game_id()) {
console.log("DDBApi.createAboveVttEncounter", campaignId);
const campaignInfo = await DDBApi.fetchCampaignInfo(campaignId);
console.log("DDBApi.createAboveVttEncounter campaignInfo", campaignInfo);
if (!campaignInfo.id) {
throw new Error(`Invalid campaignInfo ${JSON.stringify(campaignInfo)}`);
}
const url = "https://encounter-service.dndbeyond.com/v1/encounters";
const encounterData = {...DEFAULT_AVTT_ENCOUNTER_DATA, campaign: campaignInfo};
console.debug("DDBApi.createAboveVttEncounter attempting to create encounter with data", encounterData);
const response = await DDBApi.postJsonWithToken(url, encounterData);
console.debug("DDBApi.createAboveVttEncounter response", response);
return response.data;
}
static async fetchCampaignInfo(campaignId) {
console.log("DDBApi.fetchCampaignInfo");
const url = `https://www.dndbeyond.com/api/campaign/stt/active-campaigns/${campaignId}`;
const response = await DDBApi.fetchJsonWithToken(url);
return response.data;
}
static async fetchMonsters(monsterIds) {
if (!Array.isArray(monsterIds)) {
return [];
}
let uniqueMonsterIds = [...new Set(monsterIds)];
let queryParam = uniqueMonsterIds.map(id => `ids=${id}`).join("&");
console.log("DDBApi.fetchMonsters starting with ids", uniqueMonsterIds);
const url = `https://monster-service.dndbeyond.com/v1/Monster?${queryParam}`;
const response = await DDBApi.fetchJsonWithToken(url);
return response.data;
}
static async fetchCampaignCharacters(campaignId) {
// This is what the campaign page calls to fetch characters
if(window.playerUsers != undefined)
return window.playerUsers
const url = `https://www.dndbeyond.com/api/campaign/stt/active-short-characters/${campaignId}`;
const response = await DDBApi.fetchJsonWithToken(url);
return response.data;
}
static async fetchCampaignCharacterDetails(campaignId) {
const characterIds = await DDBApi.fetchCampaignCharacterIds(campaignId);
return await DDBApi.fetchCharacterDetails(characterIds);
}
static async fetchCharacterDetails(characterIds) {
if (!Array.isArray(characterIds) || characterIds.length === 0) {
console.warn("DDBApi.fetchCharacterDetails expected an array of ids, but received: ", characterIds);
return [];
}
const ids = characterIds.map(ci => parseInt(ci)); // do not use strings
const url = `https://character-service-scds.dndbeyond.com/v2/characters`;
const config = {
method: 'POST',
body: JSON.stringify({ "characterIds": ids })
}
const response = await DDBApi.fetchJsonWithToken(url, config);
return response.foundCharacters;
}
static async fetchConfigJson() {
if(window.ddbConfigJson != undefined)
return window.ddbConfigJson
const url = "https://www.dndbeyond.com/api/config/json";
return await DDBApi.fetchJsonWithToken(url);
}
static async fetchActiveCharacters(campaignId) {
// This is what the encounter page called at one point, but seems to use fetchCampaignCharacters now
const url = `https://www.dndbeyond.com/api/campaign/active-characters/${campaignId}`
const response = await DDBApi.fetchJsonWithCredentials(url);
return response.data;
}
static async fetchCampaignCharacterIds(campaignId) {
let characterIds = [];
if(window.playerUsers){
characterIds = window.playerUsers.map(c => c.id);
return characterIds;
}
try {
// This is what the campaign page calls
window.playerUsers = await DDBApi.fetchActiveCharacters(campaignId);
characterIds = window.playerUsers.map(c => c.id);
} catch (error) {
console.warn("fetchCampaignCharacterIds caught an error trying to collect ids from fetchActiveCharacters", error);
}
try {
const activeShortCharacters = await DDBApi.fetchCampaignCharacters(campaignId);
activeShortCharacters.forEach(c => {
if (!characterIds.includes(c.id)) {
characterIds.push(c.id);
}
});
} catch (error) {
console.warn("fetchCampaignCharacterIds caught an error trying to collect ids from fetchActiveCharacters", error);
}
return characterIds;
}
}