epicyon/happening.py

530 lines
19 KiB
Python
Raw Normal View History

2020-04-03 11:53:31 +00:00
__filename__ = "happening.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-04-03 11:53:31 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
2020-08-13 13:40:38 +00:00
from uuid import UUID
from datetime import datetime
2020-02-22 19:13:55 +00:00
from utils import loadJson
2020-08-13 13:40:38 +00:00
from utils import saveJson
2020-02-22 19:13:55 +00:00
from utils import locatePost
from utils import daysInMonth
from utils import mergeDicts
2020-04-03 11:53:31 +00:00
def _validUuid(testUuid: str, version=4):
2020-08-13 13:40:38 +00:00
"""Check if uuid_to_test is a valid UUID
"""
try:
uuid_obj = UUID(testUuid, version=version)
except ValueError:
return False
return str(uuid_obj) == testUuid
def _removeEventFromTimeline(eventId: str, tlEventsFilename: str) -> None:
2020-08-13 13:40:38 +00:00
"""Removes the given event Id from the timeline
"""
if eventId + '\n' not in open(tlEventsFilename).read():
return
with open(tlEventsFilename, 'r') as fp:
eventsTimeline = fp.read().replace(eventId + '\n', '')
try:
with open(tlEventsFilename, 'w+') as fp2:
fp2.write(eventsTimeline)
except BaseException:
print('ERROR: unable to save events timeline')
pass
2020-08-20 16:51:48 +00:00
def saveEventPost(baseDir: str, handle: str, postId: str,
eventJson: {}) -> bool:
2020-08-13 13:40:38 +00:00
"""Saves an event to the calendar and/or the events timeline
If an event has extra fields, as per Mobilizon,
Then it is saved as a separate entity and added to the
events timeline
2020-08-20 17:08:25 +00:00
See https://framagit.org/framasoft/mobilizon/-/blob/
master/lib/federation/activity_stream/converter/event.ex
2020-08-13 11:58:05 +00:00
"""
calendarPath = baseDir + '/accounts/' + handle + '/calendar'
if not os.path.isdir(calendarPath):
os.mkdir(calendarPath)
# get the year, month and day from the event
eventTime = datetime.strptime(eventJson['startTime'],
"%Y-%m-%dT%H:%M:%S%z")
eventYear = int(eventTime.strftime("%Y"))
2020-08-13 13:40:38 +00:00
if eventYear < 2020 or eventYear >= 2100:
return False
2020-08-13 11:58:05 +00:00
eventMonthNumber = int(eventTime.strftime("%m"))
2020-08-13 13:40:38 +00:00
if eventMonthNumber < 1 or eventMonthNumber > 12:
return False
2020-08-13 11:58:05 +00:00
eventDayOfMonth = int(eventTime.strftime("%d"))
2020-08-13 16:19:35 +00:00
if eventDayOfMonth < 1 or eventDayOfMonth > 31:
2020-08-13 13:40:38 +00:00
return False
if eventJson.get('name') and eventJson.get('actor') and \
eventJson.get('uuid') and eventJson.get('content'):
if not _validUuid(eventJson['uuid']):
2020-08-13 13:40:38 +00:00
return False
2020-08-20 16:51:48 +00:00
print('Mobilizon type event')
2020-08-13 13:40:38 +00:00
# if this is a full description of an event then save it
# as a separate json file
eventsPath = baseDir + '/accounts/' + handle + '/events'
if not os.path.isdir(eventsPath):
os.mkdir(eventsPath)
eventsYearPath = \
baseDir + '/accounts/' + handle + '/events/' + str(eventYear)
if not os.path.isdir(eventsYearPath):
os.mkdir(eventsYearPath)
eventId = str(eventYear) + '-' + eventTime.strftime("%m") + '-' + \
eventTime.strftime("%d") + '_' + eventJson['uuid']
eventFilename = eventsYearPath + '/' + eventId + '.json'
saveJson(eventJson, eventFilename)
# save to the events timeline
tlEventsFilename = baseDir + '/accounts/' + handle + '/events.txt'
if os.path.isfile(tlEventsFilename):
_removeEventFromTimeline(eventId, tlEventsFilename)
2020-08-13 13:40:38 +00:00
try:
with open(tlEventsFilename, 'r+') as tlEventsFile:
content = tlEventsFile.read()
if eventId + '\n' not in content:
tlEventsFile.seek(0, 0)
tlEventsFile.write(eventId + '\n' + content)
2020-08-13 13:40:38 +00:00
except Exception as e:
print('WARN: Failed to write entry to events file ' +
tlEventsFilename + ' ' + str(e))
return False
else:
tlEventsFile = open(tlEventsFilename, 'w+')
tlEventsFile.write(eventId + '\n')
tlEventsFile.close()
2020-08-13 11:58:05 +00:00
# create a directory for the calendar year
if not os.path.isdir(calendarPath + '/' + str(eventYear)):
os.mkdir(calendarPath + '/' + str(eventYear))
# calendar month file containing event post Ids
calendarFilename = calendarPath + '/' + str(eventYear) + \
'/' + str(eventMonthNumber) + '.txt'
# Does this event post already exist within the calendar month?
if os.path.isfile(calendarFilename):
if postId in open(calendarFilename).read():
# Event post already exists
return False
# append the post Id to the file for the calendar month
calendarFile = open(calendarFilename, 'a+')
if not calendarFile:
return False
calendarFile.write(postId + '\n')
calendarFile.close()
# create a file which will trigger a notification that
# a new event has been added
calendarNotificationFilename = \
baseDir + '/accounts/' + handle + '/.newCalendar'
calendarNotificationFile = \
open(calendarNotificationFilename, 'w+')
if not calendarNotificationFile:
return False
calendarNotificationFile.write('/calendar?year=' +
str(eventYear) +
'?month=' +
str(eventMonthNumber) +
'?day=' +
str(eventDayOfMonth))
calendarNotificationFile.close()
return True
def _isHappeningEvent(tag: {}) -> bool:
2020-02-24 11:10:48 +00:00
"""Is this tag an Event or Place ActivityStreams type?
"""
if not tag.get('type'):
return False
2020-04-03 11:53:31 +00:00
if tag['type'] != 'Event' and tag['type'] != 'Place':
2020-02-24 11:10:48 +00:00
return False
return True
2020-04-03 11:53:31 +00:00
def _isHappeningPost(postJsonObject: {}) -> bool:
2020-02-24 11:10:48 +00:00
"""Is this a post with tags?
"""
if not postJsonObject:
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['object'], dict):
return False
if not postJsonObject['object'].get('tag'):
return False
return True
2020-04-03 11:53:31 +00:00
def getTodaysEvents(baseDir: str, nickname: str, domain: str,
currYear=None, currMonthNumber=None,
2020-02-24 10:55:49 +00:00
currDayOfMonth=None) -> {}:
"""Retrieves calendar events for today
Returns a dictionary of lists containing Event and Place activities
"""
2020-04-03 11:53:31 +00:00
now = datetime.now()
if not currYear:
2020-04-03 11:53:31 +00:00
year = now.year
else:
2020-04-03 11:53:31 +00:00
year = currYear
if not currMonthNumber:
2020-04-03 11:53:31 +00:00
monthNumber = now.month
else:
2020-04-03 11:53:31 +00:00
monthNumber = currMonthNumber
if not currDayOfMonth:
2020-04-03 11:53:31 +00:00
dayNumber = now.day
else:
2020-04-03 11:53:31 +00:00
dayNumber = currDayOfMonth
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
events = {}
if not os.path.isfile(calendarFilename):
return events
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
calendarPostIds = []
recreateEventsFile = False
with open(calendarFilename, 'r') as eventsFile:
for postId in eventsFile:
2020-05-22 11:32:38 +00:00
postId = postId.replace('\n', '').replace('\r', '')
2020-04-03 11:53:31 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
2020-04-03 11:53:31 +00:00
recreateEventsFile = True
2020-02-23 09:49:10 +00:00
continue
2020-04-03 11:53:31 +00:00
postJsonObject = loadJson(postFilename)
if not _isHappeningPost(postJsonObject):
2020-02-23 09:49:10 +00:00
continue
2020-04-03 11:53:31 +00:00
postEvent = []
dayOfMonth = None
2020-02-23 09:49:10 +00:00
for tag in postJsonObject['object']['tag']:
if not _isHappeningEvent(tag):
continue
2020-02-24 11:10:48 +00:00
# this tag is an event or a place
2020-04-03 11:53:31 +00:00
if tag['type'] == 'Event':
2020-02-23 09:49:10 +00:00
# tag is an event
if not tag.get('startTime'):
continue
2020-04-03 11:53:31 +00:00
eventTime = \
datetime.strptime(tag['startTime'],
2020-02-23 09:49:10 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-03 11:53:31 +00:00
if int(eventTime.strftime("%Y")) == year and \
int(eventTime.strftime("%m")) == monthNumber and \
int(eventTime.strftime("%d")) == dayNumber:
dayOfMonth = str(int(eventTime.strftime("%d")))
2020-02-23 11:25:16 +00:00
if '#statuses#' in postId:
2020-02-24 10:55:49 +00:00
# link to the id so that the event can be
# easily deleted
2020-04-03 11:53:31 +00:00
tag['postId'] = postId.split('#statuses#')[1]
2021-03-06 18:38:36 +00:00
tag['sender'] = postId.split('#statuses#')[0]
tag['sender'] = tag['sender'].replace('#', '/')
postEvent.append(tag)
2020-02-23 09:49:10 +00:00
else:
# tag is a place
postEvent.append(tag)
if postEvent and dayOfMonth:
calendarPostIds.append(postId)
if not events.get(dayOfMonth):
2020-04-03 11:53:31 +00:00
events[dayOfMonth] = []
2020-02-23 09:49:10 +00:00
events[dayOfMonth].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
2020-08-29 11:14:19 +00:00
calendarFile = open(calendarFilename, 'w+')
for postId in calendarPostIds:
2020-05-22 11:32:38 +00:00
calendarFile.write(postId + '\n')
calendarFile.close()
return events
2020-04-03 11:53:31 +00:00
def todaysEventsCheck(baseDir: str, nickname: str, domain: str) -> bool:
"""Are there calendar events today?
"""
2020-04-03 11:53:31 +00:00
now = datetime.now()
year = now.year
monthNumber = now.month
dayNumber = now.day
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
if not os.path.isfile(calendarFilename):
return False
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
eventsExist = False
with open(calendarFilename, 'r') as eventsFile:
for postId in eventsFile:
2020-05-22 11:32:38 +00:00
postId = postId.replace('\n', '').replace('\r', '')
2020-04-03 11:53:31 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
2020-02-23 09:49:10 +00:00
if not postFilename:
continue
2020-04-03 11:53:31 +00:00
postJsonObject = loadJson(postFilename)
if not _isHappeningPost(postJsonObject):
2020-02-23 09:49:10 +00:00
continue
for tag in postJsonObject['object']['tag']:
if not _isHappeningEvent(tag):
continue
2020-02-24 11:10:48 +00:00
# this tag is an event or a place
2020-04-03 11:53:31 +00:00
if tag['type'] != 'Event':
continue
2020-02-24 11:10:48 +00:00
# tag is an event
if not tag.get('startTime'):
continue
2020-04-03 11:53:31 +00:00
eventTime = \
datetime.strptime(tag['startTime'],
2020-02-24 11:10:48 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-03 11:53:31 +00:00
if int(eventTime.strftime("%Y")) == year and \
int(eventTime.strftime("%m")) == monthNumber and \
int(eventTime.strftime("%d")) == dayNumber:
eventsExist = True
2020-02-24 11:10:48 +00:00
break
return eventsExist
2020-04-03 11:53:31 +00:00
def thisWeeksEventsCheck(baseDir: str, nickname: str, domain: str) -> bool:
"""Are there calendar events this week?
"""
2020-04-03 11:53:31 +00:00
now = datetime.now()
year = now.year
monthNumber = now.month
dayNumber = now.day
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
if not os.path.isfile(calendarFilename):
return False
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
eventsExist = False
with open(calendarFilename, 'r') as eventsFile:
for postId in eventsFile:
2020-05-22 11:32:38 +00:00
postId = postId.replace('\n', '').replace('\r', '')
2020-04-03 11:53:31 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
2020-02-23 09:49:10 +00:00
if not postFilename:
continue
2020-04-03 11:53:31 +00:00
postJsonObject = loadJson(postFilename)
if not _isHappeningPost(postJsonObject):
2020-02-23 09:49:10 +00:00
continue
for tag in postJsonObject['object']['tag']:
if not _isHappeningEvent(tag):
continue
2020-02-24 11:10:48 +00:00
# this tag is an event or a place
2020-04-03 11:53:31 +00:00
if tag['type'] != 'Event':
continue
2020-02-24 11:10:48 +00:00
# tag is an event
if not tag.get('startTime'):
continue
2020-04-03 11:53:31 +00:00
eventTime = \
datetime.strptime(tag['startTime'],
2020-02-24 11:10:48 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-03 11:53:31 +00:00
if (int(eventTime.strftime("%Y")) == year and
int(eventTime.strftime("%m")) == monthNumber and
(int(eventTime.strftime("%d")) > dayNumber and
int(eventTime.strftime("%d")) <= dayNumber + 6)):
eventsExist = True
2020-02-24 11:10:48 +00:00
break
return eventsExist
2020-04-03 11:53:31 +00:00
def getThisWeeksEvents(baseDir: str, nickname: str, domain: str) -> {}:
"""Retrieves calendar events for this week
2020-02-24 10:55:49 +00:00
Returns a dictionary indexed by day number of lists containing
Event and Place activities
2020-02-23 09:42:09 +00:00
Note: currently not used but could be with a weekly calendar screen
"""
2020-04-03 11:53:31 +00:00
now = datetime.now()
year = now.year
monthNumber = now.month
dayNumber = now.day
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
events = {}
if not os.path.isfile(calendarFilename):
return events
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
calendarPostIds = []
recreateEventsFile = False
with open(calendarFilename, 'r') as eventsFile:
for postId in eventsFile:
2020-05-22 11:32:38 +00:00
postId = postId.replace('\n', '').replace('\r', '')
2020-04-03 11:53:31 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
2020-04-03 11:53:31 +00:00
recreateEventsFile = True
2020-02-23 09:49:10 +00:00
continue
2020-04-03 11:53:31 +00:00
postJsonObject = loadJson(postFilename)
if not _isHappeningPost(postJsonObject):
2020-02-23 09:49:10 +00:00
continue
2020-04-03 11:53:31 +00:00
postEvent = []
dayOfMonth = None
weekDayIndex = None
2020-02-23 09:49:10 +00:00
for tag in postJsonObject['object']['tag']:
if not _isHappeningEvent(tag):
continue
2020-02-24 11:10:48 +00:00
# this tag is an event or a place
2020-04-03 11:53:31 +00:00
if tag['type'] == 'Event':
2020-02-23 09:49:10 +00:00
# tag is an event
if not tag.get('startTime'):
continue
2020-04-03 11:53:31 +00:00
eventTime = \
datetime.strptime(tag['startTime'],
2020-02-23 09:49:10 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-03 11:53:31 +00:00
if (int(eventTime.strftime("%Y")) == year and
int(eventTime.strftime("%m")) == monthNumber and
(int(eventTime.strftime("%d")) >= dayNumber and
int(eventTime.strftime("%d")) <= dayNumber + 6)):
dayOfMonth = str(int(eventTime.strftime("%d")))
weekDayIndex = dayOfMonth - dayNumber
postEvent.append(tag)
2020-02-23 09:49:10 +00:00
else:
# tag is a place
postEvent.append(tag)
if postEvent and weekDayIndex:
calendarPostIds.append(postId)
if not events.get(dayOfMonth):
2020-04-03 11:53:31 +00:00
events[weekDayIndex] = []
2020-02-23 09:49:10 +00:00
events[dayOfMonth].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
2020-08-29 11:14:19 +00:00
calendarFile = open(calendarFilename, 'w+')
for postId in calendarPostIds:
2020-04-03 11:53:31 +00:00
calendarFile.write(postId + '\n')
calendarFile.close()
2020-04-03 11:53:31 +00:00
lastDayOfMonth = daysInMonth(year, monthNumber)
if dayNumber+6 > lastDayOfMonth:
2020-04-03 11:53:31 +00:00
monthNumber += 1
if monthNumber > 12:
monthNumber = 1
year += 1
for d in range(1, dayNumber + 6 - lastDayOfMonth):
dailyEvents = \
getTodaysEvents(baseDir, nickname, domain,
year, monthNumber, d)
if dailyEvents:
if dailyEvents.get(d):
2020-04-03 11:53:31 +00:00
newEvents = {}
newEvents[d + (7 - (dayNumber + 6 - lastDayOfMonth))] = \
2020-02-24 10:55:49 +00:00
dailyEvents[d]
2020-04-03 11:53:31 +00:00
events = mergeDicts(events, newEvents)
return events
2020-04-03 11:53:31 +00:00
def getCalendarEvents(baseDir: str, nickname: str, domain: str,
year: int, monthNumber: int) -> {}:
"""Retrieves calendar events
2020-02-24 10:55:49 +00:00
Returns a dictionary indexed by day number of lists containing
Event and Place activities
"""
2020-04-03 11:53:31 +00:00
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
events = {}
if not os.path.isfile(calendarFilename):
return events
2020-02-23 09:45:04 +00:00
2020-04-03 11:53:31 +00:00
calendarPostIds = []
recreateEventsFile = False
with open(calendarFilename, 'r') as eventsFile:
for postId in eventsFile:
2020-05-22 11:32:38 +00:00
postId = postId.replace('\n', '').replace('\r', '')
2020-04-03 11:53:31 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
2020-04-03 11:53:31 +00:00
recreateEventsFile = True
2020-02-23 09:42:09 +00:00
continue
2020-02-24 11:10:48 +00:00
2020-04-03 11:53:31 +00:00
postJsonObject = loadJson(postFilename)
if not _isHappeningPost(postJsonObject):
2020-02-23 09:42:09 +00:00
continue
2020-04-03 11:53:31 +00:00
postEvent = []
dayOfMonth = None
2020-02-23 09:42:09 +00:00
for tag in postJsonObject['object']['tag']:
if not _isHappeningEvent(tag):
2020-02-23 09:42:09 +00:00
continue
2020-02-24 11:10:48 +00:00
# this tag is an event or a place
2020-04-03 11:53:31 +00:00
if tag['type'] == 'Event':
2020-02-23 09:42:09 +00:00
# tag is an event
if not tag.get('startTime'):
continue
2020-04-03 11:53:31 +00:00
eventTime = \
datetime.strptime(tag['startTime'],
2020-02-23 09:42:09 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-03 11:53:31 +00:00
if int(eventTime.strftime("%Y")) == year and \
int(eventTime.strftime("%m")) == monthNumber:
dayOfMonth = str(int(eventTime.strftime("%d")))
2020-02-23 09:42:09 +00:00
postEvent.append(tag)
else:
# tag is a place
postEvent.append(tag)
if postEvent and dayOfMonth:
calendarPostIds.append(postId)
if not events.get(dayOfMonth):
2020-04-03 11:53:31 +00:00
events[dayOfMonth] = []
2020-02-23 09:42:09 +00:00
events[dayOfMonth].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
2020-08-29 11:14:19 +00:00
calendarFile = open(calendarFilename, 'w+')
for postId in calendarPostIds:
2020-04-03 11:53:31 +00:00
calendarFile.write(postId + '\n')
calendarFile.close()
2020-03-22 21:16:02 +00:00
return events
2020-02-23 13:28:27 +00:00
2020-04-03 11:53:31 +00:00
def removeCalendarEvent(baseDir: str, nickname: str, domain: str,
year: int, monthNumber: int, messageId: str) -> None:
2020-02-23 13:28:27 +00:00
"""Removes a calendar event
"""
2020-04-03 11:53:31 +00:00
calendarFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
2020-02-23 13:28:27 +00:00
if not os.path.isfile(calendarFilename):
return
if '/' in messageId:
2020-04-03 11:53:31 +00:00
messageId = messageId.replace('/', '#')
2020-02-23 13:28:27 +00:00
if messageId not in open(calendarFilename).read():
return
2020-04-03 11:53:31 +00:00
lines = None
2020-02-23 13:28:27 +00:00
with open(calendarFilename, "r") as f:
2020-04-03 11:53:31 +00:00
lines = f.readlines()
2020-02-23 13:28:27 +00:00
if not lines:
return
with open(calendarFilename, "w+") as f:
for line in lines:
if messageId not in line:
f.write(line)