integrate apple icalendar (#623)

This commit is contained in:
Femi Odugbesan 2021-09-11 02:54:13 -05:00 committed by GitHub
parent 5a9401bd28
commit bc79f24fd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 623 additions and 0 deletions

View file

@ -8,6 +8,7 @@ import logger from "@lib/logger";
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
import { CalDavCalendar } from "./integrations/CalDav/CalDavCalendarAdapter";
import { AppleCalendar } from "./integrations/Apple/AppleCalendarAdapter";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { google } = require("googleapis");
@ -521,6 +522,8 @@ const calendars = (withCredentials): CalendarApiAdapter[] =>
return MicrosoftOffice365Calendar(cred);
case "caldav_calendar":
return new CalDavCalendar(cred);
case "apple_calendar":
return new AppleCalendar(cred);
default:
return; // unknown credential, could be legacy? In any case, ignore
}

View file

@ -8,6 +8,8 @@ export function getIntegrationName(name: string) {
return "Zoom";
case "caldav_calendar":
return "CalDav Server";
case "apple_calendar":
return "Apple Calendar";
default:
return "Unknown";
}

View file

@ -0,0 +1,355 @@
import { IntegrationCalendar, CalendarApiAdapter, CalendarEvent } from "../../calendarClient";
import { symmetricDecrypt } from "@lib/crypto";
import {
createAccount,
fetchCalendars,
fetchCalendarObjects,
getBasicAuthHeaders,
createCalendarObject,
updateCalendarObject,
deleteCalendarObject,
} from "tsdav";
import { Credential } from "@prisma/client";
import ICAL from "ical.js";
import { createEvent, DurationObject, Attendee, Person } from "ics";
import dayjs from "dayjs";
import { v4 as uuidv4 } from "uuid";
import { stripHtml } from "../../emails/helpers";
import logger from "@lib/logger";
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
type EventBusyDate = Record<"start" | "end", Date>;
export class AppleCalendar implements CalendarApiAdapter {
private url: string;
private credentials: Record<string, string>;
private headers: Record<string, string>;
private readonly integrationName: string = "apple_calendar";
constructor(credential: Credential) {
const decryptedCredential = JSON.parse(
symmetricDecrypt(credential.key, process.env.CALENDSO_ENCRYPTION_KEY)
);
const username = decryptedCredential.username;
const password = decryptedCredential.password;
this.url = "https://caldav.icloud.com";
this.credentials = {
username,
password,
};
this.headers = getBasicAuthHeaders({
username,
password,
});
}
convertDate(date: string): number[] {
return dayjs(date)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v));
}
getDuration(start: string, end: string): DurationObject {
return {
minutes: dayjs(end).diff(dayjs(start), "minute"),
};
}
getAttendees(attendees: Person[]): Attendee[] {
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
}
async createEvent(event: CalendarEvent): Promise<Record<string, unknown>> {
try {
const calendars = await this.listCalendars();
const uid = uuidv4();
const { error, value: iCalString } = await createEvent({
uid,
startInputType: "utc",
start: this.convertDate(event.startTime),
duration: this.getDuration(event.startTime, event.endTime),
title: event.title,
description: stripHtml(event.description ?? ""),
location: event.location,
organizer: { email: event.organizer.email, name: event.organizer.name },
attendees: this.getAttendees(event.attendees),
});
if (error) {
log.debug("Error creating iCalString");
return {};
}
if (!iCalString) {
log.debug("Error creating iCalString");
return {};
}
await Promise.all(
calendars.map((calendar) => {
return createCalendarObject({
calendar: {
url: calendar.externalId,
},
filename: `${uid}.ics`,
iCalString: iCalString,
headers: this.headers,
});
})
);
return {
uid,
id: uid,
};
} catch (reason) {
console.error(reason);
throw reason;
}
}
async updateEvent(uid: string, event: CalendarEvent): Promise<unknown> {
try {
const calendars = await this.listCalendars();
const events = [];
for (const cal of calendars) {
const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
for (const ev of calEvents) {
events.push(ev);
}
}
const { error, value: iCalString } = await createEvent({
uid,
startInputType: "utc",
start: this.convertDate(event.startTime),
duration: this.getDuration(event.startTime, event.endTime),
title: event.title,
description: stripHtml(event.description ?? ""),
location: event.location,
organizer: { email: event.organizer.email, name: event.organizer.name },
attendees: this.getAttendees(event.attendees),
});
if (error) {
log.debug("Error creating iCalString");
return {};
}
const eventsToUpdate = events.filter((event) => event.uid === uid);
return await Promise.all(
eventsToUpdate.map((event) => {
return updateCalendarObject({
calendarObject: {
url: event.url,
data: iCalString,
etag: event?.etag,
},
headers: this.headers,
});
})
);
} catch (reason) {
console.error(reason);
throw reason;
}
}
async deleteEvent(uid: string): Promise<void> {
try {
const calendars = await this.listCalendars();
const events = [];
for (const cal of calendars) {
const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
for (const ev of calEvents) {
events.push(ev);
}
}
const eventsToUpdate = events.filter((event) => event.uid === uid);
await Promise.all(
eventsToUpdate.map((event) => {
return deleteCalendarObject({
calendarObject: {
url: event.url,
etag: event?.etag,
},
headers: this.headers,
});
})
);
} catch (reason) {
console.error(reason);
throw reason;
}
}
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
try {
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId);
if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) {
// Only calendars of other integrations selected
return Promise.resolve([]);
}
return (
selectedCalendarIds.length === 0
? this.listCalendars().then((calendars) => calendars.map((calendar) => calendar.externalId))
: Promise.resolve(selectedCalendarIds)
).then(async (ids: string[]) => {
if (ids.length === 0) {
return Promise.resolve([]);
}
return (
await Promise.all(
ids.map(async (calId) => {
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
return {
start: event.startDate,
end: event.endDate,
};
});
})
)
).flatMap((event) => event);
});
} catch (reason) {
log.error(reason);
throw reason;
}
}
async listCalendars(): Promise<IntegrationCalendar[]> {
try {
const account = await this.getAccount();
const calendars = await fetchCalendars({
account,
headers: this.headers,
});
return calendars
.filter((calendar) => {
return calendar.components?.includes("VEVENT");
})
.map((calendar) => ({
externalId: calendar.url,
name: calendar.displayName ?? "",
primary: false,
integration: this.integrationName,
}));
} catch (reason) {
console.error(reason);
throw reason;
}
}
async getEvents(
calId: string,
dateFrom: string | null,
dateTo: string | null,
objectUrls: string[] | null
): Promise<unknown[]> {
try {
const objects = await fetchCalendarObjects({
calendar: {
url: calId,
},
objectUrls: objectUrls ? objectUrls : undefined,
timeRange:
dateFrom && dateTo
? {
start: dayjs(dateFrom).utc().format("YYYY-MM-DDTHH:mm:ss[Z]"),
end: dayjs(dateTo).utc().format("YYYY-MM-DDTHH:mm:ss[Z]"),
}
: undefined,
headers: this.headers,
});
const events =
objects &&
objects?.length > 0 &&
objects
.map((object) => {
if (object?.data) {
const jcalData = ICAL.parse(object.data);
const vcalendar = new ICAL.Component(jcalData);
const vevent = vcalendar.getFirstSubcomponent("vevent");
const event = new ICAL.Event(vevent);
const calendarTimezone = vcalendar.getFirstSubcomponent("vtimezone")
? vcalendar.getFirstSubcomponent("vtimezone").getFirstPropertyValue("tzid")
: "";
const startDate = calendarTimezone
? dayjs(event.startDate).tz(calendarTimezone)
: new Date(event.startDate.toUnixTime() * 1000);
const endDate = calendarTimezone
? dayjs(event.endDate).tz(calendarTimezone)
: new Date(event.endDate.toUnixTime() * 1000);
return {
uid: event.uid,
etag: object.etag,
url: object.url,
summary: event.summary,
description: event.description,
location: event.location,
sequence: event.sequence,
startDate,
endDate,
duration: {
weeks: event.duration.weeks,
days: event.duration.days,
hours: event.duration.hours,
minutes: event.duration.minutes,
seconds: event.duration.seconds,
isNegative: event.duration.isNegative,
},
organizer: event.organizer,
attendees: event.attendees.map((a) => a.getValues()),
recurrenceId: event.recurrenceId,
timezone: calendarTimezone,
};
}
})
.filter((e) => e != null);
return events;
} catch (reason) {
console.error(reason);
throw reason;
}
}
private async getAccount() {
const account = await createAccount({
account: {
serverUrl: this.url,
accountType: "caldav",
credentials: this.credentials,
},
headers: this.headers,
});
return account;
}
}

