mirror of https://gitlab.com/bashrc2/epicyon
454 lines
16 KiB
Python
454 lines
16 KiB
Python
__filename__ = "happening.py"
|
|
__author__ = "Bob Mottram"
|
|
__license__ = "AGPL3+"
|
|
__version__ = "1.2.0"
|
|
__maintainer__ = "Bob Mottram"
|
|
__email__ = "bob@freedombone.net"
|
|
__status__ = "Production"
|
|
__module_group__ = "Calendar"
|
|
|
|
import os
|
|
from uuid import UUID
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
|
|
from utils import isPublicPost
|
|
from utils import loadJson
|
|
from utils import saveJson
|
|
from utils import locatePost
|
|
from utils import hasObjectDict
|
|
|
|
|
|
def _validUuid(testUuid: str, version=4):
|
|
"""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:
|
|
"""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
|
|
|
|
|
|
def saveEventPost(baseDir: str, handle: str, postId: str,
|
|
eventJson: {}) -> bool:
|
|
"""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
|
|
See https://framagit.org/framasoft/mobilizon/-/blob/
|
|
master/lib/federation/activity_stream/converter/event.ex
|
|
"""
|
|
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"))
|
|
if eventYear < 2020 or eventYear >= 2100:
|
|
return False
|
|
eventMonthNumber = int(eventTime.strftime("%m"))
|
|
if eventMonthNumber < 1 or eventMonthNumber > 12:
|
|
return False
|
|
eventDayOfMonth = int(eventTime.strftime("%d"))
|
|
if eventDayOfMonth < 1 or eventDayOfMonth > 31:
|
|
return False
|
|
|
|
if eventJson.get('name') and eventJson.get('actor') and \
|
|
eventJson.get('uuid') and eventJson.get('content'):
|
|
if not _validUuid(eventJson['uuid']):
|
|
return False
|
|
print('Mobilizon type event')
|
|
# 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)
|
|
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)
|
|
except Exception as e:
|
|
print('WARN: Failed to write entry to events file ' +
|
|
tlEventsFilename + ' ' + str(e))
|
|
return False
|
|
else:
|
|
with open(tlEventsFilename, 'w+') as tlEventsFile:
|
|
tlEventsFile.write(eventId + '\n')
|
|
|
|
# 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
|
|
with open(calendarFilename, 'a+') as calendarFile:
|
|
calendarFile.write(postId + '\n')
|
|
|
|
# create a file which will trigger a notification that
|
|
# a new event has been added
|
|
calendarNotificationFilename = \
|
|
baseDir + '/accounts/' + handle + '/.newCalendar'
|
|
with open(calendarNotificationFilename, 'w+') as calendarNotificationFile:
|
|
calendarNotificationFile.write('/calendar?year=' +
|
|
str(eventYear) +
|
|
'?month=' +
|
|
str(eventMonthNumber) +
|
|
'?day=' +
|
|
str(eventDayOfMonth))
|
|
return True
|
|
|
|
|
|
def _isHappeningEvent(tag: {}) -> bool:
|
|
"""Is this tag an Event or Place ActivityStreams type?
|
|
"""
|
|
if not tag.get('type'):
|
|
return False
|
|
if tag['type'] != 'Event' and tag['type'] != 'Place':
|
|
return False
|
|
return True
|
|
|
|
|
|
def _isHappeningPost(postJsonObject: {}) -> bool:
|
|
"""Is this a post with tags?
|
|
"""
|
|
if not postJsonObject:
|
|
return False
|
|
if not hasObjectDict(postJsonObject):
|
|
return False
|
|
if not postJsonObject['object'].get('tag'):
|
|
return False
|
|
return True
|
|
|
|
|
|
def getTodaysEvents(baseDir: str, nickname: str, domain: str,
|
|
currYear: int = None, currMonthNumber: int = None,
|
|
currDayOfMonth: int = None) -> {}:
|
|
"""Retrieves calendar events for today
|
|
Returns a dictionary of lists containing Event and Place activities
|
|
"""
|
|
now = datetime.now()
|
|
if not currYear:
|
|
year = now.year
|
|
else:
|
|
year = currYear
|
|
if not currMonthNumber:
|
|
monthNumber = now.month
|
|
else:
|
|
monthNumber = currMonthNumber
|
|
if not currDayOfMonth:
|
|
dayNumber = now.day
|
|
else:
|
|
dayNumber = currDayOfMonth
|
|
|
|
calendarFilename = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
|
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
|
|
events = {}
|
|
if not os.path.isfile(calendarFilename):
|
|
return events
|
|
|
|
calendarPostIds = []
|
|
recreateEventsFile = False
|
|
with open(calendarFilename, 'r') as eventsFile:
|
|
for postId in eventsFile:
|
|
postId = postId.replace('\n', '').replace('\r', '')
|
|
postFilename = locatePost(baseDir, nickname, domain, postId)
|
|
if not postFilename:
|
|
recreateEventsFile = True
|
|
continue
|
|
|
|
postJsonObject = loadJson(postFilename)
|
|
if not _isHappeningPost(postJsonObject):
|
|
continue
|
|
|
|
publicEvent = isPublicPost(postJsonObject)
|
|
|
|
postEvent = []
|
|
dayOfMonth = None
|
|
for tag in postJsonObject['object']['tag']:
|
|
if not _isHappeningEvent(tag):
|
|
continue
|
|
# this tag is an event or a place
|
|
if tag['type'] == 'Event':
|
|
# tag is an event
|
|
if not tag.get('startTime'):
|
|
continue
|
|
eventTime = \
|
|
datetime.strptime(tag['startTime'],
|
|
"%Y-%m-%dT%H:%M:%S%z")
|
|
if int(eventTime.strftime("%Y")) == year and \
|
|
int(eventTime.strftime("%m")) == monthNumber and \
|
|
int(eventTime.strftime("%d")) == dayNumber:
|
|
dayOfMonth = str(int(eventTime.strftime("%d")))
|
|
if '#statuses#' in postId:
|
|
# link to the id so that the event can be
|
|
# easily deleted
|
|
tag['postId'] = postId.split('#statuses#')[1]
|
|
tag['sender'] = postId.split('#statuses#')[0]
|
|
tag['sender'] = tag['sender'].replace('#', '/')
|
|
tag['public'] = publicEvent
|
|
postEvent.append(tag)
|
|
else:
|
|
# tag is a place
|
|
postEvent.append(tag)
|
|
if postEvent and dayOfMonth:
|
|
calendarPostIds.append(postId)
|
|
if not events.get(dayOfMonth):
|
|
events[dayOfMonth] = []
|
|
events[dayOfMonth].append(postEvent)
|
|
|
|
# if some posts have been deleted then regenerate the calendar file
|
|
if recreateEventsFile:
|
|
with open(calendarFilename, 'w+') as calendarFile:
|
|
for postId in calendarPostIds:
|
|
calendarFile.write(postId + '\n')
|
|
|
|
return events
|
|
|
|
|
|
def dayEventsCheck(baseDir: str, nickname: str, domain: str, currDate) -> bool:
|
|
"""Are there calendar events for the given date?
|
|
"""
|
|
year = currDate.year
|
|
monthNumber = currDate.month
|
|
dayNumber = currDate.day
|
|
|
|
calendarFilename = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
|
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
|
|
if not os.path.isfile(calendarFilename):
|
|
return False
|
|
|
|
eventsExist = False
|
|
with open(calendarFilename, 'r') as eventsFile:
|
|
for postId in eventsFile:
|
|
postId = postId.replace('\n', '').replace('\r', '')
|
|
postFilename = locatePost(baseDir, nickname, domain, postId)
|
|
if not postFilename:
|
|
continue
|
|
|
|
postJsonObject = loadJson(postFilename)
|
|
if not _isHappeningPost(postJsonObject):
|
|
continue
|
|
|
|
for tag in postJsonObject['object']['tag']:
|
|
if not _isHappeningEvent(tag):
|
|
continue
|
|
# this tag is an event or a place
|
|
if tag['type'] != 'Event':
|
|
continue
|
|
# tag is an event
|
|
if not tag.get('startTime'):
|
|
continue
|
|
eventTime = \
|
|
datetime.strptime(tag['startTime'],
|
|
"%Y-%m-%dT%H:%M:%S%z")
|
|
if int(eventTime.strftime("%d")) != dayNumber:
|
|
continue
|
|
if int(eventTime.strftime("%m")) != monthNumber:
|
|
continue
|
|
if int(eventTime.strftime("%Y")) != year:
|
|
continue
|
|
eventsExist = True
|
|
break
|
|
|
|
return eventsExist
|
|
|
|
|
|
def getThisWeeksEvents(baseDir: str, nickname: str, domain: str) -> {}:
|
|
"""Retrieves calendar events for this week
|
|
Returns a dictionary indexed by day number of lists containing
|
|
Event and Place activities
|
|
Note: currently not used but could be with a weekly calendar screen
|
|
"""
|
|
now = datetime.now()
|
|
endOfWeek = now + timedelta(7)
|
|
year = now.year
|
|
monthNumber = now.month
|
|
|
|
calendarFilename = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
|
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
|
|
|
|
events = {}
|
|
if not os.path.isfile(calendarFilename):
|
|
return events
|
|
|
|
calendarPostIds = []
|
|
recreateEventsFile = False
|
|
with open(calendarFilename, 'r') as eventsFile:
|
|
for postId in eventsFile:
|
|
postId = postId.replace('\n', '').replace('\r', '')
|
|
postFilename = locatePost(baseDir, nickname, domain, postId)
|
|
if not postFilename:
|
|
recreateEventsFile = True
|
|
continue
|
|
|
|
postJsonObject = loadJson(postFilename)
|
|
if not _isHappeningPost(postJsonObject):
|
|
continue
|
|
|
|
postEvent = []
|
|
weekDayIndex = None
|
|
for tag in postJsonObject['object']['tag']:
|
|
if not _isHappeningEvent(tag):
|
|
continue
|
|
# this tag is an event or a place
|
|
if tag['type'] == 'Event':
|
|
# tag is an event
|
|
if not tag.get('startTime'):
|
|
continue
|
|
eventTime = \
|
|
datetime.strptime(tag['startTime'],
|
|
"%Y-%m-%dT%H:%M:%S%z")
|
|
if eventTime >= now and eventTime <= endOfWeek:
|
|
weekDayIndex = (eventTime - now).days()
|
|
postEvent.append(tag)
|
|
else:
|
|
# tag is a place
|
|
postEvent.append(tag)
|
|
if postEvent and weekDayIndex:
|
|
calendarPostIds.append(postId)
|
|
if not events.get(weekDayIndex):
|
|
events[weekDayIndex] = []
|
|
events[weekDayIndex].append(postEvent)
|
|
|
|
# if some posts have been deleted then regenerate the calendar file
|
|
if recreateEventsFile:
|
|
with open(calendarFilename, 'w+') as calendarFile:
|
|
for postId in calendarPostIds:
|
|
calendarFile.write(postId + '\n')
|
|
|
|
return events
|
|
|
|
|
|
def getCalendarEvents(baseDir: str, nickname: str, domain: str,
|
|
year: int, monthNumber: int) -> {}:
|
|
"""Retrieves calendar events
|
|
Returns a dictionary indexed by day number of lists containing
|
|
Event and Place activities
|
|
"""
|
|
calendarFilename = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
|
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
|
|
|
|
events = {}
|
|
if not os.path.isfile(calendarFilename):
|
|
return events
|
|
|
|
calendarPostIds = []
|
|
recreateEventsFile = False
|
|
with open(calendarFilename, 'r') as eventsFile:
|
|
for postId in eventsFile:
|
|
postId = postId.replace('\n', '').replace('\r', '')
|
|
postFilename = locatePost(baseDir, nickname, domain, postId)
|
|
if not postFilename:
|
|
recreateEventsFile = True
|
|
continue
|
|
|
|
postJsonObject = loadJson(postFilename)
|
|
if not _isHappeningPost(postJsonObject):
|
|
continue
|
|
|
|
postEvent = []
|
|
dayOfMonth = None
|
|
for tag in postJsonObject['object']['tag']:
|
|
if not _isHappeningEvent(tag):
|
|
continue
|
|
# this tag is an event or a place
|
|
if tag['type'] == 'Event':
|
|
# tag is an event
|
|
if not tag.get('startTime'):
|
|
continue
|
|
eventTime = \
|
|
datetime.strptime(tag['startTime'],
|
|
"%Y-%m-%dT%H:%M:%S%z")
|
|
if int(eventTime.strftime("%Y")) == year and \
|
|
int(eventTime.strftime("%m")) == monthNumber:
|
|
dayOfMonth = str(int(eventTime.strftime("%d")))
|
|
postEvent.append(tag)
|
|
else:
|
|
# tag is a place
|
|
postEvent.append(tag)
|
|
|
|
if postEvent and dayOfMonth:
|
|
calendarPostIds.append(postId)
|
|
if not events.get(dayOfMonth):
|
|
events[dayOfMonth] = []
|
|
events[dayOfMonth].append(postEvent)
|
|
|
|
# if some posts have been deleted then regenerate the calendar file
|
|
if recreateEventsFile:
|
|
with open(calendarFilename, 'w+') as calendarFile:
|
|
for postId in calendarPostIds:
|
|
calendarFile.write(postId + '\n')
|
|
|
|
return events
|
|
|
|
|
|
def removeCalendarEvent(baseDir: str, nickname: str, domain: str,
|
|
year: int, monthNumber: int, messageId: str) -> None:
|
|
"""Removes a calendar event
|
|
"""
|
|
calendarFilename = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
|
'/calendar/' + str(year) + '/' + str(monthNumber) + '.txt'
|
|
if not os.path.isfile(calendarFilename):
|
|
return
|
|
if '/' in messageId:
|
|
messageId = messageId.replace('/', '#')
|
|
if messageId not in open(calendarFilename).read():
|
|
return
|
|
lines = None
|
|
with open(calendarFilename, "r") as f:
|
|
lines = f.readlines()
|
|
if not lines:
|
|
return
|
|
with open(calendarFilename, "w+") as f:
|
|
for line in lines:
|
|
if messageId not in line:
|
|
f.write(line)
|