First working version

This commit is contained in:
Phil Gyford
2019-04-26 17:53:53 +01:00
parent 79d696fa22
commit a999170b4d
6 changed files with 249 additions and 16 deletions

11
Pipfile
View File

@@ -1,12 +1,13 @@
[[source]] [[source]]
name = "pypi"
url = "https://pypi.org/simple" url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
name = "pypi"
[packages]
foursquare = "*"
[dev-packages] [dev-packages]
[packages]
foursquare = "*"
ics = {editable = true,git = "https://github.com/C4ptainCrunch/ics.py.git",ref = "bd918ec7453a7cf73a906cdcc78bd88eb4bab71b"}
[requires] [requires]
python_version = "3.7" python_version = "3.6"

28
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "29dee3b8e3acad4b57ba9395b14419cca49fccede2ea2a7dd41d9375776c0983" "sha256": "09dd606d1eb6beaf39a922504397bc2a77c1063eed10c4d274caf1937dd80b89"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.7" "python_version": "3.6"
}, },
"sources": [ "sources": [
{ {
@@ -16,6 +16,12 @@
] ]
}, },
"default": { "default": {
"arrow": {
"hashes": [
"sha256:5c44e897cde7ff54d7336ee2072fd32395a525d070df0e9034ea64029d4a61b5"
],
"version": "==0.11.0"
},
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
@@ -38,6 +44,11 @@
"index": "pypi", "index": "pypi",
"version": "==1!2019.2.16" "version": "==1!2019.2.16"
}, },
"ics": {
"editable": true,
"git": "https://github.com/C4ptainCrunch/ics.py.git",
"ref": "bd918ec7453a7cf73a906cdcc78bd88eb4bab71b"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
@@ -45,6 +56,13 @@
], ],
"version": "==2.8" "version": "==2.8"
}, },
"python-dateutil": {
"hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
],
"version": "==2.8.0"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
@@ -61,10 +79,10 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
], ],
"version": "==1.24.1" "version": "==1.24.2"
} }
}, },
"develop": {} "develop": {}

View File

@@ -1,2 +1,93 @@
# Foursquare Feeds # 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.

View File

@@ -1,4 +1,8 @@
[Foursquare] [Foursquare]
ClientID=yourIDhere AccessToken=YourAccessTokenHere
ClientSecret=yourSecretHere
[Local]
IcsFilepath=./foursquare.ics

View File

@@ -1,17 +1,29 @@
#!/usr/bin/env python3
import configparser import configparser
import logging import logging
import arrow
import foursquare
from ics import Calendar, Event
logging.basicConfig(level=logging.INFO, format="%(message)s") logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CONFIG_FILE = "config.ini" CONFIG_FILE = "config.ini"
# How many to fetch and use. Up to 250.
NUM_CHECKINS = 100
class FeedGenerator: class FeedGenerator:
def __init__(self): def __init__(self):
"Loads config, sets up Foursquare API client."
self._load_config(CONFIG_FILE) self._load_config(CONFIG_FILE)
self.client = foursquare.Foursquare(access_token=self.api_access_token)
def _load_config(self, config_file): def _load_config(self, config_file):
"Set object variables based on supplied config file."
config = configparser.ConfigParser() config = configparser.ConfigParser()
try: try:
@@ -20,15 +32,111 @@ class FeedGenerator:
logger.critical("Can't read config file: " + config_file) logger.critical("Can't read config file: " + config_file)
exit() exit()
self.api_id = config.get("Foursquare", "ClientID") self.api_access_token = config.get("Foursquare", "AccessToken")
self.api_secret = config.get("Foursquare", "ClientSecret") self.ics_filepath = config.get("Local", "IcsFilepath")
def generate(self): 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__": if __name__ == "__main__":
generator = FeedGenerator() generator = FeedGenerator()
generator.generate() generator.generate()
exit(0)

11
requirements.txt Normal file
View File

@@ -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