View file

@ -0,0 +1,50 @@
import React from "react";
type Props = {
onSubmit: () => void;
};
export const ADD_APPLE_INTEGRATION_FORM_TITLE = "addAppleIntegration";
const AddAppleIntegration = React.forwardRef<HTMLFormElement, Props>((props, ref) => {
const onSubmit = (event) => {
event.preventDefault();
event.stopPropagation();
props.onSubmit();
};
return (
<form id={ADD_APPLE_INTEGRATION_FORM_TITLE} ref={ref} onSubmit={onSubmit}>
<div className="mb-2">
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username
</label>
<input
required
type="text"
name="username"
id="username"
placeholder="email@icloud.com"
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
/>
</div>
<div className="mb-2">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
required
type="password"
name="password"
id="password"
placeholder="•••••••••••••"
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
/>
</div>
</form>
);
});
AddAppleIntegration.displayName = "AddAppleIntegrationForm";
export default AddAppleIntegration;

View file

@ -0,0 +1,52 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/client";
import prisma from "../../../../lib/prisma";
import { symmetricEncrypt } from "@lib/crypto";
import logger from "@lib/logger";
import { AppleCalendar } from "@lib/integrations/Apple/AppleCalendarAdapter";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
// Check that user is authenticated
const session = await getSession({ req: req });
if (!session) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const { username, password } = req.body;
// Get user
await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
},
});
try {
const dav = new AppleCalendar({
id: 0,
type: "apple_calendar",
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY),
userId: session.user.id,
});
await dav.listCalendars();
await prisma.credential.create({
data: {
type: "apple_calendar",
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY),
userId: session.user.id,
},
});
} catch (reason) {
logger.error("Could not add this caldav account", reason);
return res.status(500).json({ message: "Could not add this caldav account" });
}
return res.status(200).json({});
}
}

