-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathicsgen.py
executable file
·232 lines (194 loc) · 7.66 KB
/
icsgen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/python3
# ICS Generator
# Generates ICS files for a list of events according to an index
# and adds their respective URL to the index.
__author__ = 'Daylam Tayari'
__version__ = '1.0'
__copyright__ = 'Copyright (c) 2022 Daylam Tayari'
__license__ = 'GNU GPL v3'
# Imports
import re
import json
from collections import Counter
from typing import Type
from ics import Calendar, Event
from calendar import month_abbr
from datetime import datetime
ics_path = "ics/"
# Retrieve Indexes and Validate and Parse JSON:
def retrieve_validate(jfile):
with open(jfile, 'r') as file:
try:
return json.load(file)
except json.decoder.JSONDecodeError:
raise TypeError(f"Invalid JSON in file {file}")
events = retrieve_validate('events.json').get('events')
speakers = retrieve_validate('speakers.json').get('speakers')
categories = retrieve_validate('categories.json').get('categories')
# Validate Index Data Syntax:
speaker_keys = ['id', 'name', 'description']
category_keys = ['id', 'name']
event_keys = ['name', 'date', 'duration', 'location', 'locationURL',
'speakers', 'category', 'description', 'icsURL']
def validate_speaker(speaker):
if Counter(speaker.keys()) != Counter(speaker_keys):
raise KeyError('The keys for at least one speaker are not valid.')
id = speaker.get('id')
name = speaker.get('name')
description = speaker.get('description')
if not isinstance(id, int):
raise TypeError(
f"The ID of the speaker with ID '{id}' is not an integer.")
if not isinstance(name, str):
raise TypeError(
f"The name of the speaker with ID '{id}' is not a string.")
if not isinstance(description, str):
raise TypeError(
f"The description of the speaker with ID '{id}' is not a string.")
# Return True if the speaker is valid.
return True
def validate_category(category):
if Counter(category.keys()) != Counter(category_keys):
raise KeyError('The keys for at least one category are not valid.')
id = category.get('id')
name = category.get('name')
if not isinstance(id, int):
raise TypeError(
f"The ID of the speaker with ID '{id}' is not an integer.")
if not isinstance(name, str):
raise TypeError(
f"The name of the speaker with ID '{id}' is not a string.")
# Return True if the category is valid.
return True
speaker_ind = []
for s in speakers:
validate_speaker(s)
speaker_ind.append(s.get('id'))
category_ind = []
for c in categories:
validate_category(c)
category_ind.append(c.get('id'))
def validate_event(event):
# Check if the keys are the same:
if Counter(event.keys()) != Counter(event_keys):
raise KeyError('The keys for at least one event are not valid.')
# Retrieve all values for reuse:
name = event.get('name')
date = event.get('date')
duration = event.get('duration')
location = event.get('location')
location_url = event.get('locationURL')
speakers = event.get('speakers')
category = event.get('category')
description = event.get('description')
date_reg = re.compile(
r"^(\-\-|[0-9]{4}[\-]?)([0-1][0-9])[\-]?[0-3][0-9]T[0-2][0-9](:[0-6][0-9])?(:[0-6][0-9])?(:[0-9]{2})?([+-][0-2][0-9](:[0-6][0-9])?)?$")
# Validate the values:
if not isinstance(name, str):
print(type(name))
raise TypeError(f"Event name '{name}' is not a string.")
if not isinstance(date, str):
raise TypeError(f"Date value '{date}' is not a string.")
elif date_reg.fullmatch(date) is None:
raise ValueError(
f"Date value '{date}' is not a valid ISO 8601 2019 format.'")
if not isinstance(duration, str):
raise TypeError(f"Duration value '{duration} is not a string.'")
elif re.fullmatch(r"^([0-9]*[dhms])([0-9]+h)?([0-9]+m)?([0-9]+s)?$", duration) is None:
raise ValueError(
f"Duration value '{duration}' is not in a valid format.")
if not isinstance(location, str):
raise TypeError(f"Location name '{location}' is not a string.")
if not isinstance(location_url, str) or re.fullmatch(r"http[s]?://(www\.)?.*", location_url) is None:
if not isinstance(location_url, str):
raise TypeError(f"Location URL {location_url} is not a string.")
raise ValueError(f"Location URL '{location_url}' is not a valid URL.")
if not isinstance(speakers, list):
raise TypeError(f"Speakers for event with name {name} is not a list.")
else:
for s in speakers:
if isinstance(s, int):
if s not in speaker_ind:
raise IndexError(
f"Speaker index {s} for event with name {name} is not a valid speaker index.")
else:
raise TypeError(f"Speaker index '{s}' is not an integer.")
if not isinstance(category, int):
raise TypeError(
f"Category '{category}' for event with name {name} is not an integer.")
else:
if category not in category_ind:
raise IndexError(
f"Category {category} is not a valid category index.")
if not isinstance(description, str):
raise TypeError(
f"Description for event with name {name} is not a string.")
# Return True if the syntax of the event name is valid:
return True
def ics_duration(event):
duration_groups = re.search(
"^([0-9]*[dhms])([0-9]+h)?([0-9]+m)?([0-9]+s)?$", event.get('duration'))
days = None
hours = None
minutes = None
seconds = None
duration_dict = {}
if duration_groups is None:
raise ValueError(
f"Invalid duration for event with ID {event.get('id')}.")
for g in duration_groups.groups():
if g is not None:
spec = g[-1].lower()
if spec == 'd':
days = g
elif spec == 'h':
hours = g
elif spec == 'm':
minutes = g
else:
seconds = g
if days is not None:
duration_dict['days'] = int(days[:-1])
if hours is not None:
duration_dict['hours'] = int(hours[:-1])
if minutes is not None:
duration_dict['minutes'] = int(minutes[:-1])
if seconds is not None:
duration_dict['seconds'] = int(seconds[:-1])
return duration_dict
def valid_path_name(name):
name.replace(' ', '_') # Not invalid, stylistic inclusion.
name.replace('\"', '-')
name.replace('\\', '-')
name.replace('/', '-') # Not invalid, stylistic inclusion.
name.replace('\'', '-') # Not invalid, stylistic inclusion.
name.replace('>', '-')
name.replace('<', '-')
name.replace('|', '-')
return name
def export_ics(cal, event_dict):
event = list(cal.events)[0]
file_name = f"{ics_path}DevilSec-{valid_path_name(event.name)}-{event.begin.day}-{month_abbr[event.begin.month]}-{event.begin.year}.ics"
with open(file_name, 'w') as f:
f.writelines(cal)
event_dict[
'icsURL'] = f"https://raw.githubusercontent.com/devilsec/events-index/master/{file_name}"
def gen_ics(event):
cal = Calendar()
ev = Event()
ev.name = event.get('name')
ev.begin = event.get('date')
ev.duration = ics_duration(event)
ev.location = event.get('location')
ev.url = 'https://devilsec.org'
ev.description = f"Location: {ev.location} - {event.get('locationURL')}\n{event.get('description')}"
ev.status = "CONFIRMED"
ev.created = datetime.utcnow()
cal.events.add(ev)
export_ics(cal, event)
for event in events:
validate_event(event)
gen_ics(event)
events = {"events": events}
with open('events.json', 'w') as f:
json.dump(events, f, indent=4)