diff --git a/README.md b/README.md index beba684..4418e5a 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,9 @@ Around 20 June 2019, Facebook removed their Facebook Birthday ICS export option. This change was unannounced and no reason was ever released. fb2cal is a tool which restores this functionality. -It works by calling various async endpoints that power the https://www.facebook.com/events/birthdays/ page. +It works by calling endpoints that power the https://www.facebook.com/events/birthdays/ page. After gathering a list of birthdays for all the users friends for a full year, it creates a ICS calendar file. This ICS file can then be imported into third party tools (such as Google Calendar). -This tool **does not** use the Facebook API. - ## Requirements * Facebook account * Python 3.6+ @@ -59,9 +57,6 @@ It is recommended to run the script **once every 24 hours** to update the ICS fi ## Caveats * Facebook accounts secured with 2FA are currently not supported (see [#9](../../issues/9)) * During Facebook authentication, a security checkpoint may trigger that will force you to change your Facebook password. -* Some locales are currently not supported (see [#13](../../issues/13)) -* Some supported locales may fail. Consider changing your Facebook language to English temporarily as a workaround. (see [#52](../../issues/52)) -* Duplicate birthday events may appear if calendar is reimported after Facebook friends change their username due to performance optimizations. (see [#65](../../pull/65)) ## Contributions Contributions are always welcome! diff --git a/requirements.txt b/requirements.txt index ab8d645..7b13b05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,4 @@ MechanicalSoup ics>=0.6 -babel -pytz requests -beautifulsoup4 -lxml -python_dateutil \ No newline at end of file +lxml \ No newline at end of file diff --git a/src/birthday.py b/src/birthday.py deleted file mode 100644 index 46918aa..0000000 --- a/src/birthday.py +++ /dev/null @@ -1,12 +0,0 @@ -class Birthday: - def __init__(self, uid, name, day, month): - self.uid = uid # Unique identififer for person (required for ics events) - self.name = name - self.day = day - self.month = month - - def __str__(self): - return f'{self.name} ({self.day}/{self.month})' - - def __unicode__(self): - return u'{self.name} ({self.day}/{self.month})' \ No newline at end of file diff --git a/src/facebook_browser.py b/src/facebook_browser.py index 84475b5..50fb360 100644 --- a/src/facebook_browser.py +++ b/src/facebook_browser.py @@ -2,11 +2,7 @@ import re import requests import json -from datetime import datetime from logger import Logger -from utils import get_next_12_month_epoch_timestamps, strip_ajax_response_prefix -import urllib.parse -from transformer import Transformer class FacebookBrowser: def __init__(self): @@ -14,7 +10,7 @@ def __init__(self): self.logger = Logger('fb2cal').getLogger() self.browser = mechanicalsoup.StatefulBrowser() self.browser.set_user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36') - self.__cached_async_token = None + self.__cached_token = None self.__cached_locale = None def authenticate(self, email, password): @@ -76,47 +72,15 @@ def authenticate(self, email, password): self.logger.error(f'Hit Facebook security checkpoint. Please login to Facebook manually and follow prompts to authorize this device.') raise SystemError + def get_token(self): + """ Get authorization token (CSRF protection token) that must be included in all requests """ - def get_async_birthdays(self): - """ Returns list of birthday objects by querying the Facebook birthday async page """ - - FACEBOOK_BIRTHDAY_ASYNC_ENDPOINT = 'https://www.facebook.com/async/birthdays/?' - birthdays = [] - next_12_months_epoch_timestamps = get_next_12_month_epoch_timestamps() - - transformer = Transformer() - user_locale = self.get_facebook_locale() - - for epoch_timestamp in next_12_months_epoch_timestamps: - self.logger.info(f'Processing birthdays for month {datetime.fromtimestamp(epoch_timestamp).strftime("%B")}.') - - # Not all fields are required for response to be given, required fields are date, fb_dtsg_ag and __a - query_params = {'date': epoch_timestamp, - 'fb_dtsg_ag': self.get_async_token(), - '__a': '1'} - - response = self.browser.get(FACEBOOK_BIRTHDAY_ASYNC_ENDPOINT + urllib.parse.urlencode(query_params)) - - if response.status_code != 200: - self.logger.debug(response.text) - self.logger.error(f'Failed to get async birthday response. Params: {query_params}. Status code: {response.status_code}.') - raise SystemError - - birthdays_for_month = transformer.parse_birthday_async_output(response.text, user_locale) - birthdays.extend(birthdays_for_month) - self.logger.info(f'Found {len(birthdays_for_month)} birthdays for month {datetime.fromtimestamp(epoch_timestamp).strftime("%B")}.') - - return birthdays - - def get_async_token(self): - """ Get async authorization token (CSRF protection token) that must be included in all async requests """ - - if self.__cached_async_token: - return self.__cached_async_token - - FACEBOOK_BIRTHDAY_EVENT_PAGE_URL = 'https://www.facebook.com/events/birthdays/' # async token is present on this page - FACEBOOK_ASYNC_TOKEN_REGEXP_STRING = r'{\"token\":\".*?\",\"async_get_token\":\"(.*?)\"}' - regexp = re.compile(FACEBOOK_ASYNC_TOKEN_REGEXP_STRING, re.MULTILINE) + if self.__cached_token: + return self.__cached_token + + FACEBOOK_BIRTHDAY_EVENT_PAGE_URL = 'https://www.facebook.com/events/birthdays/' # token is present on this page + FACEBOOK_TOKEN_REGEXP_STRING = r'{\"token\":\"(.*?)\"' + regexp = re.compile(FACEBOOK_TOKEN_REGEXP_STRING, re.MULTILINE) birthday_event_page = self.browser.get(FACEBOOK_BIRTHDAY_EVENT_PAGE_URL) @@ -132,49 +96,36 @@ def get_async_token(self): self.logger.error(f'Match failed or unexpected number of regexp matches when trying to get async token.') raise SystemError - self.__cached_async_token = matches[1] + self.__cached_token = matches[1] - return self.__cached_async_token + return self.__cached_token - def get_facebook_locale(self): - """ Returns users Facebook locale """ - - if self.__cached_locale: - return self.__cached_locale + def query_graph_ql_birthday_comet_root(self, offset_month): + """ Query the GraphQL BirthdayCometRootQuery endpoint that powers the https://www.facebook.com/events/birthdays page + This endpoint will return all Birthdays for the offset_month plus the following 2 consecutive months. """ - FACEBOOK_LOCALE_ENDPOINT = 'https://www.facebook.com/ajax/settings/language/account.php?' - FACEBOOK_LOCALE_REGEXP_STRING = r'[a-z]{2}_[A-Z]{2}' - regexp = re.compile(FACEBOOK_LOCALE_REGEXP_STRING, re.MULTILINE) + FACEBOOK_GRAPHQL_ENDPOINT = 'https://www.facebook.com/api/graphql/' + FACEBOOK_GRAPHQL_API_REQ_FRIENDLY_NAME = 'BirthdayCometRootQuery' + DOC_ID = 3382519521824494 - # Not all fields are required for response to be given, required fields are fb_dtsg_ag and __a - query_params = {'fb_dtsg_ag': self.get_async_token(), - '__a': '1'} + variables = { + 'offset_month': offset_month, + 'scale': 1.5 + } - response = self.browser.get(FACEBOOK_LOCALE_ENDPOINT + urllib.parse.urlencode(query_params)) - - if response.status_code != 200: - self.logger.debug(response.text) - self.logger.error(f'Failed to get Facebook locale. Params: {query_params}. Status code: {response.status_code}.') - raise SystemError + payload = { + 'fb_api_req_friendly_name': FACEBOOK_GRAPHQL_API_REQ_FRIENDLY_NAME, + 'variables': json.dumps(variables), + 'doc_id': DOC_ID, + 'fb_dtsg': self.get_token(), + '__a': '1' + } - # Parse json response - try: - json_response = json.loads(strip_ajax_response_prefix(response.text)) - current_locale = json_response['jsmods']['require'][0][3][1]['currentLocale'] - except json.decoder.JSONDecodeError as e: - self.logger.debug(response.text) - self.logger.error(f'JSONDecodeError: {e}') - raise SystemError - except KeyError as e: - self.logger.debug(json_response) - self.logger.error(f'KeyError: {e}') - raise SystemError + response = self.browser.post(FACEBOOK_GRAPHQL_ENDPOINT, data=payload) - # Validate locale - if not regexp.match(current_locale): - self.logger.error(f'Invalid Facebook locale fetched: {current_locale}.') + if response.status_code != 200: + self.logger.debug(response.text) + self.logger.error(f'Failed to get {FACEBOOK_GRAPHQL_API_REQ_FRIENDLY_NAME} response. Payload: {payload}. Status code: {response.status_code}.') raise SystemError - - self.__cached_locale = current_locale - - return self.__cached_locale \ No newline at end of file + + return response.json() diff --git a/src/facebook_user.py b/src/facebook_user.py new file mode 100644 index 0000000..8266f45 --- /dev/null +++ b/src/facebook_user.py @@ -0,0 +1,14 @@ +class FacebookUser: + def __init__(self, id, name, profile_url, profile_picture_uri, birthday_day, birthday_month): + self.id = id + self.name = name + self.profile_url = profile_url + self.profile_picture_uri = profile_picture_uri + self.birthday_day = birthday_day + self.birthday_month = birthday_month + + def __str__(self): + return f'{self.name} ({self.birthday_day}/{self.birthday_month})' + + def __unicode__(self): + return u'{self.name} ({self.birthday_day}/{self.birthday_month})' \ No newline at end of file diff --git a/src/fb2cal.py b/src/fb2cal.py index 52a99f6..b16b0e8 100644 --- a/src/fb2cal.py +++ b/src/fb2cal.py @@ -23,11 +23,11 @@ import logging from distutils import util -from birthday import Birthday from ics_writer import ICSWriter from logger import Logger from config import Config from facebook_browser import FacebookBrowser +from transformer import Transformer if __name__ == '__main__': # Set CWD to script directory @@ -62,18 +62,25 @@ facebook_browser.authenticate(config['AUTH']['FB_EMAIL'], config['AUTH']['FB_PASS']) logger.info('Successfully authenticated with Facebook.') - # Get birthday objects for all friends via async endpoint - logger.info('Fetching all Birthdays via async endpoint...') - birthdays = facebook_browser.get_async_birthdays() + # Fetch birthdays for a full calendar year and transform them + facebook_users = [] + transformer = Transformer() - if len(birthdays) == 0: - logger.warning(f'Birthday list is empty. Failed to fetch any birthdays.') + # Endpoint will return all birthdays for offset_month plus the following 2 consecutive months. + logger.info('Fetching all Birthdays via BirthdayCometRootQuery endpoint...') + for offset_month in [1, 4, 7, 10]: + birthday_comet_root_json = facebook_browser.query_graph_ql_birthday_comet_root(offset_month) + facebook_users_for_quarter = transformer.transform_birthday_comet_root_to_birthdays(birthday_comet_root_json) + facebook_users.extend(facebook_users_for_quarter) + + if len(facebook_users) == 0: + logger.warning(f'Facebook user list is empty. Failed to fetch any birthdays.') raise SystemError - logger.info(f'A total of {len(birthdays)} birthdays were found.') + logger.info(f'A total of {len(facebook_users)} birthdays were found.') # Generate ICS - ics_writer = ICSWriter(birthdays) + ics_writer = ICSWriter(facebook_users) logger.info('Creating birthday ICS file...') ics_writer.generate() logger.info('ICS file created successfully.') diff --git a/src/ics_writer.py b/src/ics_writer.py index fac52e0..11fd08c 100644 --- a/src/ics_writer.py +++ b/src/ics_writer.py @@ -12,9 +12,9 @@ """ Write Birthdays to an ICS file """ class ICSWriter: - def __init__(self, birthdays): + def __init__(self, facebook_users): self.logger = Logger('fb2cal').getLogger() - self.birthdays = birthdays + self.facebook_users = facebook_users def generate(self): c = Calendar() @@ -27,23 +27,23 @@ def generate(self): cur_date = datetime.now() - for birthday in self.birthdays: + for facebook_user in self.facebook_users: e = Event() - e.uid = birthday.uid + e.uid = facebook_user.id e.created = cur_date - e.name = f"{birthday.name}'s Birthday" + e.name = f"{facebook_user.name}'s Birthday" # Calculate the year as this year or next year based on if its past current month or not # Also pad day, month with leading zeros to 2dp - year = cur_date.year if birthday.month >= cur_date.month else (cur_date + relativedelta(years=1)).year + year = cur_date.year if facebook_user.birthday_month >= cur_date.month else (cur_date + relativedelta(years=1)).year # Feb 29 special case: # If event year is not a leap year, use Feb 28 as birthday date instead - if birthday.month == 2 and birthday.day == 29 and not calendar.isleap(year): - birthday.day = 28 + if facebook_user.birthday_month == 2 and facebook_user.birthday_day == 29 and not calendar.isleap(year): + facebook_user.birthday_day = 28 - month = '{:02d}'.format(birthday.month) - day = '{:02d}'.format(birthday.day) + month = '{:02d}'.format(facebook_user.birthday_month) + day = '{:02d}'.format(facebook_user.birthday_day) e.begin = f'{year}-{month}-{day} 00:00:00' e.make_all_day() e.duration = timedelta(days=1) diff --git a/src/transformer.py b/src/transformer.py index ddba89b..69cd751 100644 --- a/src/transformer.py +++ b/src/transformer.py @@ -1,274 +1,25 @@ -import re -import json -from datetime import datetime -from dateutil.relativedelta import relativedelta -from bs4 import BeautifulSoup -from logger import Logger -from utils import strip_ajax_response_prefix -from birthday import Birthday -from babel import Locale -from babel.core import UnknownLocaleError -from babel.dates import format_date -import locale -import platform -import html +from facebook_user import FacebookUser class Transformer: - def __init__(self): - self.logger = Logger('fb2cal').getLogger() - def parse_birthday_async_output(self, text, user_locale): - """ Parsed Birthday Async output text and returns list of Birthday objects """ - BIRTHDAY_STRING_REGEXP_STRING = r'class=\"_43q7\".*?href=\"https://www\.facebook\.com/(.*?)\".*?data-tooltip-content=\"(.*?)\">.*?alt=\"(.*?)\".*?/>' - regexp = re.compile(BIRTHDAY_STRING_REGEXP_STRING, re.MULTILINE) - - birthdays = [] - - # Fetch birthday card html payload from json response - try: - json_response = json.loads(strip_ajax_response_prefix(text)) - self.logger.debug(json_response) # TODO: Remove once domops error fixed #32 - birthday_card_html = json_response['domops'][0][3]['__html'] - except json.decoder.JSONDecodeError as e: - self.logger.debug(text) - self.logger.error(f'JSONDecodeError: {e}') - raise SystemError - except KeyError as e: - self.logger.debug(json_response) - self.logger.error(f'KeyError: {e}') - raise SystemError - - for vanity_name, tooltip_content, name in regexp.findall(birthday_card_html): - # Generate a unique ID in compliance with RFC 2445 ICS - 4.8.4.7 Unique Identifier - trim_start = 15 if vanity_name.startswith('profile.php?id=') else 0 - uid = f'{vanity_name[trim_start:]}@github.com/mobeigi/fb2cal' - - # Parse tooltip content into day/month - day, month = self.__parse_birthday_day_month(tooltip_content, name, user_locale) - - birthdays.append(Birthday(uid, html.unescape(name), day, month)) - - return birthdays - - def __parse_birthday_day_month(self, tooltip_content, name, user_locale): - """ Convert the Facebook birthday tooltip content to a day and month number. Facebook will use a tooltip format based on the users Facebook language (locale). - The date will be in some date format which reveals the birthday day and birthday month. - This is done for all birthdays expect those in the following week relative to the current date. - Those will instead show day names such as 'Monday', 'Tuesday' etc for the next 7 days. """ - - birthday_date_str = tooltip_content - - # List of strings that will be stripped away from tooltip_content - # The goal here is to remove all other characters except the birthday day, birthday month and day/month seperator symbol - strip_list = [ - name, # Full name of user which will appear somewhere in the string - '(', # Regular left bracket - ')', # Regular right bracket - '‏', # Remove right-to-left mark (RLM) - '‎', # Remove left-to-right mark (LRM) - '՝' # Backtick character name postfix in Armenian - ] - - for string in strip_list: - birthday_date_str = birthday_date_str.replace(string, '') - - birthday_date_str = birthday_date_str.strip() - - # Dict with mapping of locale identifier to month/day datetime format - locale_date_format_mapping = { - 'af_ZA': '%d-%m', - 'am_ET': '%m/%d', - # 'ar_AR': '', # TODO: parse Arabic numeric characters - # 'as_IN': '', # TODO: parse Assamese numeric characters - 'az_AZ': '%d.%m', - 'be_BY': '%d.%m', - 'bg_BG': '%d.%m', - 'bn_IN': '%d/%m', - 'br_FR': '%d/%m', - 'bs_BA': '%d.%m.', - 'ca_ES': '%d/%m', - # 'cb_IQ': '', # TODO: parse Arabic numeric characters - 'co_FR': '%m-%d', - 'cs_CZ': '%d. %m.', - 'cx_PH': '%m-%d', - 'cy_GB': '%d/%m', - 'da_DK': '%d.%m', - 'de_DE': '%d.%m.', - 'el_GR': '%d/%m', - 'en_GB': '%d/%m', - 'en_UD': '%m/%d', - 'en_US': '%m/%d', - 'eo_EO': '%m-%d', - 'es_ES': '%d/%m', - 'es_LA': '%d/%m', - 'et_EE': '%d.%m', - 'eu_ES': '%m/%d', - # 'fa_IR': '', # TODO: parse Persian numeric characters - 'ff_NG': '%d/%m', - 'fi_FI': '%d.%m.', - 'fo_FO': '%d.%m', - 'fr_CA': '%m-%d', - 'fr_FR': '%d/%m', - 'fy_NL': '%d-%m', - 'ga_IE': '%d/%m', - 'gl_ES': '%d/%m', - 'gn_PY': '%m-%d', - 'gu_IN': '%d/%m', - 'ha_NG': '%m/%d', - 'he_IL': '%d.%m', - 'hi_IN': '%d/%m', - 'hr_HR': '%d. %m.', - 'ht_HT': '%m-%d', - 'hu_HU': '%m. %d.', - 'hy_AM': '%d.%m', - 'id_ID': '%d/%m', - 'is_IS': '%d.%m.', - 'it_IT': '%d/%m', - 'ja_JP': '%m/%d', - 'ja_KS': '%m/%d', - 'jv_ID': '%d/%m', - 'ka_GE': '%d.%m', - 'kk_KZ': '%d.%m', - 'km_KH': '%d/%m', - 'kn_IN': '%d/%m', - 'ko_KR': '%m. %d.', - 'ku_TR': '%m-%d', - 'ky_KG': '%d-%m', - 'lo_LA': '%d/%m', - 'lt_LT': '%m-%d', - 'lv_LV': '%d.%m.', - 'mg_MG': '%d/%m', - 'mk_MK': '%d.%m', - 'ml_IN': '%d/%m', - 'mn_MN': '%m-р сар/%d', - # 'mr_IN': '', # TODO: parse Marathi numeric characters - 'ms_MY': '%d-%m', - 'mt_MT': '%m-%d', - # 'my_MM': '', # TODO: parse Myanmar numeric characters - 'nb_NO': '%d.%m.', - # 'ne_NP': '', # TODO: parse Nepali numeric characters - 'nl_BE': '%d/%m', - 'nl_NL': '%d-%m', - 'nn_NO': '%d.%m.', - 'or_IN': '%m/%d', - 'pa_IN': '%d/%m', - 'pl_PL': '%d.%m', - # 'ps_AF': '', # TODO: parse Afghani numeric characters - 'pt_BR': '%d/%m', - 'pt_PT': '%d/%m', - 'ro_RO': '%d.%m', - 'ru_RU': '%d.%m', - 'rw_RW': '%m-%d', - 'sc_IT': '%m-%d', - 'si_LK': '%m-%d', - 'sk_SK': '%d. %m.', - 'sl_SI': '%d. %m.', - 'sn_ZW': '%m-%d', - 'so_SO': '%m/%d', - 'sq_AL': '%d.%m', - 'sr_RS': '%d.%m.', - 'sv_SE': '%d/%m', - 'sw_KE': '%d/%m', - 'sy_SY': '%m-%d', - 'sz_PL': '%m-%d', - 'ta_IN': '%d/%m', - 'te_IN': '%d/%m', - 'tg_TJ': '%m-%d', - 'th_TH': '%d/%m', - 'tl_PH': '%m/%d', - 'tr_TR': '%d/%m', - 'tt_RU': '%d.%m', - 'tz_MA': '%m/%d', - 'uk_UA': '%d.%m', - 'ur_PK': '%d/%m', - 'uz_UZ': '%d/%m', - 'vi_VN': '%d/%m', - 'zh_CN': '%m/%d', - 'zh_HK': '%d/%m', - 'zh_TW': '%m/%d', - 'zz_TR': '%m-%d' - } - - # Ensure a supported locale is being used - if user_locale not in locale_date_format_mapping: - self.logger.error(f'The locale {user_locale} is not supported by Facebook.') - raise SystemError - - try: - # Try to parse the date using appropriate format based on locale - # We are only interested in the parsed day and month here so we also pass in a leap year to cover the special case of Feb 29 - parsed_date = datetime.strptime(f'{birthday_date_str}/1972', locale_date_format_mapping[user_locale] + "/%Y") - return (parsed_date.day, parsed_date.month) - except ValueError: - # Otherwise, have to convert day names to a day and month - offset_dict = self.__get_day_name_offset_dict(user_locale) - cur_date = datetime.now() - - # Use beautiful soup to parse special html codes properly before matching with our dict - day_name = BeautifulSoup(birthday_date_str).get_text().lower() - - if day_name in offset_dict: - cur_date = cur_date + relativedelta(days=offset_dict[day_name]) - return (cur_date.day, cur_date.month) - - self.logger.error(f'Failed to parse birthday day/month. Parse failed with tooltip_content: "{tooltip_content}", locale: "{user_locale}". Day name "{day_name}" is not in the offset dict {offset_dict}') - raise SystemError + def transform_birthday_comet_root_to_birthdays(self, birthday_comet_root_json): + """ Transforms outfrom from BirthdayCometRootQuery to list of Birthdays """ - def __get_day_name_offset_dict(self, user_locale): - """ The day name to offset dict maps a day name to a numerical day offset which can be used to add days to the current date. - Day names will match the provided user locale and will be in lowercase. - """ - - offset_dict = {} - - # Todays birthdays will be shown normally (as a date) so start from tomorrow - start_date = datetime.now() + relativedelta(days=1) - - # Method 1: Babel - try: - babel_locale = Locale.parse(user_locale, sep='_') - cur_date = start_date - - # Iterate through the following 7 days - for i in range(1, 8): - offset_dict[format_date(cur_date, 'EEEE', locale=babel_locale).lower()] = i - cur_date = cur_date + relativedelta(days=1) - - return offset_dict - except UnknownLocaleError as e: - self.logger.debug(f'Babel UnknownLocaleError: {e}') - - # Method 2: System locale - cur_date = start_date - locale_check_list = [user_locale, user_locale + 'UTF-8', user_locale + 'utf-8'] - system_locale = None - - # Windows - if any(platform.win32_ver()): - for locale_to_check in locale_check_list: - if locale_to_check in locale.windows_locale.values(): - system_locale = locale_to_check - break - # POSIX - else: - for locale_to_check in locale_check_list: - if locale_to_check in locale.locale_alias.values(): - system_locale = locale_to_check - break - - # Check if system locale was found - if system_locale: - locale.setlocale(locale.LC_ALL, system_locale) - - # Iterate through the following 7 days - for i in range(1, 8): - offset_dict[cur_date.strftime('%A').lower()] = i - cur_date = cur_date + relativedelta(days=1) - - return offset_dict - else: - self.logger.debug(f"Unable to find system locale for provided user locale: '{user_locale}'") + birthdays = [] - # Failure - self.logger.error(f"Failed to generate day name offset dictionary for provided user locale: '{user_locale}'") - raise SystemError \ No newline at end of file + for all_friends_by_birthday_month_edge in birthday_comet_root_json['data']['viewer']['all_friends_by_birthday_month']['edges']: + for friend_edge in all_friends_by_birthday_month_edge['node']['friends']['edges']: + friend = friend_edge['node'] + + # Create Birthday object + birthdays.append( + FacebookUser( + friend["id"], + friend["name"], + friend["profile_url"], + friend["profile_picture"]["uri"], + friend["birthdate"]["day"], + friend["birthdate"]["month"] + )) + + return birthdays \ No newline at end of file diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index 680f03e..0000000 --- a/src/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytz -from datetime import datetime -from dateutil.relativedelta import relativedelta -from logger import Logger - -def get_next_12_month_epoch_timestamps(): - """ Returns array of epoch timestamps corresponding to the 1st day of the next 12 months starting from the current month. - For example, if the current date is 2000-05-20, will return epoch for 2000-05-01, 2000-06-01, 2000-07-01 etc for 12 months """ - - logger = Logger('fb2cal').getLogger() - - epoch_timestamps = [] - - # Facebook timezone seems to use Pacific Standard Time locally for these epochs - # So we have to convert our 00:00:01 datetime on 1st of month from Pacific to UTC before getting our epoch timestamps - pdt = pytz.timezone('America/Los_Angeles') - cur_date = datetime.now() - - # Loop for next 12 months - for _ in range(12): - # Reset day to 1 and time to 00:00:01 - cur_date = cur_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - - # Convert from Pacific to UTC and store timestamp - utc_date = pdt.localize(cur_date).astimezone(pytz.utc) - epoch_timestamps.append(int(utc_date.timestamp())) - - # Move cur_date to 1st of next month - cur_date = cur_date + relativedelta(months=1) - - logger.debug(f'Epoch timestamps are: {epoch_timestamps}') - return epoch_timestamps - -def strip_ajax_response_prefix(payload): - """ Strip the prefix that Facebook puts in front of AJAX responses """ - - if payload.startswith('for (;;);'): - return payload[9:] - return payload