mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Teams addon for messaging on Microsoft teams (#814)
This commit is contained in:
		
							parent
							
								
									517f3e2170
								
							
						
					
					
						commit
						6c57aeb232
					
				
							
								
								
									
										33
									
								
								docs/addons/teams.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								docs/addons/teams.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					id: teams
 | 
				
			||||||
 | 
					title: Microsoft Teams
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> This feature was introduced in \_Unleash v4.0.x.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The MicrosoftTeams addon allows Unleash to post Updates when a feature toggle is updated. To set up this addon, you need to set up a webhook connector for your channel. You can follow [Creating an Incoming Webhook for a channel](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) on how to do that.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Microsoft Teams addon will perform a single retry if the HTTP POST against the Microsoft Teams Webhook URL fails (either a 50x or network error). Duplicate events may happen, and you should never assume events always comes in order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can choose to trigger updates for the following events (we might add more event types in the future):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- feature-created
 | 
				
			||||||
 | 
					- feature-updated
 | 
				
			||||||
 | 
					- feature-archived
 | 
				
			||||||
 | 
					- feature-revived
 | 
				
			||||||
 | 
					- feature-stale-on
 | 
				
			||||||
 | 
					- feature-stale-off
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unleash Microsoft Teams addon takes the following parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **Microsoft Teams Webhook URL** - This is the only required property.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Microsoft teams's income webhooks are channel specific. You will be able to create multiple addons to support messaging on multiple channels.
 | 
				
			||||||
							
								
								
									
										29
									
								
								snapshots/src/lib/addons/teams.test.js.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								snapshots/src/lib/addons/teams.test.js.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					# Snapshot report for `src/lib/addons/teams.test.js`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The actual snapshot is saved in `teams.test.js.snap`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Generated by [AVA](https://avajs.dev).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Should call slack webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Snapshot 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"Feature toggle some-toggle | *Type*: undefined | *Project*: undefined <br /> *Activation strategies*: \\n- name: default\\n","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"Create"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/#/features/strategies/some-toggle"}]}]}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Should call slack webhook for archived toggle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Snapshot 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"The feature toggle *some-toggle* was *archived*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/#/archive/strategies/some-toggle"}]}]}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Should call teams webhook
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Snapshot 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"Feature toggle some-toggle | *Type*: undefined | *Project*: undefined <br /> *Activation strategies*: \\n- name: default\\n","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"Create"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/#/features/strategies/some-toggle"}]}]}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Should call teams webhook for archived toggle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Snapshot 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"The feature toggle *some-toggle* was *archived*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/#/archive/strategies/some-toggle"}]}]}'
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								snapshots/src/lib/addons/teams.test.js.snap
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								snapshots/src/lib/addons/teams.test.js.snap
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@ -1,6 +1,7 @@
 | 
				
			|||||||
const webhook = require('./webhook');
 | 
					const webhook = require('./webhook');
 | 
				
			||||||
