n8n-nodes-notion-advanced 1.2.3-beta → 1.2.5-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nodes/NotionAdvanced/NotionAITool.node.d.ts +18 -0
- package/nodes/NotionAdvanced/NotionAITool.node.js +1154 -0
- package/nodes/NotionAdvanced/NotionAITool.node.ts +611 -0
- package/nodes/NotionAdvanced/NotionAdvanced.node.d.ts +25 -0
- package/nodes/NotionAdvanced/NotionAdvanced.node.js +905 -0
- package/nodes/NotionAdvanced/NotionAdvanced.node.ts +1022 -0
- package/nodes/NotionAdvanced/NotionUtils.d.ts +104 -0
- package/nodes/NotionAdvanced/NotionUtils.js +465 -0
- package/nodes/NotionAdvanced/NotionUtils.ts +588 -0
- package/nodes/NotionAdvanced/notion.svg +3 -0
- package/package.json +69 -70
- package/types/NotionTypes.d.ts +359 -0
- package/types/NotionTypes.js +3 -0
- package/types/NotionTypes.ts +411 -0
@@ -0,0 +1,588 @@
|
|
1
|
+
import {
|
2
|
+
IDataObject,
|
3
|
+
IExecuteFunctions,
|
4
|
+
IHttpRequestMethods,
|
5
|
+
IHttpRequestOptions,
|
6
|
+
INodeExecutionData,
|
7
|
+
NodeApiError,
|
8
|
+
NodeOperationError,
|
9
|
+
} from 'n8n-workflow';
|
10
|
+
|
11
|
+
import {
|
12
|
+
Block,
|
13
|
+
BlockInput,
|
14
|
+
BlockWithId,
|
15
|
+
NotionCredentials,
|
16
|
+
RichTextObject,
|
17
|
+
RichTextAnnotations,
|
18
|
+
NotionColor,
|
19
|
+
NotionPage,
|
20
|
+
NotionApiResponse,
|
21
|
+
NotionBlockChildrenResponse,
|
22
|
+
NotionSearchResponse,
|
23
|
+
PageInput,
|
24
|
+
} from '../../types/NotionTypes';
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Makes an authenticated request to the Notion API
|
28
|
+
*/
|
29
|
+
export async function notionApiRequest(
|
30
|
+
this: IExecuteFunctions,
|
31
|
+
method: IHttpRequestMethods,
|
32
|
+
endpoint: string,
|
33
|
+
body: IDataObject = {},
|
34
|
+
qs: IDataObject = {},
|
35
|
+
): Promise<any> {
|
36
|
+
const credentials = (await this.getCredentials('notionApi')) as NotionCredentials;
|
37
|
+
|
38
|
+
const options: IHttpRequestOptions = {
|
39
|
+
method,
|
40
|
+
headers: {
|
41
|
+
'Authorization': `Bearer ${credentials.apiKey}`,
|
42
|
+
'Notion-Version': '2022-06-28',
|
43
|
+
'Content-Type': 'application/json',
|
44
|
+
},
|
45
|
+
uri: `https://api.notion.com/v1${endpoint}`,
|
46
|
+
body,
|
47
|
+
qs,
|
48
|
+
json: true,
|
49
|
+
};
|
50
|
+
|
51
|
+
try {
|
52
|
+
return await this.helpers.httpRequest(options);
|
53
|
+
} catch (error) {
|
54
|
+
throw new NodeApiError(this.getNode(), error as Error);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Validates Notion API credentials by making a test request
|
60
|
+
*/
|
61
|
+
export async function validateCredentials(this: IExecuteFunctions): Promise<boolean> {
|
62
|
+
try {
|
63
|
+
await notionApiRequest.call(this, 'GET', '/users/me');
|
64
|
+
return true;
|
65
|
+
} catch (error) {
|
66
|
+
return false;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Creates a rich text object from a string with optional formatting
|
72
|
+
*/
|
73
|
+
export function createRichText(
|
74
|
+
text: string,
|
75
|
+
annotations: Partial<RichTextAnnotations> = {},
|
76
|
+
link?: string,
|
77
|
+
): RichTextObject {
|
78
|
+
return {
|
79
|
+
type: 'text',
|
80
|
+
text: {
|
81
|
+
content: text,
|
82
|
+
link: link ? { url: link } : null,
|
83
|
+
},
|
84
|
+
annotations: {
|
85
|
+
bold: annotations.bold || false,
|
86
|
+
italic: annotations.italic || false,
|
87
|
+
strikethrough: annotations.strikethrough || false,
|
88
|
+
underline: annotations.underline || false,
|
89
|
+
code: annotations.code || false,
|
90
|
+
color: annotations.color || 'default',
|
91
|
+
},
|
92
|
+
plain_text: text,
|
93
|
+
href: link || null,
|
94
|
+
};
|
95
|
+
}
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Parses rich text input from various formats
|
99
|
+
*/
|
100
|
+
export function parseRichTextInput(input: any): RichTextObject[] {
|
101
|
+
if (typeof input === 'string') {
|
102
|
+
return [createRichText(input)];
|
103
|
+
}
|
104
|
+
|
105
|
+
if (Array.isArray(input)) {
|
106
|
+
return input.map((item: any) => {
|
107
|
+
if (typeof item === 'string') {
|
108
|
+
return createRichText(item);
|
109
|
+
}
|
110
|
+
|
111
|
+
if (typeof item === 'object' && item !== null) {
|
112
|
+
return createRichText(
|
113
|
+
item.text || item.content || '',
|
114
|
+
item.annotations || {},
|
115
|
+
item.link || item.url,
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
119
|
+
return createRichText('');
|
120
|
+
});
|
121
|
+
}
|
122
|
+
|
123
|
+
if (typeof input === 'object' && input !== null) {
|
124
|
+
if (input.type === 'text' || input.type === 'mention' || input.type === 'equation') {
|
125
|
+
return [input as RichTextObject];
|
126
|
+
}
|
127
|
+
|
128
|
+
return [createRichText(
|
129
|
+
input.text || input.content || '',
|
130
|
+
input.annotations || {},
|
131
|
+
input.link || input.url,
|
132
|
+
)];
|
133
|
+
}
|
134
|
+
|
135
|
+
return [createRichText('')];
|
136
|
+
}
|
137
|
+
|
138
|
+
/**
|
139
|
+
* Creates a paragraph block
|
140
|
+
*/
|
141
|
+
export function createParagraphBlock(
|
142
|
+
text: string | RichTextObject[],
|
143
|
+
color?: NotionColor,
|
144
|
+
children?: Block[],
|
145
|
+
): Block {
|
146
|
+
return {
|
147
|
+
type: 'paragraph',
|
148
|
+
paragraph: {
|
149
|
+
rich_text: typeof text === 'string' ? [createRichText(text)] : text,
|
150
|
+
color,
|
151
|
+
children,
|
152
|
+
},
|
153
|
+
};
|
154
|
+
}
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Creates a heading block
|
158
|
+
*/
|
159
|
+
export function createHeadingBlock(
|
160
|
+
level: 1 | 2 | 3,
|
161
|
+
text: string | RichTextObject[],
|
162
|
+
color?: NotionColor,
|
163
|
+
isToggleable?: boolean,
|
164
|
+
): Block {
|
165
|
+
const richText = typeof text === 'string' ? [createRichText(text)] : text;
|
166
|
+
const headingData = {
|
167
|
+
rich_text: richText,
|
168
|
+
color,
|
169
|
+
is_toggleable: isToggleable,
|
170
|
+
};
|
171
|
+
|
172
|
+
switch (level) {
|
173
|
+
case 1:
|
174
|
+
return {
|
175
|
+
type: 'heading_1',
|
176
|
+
heading_1: headingData,
|
177
|
+
};
|
178
|
+
case 2:
|
179
|
+
return {
|
180
|
+
type: 'heading_2',
|
181
|
+
heading_2: headingData,
|
182
|
+
};
|
183
|
+
case 3:
|
184
|
+
return {
|
185
|
+
type: 'heading_3',
|
186
|
+
heading_3: headingData,
|
187
|
+
};
|
188
|
+
default:
|
189
|
+
throw new Error('Invalid heading level. Must be 1, 2, or 3.');
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Creates a list item block
|
195
|
+
*/
|
196
|
+
export function createListItemBlock(
|
197
|
+
type: 'bulleted_list_item' | 'numbered_list_item',
|
198
|
+
text: string | RichTextObject[],
|
199
|
+
color?: NotionColor,
|
200
|
+
children?: Block[],
|
201
|
+
): Block {
|
202
|
+
const richText = typeof text === 'string' ? [createRichText(text)] : text;
|
203
|
+
|
204
|
+
if (type === 'bulleted_list_item') {
|
205
|
+
return {
|
206
|
+
type: 'bulleted_list_item',
|
207
|
+
bulleted_list_item: {
|
208
|
+
rich_text: richText,
|
209
|
+
color,
|
210
|
+
children,
|
211
|
+
},
|
212
|
+
};
|
213
|
+
} else {
|
214
|
+
return {
|
215
|
+
type: 'numbered_list_item',
|
216
|
+
numbered_list_item: {
|
217
|
+
rich_text: richText,
|
218
|
+
color,
|
219
|
+
children,
|
220
|
+
},
|
221
|
+
};
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
/**
|
226
|
+
* Creates a to-do block
|
227
|
+
*/
|
228
|
+
export function createToDoBlock(
|
229
|
+
text: string | RichTextObject[],
|
230
|
+
checked: boolean = false,
|
231
|
+
color?: NotionColor,
|
232
|
+
children?: Block[],
|
233
|
+
): Block {
|
234
|
+
return {
|
235
|
+
type: 'to_do',
|
236
|
+
to_do: {
|
237
|
+
rich_text: typeof text === 'string' ? [createRichText(text)] : text,
|
238
|
+
checked,
|
239
|
+
color,
|
240
|
+
children,
|
241
|
+
},
|
242
|
+
};
|
243
|
+
}
|
244
|
+
|
245
|
+
/**
|
246
|
+
* Creates a code block
|
247
|
+
*/
|
248
|
+
export function createCodeBlock(
|
249
|
+
code: string,
|
250
|
+
language?: string,
|
251
|
+
caption?: RichTextObject[],
|
252
|
+
): Block {
|
253
|
+
return {
|
254
|
+
type: 'code',
|
255
|
+
code: {
|
256
|
+
rich_text: [createRichText(code)],
|
257
|
+
language,
|
258
|
+
caption,
|
259
|
+
},
|
260
|
+
};
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Creates a quote block
|
265
|
+
*/
|
266
|
+
export function createQuoteBlock(
|
267
|
+
text: string | RichTextObject[],
|
268
|
+
color?: NotionColor,
|
269
|
+
children?: Block[],
|
270
|
+
): Block {
|
271
|
+
return {
|
272
|
+
type: 'quote',
|
273
|
+
quote: {
|
274
|
+
rich_text: typeof text === 'string' ? [createRichText(text)] : text,
|
275
|
+
color,
|
276
|
+
children,
|
277
|
+
},
|
278
|
+
};
|
279
|
+
}
|
280
|
+
|
281
|
+
/**
|
282
|
+
* Creates a callout block
|
283
|
+
*/
|
284
|
+
export function createCalloutBlock(
|
285
|
+
text: string | RichTextObject[],
|
286
|
+
icon?: string,
|
287
|
+
color?: NotionColor,
|
288
|
+
children?: Block[],
|
289
|
+
): Block {
|
290
|
+
const iconObject = icon ? { type: 'emoji' as const, emoji: icon } : undefined;
|
291
|
+
|
292
|
+
return {
|
293
|
+
type: 'callout',
|
294
|
+
callout: {
|
295
|
+
rich_text: typeof text === 'string' ? [createRichText(text)] : text,
|
296
|
+
icon: iconObject,
|
297
|
+
color,
|
298
|
+
children,
|
299
|
+
},
|
300
|
+
};
|
301
|
+
}
|
302
|
+
|
303
|
+
/**
|
304
|
+
* Creates a divider block
|
305
|
+
*/
|
306
|
+
export function createDividerBlock(): Block {
|
307
|
+
return {
|
308
|
+
type: 'divider',
|
309
|
+
divider: {},
|
310
|
+
};
|
311
|
+
}
|
312
|
+
|
313
|
+
/**
|
314
|
+
* Creates an equation block
|
315
|
+
*/
|
316
|
+
export function createEquationBlock(expression: string): Block {
|
317
|
+
return {
|
318
|
+
type: 'equation',
|
319
|
+
equation: {
|
320
|
+
expression,
|
321
|
+
},
|
322
|
+
};
|
323
|
+
}
|
324
|
+
|
325
|
+
/**
|
326
|
+
* Creates an image block
|
327
|
+
*/
|
328
|
+
export function createImageBlock(url: string, caption?: RichTextObject[]): Block {
|
329
|
+
return {
|
330
|
+
type: 'image',
|
331
|
+
image: {
|
332
|
+
type: 'external',
|
333
|
+
external: { url },
|
334
|
+
caption,
|
335
|
+
},
|
336
|
+
};
|
337
|
+
}
|
338
|
+
|
339
|
+
/**
|
340
|
+
* Creates a bookmark block
|
341
|
+
*/
|
342
|
+
export function createBookmarkBlock(url: string, caption?: RichTextObject[]): Block {
|
343
|
+
return {
|
344
|
+
type: 'bookmark',
|
345
|
+
bookmark: {
|
346
|
+
url,
|
347
|
+
caption,
|
348
|
+
},
|
349
|
+
};
|
350
|
+
}
|
351
|
+
|
352
|
+
/**
|
353
|
+
* Creates an embed block
|
354
|
+
*/
|
355
|
+
export function createEmbedBlock(url: string, caption?: RichTextObject[]): Block {
|
356
|
+
return {
|
357
|
+
type: 'embed',
|
358
|
+
embed: {
|
359
|
+
url,
|
360
|
+
caption,
|
361
|
+
},
|
362
|
+
};
|
363
|
+
}
|
364
|
+
|
365
|
+
/**
|
366
|
+
* Creates a table block with rows
|
367
|
+
*/
|
368
|
+
export function createTableBlock(
|
369
|
+
tableWidth: number,
|
370
|
+
rows: RichTextObject[][][],
|
371
|
+
hasColumnHeader: boolean = false,
|
372
|
+
hasRowHeader: boolean = false,
|
373
|
+
): Block {
|
374
|
+
const tableRows = rows.map(cells => ({
|
375
|
+
type: 'table_row' as const,
|
376
|
+
table_row: { cells },
|
377
|
+
}));
|
378
|
+
|
379
|
+
return {
|
380
|
+
type: 'table',
|
381
|
+
table: {
|
382
|
+
table_width: tableWidth,
|
383
|
+
has_column_header: hasColumnHeader,
|
384
|
+
has_row_header: hasRowHeader,
|
385
|
+
children: tableRows,
|
386
|
+
},
|
387
|
+
};
|
388
|
+
}
|
389
|
+
|
390
|
+
/**
|
391
|
+
* Converts BlockInput to actual Block objects
|
392
|
+
*/
|
393
|
+
export function convertBlockInput(blockInput: BlockInput): Block {
|
394
|
+
const { type, content = '', properties = {}, children } = blockInput;
|
395
|
+
const richText = parseRichTextInput(blockInput.richText || content);
|
396
|
+
const childBlocks = children ? children.map(convertBlockInput) : undefined;
|
397
|
+
|
398
|
+
switch (type) {
|
399
|
+
case 'paragraph':
|
400
|
+
return createParagraphBlock(richText, properties.color, childBlocks);
|
401
|
+
|
402
|
+
case 'heading_1':
|
403
|
+
case 'heading_2':
|
404
|
+
case 'heading_3':
|
405
|
+
const level = parseInt(type.split('_')[1]) as 1 | 2 | 3;
|
406
|
+
return createHeadingBlock(level, richText, properties.color, properties.isToggleable);
|
407
|
+
|
408
|
+
case 'bulleted_list_item':
|
409
|
+
case 'numbered_list_item':
|
410
|
+
return createListItemBlock(type, richText, properties.color, childBlocks);
|
411
|
+
|
412
|
+
case 'to_do':
|
413
|
+
return createToDoBlock(richText, properties.checked, properties.color, childBlocks);
|
414
|
+
|
415
|
+
case 'code':
|
416
|
+
return createCodeBlock(content, properties.language, properties.caption);
|
417
|
+
|
418
|
+
case 'quote':
|
419
|
+
return createQuoteBlock(richText, properties.color, childBlocks);
|
420
|
+
|
421
|
+
case 'callout':
|
422
|
+
return createCalloutBlock(richText, properties.icon, properties.color, childBlocks);
|
423
|
+
|
424
|
+
case 'divider':
|
425
|
+
return createDividerBlock();
|
426
|
+
|
427
|
+
case 'equation':
|
428
|
+
return createEquationBlock(properties.expression || content);
|
429
|
+
|
430
|
+
case 'image':
|
431
|
+
return createImageBlock(properties.url || content, properties.caption);
|
432
|
+
|
433
|
+
case 'bookmark':
|
434
|
+
return createBookmarkBlock(properties.url || content, properties.caption);
|
435
|
+
|
436
|
+
case 'embed':
|
437
|
+
return createEmbedBlock(properties.url || content, properties.caption);
|
438
|
+
|
439
|
+
default:
|
440
|
+
throw new Error(`Unsupported block type: ${type}`);
|
441
|
+
}
|
442
|
+
}
|
443
|
+
|
444
|
+
/**
|
445
|
+
* Resolves a page ID from various input formats (URL, ID, title search)
|
446
|
+
*/
|
447
|
+
export async function resolvePageId(
|
448
|
+
this: IExecuteFunctions,
|
449
|
+
pageInput: string,
|
450
|
+
): Promise<string> {
|
451
|
+
// If it looks like a UUID, return it directly
|
452
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
453
|
+
if (uuidRegex.test(pageInput)) {
|
454
|
+
return pageInput;
|
455
|
+
}
|
456
|
+
|
457
|
+
// If it looks like a Notion URL, extract the ID
|
458
|
+
const urlRegex = /https:\/\/www\.notion\.so\/[^\/]*-([a-f0-9]{32})/;
|
459
|
+
const urlMatch = pageInput.match(urlRegex);
|
460
|
+
if (urlMatch) {
|
461
|
+
const rawId = urlMatch[1];
|
462
|
+
// Convert 32-character ID to UUID format
|
463
|
+
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20)}`;
|
464
|
+
}
|
465
|
+
|
466
|
+
// Otherwise, search for page by title
|
467
|
+
const searchResponse = await notionApiRequest.call(
|
468
|
+
this,
|
469
|
+
'POST',
|
470
|
+
'/search',
|
471
|
+
{
|
472
|
+
query: pageInput,
|
473
|
+
filter: { property: 'object', value: 'page' },
|
474
|
+
},
|
475
|
+
) as NotionSearchResponse;
|
476
|
+
|
477
|
+
if (searchResponse.results && searchResponse.results.length > 0) {
|
478
|
+
const page = searchResponse.results[0] as NotionPage;
|
479
|
+
return page.id;
|
480
|
+
}
|
481
|
+
|
482
|
+
throw new NodeOperationError(
|
483
|
+
this.getNode(),
|
484
|
+
`Could not find page with identifier: ${pageInput}`,
|
485
|
+
);
|
486
|
+
}
|
487
|
+
|
488
|
+
/**
|
489
|
+
* Validates a block structure before sending to Notion API
|
490
|
+
*/
|
491
|
+
export function validateBlock(block: Block): void {
|
492
|
+
if (!block.type) {
|
493
|
+
throw new Error('Block must have a type property');
|
494
|
+
}
|
495
|
+
|
496
|
+
// Add specific validation for each block type as needed
|
497
|
+
switch (block.type) {
|
498
|
+
case 'paragraph':
|
499
|
+
if (!('paragraph' in block) || !block.paragraph || !Array.isArray(block.paragraph.rich_text)) {
|
500
|
+
throw new Error('Paragraph block must have rich_text array');
|
501
|
+
}
|
502
|
+
break;
|
503
|
+
|
504
|
+
case 'code':
|
505
|
+
if (!('code' in block) || !block.code || !Array.isArray(block.code.rich_text)) {
|
506
|
+
throw new Error('Code block must have rich_text array');
|
507
|
+
}
|
508
|
+
break;
|
509
|
+
|
510
|
+
// Add more validation cases as needed
|
511
|
+
}
|
512
|
+
}
|
513
|
+
|
514
|
+
/**
|
515
|
+
* Paginated request helper for Notion API
|
516
|
+
*/
|
517
|
+
export async function paginatedRequest(
|
518
|
+
this: IExecuteFunctions,
|
519
|
+
method: IHttpRequestMethods,
|
520
|
+
endpoint: string,
|
521
|
+
body: IDataObject = {},
|
522
|
+
): Promise<any[]> {
|
523
|
+
const results: any[] = [];
|
524
|
+
let hasMore = true;
|
525
|
+
let nextCursor: string | undefined;
|
526
|
+
|
527
|
+
while (hasMore) {
|
528
|
+
const requestBody = { ...body };
|
529
|
+
if (nextCursor) {
|
530
|
+
requestBody.start_cursor = nextCursor;
|
531
|
+
}
|
532
|
+
|
533
|
+
const response: NotionApiResponse = await notionApiRequest.call(this, method, endpoint, requestBody);
|
534
|
+
|
535
|
+
if (response.results) {
|
536
|
+
results.push(...response.results);
|
537
|
+
}
|
538
|
+
|
539
|
+
hasMore = response.has_more || false;
|
540
|
+
nextCursor = response.next_cursor || undefined;
|
541
|
+
}
|
542
|
+
|
543
|
+
return results;
|
544
|
+
}
|
545
|
+
|
546
|
+
/**
|
547
|
+
* Creates page input from parameters for validation
|
548
|
+
*/
|
549
|
+
export function createPageInput(
|
550
|
+
title: string,
|
551
|
+
parent: string,
|
552
|
+
properties?: { [key: string]: any },
|
553
|
+
children?: BlockInput[],
|
554
|
+
icon?: string,
|
555
|
+
cover?: string,
|
556
|
+
): PageInput {
|
557
|
+
return {
|
558
|
+
title,
|
559
|
+
parent,
|
560
|
+
properties,
|
561
|
+
children,
|
562
|
+
icon,
|
563
|
+
cover,
|
564
|
+
};
|
565
|
+
}
|
566
|
+
|
567
|
+
/**
|
568
|
+
* Gets blocks with full metadata including IDs
|
569
|
+
*/
|
570
|
+
export async function getBlocksWithIds(
|
571
|
+
this: IExecuteFunctions,
|
572
|
+
blockId: string,
|
573
|
+
): Promise<BlockWithId[]> {
|
574
|
+
const response: NotionBlockChildrenResponse = await notionApiRequest.call(
|
575
|
+
this,
|
576
|
+
'GET',
|
577
|
+
`/blocks/${blockId}/children`,
|
578
|
+
);
|
579
|
+
|
580
|
+
return response.results;
|
581
|
+
}
|
582
|
+
|
583
|
+
/**
|
584
|
+
* Creates execution data from results
|
585
|
+
*/
|
586
|
+
export function createExecutionData(data: IDataObject[]): INodeExecutionData[] {
|
587
|
+
return data.map(item => ({ json: item }));
|
588
|
+
}
|
@@ -0,0 +1,3 @@
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
2
|
+
<path d="M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.533 2.221c-.466.047-.56.28-.374.466l1.3.979zM5.206 6.678v14.018c0 .746.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.305c0-.607-.28-.887-.747-.84l-15.177.887c-.56.047-.748.327-.748.746v-.42zm14.337.793c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 18s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933l3.176-.233z" fill="currentColor"/>
|
3
|
+
</svg>
|