mirror of
https://github.com/nikdoof/foursquare-feeds.git
synced 2025-12-13 13:32:22 +00:00
First working version
This commit is contained in:
11
Pipfile
11
Pipfile
@@ -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
28
Pipfile.lock
generated
@@ -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": {}
|
||||||
|
|||||||
91
README.md
91
README.md
@@ -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.
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
[Foursquare]
|
[Foursquare]
|
||||||
|
|
||||||
ClientID=yourIDhere
|
AccessToken=YourAccessTokenHere
|
||||||
ClientSecret=yourSecretHere
|
|
||||||
|
[Local]
|
||||||
|
|
||||||
|
IcsFilepath=./foursquare.ics
|
||||||
|
|
||||||
|
|||||||
@@ -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
11
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user