mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
🚚 Directorize and alphabetize nodes (#2445)
* 🚚 Directorize nodes * ⚡ Alphabetize nodes and credentials * 🔥 Remove unused node * 🔥 Remove unused codex * 🔥 Remove duplicate cred file references * 🐛 Fix node file paths * 🔥 Remove duplicate node reference
This commit is contained in:
99
packages/nodes-base/nodes/Webhook/Webhook.node.json
Normal file
99
packages/nodes-base/nodes/Webhook/Webhook.node.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.webhook",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": [
|
||||
"Development",
|
||||
"Core Nodes"
|
||||
],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.webhook/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Learn how to automatically cross-post your content with n8n",
|
||||
"icon": "✍️",
|
||||
"url": "https://n8n.io/blog/learn-how-to-automatically-cross-post-your-content-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "Running n8n on ships: An interview with Maranics",
|
||||
"icon": "🛳",
|
||||
"url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/"
|
||||
},
|
||||
{
|
||||
"label": "How to build a low-code, self-hosted URL shortener in 3 steps",
|
||||
"icon": "🔗",
|
||||
"url": "https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/"
|
||||
},
|
||||
{
|
||||
"label": "What are APIs and how to use them with no code",
|
||||
"icon": " 🪢",
|
||||
"url": "https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/"
|
||||
},
|
||||
{
|
||||
"label": "5 tasks you can automate with the new Notion API ",
|
||||
"icon": "⚡️",
|
||||
"url": "https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/"
|
||||
},
|
||||
{
|
||||
"label": "How a digital strategist uses n8n for online marketing",
|
||||
"icon": "💻",
|
||||
"url": "https://n8n.io/blog/how-a-digital-strategist-uses-n8n-for-online-marketing/"
|
||||
},
|
||||
{
|
||||
"label": "The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n",
|
||||
"icon": "📹",
|
||||
"url": "https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "How to automatically give kudos to contributors with GitHub, Slack, and n8n",
|
||||
"icon": "👏",
|
||||
"url": "https://n8n.io/blog/how-to-automatically-give-kudos-to-contributors-with-github-slack-and-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "5 workflow automations for Mattermost that we love at n8n",
|
||||
"icon": "🤖",
|
||||
"url": "https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "Why this Product Manager loves workflow automation with n8n",
|
||||
"icon": "🧠",
|
||||
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "Creating Custom Incident Response Workflows with n8n",
|
||||
"icon": "🚨",
|
||||
"url": "https://n8n.io/blog/creating-custom-incident-response-workflows-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "Learn to Build Powerful API Endpoints Using Webhooks",
|
||||
"icon": "🧰",
|
||||
"url": "https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/"
|
||||
},
|
||||
{
|
||||
"label": "Learn how to use webhooks with Mattermost slash commands",
|
||||
"icon": "🦄",
|
||||
"url": "https://n8n.io/blog/learn-how-to-use-webhooks-with-mattermost-slash-commands/"
|
||||
},
|
||||
{
|
||||
"label": "How Goomer automated their operations with over 200 n8n workflows",
|
||||
"icon": "🛵",
|
||||
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": [
|
||||
"HTTP",
|
||||
"API",
|
||||
"Build",
|
||||
"WH"
|
||||
],
|
||||
"subcategories": {
|
||||
"Core Nodes": [
|
||||
"Flow"
|
||||
]
|
||||
}
|
||||
}
|
||||
564
packages/nodes-base/nodes/Webhook/Webhook.node.ts
Normal file
564
packages/nodes-base/nodes/Webhook/Webhook.node.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as basicAuth from 'basic-auth';
|
||||
|
||||
import { Response } from 'express';
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import * as formidable from 'formidable';
|
||||
|
||||
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
||||
if (message === undefined) {
|
||||
message = 'Authorization problem!';
|
||||
if (responseCode === 401) {
|
||||
message = 'Authorization is required!';
|
||||
} else if (responseCode === 403) {
|
||||
message = 'Authorization data is wrong!';
|
||||
}
|
||||
}
|
||||
|
||||
resp.writeHead(responseCode, { 'WWW-Authenticate': `Basic realm="${realm}"` });
|
||||
resp.end(message);
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
export class Webhook implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Webhook',
|
||||
icon: 'file:webhook.svg',
|
||||
name: 'webhook',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when a webhook is called',
|
||||
defaults: {
|
||||
name: 'Webhook',
|
||||
color: '#885577',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'httpBasicAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'basicAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'httpHeaderAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'headerAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: '={{$parameter["httpMethod"]}}',
|
||||
isFullPath: true,
|
||||
responseCode: '={{$parameter["responseCode"]}}',
|
||||
responseMode: '={{$parameter["responseMode"]}}',
|
||||
responseData: '={{$parameter["responseData"]}}',
|
||||
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
|
||||
responseContentType: '={{$parameter["options"]["responseContentType"]}}',
|
||||
responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}',
|
||||
responseHeaders: '={{$parameter["options"]["responseHeaders"]}}',
|
||||
path: '={{$parameter["path"]}}',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth',
|
||||
},
|
||||
{
|
||||
name: 'Header Auth',
|
||||
value: 'headerAuth',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate.',
|
||||
},
|
||||
{
|
||||
displayName: 'HTTP Method',
|
||||
name: 'httpMethod',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'GET',
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'HEAD',
|
||||
value: 'HEAD',
|
||||
},
|
||||
{
|
||||
name: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
],
|
||||
default: 'GET',
|
||||
description: 'The HTTP method to listen to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'webhook',
|
||||
required: true,
|
||||
description: 'The path to listen to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Respond',
|
||||
name: 'responseMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Immediately',
|
||||
value: 'onReceived',
|
||||
description: 'As soon as this node executes',
|
||||
},
|
||||
{
|
||||
name: 'When last node finishes',
|
||||
value: 'lastNode',
|
||||
description: 'Returns data of the last-executed node',
|
||||
},
|
||||
{
|
||||
name: 'Using \'Respond to Webhook\' node',
|
||||
value: 'responseNode',
|
||||
description: 'Response defined in that node',
|
||||
},
|
||||
],
|
||||
default: 'onReceived',
|
||||
description: 'When and how to respond to the webhook.',
|
||||
},
|
||||
{
|
||||
displayName: 'Insert a \'Respond to Webhook\' node to control when and how you respond. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.respondToWebhook" target="_blank">More details</a>',
|
||||
name: 'webhookNotice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
responseMode: [
|
||||
'responseNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Code',
|
||||
name: 'responseCode',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
responseMode: [
|
||||
'responseNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 100,
|
||||
maxValue: 599,
|
||||
},
|
||||
default: 200,
|
||||
description: 'The HTTP Response code to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Data',
|
||||
name: 'responseData',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
responseMode: [
|
||||
'lastNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'All Entries',
|
||||
value: 'allEntries',
|
||||
description: 'Returns all the entries of the last node. Always returns an array.',
|
||||
},
|
||||
{
|
||||
name: 'First Entry JSON',
|
||||
value: 'firstEntryJson',
|
||||
description: 'Returns the JSON data of the first entry of the last node. Always returns a JSON object.',
|
||||
},
|
||||
{
|
||||
name: 'First Entry Binary',
|
||||
value: 'firstEntryBinary',
|
||||
description: 'Returns the binary data of the first entry of the last node. Always returns a binary file.',
|
||||
},
|
||||
],
|
||||
default: 'firstEntryJson',
|
||||
description: 'What data should be returned. If it should return all items as an array or only the first item as object.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'responseBinaryPropertyName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
responseData: [
|
||||
'firstEntryBinary',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Binary Data',
|
||||
name: 'binaryData',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/httpMethod': [
|
||||
'POST',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Set to true if webhook will receive binary data.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
binaryData: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
description: `Name of the binary property to write the data of
|
||||
the received file to. If the data gets received via "Form-Data Multipart"
|
||||
it will be the prefix and a number starting with 0 will be attached to it.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Response Data',
|
||||
name: 'responseData',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/responseMode': [
|
||||
'onReceived',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'success',
|
||||
description: 'Custom response data to send.',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Content-Type',
|
||||
name: 'responseContentType',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/responseData': [
|
||||
'firstEntryJson',
|
||||
],
|
||||
'/responseMode': [
|
||||
'lastNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'application/xml',
|
||||
description: 'Set a custom content-type to return if another one as the "application/json" should be returned.',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Headers',
|
||||
name: 'responseHeaders',
|
||||
placeholder: 'Add Response Header',
|
||||
description: 'Add headers to the webhook response.',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'entries',
|
||||
displayName: 'Entries',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the header.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the header.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'responsePropertyName',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/responseData': [
|
||||
'firstEntryJson',
|
||||
],
|
||||
'/responseMode': [
|
||||
'lastNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: 'data',
|
||||
description: 'Name of the property to return the data of instead of the whole JSON.',
|
||||
},
|
||||
{
|
||||
displayName: 'Raw Body',
|
||||
name: 'rawBody',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
binaryData: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Raw body (binary)',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const authentication = this.getNodeParameter('authentication') as string;
|
||||
const options = this.getNodeParameter('options', {}) as IDataObject;
|
||||
const req = this.getRequestObject();
|
||||
const resp = this.getResponseObject();
|
||||
const headers = this.getHeaderData();
|
||||
const realm = 'Webhook';
|
||||
|
||||
if (authentication === 'basicAuth') {
|
||||
// Basic authorization is needed to call webhook
|
||||
const httpBasicAuth = await this.getCredentials('httpBasicAuth');
|
||||
|
||||
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
|
||||
// Data is not defined on node so can not authenticate
|
||||
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
|
||||
}
|
||||
|
||||
const basicAuthData = basicAuth(req);
|
||||
|
||||
if (basicAuthData === undefined) {
|
||||
// Authorization data is missing
|
||||
return authorizationError(resp, realm, 401);
|
||||
}
|
||||
|
||||
if (basicAuthData.name !== httpBasicAuth!.user || basicAuthData.pass !== httpBasicAuth!.password) {
|
||||
// Provided authentication data is wrong
|
||||
return authorizationError(resp, realm, 403);
|
||||
}
|
||||
} else if (authentication === 'headerAuth') {
|
||||
// Special header with value is needed to call webhook
|
||||
const httpHeaderAuth = await this.getCredentials('httpHeaderAuth');
|
||||
|
||||
if (httpHeaderAuth === undefined || !httpHeaderAuth.name || !httpHeaderAuth.value) {
|
||||
// Data is not defined on node so can not authenticate
|
||||
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
|
||||
}
|
||||
const headerName = (httpHeaderAuth.name as string).toLowerCase();
|
||||
const headerValue = (httpHeaderAuth.value as string);
|
||||
|
||||
if (!headers.hasOwnProperty(headerName) || (headers as IDataObject)[headerName] !== headerValue) {
|
||||
// Provided authentication data is wrong
|
||||
return authorizationError(resp, realm, 403);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const mimeType = headers['content-type'] || 'application/json';
|
||||
if (mimeType.includes('multipart/form-data')) {
|
||||
// @ts-ignore
|
||||
const form = new formidable.IncomingForm({ multiples: true });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
form.parse(req, async (err, data, files) => {
|
||||
const returnItem: INodeExecutionData = {
|
||||
binary: {},
|
||||
json: {
|
||||
headers,
|
||||
params: this.getParamsData(),
|
||||
query: this.getQueryData(),
|
||||
body: data,
|
||||
},
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
for (const xfile of Object.keys(files)) {
|
||||
const processFiles: formidable.File[] = [];
|
||||
let multiFile = false;
|
||||
if (Array.isArray(files[xfile])) {
|
||||
processFiles.push(...files[xfile] as formidable.File[]);
|
||||
multiFile = true;
|
||||
} else {
|
||||
processFiles.push(files[xfile] as formidable.File);
|
||||
}
|
||||
|
||||
let fileCount = 0;
|
||||
for (const file of processFiles) {
|
||||
let binaryPropertyName = xfile;
|
||||
if (binaryPropertyName.endsWith('[]')) {
|
||||
binaryPropertyName = binaryPropertyName.slice(0, -2);
|
||||
}
|
||||
if (multiFile === true) {
|
||||
binaryPropertyName += fileCount++;
|
||||
}
|
||||
if (options.binaryPropertyName) {
|
||||
binaryPropertyName = `${options.binaryPropertyName}${count}`;
|
||||
}
|
||||
|
||||
const fileJson = file.toJSON() as unknown as IDataObject;
|
||||
const fileContent = await fs.promises.readFile(file.path);
|
||||
|
||||
returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(Buffer.from(fileContent), fileJson.name as string, fileJson.type as string);
|
||||
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
resolve({
|
||||
workflowData: [
|
||||
[
|
||||
returnItem,
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if (options.binaryData === true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const binaryPropertyName = options.binaryPropertyName || 'data';
|
||||
const data: Buffer[] = [];
|
||||
|
||||
req.on('data', (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
const returnItem: INodeExecutionData = {
|
||||
binary: {},
|
||||
json: {
|
||||
headers,
|
||||
params: this.getParamsData(),
|
||||
query: this.getQueryData(),
|
||||
body: this.getBodyData(),
|
||||
},
|
||||
};
|
||||
|
||||
returnItem.binary![binaryPropertyName as string] = await this.helpers.prepareBinaryData(Buffer.concat(data));
|
||||
|
||||
return resolve({
|
||||
workflowData: [
|
||||
[
|
||||
returnItem,
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
throw new NodeOperationError(this.getNode(), error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const response: INodeExecutionData = {
|
||||
json: {
|
||||
headers,
|
||||
params: this.getParamsData(),
|
||||
query: this.getQueryData(),
|
||||
body: this.getBodyData(),
|
||||
},
|
||||
};
|
||||
|
||||
if (options.rawBody) {
|
||||
response.binary = {
|
||||
data: {
|
||||
// @ts-ignore
|
||||
data: req.rawBody.toString(BINARY_ENCODING),
|
||||
mimeType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let webhookResponse: string | undefined;
|
||||
if (options.responseData) {
|
||||
webhookResponse = options.responseData as string;
|
||||
}
|
||||
|
||||
return {
|
||||
webhookResponse,
|
||||
workflowData: [
|
||||
[
|
||||
response,
|
||||
],
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
1
packages/nodes-base/nodes/Webhook/webhook.svg
Normal file
1
packages/nodes-base/nodes/Webhook/webhook.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#37474f" d="M35,37c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S37.2,37,35,37z"/><path fill="#37474f" d="M35,43c-3,0-5.9-1.4-7.8-3.7l3.1-2.5c1.1,1.4,2.9,2.3,4.7,2.3c3.3,0,6-2.7,6-6s-2.7-6-6-6 c-1,0-2,0.3-2.9,0.7l-1.7,1L23.3,16l3.5-1.9l5.3,9.4c1-0.3,2-0.5,3-0.5c5.5,0,10,4.5,10,10S40.5,43,35,43z"/><path fill="#37474f" d="M14,43C8.5,43,4,38.5,4,33c0-4.6,3.1-8.5,7.5-9.7l1,3.9C9.9,27.9,8,30.3,8,33c0,3.3,2.7,6,6,6 s6-2.7,6-6v-2h15v4H23.8C22.9,39.6,18.8,43,14,43z"/><path fill="#e91e63" d="M14,37c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S16.2,37,14,37z"/><path fill="#37474f" d="M25,19c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S27.2,19,25,19z"/><path fill="#e91e63" d="M15.7,34L12.3,32l5.9-9.7c-2-1.9-3.2-4.5-3.2-7.3c0-5.5,4.5-10,10-10c5.5,0,10,4.5,10,10 c0,0.9-0.1,1.7-0.3,2.5l-3.9-1c0.1-0.5,0.2-1,0.2-1.5c0-3.3-2.7-6-6-6s-6,2.7-6,6c0,2.1,1.1,4,2.9,5.1l1.7,1L15.7,34z"/></svg>
|
||||
|
After Width: | Height: | Size: 958 B |
Reference in New Issue
Block a user