Skip to content
This repository was archived by the owner on Jan 13, 2024. It is now read-only.

Commit 0c7e187

Browse files
authored
Merge pull request #14 from georgedorn/master
Mastodon Favorites feed, mastodon client script, refactor
2 parents 4bc26e2 + cb3b907 commit 0c7e187

File tree

6 files changed

+145
-39
lines changed

6 files changed

+145
-39
lines changed

README.md

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,47 +21,33 @@ The RSS feed displays only the original tweets (not the retweets) and :
2121
- [Feedgenerator](https://pypi.python.org/pypi/feedgenerator)
2222
- [Tweepy](https://github.com/tweepy/tweepy)
2323
- [pytz](https://pypi.python.org/pypi/pytz/)
24+
- [PyYAML](https://github.com/yaml/pyyaml)
2425
- [BeautifulSoup](https://pypi.python.org/pypi/beautifulsoup4)
2526
- [Mastodon.py](https://github.com/halcy/Mastodon.py)
2627
- API keys Twitter and/or Mastodon
2728

2829

2930
## **Steps :**
30-
- install Python packages : flask, BeautifulSoup, Mastodon.py, feedgenerator, tweepy and pytz
31+
- install Python packages : flask, BeautifulSoup, Mastodon.py, feedgenerator, tweepy, PyYAML and pytz
3132
```bash
32-
$ pip3 install flask bs4 feedgenerator tweepy pytz Mastodon.py
33+
$ pip3 install -r requirements.txt
3334
```
3435

3536
- clone this repo :
3637
```bash
3738
$ git clone https://github.com/SamR1/python-twootfeed.git
3839
```
3940

41+
- Copy the included **config.example.yml** to **config.yml** and fill in fields for the client(s) you will use.
42+
4043
- API Keys
4144
- for **Twitter** : see https://dev.twitter.com
4245
copy/paste the Twitter API key values in **config.yml.example** ('_consumerKey_' and '_consumerSecret_')
43-
- for **Mastodon** : see [Python wrapper for the Mastodon API](https://github.com/halcy/Mastodon.py)
44-
- generate client and user credentials files :
45-
```python
46-
from mastodon import Mastodon
47-
48-
# Register app - only once!
49-
Mastodon.create_app(
50-
'pytooterapp',
51-
to_file = 'tootrss_clientcred.txt'
52-
)
53-
54-
# Log in - either every time, or use persisted
55-
mastodon = Mastodon(client_id = 'tootrss_clientcred.txt')
56-
mastodon.log_in(
57-
'my_login_email@example.com',
58-
'incrediblygoodpassword',
59-
to_file = 'tootrss_usercred.txt'
60-
)
61-
```
62-
- copy/paste file names in **config.yml.example** ('_client_id_file_' and '_access_token_file_')
63-
64-
Rename the config file **config.yml**.
46+
- for **Mastodon** : see [Python wrapper for the Mastodon API](https://mastodonpy.readthedocs.io/)
47+
- Generate the client and user credentials manually via the [Mastodon client](https://mastodonpy.readthedocs.io/en/latest/#app-registration-and-user-authentication)
48+
- note that using an instance other than https://mastodon.social requires adding `api_base_url` to most method calls.
49+
- the file names for **client_id** and **access_token_file** go in the mastodon section of **config.yml**
50+
- Or use the included script (`python3 create_mastodon_client.py`) which will register your app and prompt you to log in, creating the credential files for you.
6551

6652
- Start the server
6753
```bash
@@ -71,7 +57,7 @@ $ python3 -m flask run --host=0.0.0.0
7157

7258
- the RSS feeds are available on these urls :
7359
- for Twitter : http://localhost:5000/_keywords_ or http://localhost:5000/tweets/_keywords_
74-
- for Mastodon : http://localhost:5000/toots/_keywords_
60+
- for Mastodon : http://localhost:5000/toots/_keywords_ and http://localhost:5000/toot_favorites
7561

7662
## Examples :
7763
### Search on Twitter :

app.py

100644100755
Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,15 @@
1111
import tweepy
1212
import yaml
1313
import sys
14-
14+
import os
15+
from config import get_config
1516

1617
app = Flask(__name__)
1718
app.debug = True
1819

20+
param = get_config()
1921

20-
with open('config.yml', 'r') as stream:
21-
try:
22-
param = yaml.safe_load(stream)
23-
except yaml.YAMLError as e:
24-
print(e)
25-
sys.exit()
22+
text_length_limit = int(param['feed'].get('text_length_limit', 100))
2623

2724
# Twitter
2825
try:
@@ -40,9 +37,19 @@
4037

4138
# Mastodon
4239
try:
40+
client_file = param['mastodon']['client_id_file']
41+
if not os.path.exists(client_file):
42+
raise Exception("File not found: " + client_file)
43+
access_token_file = param['mastodon']['access_token_file']
44+
if not os.path.exists(access_token_file):
45+
raise Exception("File not found: " + client_file)
46+
47+
mastodon_url = param['mastodon'].get('url', 'https://mastodon.social')
48+
4349
mastodon = Mastodon(
44-
client_id=param['mastodon']['client_id_file'],
45-
access_token=param['mastodon']['access_token_file']
50+
client_id=client_file,
51+
access_token=access_token_file,
52+
api_base_url=mastodon_url
4653
)
4754
except Exception as e:
4855
print('Error Mastodon instance creation: ' + str(e))
@@ -189,7 +196,8 @@ def tootfeed(query_feed):
189196
'♻ : ' + str(toot['reblogs_count']) + ', ' + \
190197
'✰ : ' + str(toot['favourites_count']) + '</div></blockquote>'
191198

192-
toot['created_at'] = datetime.datetime.strptime(toot['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ')
199+
if isinstance(toot['created_at'], str):
200+
toot['created_at'] = datetime.datetime.strptime(toot['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ')
193201

194202
buffered.append(toot.copy())
195203

@@ -204,12 +212,66 @@ def tootfeed(query_feed):
204212
for toot in buffered:
205213

206214
text = BeautifulSoup(toot['content'], "html.parser").text
207-
if len(text) > 100:
208-
text = text[:100] + '... '
215+
pubdate = toot['created_at']
216+
if not pubdate.tzinfo:
217+
pubdate = utc.localize(pubdate).astimezone(pytz.timezone(param['feed']['timezone']))
218+
219+
if len(text) > text_length_limit:
220+
text = text[:text_length_limit] + '... '
221+
f.add_item(title=toot['account']['display_name'] + ' (' + toot['account']['username'] + '): '
222+
+ text,
223+
link=toot['url'],
224+
pubdate=pubdate,
225+
description=toot['htmltext'])
226+
227+
xml = f.writeString('UTF-8')
228+
else:
229+
xml = 'error - Mastodon parameters not defined'
230+
231+
return xml
232+
233+
@app.route('/toot_favorites')
234+
def toot_favorites_feed():
235+
""" generate an rss feed authenticated user's favorites """
236+
237+
if mastodonOK:
238+
buffered = []
239+
favorite_toots = mastodon.favourites()
240+
for toot in favorite_toots:
241+
242+
toot['htmltext'] = '<blockquote><div><img src="' + toot['account']['avatar_static'] + \
243+
'" alt="' + toot['account']['display_name'] + \
244+
'" /> <strong>' + toot['account']['username'] + \
245+
': </strong>' + toot['content'] + '<br>' + \
246+
'♻ : ' + str(toot['reblogs_count']) + ', ' + \
247+
'✰ : ' + str(toot['favourites_count']) + '</div></blockquote>'
248+
249+
if isinstance(toot['created_at'], str):
250+
toot['created_at'] = datetime.datetime.strptime(toot['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ')
251+
252+
buffered.append(toot.copy())
253+
254+
utc = pytz.utc
255+
f = feedgenerator.Rss201rev2Feed(title=param['mastodon']['title'] + ' Favourites ',
256+
link=param['mastodon']['url'] + '/web/favourites',
257+
description=param['mastodon']['description'],
258+
language=param['feed']['language'],
259+
author_name=param['feed']['author_name'],
260+
feed_url=param['feed']['feed_url'])
261+
262+
for toot in buffered:
263+
264+
text = BeautifulSoup(toot['content'], "html.parser").text
265+
pubdate = toot['created_at']
266+
if not pubdate.tzinfo:
267+
pubdate = utc.localize(pubdate).astimezone(pytz.timezone(param['feed']['timezone']))
268+
269+
if len(text) > text_length_limit:
270+
text = text[:text_length_limit] + '... '
209271
f.add_item(title=toot['account']['display_name'] + ' (' + toot['account']['username'] + '): '
210272
+ text,
211273
link=toot['url'],
212-
pubdate=utc.localize(toot['created_at']).astimezone(pytz.timezone(param['feed']['timezone'])),
274+
pubdate=pubdate,
213275
description=toot['htmltext'])
214276

215277
xml = f.writeString('UTF-8')
@@ -220,4 +282,5 @@ def tootfeed(query_feed):
220282

221283

222284
if __name__ == "__main__":
223-
app.run()
285+
app.run(use_reloader=True)
286+

config.example.yml

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ mastodon:
88
url: 'https://mastodon.social'
99
client_id_file: 'tootrss_clientcred.txt'
1010
access_token_file: 'tootrss_usercred.txt'
11+
app_name: 'tootrss' # Used to identify authenticated apps
1112
title: 'Recherche Mastodon : '
1213
description: "Résultat d'une recherche Mastodon retournée dans un flux RSS."
1314
feed:
1415
language: 'fr'
1516
author_name: ''
1617
feed_url: 'http://localhost:5000/'
1718
timezone: 'Europe/Paris'
19+
text_length_limit: 100

config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Loads and parses the configuration file.
3+
"""
4+
import yaml
5+
6+
def get_config():
7+
with open('config.yml', 'r', encoding='utf-8') as stream:
8+
try:
9+
return yaml.safe_load(stream)
10+
except yaml.YAMLError as e:
11+
print(e)
12+
sys.exit()

create_mastodon_client.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from config import get_config
2+
from mastodon import Mastodon
3+
from getpass import getpass
4+
5+
if __name__ == '__main__':
6+
print("This script helps you create a new mastodon client and log in.")
7+
print("Before we start, make sure config.yml exists.")
8+
config = get_config()
9+
10+
mast_cfg = config['mastodon']
11+
print("Configuration found.")
12+
print("Looks like you want to use this instance: ", mast_cfg['url'])
13+
print("If that's wrong, now is a good time to cancel (^C) and fix it.")
14+
input("<enter> to continue")
15+
16+
print("Registering a new app with {url} called {app_name} and saving credentials in {client_id_file}".format(**mast_cfg))
17+
18+
Mastodon.create_app(mast_cfg['app_name'], api_base_url=mast_cfg['url'], to_file=mast_cfg['client_id_file'], scopes=['read'])
19+
mastodon = Mastodon(client_id=mast_cfg['client_id_file'], api_base_url=mast_cfg['url'])
20+
print("Registration successful. Now to log in.")
21+
user_email = input("User email: ")
22+
password = getpass("Password (not shown and not saved):")
23+
24+
# Log in - either every time, or use persisted
25+
mastodon.log_in(user_email, password, to_file=mast_cfg['access_token_file'], scopes=['read'])
26+
27+
print("Verifying credentials...")
28+
try:
29+
res = mastodon.account_verify_credentials()
30+
print("Credentials look good; client reports user's account name is: " + res['acct'])
31+
print("Configuration complete; app should appear at: " + mast_cfg['url'] + "/oauth/authorized_applications")
32+
print("You should not need to log in again unless this app is removed or credentials expire.")
33+
except Exception as ex:
34+
print("Something went wrong; mastodon client reported an error:")
35+
print(ex)
36+

requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Flask==0.12.2
2+
bs4==0.0.1
3+
feedgenerator==1.9
4+
tweepy==3.5.0
5+
pytz==2017.3
6+
Mastodon.py==1.1.2
7+
PyYAML==3.12

0 commit comments

Comments
 (0)