View file

@ -12,6 +12,9 @@ import AddCalDavIntegration, {
ADD_CALDAV_INTEGRATION_FORM_TITLE,
} from "@lib/integrations/CalDav/components/AddCalDavIntegration";
import { getSession } from "@lib/auth";
import AddAppleIntegration, {
ADD_APPLE_INTEGRATION_FORM_TITLE,
} from "@lib/integrations/Apple/components/AddAppleIntegration";
export type Integration = {
installed: boolean;
@ -34,6 +37,10 @@ export default function Home({ integrations }: Props) {
const [isAddCalDavIntegrationDialogOpen, setIsAddCalDavIntegrationDialogOpen] = useState(false);
const [addCalDavError, setAddCalDavError] = useState<{ message: string } | null>(null);
const addAppleIntegrationRef = useRef<HTMLFormElement>(null);
const [isAddAppleIntegrationDialogOpen, setIsAddAppleIntegrationDialogOpen] = useState(false);
const [addAppleError, setAddAppleError] = useState<{ message: string } | null>(null);
useEffect(loadCalendars, [integrations]);
function loadCalendars() {
@ -51,6 +58,12 @@ export default function Home({ integrations }: Props) {
return;
}
if (type === "apple_calendar") {
setAddAppleError(null);
setIsAddAppleIntegrationDialogOpen(true);
return;
}
fetch("/api/integrations/" + type.replace("_", "") + "/add")
.then((response) => response.json())
.then((data) => (window.location.href = data.url));
@ -72,6 +85,21 @@ export default function Home({ integrations }: Props) {
});
};
const handleAddAppleIntegration = async ({ username, password }) => {
const requestBody = JSON.stringify({
username,
password,
});
return await fetch("/api/integrations/apple/add", {
method: "POST",
body: requestBody,
headers: {
"Content-Type": "application/json",
},
});
};
function calendarSelectionHandler(calendar) {
return (selected) => {
const i = selectableCalendars.findIndex((c) => c.externalId === calendar.externalId);
@ -104,6 +132,8 @@ export default function Home({ integrations }: Props) {
return "integrations/outlook.svg";
case "caldav_calendar":
return "integrations/caldav.svg";
case "apple_calendar":
return "integrations/apple-calendar.svg";
default:
return "";
}
@ -221,6 +251,25 @@ export default function Home({ integrations }: Props) {
}
};
const handleAddAppleIntegrationSaveButtonPress = async () => {
const form = addAppleIntegrationRef.current.elements;
const password = form.password.value;
const username = form.username.value;
try {
setAddAppleError(null);
const addAppleIntegrationResponse = await handleAddAppleIntegration({ username, password });
if (addAppleIntegrationResponse.ok) {
setIsAddAppleIntegrationDialogOpen(false);
} else {
const j = await addAppleIntegrationResponse.json();
setAddAppleError({ message: j.message });
}
} catch (reason) {
console.error(reason);
}
};
const ConnectCalDavServerDialog = useCallback(() => {
return (
<Dialog
@ -264,6 +313,49 @@ export default function Home({ integrations }: Props) {
);
}, [isAddCalDavIntegrationDialogOpen, addCalDavError]);
const ConnectAppleServerDialog = useCallback(() => {
return (
<Dialog
open={isAddAppleIntegrationDialogOpen}
onOpenChange={(isOpen) => setIsAddAppleIntegrationDialogOpen(isOpen)}>
<DialogContent>
<DialogHeader
title="Connect to Apple Server"
subtitle="Your credentials will be stored and encrypted. Generate an app specific password."
/>
<div className="my-4">
{addAppleError && (
<p className="text-red-700 text-sm">
<span className="font-bold">Error: </span>
{addAppleError.message}
</p>
)}
<AddAppleIntegration
ref={addAppleIntegrationRef}
onSubmit={handleAddAppleIntegrationSaveButtonPress}
/>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="submit"
form={ADD_APPLE_INTEGRATION_FORM_TITLE}
className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
Save
</button>
<DialogClose
onClick={() => {
setIsAddAppleIntegrationDialogOpen(false);
}}
as="button"
className="btn btn-white mx-2">
Cancel
</DialogClose>
</div>
</DialogContent>
</Dialog>
);
}, [isAddAppleIntegrationDialogOpen, addAppleError]);
if (loading) {
return <Loader />;
}
@ -366,6 +458,7 @@ export default function Home({ integrations }: Props) {
</div>
</div>
<ConnectCalDavServerDialog />
<ConnectAppleServerDialog />
</Shell>
</div>
);
@ -441,6 +534,14 @@ export async function getServerSideProps(context) {
imageSrc: "integrations/caldav.svg",
description: "For personal and business calendars",
},
{
installed: true,
type: "apple_calendar",
credential: credentials.find((integration) => integration.type === "apple_calendar") || null,
title: "Apple Calendar",
imageSrc: "integrations/apple-calendar.svg",
description: "For personal and business calendars",
},
];
return {

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
]>
<svg version="1.1" id="Livello_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2160 2160"
style="enable-background:new 0 0 2160 2160;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;enable-background:new ;}
.st1{fill:#FF0000;}
</style>
<metadata>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds bottomLeftOrigin="true" height="2160" width="2160" x="840" y="0"></sliceSourceBounds>
</sfw>
</metadata>
<g>
<path class="st0" d="M2160,675.7c0-25.8,0-51.6-0.1-77.4c-0.1-21.7-0.4-43.5-1-65.2c-1.3-47.3-4.1-95.1-12.5-141.9
c-8.5-47.5-22.5-91.7-44.5-134.9c-21.6-42.4-49.8-81.2-83.4-114.9c-33.7-33.6-72.4-61.8-114.9-83.4c-43.1-22-87.3-35.9-134.8-44.4
c-46.8-8.4-94.6-11.2-141.9-12.5c-21.7-0.6-43.5-0.8-65.2-1C1535.9,0,1510.1,0,1484.3,0H675.7c-25.8,0-51.6,0-77.4,0.1
c-21.7,0.1-43.5,0.4-65.2,1C485.8,2.4,438,5.2,391.2,13.6c-47.5,8.5-91.7,22.5-134.8,44.4c-42.4,21.6-81.3,49.8-114.9,83.4
c-33.7,33.7-61.8,72.4-83.4,114.9c-22,43.2-35.9,87.4-44.5,134.9C5.2,438,2.4,485.8,1.2,533.1c-0.6,21.7-0.9,43.5-1,65.2
C0,624.1,0,649.9,0,675.7v808.6c0,25.8,0,51.6,0.2,77.4c0.1,21.7,0.4,43.5,1,65.2c1.3,47.3,4.1,95.1,12.5,141.9
c8.5,47.5,22.5,91.8,44.5,134.9c21.6,42.4,49.8,81.2,83.4,114.8c33.6,33.7,72.4,61.9,114.9,83.5c43.1,22,87.3,35.9,134.8,44.4
c46.8,8.4,94.6,11.2,141.9,12.5c21.7,0.6,43.5,0.8,65.2,1c25.8,0.1,51.6,0.1,77.4,0.1h808.6c25.8,0,51.6,0,77.4-0.1
c21.7-0.1,43.5-0.4,65.2-1c47.3-1.3,95.1-4.1,141.9-12.5c47.5-8.5,91.7-22.5,134.8-44.4c42.4-21.6,81.2-49.8,114.9-83.5
c33.7-33.6,61.8-72.4,83.4-114.8c22-43.2,35.9-87.4,44.5-134.9c8.4-46.8,11.2-94.5,12.5-141.9c0.6-21.7,0.8-43.5,1-65.2
c0.1-25.8,0.1-51.6,0.1-77.4V675.7z"/>
<g>
<path d="M806.2,1767.1V762.2H764L508,931.7v42.9l253.1-165.7h3v958.3H806.2L806.2,1767.1z"/>
<path d="M1056.3,762.2v38.5h552v3l-443.3,963.5h47.4L1652,802.9v-40.7L1056.3,762.2L1056.3,762.2z"/>
</g>
<path class="st1" d="M392.5,565.1V347.8h0.8l85.9,195.1l28.5,0l85.6-195.1h1v217.3h28.5V277.3h-30.2L494,507.9h-0.8l-98.9-230.5
h-30.2v287.7H392.5L392.5,565.1z"/>
<path class="st1" d="M777.7,340.6c-60.1,0-93.5,43.7-93.5,102.9v21.7c0,59.5,33.1,103.4,93.5,103.4c60.3,0,93.2-43.9,93.2-103.4
v-21.7C871,384.3,837.6,340.6,777.7,340.6L777.7,340.6z M777.7,367.8c39.2,0,62.2,29.3,62.2,77.2l0,18.6c0,48.1-23,77.4-62.2,77.4
c-39.5,0-62.4-29.5-62.4-77.4V445C715.3,397.2,738.3,367.8,777.7,367.8L777.7,367.8z"/>
<path class="st1" d="M929,565.1h31V429.8c0-31.6,17.3-60.8,57.8-60.8c34.6,0,56.8,20.9,56.8,58.6v137.3h31v-142
c0-53.6-34-82.5-79.5-82.5c-36.3,0-57,19-65.2,33.8H960v-30.2h-31L929,565.1L929,565.1z"/>
<path class="st1" d="M1248.3,340.4c-54.9,0-88.4,42.8-88.4,102.7v22.6c0,61,31,102.7,88.4,102.7c31.7,0,54.4-16,66.2-37.8h0.8
l0,34.4h29.3V261.9h-31v113.9h-0.8C1302.1,356.7,1279.3,340.4,1248.3,340.4L1248.3,340.4z M1251.1,368.1
c39.2,0,63.3,30.8,63.3,76.4v20.5c0,47.5-23.4,75.9-62.9,75.9c-35,0-60.5-25.5-60.5-76.1v-20
C1190.9,393.2,1216.9,368.1,1251.1,368.1L1251.1,368.1z"/>
<path class="st1" d="M1536.5,536.4h0.8v28.7h30.2V411.5c0-45.4-33.3-70.9-79.3-70.9c-51.2,0-77.6,26.8-80.2,65.2h29.3
c2.5-23.4,19.2-38.4,49.6-38.4c31.4,0,49.6,16.7,49.6,47.7v23.8h-60.1c-50.4,0.2-77,24.7-77,62.9c0,40.5,29.3,66.9,72.2,66.9
C1505.3,568.6,1525.9,554.1,1536.5,536.4L1536.5,536.4z M1478.9,541.9c-24.9,0-47.5-13.3-47.5-40.7c0-21.5,13.7-36.9,46-36.9h59.1
v27.2C1536.5,521.2,1511.8,541.9,1478.9,541.9z"/>
<path class="st1" d="M1795.9,344.2h-33.1L1699.7,529h-1.1l-63.1-184.8H1601l82.5,224.4l-4.2,13.5c-6.5,22.8-17.7,36.1-43.7,36.1
c-4.8,0-12.7-0.6-16.2-1.3v26.4c5.5,0.8,13.7,1.7,21.1,1.7c43,0,57.8-29.5,69.4-61.4l6.3-16.5L1795.9,344.2L1795.9,344.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB