Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / helper.ts
blob8381b4a4263d084184aecb1f13f4372f944c2ad4
1 import { c } from 'ttag';
3 import { CryptoProxy } from '@proton/crypto';
4 import { arrayToHexString, binaryStringToArray } from '@proton/crypto/lib/utils';
6 import { API_CODES } from '../constants';
7 import { encodeBase64URL, uint8ArrayToString } from '../helpers/encoding';
8 import type {
9     SyncMultipleApiResponses,
10     SyncMultipleApiSuccessResponses,
11     VcalDateOrDateTimeProperty,
12     VcalDateTimeProperty,
13 } from '../interfaces/calendar';
14 import { ACTION_VIEWS, MAXIMUM_DATE_UTC, MAX_CHARS_API, MINIMUM_DATE_UTC } from './constants';
15 import { propertyToUTCDate } from './vcalConverter';
16 import { getIsPropertyAllDay } from './vcalHelper';
18 export const HASH_UID_PREFIX = 'sha1-uid-';
19 export const ORIGINAL_UID_PREFIX = 'original-uid-';
21 export const getIsSuccessSyncApiResponse = (
22     response: SyncMultipleApiResponses
23 ): response is SyncMultipleApiSuccessResponses => {
24     const {
25         Response: { Code, Event },
26     } = response;
27     return Code === API_CODES.SINGLE_SUCCESS && !!Event;
30 /**
31  * Generates a calendar UID of the form 'RandomBase64String@proton.me'
32  * RandomBase64String has a length of 28 characters
33  */
34 export const generateProtonCalendarUID = () => {
35     // by convention we generate 21 bytes of random data
36     const randomBytes = crypto.getRandomValues(new Uint8Array(21));
37     const base64String = encodeBase64URL(uint8ArrayToString(randomBytes));
38     // and we encode them in base 64
39     return `${base64String}@proton.me`;
42 export const generateVeventHashUID = async (binaryString: string, uid = '', legacyFormat = false) => {
43     const hash = arrayToHexString(
44         await CryptoProxy.computeHash({ algorithm: 'unsafeSHA1', data: binaryStringToArray(binaryString) })
45     );
46     const hashUid = `${HASH_UID_PREFIX}${hash}`;
47     if (!uid) {
48         return hashUid;
49     }
50     const join = '-';
51     const uidLength = uid.length;
52     const availableLength = MAX_CHARS_API.UID - ORIGINAL_UID_PREFIX.length - hashUid.length - join.length;
53     const croppedUID = uid.substring(uidLength - availableLength, uidLength);
54     return legacyFormat
55         ? `${hashUid}${join}${ORIGINAL_UID_PREFIX}${croppedUID}`
56         : `${ORIGINAL_UID_PREFIX}${croppedUID}${join}${hashUid}`;
59 export const getOriginalUID = (uid = '') => {
60     if (!uid) {
61         return '';
62     }
63     const regexWithOriginalUid = new RegExp(`^${ORIGINAL_UID_PREFIX}(.+)-${HASH_UID_PREFIX}[abcdef\\d]{40}`);
64     const regexWithOriginalUidLegacyFormat = new RegExp(
65         `^${HASH_UID_PREFIX}[abcdef\\d]{40}-${ORIGINAL_UID_PREFIX}(.+)`
66     );
67     const [, match] = uid.match(regexWithOriginalUid) || uid.match(regexWithOriginalUidLegacyFormat) || [];
68     if (match) {
69         return match;
70     }
71     const regexWithoutOriginalUid = new RegExp(`^${HASH_UID_PREFIX}[abcdef\\d]{40}$`);
72     if (regexWithoutOriginalUid.test(uid)) {
73         return '';
74     }
75     return uid;
78 export const getHasLegacyHashUID = (uid = '') => {
79     if (!uid) {
80         return false;
81     }
82     return new RegExp(`^${HASH_UID_PREFIX}[abcdef\\d]{40}-${ORIGINAL_UID_PREFIX}`).test(uid);
85 export const getSupportedUID = (uid: string) => {
86     const uidLength = uid.length;
87     return uid.substring(uidLength - MAX_CHARS_API.UID, uidLength);
90 const getIsWellFormedDateTime = (property: VcalDateTimeProperty) => {
91     return property.value.isUTC || !!property.parameters!.tzid;
94 export const getIsWellFormedDateOrDateTime = (property: VcalDateOrDateTimeProperty) => {
95     return getIsPropertyAllDay(property) || getIsWellFormedDateTime(property);
98 export const getIsDateOutOfBounds = (property: VcalDateOrDateTimeProperty) => {
99     const dateUTC: Date = propertyToUTCDate(property);
100     return +dateUTC < +MINIMUM_DATE_UTC || +dateUTC > +MAXIMUM_DATE_UTC;
104  * Try to guess from the event uid if an event was generated by Proton. In pple there are two possibilities
105  * * Old uids of the form 'proton-calendar-350095ea-4368-26f0-4fc9-60a56015b02e' and derived ones from "this and future" editions
106  * * New uids of the form 'RandomBase64String@proton.me' and derived ones from "this and future" editions
107  */
108 export const getIsProtonUID = (uid = '') => {
109     return uid.endsWith('@proton.me') || uid.startsWith('proton-calendar-');
113  * Try to naively guess the domain of a provider from the uid.
114  * This helper only works when the uid is of the form `${randomString}@${domain}`
115  */
116 export const getNaiveDomainFromUID = (uid = '') => {
117     const parts = uid.split('@');
118     const numberOfParts = parts.length;
120     if (numberOfParts <= 1) {
121         return '';
122     }
124     return parts[numberOfParts - 1];
127 export const getDisplayTitle = (title = '') => {
128     return title.trim() || c('Event title').t`(no title)`;
132  * Check whether an object has more keys than a set of keys.
133  */
134 export const hasMoreThan = (set: Set<string>, properties: { [key: string]: any } = {}) => {
135     return Object.keys(properties).some((key) => !set.has(key));
138 export const wrap = (res: string, prodId?: string) => {
139     // Wrap in CRLF according to the rfc
140     return prodId
141         ? `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:${prodId}\r\n${res}\r\nEND:VCALENDAR`
142         : `BEGIN:VCALENDAR\r\nVERSION:2.0\r\n${res}\r\nEND:VCALENDAR`;
145 export const unwrap = (res: string) => {
146     if (res.slice(0, 15) !== 'BEGIN:VCALENDAR') {
147         return res;
148     }
149     const startIdx = res.indexOf('BEGIN:', 1);
150     if (startIdx === -1 || startIdx === 0) {
151         return '';
152     }
153     const endIdx = res.lastIndexOf('END:VCALENDAR');
154     return res.slice(startIdx, endIdx).trim();
157 export const getLinkToCalendarEvent = ({
158     calendarID,
159     eventID,
160     recurrenceID,
161 }: {
162     calendarID: string;
163     eventID: string;
164     recurrenceID?: number;
165 }) => {
166     const params = new URLSearchParams();
167     params.set('Action', ACTION_VIEWS.VIEW);
168     params.set('EventID', eventID);
169     params.set('CalendarID', calendarID);
170     if (recurrenceID) {
171         params.set('RecurrenceID', `${recurrenceID}`);
172     }
174     return `/event?${params.toString()}`;
177 export const naiveGetIsDecryptionError = (error: any) => {
178     // We sometimes need to detect if an error produced while reading an event is due to a failed decryption.
179     // We don't have a great way of doing this as the error comes from openpgp
180     const errorMessage = error?.message || '';
182     return errorMessage.toLowerCase().includes('decrypt');