const slackAddon = require('./slack');
 | 
					const slackAddon = require('./slack');
 | 
				
			||||||
 | 
					const teamsAddon = require('./teams');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const addons = [webhook, slackAddon];
 | 
					const addons = [webhook, slackAddon, teamsAddon];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = addons;
 | 
					module.exports = addons;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										34
									
								
								src/lib/addons/teams-definition.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/lib/addons/teams-definition.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					    FEATURE_CREATED,
 | 
				
			||||||
 | 
					    FEATURE_UPDATED,
 | 
				
			||||||
 | 
					    FEATURE_ARCHIVED,
 | 
				
			||||||
 | 
					    FEATURE_REVIVED,
 | 
				
			||||||
 | 
					    FEATURE_STALE_ON,
 | 
				
			||||||
 | 
					    FEATURE_STALE_OFF,
 | 
				
			||||||
 | 
					} = require('../event-type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    name: 'teams',
 | 
				
			||||||
 | 
					    displayName: 'Microsoft Teams',
 | 
				
			||||||
 | 
					    description: 'Allows Unleash to post updates to Microsoft Teams.',
 | 
				
			||||||
 | 
					    documentationUrl: 'https://docs.getunleash.io/docs/addons/teams',
 | 
				
			||||||
 | 
					    parameters: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'url',
 | 
				
			||||||
 | 
					            displayName: 'Microsoft Teams webhook URL',
 | 
				
			||||||
 | 
					            type: 'url',
 | 
				
			||||||
 | 
					            required: true,
 | 
				
			||||||
 | 
					            sensitive: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    events: [
 | 
				
			||||||
 | 
					        FEATURE_CREATED,
 | 
				
			||||||
 | 
					        FEATURE_UPDATED,
 | 
				
			||||||
 | 
					        FEATURE_ARCHIVED,
 | 
				
			||||||
 | 
					        FEATURE_REVIVED,
 | 
				
			||||||
 | 
					        FEATURE_STALE_ON,
 | 
				
			||||||
 | 
					        FEATURE_STALE_OFF,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										126
									
								
								src/lib/addons/teams.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/lib/addons/teams.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const YAML = require('js-yaml');
 | 
				
			||||||
 | 
					const Addon = require('./addon');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					    FEATURE_CREATED,
 | 
				
			||||||
 | 
					    FEATURE_UPDATED,
 | 
				
			||||||
 | 
					    FEATURE_ARCHIVED,
 | 
				
			||||||
 | 
					    FEATURE_REVIVED,
 | 
				
			||||||
 | 
					    FEATURE_STALE_ON,
 | 
				
			||||||
 | 
					    FEATURE_STALE_OFF,
 | 
				
			||||||
 | 
					} = require('../event-type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const definition = require('./teams-definition');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TeamsAddon extends Addon {
 | 
				
			||||||
 | 
					    constructor(args) {
 | 
				
			||||||
 | 
					        super(definition, args);
 | 
				
			||||||
 | 
					        this.unleashUrl = args.unleashUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleEvent(event, parameters) {
 | 
				
			||||||
 | 
					        const { url } = parameters;
 | 
				
			||||||
 | 
					        const { createdBy, data, type } = event;
 | 
				
			||||||
 | 
					        let text = '';
 | 
				
			||||||
 | 
					        if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) {
 | 
				
			||||||
 | 
					            text = this.generateArchivedText(event);
 | 
				
			||||||
 | 
					        } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) {
 | 
				
			||||||
 | 
					            text = this.generateStaleText(event);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            text = this.generateText(event);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const enabled = `*${data.enabled ? 'yes' : 'no'}*`;
 | 
				
			||||||
 | 
					        const stale = data.stale ? '("stale")' : '';
 | 
				
			||||||
 | 
					        const body = {
 | 
				
			||||||
 | 
					            themeColor: '0076D7',
 | 
				
			||||||
 | 
					            summary: 'Message',
 | 
				
			||||||
 | 
					            sections: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    activityTitle: text,
 | 
				
			||||||
 | 
					                    activitySubtitle: 'Unleash notification update',
 | 
				
			||||||
 | 
					                    facts: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            name: 'User',
 | 
				
			||||||
 | 
					                            value: createdBy,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            name: 'Action',
 | 
				
			||||||
 | 
					                            value: this.getAction(type),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            name: 'Enabled',
 | 
				
			||||||
 | 
					                            value: `${enabled}${stale}`,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            potentialAction: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    '@type': 'OpenUri',
 | 
				
			||||||
 | 
					                    name: 'Go to feature',
 | 
				
			||||||
 | 
					                    targets: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            os: 'default',
 | 
				
			||||||
 | 
					                            uri: this.featureLink(event),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const requestOpts = {
 | 
				
			||||||
 | 
					            method: 'POST',
 | 
				
			||||||
 | 
					            headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					            body: JSON.stringify(body),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const result = await this.fetchRetry(url, requestOpts);
 | 
				
			||||||
 | 
					        this.logger.info(`Handled event ${event.type}. Status codes=${result}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    featureLink(event) {
 | 
				
			||||||
 | 
					        const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features';
 | 
				
			||||||
 | 
					        return `${this.unleashUrl}/#/${path}/strategies/${event.data.name}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generateStaleText(event) {
 | 
				
			||||||
 | 
					        const { data, type } = event;
 | 
				
			||||||
 | 
					        const isStale = type === FEATURE_STALE_ON;
 | 
				
			||||||
 | 
					        if (isStale) {
 | 
				
			||||||
 | 
					            return `The feature toggle *${data.name}* is now *ready to be removed* from the code.`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return `The feature toggle *${data.name}* was *unmarked* as stale`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generateArchivedText(event) {
 | 
				
			||||||
 | 
					        const { data, type } = event;
 | 
				
			||||||
 | 
					        const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived';
 | 
				
			||||||
 | 
					        return `The feature toggle *${data.name}* was *${action}*`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generateText(event) {
 | 
				
			||||||
 | 
					        const { data } = event;
 | 
				
			||||||
 | 
					        const typeStr = `*Type*: ${data.type}`;
 | 
				
			||||||
 | 
					        const project = `*Project*: ${data.project}`;
 | 
				
			||||||
 | 
					        const strategies = `*Activation strategies*: \n${YAML.safeDump(
 | 
				
			||||||
 | 
					            data.strategies,
 | 
				
			||||||
 | 
					            { skipInvalid: true },
 | 
				
			||||||
 | 
					        )}`;
 | 
				
			||||||
 | 
					        return `Feature toggle ${data.name} | ${typeStr} | ${project} <br /> ${strategies}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getAction(type) {
 | 
				
			||||||
 | 
					        switch (type) {
 | 
				
			||||||
 | 
					            case FEATURE_CREATED:
 | 
				
			||||||
 | 
					                return 'Create';
 | 
				
			||||||
 | 
					            case FEATURE_UPDATED:
 | 
				
			||||||
 | 
					                return 'Update';
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return type;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = TeamsAddon;
 | 
				
			||||||
							
								
								
									
										67
									
								
								src/lib/addons/teams.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/lib/addons/teams.test.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					const test = require('ava');
 | 
				
			||||||
 | 
					const proxyquire = require('proxyquire').noCallThru();
 | 
				
			||||||
 | 
					const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../event-type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TeamsAddon = proxyquire.load('./teams', {
 | 
				
			||||||
 | 
					    './addon': class Addon {
 | 
				
			||||||
 | 
					        constructor(definition, { getLogger }) {
 | 
				
			||||||
 | 
					            this.logger = getLogger('addon/test');
 | 
				
			||||||
 | 
					            this.fetchRetryCalls = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async fetchRetry(url, options, retries, backoff) {
 | 
				
			||||||
 | 
					            this.fetchRetryCalls.push({ url, options, retries, backoff });
 | 
				
			||||||
 | 
					            return Promise.resolve({ status: 200 });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const noLogger = require('../../test/fixtures/no-logger');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should call teams webhook', async t => {
 | 
				
			||||||
 | 
					    const addon = new TeamsAddon({
 | 
				
			||||||
 | 
					        getLogger: noLogger,
 | 
				
			||||||
 | 
					        unleashUrl: 'http://some-url.com',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const event = {
 | 
				
			||||||
 | 
					        type: FEATURE_CREATED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					            enabled: false,
 | 
				
			||||||
 | 
					            strategies: [{ name: 'default' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://hooks.office.com',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    t.is(addon.fetchRetryCalls.length, 1);
 | 
				
			||||||
 | 
					    t.is(addon.fetchRetryCalls[0].url, parameters.url);
 | 
				
			||||||
 | 
					    t.snapshot(addon.fetchRetryCalls[0].options.body);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should call teams webhook for archived toggle', async t => {
 | 
				
			||||||
 | 
					    const addon = new TeamsAddon({
 | 
				
			||||||
 | 
					        getLogger: noLogger,
 | 
				
			||||||
 | 
					        unleashUrl: 'http://some-url.com',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const event = {
 | 
				
			||||||
 | 
					        type: FEATURE_ARCHIVED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://hooks.office.com',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    t.is(addon.fetchRetryCalls.length, 1);
 | 
				
			||||||
 | 
					    t.is(addon.fetchRetryCalls[0].url, parameters.url);
 | 
				
			||||||
 | 
					    t.snapshot(addon.fetchRetryCalls[0].options.body);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -29,7 +29,7 @@ test.serial('gets all addons', async t => {
 | 
				
			|||||||
        .expect(200)
 | 
					        .expect(200)
 | 
				
			||||||
        .expect(res => {
 | 
					        .expect(res => {
 | 
				
			||||||
            t.is(res.body.addons.length, 0, 'expected 0 configured addons');
 | 
					            t.is(res.body.addons.length, 0, 'expected 0 configured addons');
 | 
				
			||||||
            t.is(res.body.providers.length, 2, 'expected 2 addon providers');
 | 
					            t.is(res.body.providers.length, 3, 'expected 3 addon providers');
 | 
				
			||||||
            t.is(res.body.providers[0].name, 'webhook');
 | 
					            t.is(res.body.providers[0].name, 'webhook');
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user