start of project
This commit is contained in:
commit
284f6b2bd0
5 changed files with 226 additions and 0 deletions
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Morgan McMillian
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# feedbot
|
||||||
|
|
||||||
|
My quick and dirty cross posting script to take posts from Mastodon and cross post to pnut.io.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Report bugs, send patches to [~thrrgilag/public-inbox@lists.sr.ht](https://lists.sr.ht/~thrrgilag/public-inbox).
|
||||||
|
|
||||||
|
Discussion also in [thrrgilag's dev chat](https://thrrgilag.net/chat/dev).
|
||||||
|
|
142
feedbot.py
Normal file
142
feedbot.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import feedparser
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import pnutpy
|
||||||
|
import click
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
import models as zdb
|
||||||
|
|
||||||
|
from PIL import ImageFile
|
||||||
|
|
||||||
|
SNAP_USER_DATA = os.environ.get('SNAP_USER_DATA')
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.pass_context
|
||||||
|
@click.option('--db')
|
||||||
|
@click.option('--prime', is_flag=True)
|
||||||
|
@click.option('--debug', is_flag=True)
|
||||||
|
@click.version_option(version='0.1.0')
|
||||||
|
def main(ctx, db, prime, debug):
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
if SNAP_USER_DATA is None and db is not None:
|
||||||
|
db_file = db
|
||||||
|
else:
|
||||||
|
db_file = SNAP_USER_DATA + '/feeds.db'
|
||||||
|
|
||||||
|
zdb.db.init(db_file)
|
||||||
|
zdb.create_tables()
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
for feed in zdb.Feeds.select():
|
||||||
|
fetch(feed.url, feed.pnut_uid, feed.id, prime)
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument('username')
|
||||||
|
@click.argument('token')
|
||||||
|
def adduser(username, token):
|
||||||
|
'''Add a pnut user'''
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
pnut_user, meta = pnutpy.api.get_user('me')
|
||||||
|
|
||||||
|
user = zdb.User(pnut_uid=pnut_user.id, pnut_token=token, pnut_enabled=True)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException:
|
||||||
|
logging.error(f"pnut user token not valid")
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument('uid')
|
||||||
|
@click.argument('url')
|
||||||
|
def add(uid, url):
|
||||||
|
'''Add a feed'''
|
||||||
|
feed = zdb.Feeds(pnut_uid=uid, url=url)
|
||||||
|
feed.save()
|
||||||
|
|
||||||
|
def fetch(url, pnut_uid, fid, prime):
|
||||||
|
feed = feedparser.parse(url)
|
||||||
|
try:
|
||||||
|
user = zdb.User.get(pnut_uid=pnut_uid)
|
||||||
|
|
||||||
|
source = {
|
||||||
|
'link': feed.feed.link,
|
||||||
|
'username': feed.feed.title,
|
||||||
|
'avatar': feed.feed.image.href
|
||||||
|
}
|
||||||
|
|
||||||
|
for post in reversed(feed.entries):
|
||||||
|
link = post.link
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = zdb.Entries.get(feedid=fid, link=link)
|
||||||
|
logging.debug(f"skipping {link}...")
|
||||||
|
|
||||||
|
except zdb.Entries.DoesNotExist:
|
||||||
|
entry = zdb.Entries(feedid=fid, link=link)
|
||||||
|
entry.save()
|
||||||
|
if prime:
|
||||||
|
logging.debug(f"saving {link}...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.debug(f"posting {link}...")
|
||||||
|
pnutpost(post, source, user.pnut_token)
|
||||||
|
|
||||||
|
except zdb.User.DoesNotExist:
|
||||||
|
logging.error(f"user {pnut_uid} not found")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception("bad stuff")
|
||||||
|
|
||||||
|
def pnutpost(entry, source, token):
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
crosspost = {
|
||||||
|
'type': "io.pnut.core.crosspost",
|
||||||
|
'value': {
|
||||||
|
'canonical_url': entry.link,
|
||||||
|
#'source': {'url': source['link']},
|
||||||
|
'user': {
|
||||||
|
'username': source['username'],
|
||||||
|
'avatar_image': source['avatar']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw = [crosspost]
|
||||||
|
for link in entry.links:
|
||||||
|
if link.rel == "enclosure":
|
||||||
|
if "image" in link.type:
|
||||||
|
raw.append(embed_image(link))
|
||||||
|
|
||||||
|
try:
|
||||||
|
rx = re.compile('<.*?>')
|
||||||
|
text = re.sub(rx, '', entry.summary)
|
||||||
|
p, meta = pnutpy.api.create_post(data={'text': text, 'raw': raw})
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception("bad stuff")
|
||||||
|
|
||||||
|
def embed_image(link):
|
||||||
|
resume_header = {'Range': 'bytes=0-2000000'}
|
||||||
|
r = requests.get(link.href, stream=True, headers=resume_header)
|
||||||
|
|
||||||
|
p = ImageFile.Parser()
|
||||||
|
p.feed(r.content)
|
||||||
|
if p.image:
|
||||||
|
width, height = p.image.size
|
||||||
|
embed = {
|
||||||
|
'version': "1.0",
|
||||||
|
'type': "photo",
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'url': link.href
|
||||||
|
}
|
||||||
|
return {'type': "io.pnut.core.oembed", 'value': embed}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
29
models.py
Normal file
29
models.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from peewee import *
|
||||||
|
|
||||||
|
db = SqliteDatabase(None)
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
|
||||||
|
class Feeds(BaseModel):
|
||||||
|
url = CharField()
|
||||||
|
pnut_uid = CharField()
|
||||||
|
|
||||||
|
class Entries(BaseModel):
|
||||||
|
feedid = IntegerField()
|
||||||
|
link = CharField()
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
pnut_uid = CharField(unique=True)
|
||||||
|
pnut_token = CharField(null=True)
|
||||||
|
pnut_enabled = BooleanField(default=False)
|
||||||
|
|
||||||
|
class System(BaseModel):
|
||||||
|
key = CharField(unique=True)
|
||||||
|
value = CharField()
|
||||||
|
|
||||||
|
def create_tables():
|
||||||
|
with db:
|
||||||
|
db.create_tables([Feeds, Entries, User, System])
|
||||||
|
|
23
setup.py
Normal file
23
setup.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='feedbot',
|
||||||
|
version='0.1.0',
|
||||||
|
py_modules=[
|
||||||
|
'feedbot',
|
||||||
|
],
|
||||||
|
install_requires=[
|
||||||
|
'requests',
|
||||||
|
'pnutpy',
|
||||||
|
'click',
|
||||||
|
'feedparser',
|
||||||
|
'peewee',
|
||||||
|
'pillow',
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'feedbot = feedbot:main',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue