Atomstrom/atomstrom.py

237 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python
# Atomstrom is a small but flexible feed aggregator.
# Copyright (C) 2013 Ronald Schaten <ronald@schatenseite.de>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
from models import Base, Feed, Feedinfo, Entry
from sqlalchemy import create_engine, desc, func
from sqlalchemy.orm import sessionmaker
from datetime import datetime
from ddate import ddate
import sys
import codecs
import ConfigParser
from argparse import ArgumentParser
from email.header import Header
import smtplib
import textwrap
def send_mail(sender, receiver, subject, body):
print 'sending to %s: %s' % (receiver[0], subject)
message = 'From: "%s" <%s>\n' % (Header(sender[0], 'utf-8'), sender[1])
message += 'To: "%s" <%s>\n' % (Header(receiver[0], 'utf-8'), receiver[1])
message += 'Subject: %s\n' % Header(subject, 'utf-8')
message += 'Content-Type: text/plain; charset="utf-8"\n\n'
message += body.encode('utf-8')
server = smtplib.SMTP('localhost')
server.sendmail(sender[1], [receiver[1]], message)
server.close()
def truncate_text(content, length=100, suffix='...'):
content = " ".join(content.split())
if len(content) <= length:
return content
else:
return content[:length].rsplit(' ', 1)[0]+suffix
def mail_daily_digest(session, sender, receiver, prefix):
print 'mailing daily digest...'
entries = session.query(Feed, Feedinfo, Entry).\
filter(Feed.id == Feedinfo.feed_id).\
filter(Feed.id == Entry.feed_id).\
filter(Feed.enabled == 1).\
filter(Feed.daily == 1).\
filter(Entry.sent == None).\
order_by(desc(Entry.firstfetched), Feedinfo.title, Entry.title).\
all()
body = ''
count = 0
for feed, feedinfo, entry in entries:
count = count + 1
link = entry.link
if entry.resolvedlink:
link = entry.resolvedlink
text = truncate_text(entry.get_text(enclosures=False), 250)
text = textwrap.fill(text, width=78)
try:
body += '=> %s - %s\n' % (entry.firstfetched.strftime('%Y-%m-%dT%H:%M'), feedinfo.title)
body += '> %s\n' % entry.title
body += '%s\n' % text
body += '%s\n\n' % link
except:
print 'ERROR processing entry %s' % entry.id;
print sys.exc_info()
print 'not sending mail'
return
if count > 0:
today = datetime.now()
subject = '%s (%s) - %d entries' % (today.strftime('%Y-%m-%d'), today.strftime('%A'), count)
body = '%s\n\n%s\n\n%s' % (subject, ddate(), body)
if prefix != '':
subject = '%s %s' % (prefix, subject)
send_mail(sender, receiver, subject, body)
for feed, feedinfo, entry in entries:
entry.sent = datetime.now()
else:
print 'no unmailed digest-entries found... not sending mail.'
def mail_single_entry(feed, feedinfo, entry, sender, receiver, prefix):
subject = '%s' % (entry.title)
if prefix != '':
subject = '%s %s' % (prefix, subject)
link = entry.link
if entry.resolvedlink:
link = entry.resolvedlink
body = entry.get_text()
#body = '\n'.join(textwrap.wrap(body, width=78, break_long_words=False, replace_whitespace=False, break_on_hyphens=False))
body += '\n\n%s\n%s\n' % (feedinfo.link, link)
sender[0] = feedinfo.title
send_mail(sender, receiver, subject, body)
entry.sent = datetime.now()
def mail_single_entries(session, sender, receiver, prefix):
print 'mailing single entries...'
count = 0
entries = session.query(Feed, Feedinfo, Entry).\
filter(Feed.id == Feedinfo.feed_id).\
filter(Feed.id == Entry.feed_id).\
filter(Feed.enabled == 1).\
filter(Feed.daily == 0 or Feed.daily == None).\
filter(Entry.sent == None).\
all()
for feed, feedinfo, entry in entries:
mail_single_entry(feed, feedinfo, entry, sender, receiver, prefix)
count = count + 1
if count > 0:
print 'sent %d mails' % count
else:
print 'no unmailed single entries found... not sending mail.'
def list_all_feeds(session):
allfeeds = session.query(Feed).\
order_by(Feed.id)
totalfeeds = 0
totalentries = 0
for feed in allfeeds:
print feed
totalfeeds += 1
totalentries += len(feed.entries)
print 'TOTAL: %d entries in %d feeds.' % (totalentries, totalfeeds)
def fetch_all_feeds(session):
print 'fetching all feeds...'
allfeeds = session.query(Feed).\
filter_by(enabled=1).\
order_by(Feed.id)
for feed in allfeeds:
feed.fetch(session)
print
def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
while True:
ok = raw_input(prompt)
if ok in ('y', 'ye', 'yes'):
return True
if ok in ('n', 'no'):
return False
retries = retries - 1
if retries < 0:
return False
print complaint
def feed_ask_delete(session, feed_id):
feed = session.query(Feed).\
filter(Feed.id == feed_id).\
first()
print feed
if ask_ok('> Do you really want to delete feed %d? ' % feed.id):
print 'deleting...'
session.delete(feed)
print '... done.'
def feed_ask_reset(session, feed_id):
feed = session.query(Feed).\
filter(Feed.id == feed_id).\
first()
print feed
if ask_ok('> Do you really want to reset feed %d? ' % feed.id):
print 'resetting...'
feed.reset()
print '... done.'
def main():
if (sys.stdout.encoding is None):
streamWriter = codecs.lookup('utf-8')[-1]
sys.stdout = streamWriter(sys.stdout)
config = ConfigParser.ConfigParser()
config.read('atomstrom.conf')
dbconnectstring = '%s://%s:%s@%s/%s?charset=utf8' % (
config.get('database', 'engine'),
config.get('database', 'user'),
config.get('database', 'password'),
config.get('database', 'hostname'),
config.get('database', 'database'),
)
engine = create_engine(dbconnectstring)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
#session.add(Feed('http://www.heise.de/newsticker/heise-atom.xml', 1, 0, 0, 1, 1))
#session.add(Feed('http://blog.schatenseite.de/feed/', 1, 0, 0, 1, 1))
parser = ArgumentParser(description='Fetch RSS- and Atom-feeds and send mails.')
parser.add_argument('-f', '--fetch', action='store_true', help='fetch all feeds')
parser.add_argument('-s', '--single', action='store_true', help='send single mails')
parser.add_argument('-d', '--daily', action='store_true', help='send daily digest')
parser.add_argument('-l', '--list', action='store_true', help='list all configured feeds')
parser.add_argument('-e', '--delete', action='store', type=int, metavar='ID', help='delete feed <ID> from configuration')
parser.add_argument('-r', '--reset', action='store', type=int, metavar='ID', help='reset data for feed <ID>')
args = parser.parse_args()
if args.fetch:
fetch_all_feeds(session)
if args.single:
sender = [config.get('email', 'sender_name'), config.get('email', 'sender_address')]
receiver = [config.get('email', 'receiver_name'), config.get('email', 'receiver_address')]
prefix = config.get('email', 'prefix_single')
mail_single_entries(session, sender, receiver, prefix)
if args.daily:
sender = [config.get('email', 'sender_name'), config.get('email', 'sender_address')]
receiver = [config.get('email', 'receiver_name'), config.get('email', 'receiver_address')]
prefix = config.get('email', 'prefix_digest')
mail_daily_digest(session, sender, receiver, prefix)
if args.list:
list_all_feeds(session)
if args.delete:
feed_ask_delete(session, args.delete)
if args.reset:
feed_ask_reset(session, args.reset)
if not (args.fetch or args.single or args.daily or args.list or args.delete or args.reset):
parser.print_help()
session.commit()
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-