epicyon/happening.py

477 lines
17 KiB
Python

__filename__ = "happening.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Core"
import os
from uuid import UUID
from datetime import datetime
from datetime import timedelta
from utils import is_public_post
from utils import load_json
from utils import save_json
from utils import locate_post
from utils import has_object_dict
from utils import acct_dir
def _validUuid(testUuid: str, version: int):
"""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 OSError:
print('EX: ERROR: unable to save events timeline')
def saveEventPost(base_dir: str, handle: str, post_id: 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
"""
if not os.path.isdir(base_dir + '/accounts/' + handle):
print('WARN: Account does not exist at ' +
base_dir + '/accounts/' + handle)
calendarPath = base_dir + '/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'], 4):
return False
print('Mobilizon type event')
# if this is a full description of an event then save it
# as a separate json file
eventsPath = base_dir + '/accounts/' + handle + '/events'
if not os.path.isdir(eventsPath):
os.mkdir(eventsPath)
eventsYearPath = \
base_dir + '/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'
save_json(eventJson, eventFilename)
# save to the events timeline
tlEventsFilename = base_dir + '/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 OSError as ex:
print('EX: Failed to write entry to events file ' +
tlEventsFilename + ' ' + str(ex))
return False
else:
try:
with open(tlEventsFilename, 'w+') as tlEventsFile:
tlEventsFile.write(eventId + '\n')
except OSError:
print('EX: unable to write ' + tlEventsFilename)
# 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 post_id in open(calendarFilename).read():
# Event post already exists
return False
# append the post Id to the file for the calendar month
try:
with open(calendarFilename, 'a+') as calendarFile:
calendarFile.write(post_id + '\n')
except OSError:
print('EX: unable to append ' + calendarFilename)
# create a file which will trigger a notification that
# a new event has been added
calNotifyFilename = base_dir + '/accounts/' + handle + '/.newCalendar'
notifyStr = \
'/calendar?year=' + str(eventYear) + '?month=' + \
str(eventMonthNumber) + '?day=' + str(eventDayOfMonth)
try:
with open(calNotifyFilename, 'w+') as calendarNotificationFile:
calendarNotificationFile.write(notifyStr)
except OSError:
print('EX: unable to write ' + calNotifyFilename)
return False
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(post_json_object: {}) -> bool:
"""Is this a post with tags?
"""
if not post_json_object:
return False
if not has_object_dict(post_json_object):
return False
if not post_json_object['object'].get('tag'):
return False
return True
def getTodaysEvents(base_dir: str, nickname: str, domain: str,
currYear: int, currMonthNumber: int,
currDayOfMonth: int) -> {}:
"""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 = \
acct_dir(base_dir, 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 post_id in eventsFile:
post_id = post_id.replace('\n', '').replace('\r', '')
post_filename = locate_post(base_dir, nickname, domain, post_id)
if not post_filename:
recreateEventsFile = True
continue
post_json_object = load_json(post_filename)
if not _isHappeningPost(post_json_object):
continue
publicEvent = is_public_post(post_json_object)
postEvent = []
dayOfMonth = None
for tag in post_json_object['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 post_id:
# link to the id so that the event can be
# easily deleted
tag['post_id'] = post_id.split('#statuses#')[1]
tag['sender'] = post_id.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(post_id)
if not events.get(dayOfMonth):
events[dayOfMonth] = []
events[dayOfMonth].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
try:
with open(calendarFilename, 'w+') as calendarFile:
for post_id in calendarPostIds:
calendarFile.write(post_id + '\n')
except OSError:
print('EX: unable to write ' + calendarFilename)
return events
def dayEventsCheck(base_dir: 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 = \
acct_dir(base_dir, 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 post_id in eventsFile:
post_id = post_id.replace('\n', '').replace('\r', '')
post_filename = locate_post(base_dir, nickname, domain, post_id)
if not post_filename:
continue
post_json_object = load_json(post_filename)
if not _isHappeningPost(post_json_object):
continue
for tag in post_json_object['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(base_dir: 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 = \
acct_dir(base_dir, 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 post_id in eventsFile:
post_id = post_id.replace('\n', '').replace('\r', '')
post_filename = locate_post(base_dir, nickname, domain, post_id)
if not post_filename:
recreateEventsFile = True
continue
post_json_object = load_json(post_filename)
if not _isHappeningPost(post_json_object):
continue
postEvent = []
weekDayIndex = None
for tag in post_json_object['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(post_id)
if not events.get(weekDayIndex):
events[weekDayIndex] = []
events[weekDayIndex].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
try:
with open(calendarFilename, 'w+') as calendarFile:
for post_id in calendarPostIds:
calendarFile.write(post_id + '\n')
except OSError:
print('EX: unable to write ' + calendarFilename)
return events
def getCalendarEvents(base_dir: 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 = \
acct_dir(base_dir, 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 post_id in eventsFile:
post_id = post_id.replace('\n', '').replace('\r', '')
post_filename = locate_post(base_dir, nickname, domain, post_id)
if not post_filename:
recreateEventsFile = True
continue
post_json_object = load_json(post_filename)
if not _isHappeningPost(post_json_object):
continue
postEvent = []
dayOfMonth = None
for tag in post_json_object['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(post_id)
if not events.get(dayOfMonth):
events[dayOfMonth] = []
events[dayOfMonth].append(postEvent)
# if some posts have been deleted then regenerate the calendar file
if recreateEventsFile:
try:
with open(calendarFilename, 'w+') as calendarFile:
for post_id in calendarPostIds:
calendarFile.write(post_id + '\n')
except OSError:
print('EX: unable to write ' + calendarFilename)
return events
def removeCalendarEvent(base_dir: str, nickname: str, domain: str,
year: int, monthNumber: int, messageId: str) -> None:
"""Removes a calendar event
"""
calendarFilename = \
acct_dir(base_dir, 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
try:
with open(calendarFilename, 'w+') as f:
for line in lines:
if messageId not in line:
f.write(line)
except OSError:
print('EX: unable to write ' + calendarFilename)