From a999170b4d206c4571b17951990d6f8c9376d895 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Fri, 26 Apr 2019 17:53:53 +0100 Subject: [PATCH] First working version --- Pipfile | 11 +++-- Pipfile.lock | 28 +++++++++-- README.md | 91 +++++++++++++++++++++++++++++++++++ config_example.ini | 8 +++- generate_feeds.py | 116 +++++++++++++++++++++++++++++++++++++++++++-- requirements.txt | 11 +++++ 6 files changed, 249 insertions(+), 16 deletions(-) create mode 100644 requirements.txt diff --git a/Pipfile b/Pipfile index bbf2514..286f8d8 100644 --- a/Pipfile +++ b/Pipfile @@ -1,12 +1,13 @@ [[source]] +name = "pypi" url = "https://pypi.org/simple" verify_ssl = true -name = "pypi" - -[packages] -foursquare = "*" [dev-packages] +[packages] +foursquare = "*" +ics = {editable = true,git = "https://github.com/C4ptainCrunch/ics.py.git",ref = "bd918ec7453a7cf73a906cdcc78bd88eb4bab71b"} + [requires] -python_version = "3.7" +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index d996a41..a5dbbfb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "29dee3b8e3acad4b57ba9395b14419cca49fccede2ea2a7dd41d9375776c0983" + "sha256": "09dd606d1eb6beaf39a922504397bc2a77c1063eed10c4d274caf1937dd80b89" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.6" }, "sources": [ { @@ -16,6 +16,12 @@ ] }, "default": { + "arrow": { + "hashes": [ + "sha256:5c44e897cde7ff54d7336ee2072fd32395a525d070df0e9034ea64029d4a61b5" + ], + "version": "==0.11.0" + }, "certifi": { "hashes": [ "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", @@ -38,6 +44,11 @@ "index": "pypi", "version": "==1!2019.2.16" }, + "ics": { + "editable": true, + "git": "https://github.com/C4ptainCrunch/ics.py.git", + "ref": "bd918ec7453a7cf73a906cdcc78bd88eb4bab71b" + }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -45,6 +56,13 @@ ], "version": "==2.8" }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "version": "==2.8.0" + }, "requests": { "hashes": [ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", @@ -61,10 +79,10 @@ }, "urllib3": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" ], - "version": "==1.24.1" + "version": "==1.24.2" } }, "develop": {} diff --git a/README.md b/README.md index 30b5fe7..d9a3019 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ # Foursquare Feeds +A python script that will generate a iCal (`.ics`) feed of your recent checkins on [Foursquare][4sq]/[Swarm][swarm]. + +Foursquare [used to have such feeds][feeds] but they've stopped working for me. + +**NOTE: This is new and untested!** + +[4sq]: https://foursquare.com +[swarm]: https://www.swarmapp.com +[feeds]: https://foursquare.com/feeds/ + + +## Installation + + +### 1. Make a Foursquare app + +Go to https://foursquare.com/developers/apps and create a new App. + + +### 2. Install python requirements + +Either: + + $ pipenv install + +or: + + $ pip install -r requirements.txt + + +### 3. Set up config file + +Copy `config_example.ini` to `config.ini`. Change the `IcsFilepath` to wherever you want your file to be saved. + +To get the `AccessToken` for your Foursquare app, you will have to go through the laborious procedure in step 4... + + +### 4. Get an access token + +On https://foursquare.com/developers/apps, in your app, set the Redirect URI to `http://localhost:8000/` + +In a terminal window, open a python shell: + + $ python + +and, using your app's Client ID and Client Secret enter this: + +```python +import foursquare +client = foursquare.Foursquare(client_id='YOUR_CLIENT_ID' client_secret='YOUR_CLIENT_SECRET', redirect_uri='http://localhost:8000') +client.oauth.auth_url() +``` + +This will output something like: + + 'https://foursquare.com/oauth2/authenticate?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2F' + +Copy the URL from your terminal *without the surrounding quotes* and paste it into a web browser. + +Your browser should redirect to a URL like the one below, with an error about not being able to connect to the server (unless you have a webserver running locally on your machine): + + http://localhost:8000/?code=XX_CODE_RETURNED_IN_REDIRECT_XX#_=_ + +Copy the code represented by `XX_CODE_RETURNED_IN_REDIRECT_XX` (note that there may be an extra `#_=_` on the end which *you should not copy*). + +Back in your python shell, with that code: + +```python +client.oauth.get_token('XX_CODE_RETURNED_IN_REDIRECT_XX') +``` + +This will output another long code, which is your Access Token. + +Enter this in your `config.ini`. + +### 5. Run it + +Then run the script: + + $ ./generate_feeds.py + +It should create an `.ics` file. + +If the file is generated in a location on your website that's publicly-readable, you should be able to subscribe to it from a calendar application. Then run the script periodically to have it update. + +Note that the file might contain private checkins or information you don't want to be public. In which case, probably best to make the name of any such publicly-readable file very obscure. + + +## TODO + +* Upgrade `ics` when there's a release newer than 0.4. We currently use a specific commit because it's newer than 0.4 and uses a newer version of arrow, which we need for timezone shifts. diff --git a/config_example.ini b/config_example.ini index abf9c00..1b5aabc 100644 --- a/config_example.ini +++ b/config_example.ini @@ -1,4 +1,8 @@ [Foursquare] -ClientID=yourIDhere -ClientSecret=yourSecretHere +AccessToken=YourAccessTokenHere + +[Local] + +IcsFilepath=./foursquare.ics + diff --git a/generate_feeds.py b/generate_feeds.py index 60a2723..285ab3c 100755 --- a/generate_feeds.py +++ b/generate_feeds.py @@ -1,17 +1,29 @@ +#!/usr/bin/env python3 import configparser import logging +import arrow +import foursquare +from ics import Calendar, Event + logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) CONFIG_FILE = "config.ini" +# How many to fetch and use. Up to 250. +NUM_CHECKINS = 100 + class FeedGenerator: def __init__(self): + "Loads config, sets up Foursquare API client." self._load_config(CONFIG_FILE) + self.client = foursquare.Foursquare(access_token=self.api_access_token) + def _load_config(self, config_file): + "Set object variables based on supplied config file." config = configparser.ConfigParser() try: @@ -20,15 +32,111 @@ class FeedGenerator: logger.critical("Can't read config file: " + config_file) exit() - self.api_id = config.get("Foursquare", "ClientID") - self.api_secret = config.get("Foursquare", "ClientSecret") + self.api_access_token = config.get("Foursquare", "AccessToken") + self.ics_filepath = config.get("Local", "IcsFilepath") def generate(self): - logger.info("GENERATE") + "" + checkins = self._get_checkins() + + calendar = self._generate_calendar(checkins) + + with open(self.ics_filepath, "w") as f: + f.writelines(calendar) + + exit(0) + + def _get_checkins(self): + "Returns a list of recent checkins for the authenticated user." + + try: + return self.client.users.checkins( + params={"limit": NUM_CHECKINS, "sort": "newestfirst"} + ) + except foursquare.FoursquareException as e: + logger.error("Error getting checkins: {}".format(e)) + exit(1) + + def _get_user_url(self): + "Returns the Foursquare URL for the authenticated user." + try: + user = self.client.users() + except foursquare.FoursquareException as e: + logger.error("Error getting user: {}".format(e)) + exit(1) + + return user["user"]["canonicalUrl"] + + def _generate_calendar(self, checkins): + """Supplied with a list of checkin data from the API, generates an + ics Calendar object and returns it. + """ + user_url = self._get_user_url() + + c = Calendar() + + for checkin in checkins["checkins"]["items"]: + venue_name = checkin["venue"]["name"] + tz_offset = self._get_checkin_timezone(checkin) + + e = Event() + + e.name = "@ {}".format(venue_name) + e.location = venue_name + e.url = "{}/checkin/{}".format(user_url, checkin["id"]) + e.uid = "{}@foursquare.com".format(checkin["id"]) + e.begin = checkin["createdAt"] + + # Use the 'shout', if any, and the timezone offset in the description. + description = [] + if "shout" in checkin and len(checkin["shout"]) > 0: + description = [checkin["shout"]] + description.append("Timezone offset: {}".format(tz_offset)) + e.description = "\n".join(description) + + # Use the venue_name and the address, if any, for the location. + location = venue_name + if "location" in checkin["venue"]: + loc = checkin["venue"]["location"] + if "formattedAddress" in loc and len(loc["formattedAddress"]) > 0: + address = ", ".join(loc["formattedAddress"]) + location = "{}, {}".format(location, address) + e.location = location + + c.events.add(e) + + return c + + def _get_checkin_timezone(self, checkin): + """Given a checkin from the API, returns a string representing the + timezone offset of that checkin. + In the API they're given as a number of minutes, positive or negative. + + e.g. if offset is 60, this returns '+01:00' + if offset is 0, this returns '+00:00' + if offset is -480, this returns '-08:00' + """ + # In minutes, e.g. 60 or -480 + minutes = checkin["timeZoneOffset"] + + # e.g. 1 or -8 + hours = minutes / 60 + + # e.g. '01.00' or '-08.00' + if hours >= 0: + offset = "{:05.2f}".format(hours) + symbol = "+" + else: + offset = "{:06.2f}".format(hours) + symbol = "" + + # e.g. '+01:00' or '-08.00' + return "{}{}".format(symbol, offset).replace(".", ":") if __name__ == "__main__": generator = FeedGenerator() - generator.generate() + + exit(0) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8146d92 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +-i https://pypi.org/simple +-e git+https://github.com/C4ptainCrunch/ics.py.git@bd918ec7453a7cf73a906cdcc78bd88eb4bab71b#egg=ics +arrow==0.11.0 +certifi==2019.3.9 +chardet==3.0.4 +foursquare==1!2019.2.16 +idna==2.8 +python-dateutil==2.8.0 +requests==2.21.0 +six==1.12.0 +urllib3==1.24.2