Skip to content

Commit

Permalink
Add Mastodon service module
Browse files Browse the repository at this point in the history
Signed-off-by: Jim Easterbrook <jim@jim-easterbrook.me.uk>
  • Loading branch information
jim-easterbrook committed Aug 21, 2018
1 parent 5bfc528 commit 7f1e07e
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/doc/api_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Upload data to online "services"
:toctree: api

pywws.service.cwop
pywws.service.mastodon
pywws.service.metoffice
pywws.service.mqtt
pywws.service.openweathermap
Expand Down
1 change: 1 addition & 0 deletions src/doc/guides/integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The remaining weather service uploads are handled by modules in the :ref:`pywws.
.. autosummary::

pywws.service.cwop
pywws.service.mastodon
pywws.service.metoffice
pywws.service.mqtt
pywws.service.openweathermap
Expand Down
4 changes: 2 additions & 2 deletions src/pywws/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '18.8.0'
_release = '1563'
_commit = '46de5de'
_release = '1564'
_commit = '5bfc528'
17 changes: 17 additions & 0 deletions src/pywws/examples/templates/toot.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
media /tmp/pywws/results/tweet.png
#live#
#timezone local#
#roundtime True#
#idx "%H:%M %Z: "#
temperature #temp_out "%.1f�C" "-"#
, humidity #hum_out "%d%%" "-"#
#hourly#
, wind #wind_dir "%s" "-" "winddir_text(x)"# #wind_ave "%.1f" "-" "wind_mph(x)"#
mph ave, #wind_gust "%.1f" "-" "wind_mph(x)"#
mph gust, rain #rain "%.1f mm/hr"#
#live#
, pressure #rel_pressure "%.1f hPa "#
#hourly#
#pressure_trend "%s" "" "pressure_trend_text(x)"#

##pywws ##weather ##epsom ##ewell
7 changes: 7 additions & 0 deletions src/pywws/examples/templates/toot_forecast.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#live#
#timezone local#
#idx "%H:%M %Z: "#
#hourly#
Unreliable forecast for the next 12 hours: #calc "Zambretti(params, data)"#

##pywws ##weather ##epsom ##ewell
252 changes: 252 additions & 0 deletions src/pywws/service/mastodon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# pywws - Python software for USB Wireless Weather Stations
# http://github.com/jim-easterbrook/pywws
# Copyright (C) 2018 pywws contributors

# 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 2
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

"""Post messages to Mastodon.
Mastodon_ is a micro-blogging system that at first sight looks a bit
like Twitter. In many ways it's quite different though. This module
sends "toots", with optional image files, typically to report on weather
conditions every hour.
* Create account: https://joinmastodon.org/
* Additional dependency: https://mastodonpy.readthedocs.io/
* Example ``weather.ini`` configuration::
[mastodon]
handle = kt19weather@botsin.space
client_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
client_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
access_token = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[hourly]
text = [('toot.txt', 'L')]
plot = [('tweet.png.xml', 'L')]
services = [('mastodon', '/tmp/pywws/results/toot.txt')]
Create an account
-----------------
Before creating a Mastodon account for your weather reports you need to
choose an "instance". Mastodon is a federated system, running on many
interconnected servers, each of which is called an instance. Choose one
whose rules allow "bots" (i.e. automated posts) such as `botsin.space`_.
After creating an account, edit its profile and make sure the "this is a
bot account" box is selected.
The :py:mod:`pywws.service.mastodon` module requires you to install an
additional dependency::
sudo pip install mastodon.py
Authorise pywws
---------------
Before you can send "toots" you need to authorise pywws to post to your
account. If you run pywws on a low power device such as a Raspberry Pi,
you may find it easier to run this authorisation step on another
computer, as long as it has the required dependencies installed. You can
use an empty 'data' directory -- a ``weather.ini`` file will be created
whose contents can be copied into your real ``weather.ini`` file using
any text editor.
Make sure no other pywws software is running, then run the module with
the ``-r`` option. (Remember to replace ``data_dir`` with your data
directory.) ::
python -m pywws.service.mastodon -r data_dir
The first time you do this it will probably crash because you haven't
set your Mastodon "handle" in ``weather.ini``. Edit ``weather.ini`` and
add your handle as shown above, then run the module with the ``-r``
option again.
This will open a web browser window (or give you a URL to copy to your
web browser) where you can log in to your Mastodon account and authorise
pywws to post. If the login is successful the browser will display a
long code string which you then copy to pywws::
jim@brains:~$ python3 -m pywws.service.mastodon -r weather_data
07:45:34:pywws.logger:pywws version 18.8.0, build 1564 (5bfc528)
Please enter the auth code shown in your web browser: 12573ba24341b5de2a1f2930fc93889c3576af7b50ecd9d713fa502d773805b4
jim@brains:~$
The ``access_token`` value stored in ``weather.ini`` gives access to
your Mastodon account and should be kept confidential.
Create a template
-----------------
A "toot" is a short text of up to 500 characters. It's up to you what to
put in your "toots" but an example is included to get your started. Copy
the example template ``toot.txt`` to your template directory, then edit
it to suit your preferences. (At least change the hashtags to suit your
location.) You should also check it uses the same text encoding as your
other templates. The example template includes a ``media`` line to send
a graph. Either remove this or copy the example graph template
``tweet.png.xml`` to your graph templates directory, if you don't
already have one there, and edit the ``media`` line to use your
``local_files`` directory.
Now generate a toot file from your template, for example::
python -m pywws.template ~/weather/data ~/weather/templates/toot.txt toot.txt
cat toot.txt
Post your first toot
--------------------
Now you are ready to run :py:mod:`pywws.service.mastodon`::
python -m pywws.service.mastodon ~/weather/data toot.txt
If this works, your new Mastodon account will have posted its first
weather report. (You can delete the toot.txt file now.)
Add Mastodon posts to your hourly tasks
---------------------------------------
Edit the ``[hourly]`` section in ``weather.ini``. If your toots include
one or more graphs you need to add the graph templates to the ``plot``
list, with an ``L`` flag to store the result in your ``local_files``
directory. Note that if you reuse your Twitter graph you only need to
generate it once. Add your toot template to the ``text`` list, again
with an ``L`` flag to store the result locally. Finally, add
``mastodon`` to the ``services`` list, with an option specifying the
full path of the template processing result. For example::
[hourly]
text = [('toot.txt', 'L')]
plot = [('tweet.png.xml', 'L')]
services = [('mastodon', '/tmp/pywws/results/toot.txt')]
You could use the ``[logged]``, ``[12 hourly]`` or ``[daily]`` sections
instead, but I think ``[hourly]`` is most appropriate for Mastodon
updates.
Add more images
---------------
Mastodon allows up to four images per "toot", so you could add more
graphs, or a webcam image, or a weather forecast icon. Use one ``media``
line per image at the start of your toot template and specify the full
path for each one.
.. _botsin.space: https://botsin.space/
.. _Mastodon: https://joinmastodon.org/
"""

from __future__ import absolute_import, print_function

import codecs
from contextlib import contextmanager
from datetime import timedelta
import logging
import os
import sys

from mastodon import Mastodon

import pywws.localisation
import pywws.logger
import pywws.service
import pywws.storage

__docformat__ = "restructuredtext en"
service_name = os.path.splitext(os.path.basename(__file__))[0]
logger = logging.getLogger(__name__)


class ToService(pywws.service.FileService):
logger = logger
service_name = service_name

def __init__(self, context):
# check config
handle = context.params.get(service_name, 'handle', '')
if not handle:
raise RuntimeError('No user handle specified in weather.ini')
# get default character encoding of template output
self.encoding = context.params.get(
'config', 'template encoding', 'iso-8859-1')
# base class init
super(ToService, self).__init__(context)

@contextmanager
def session(self):
handle = self.context.params.get(service_name, 'handle')
api_base_url = handle.split('@')[-1]
access_token = self.context.params.get(service_name, 'access_token')
if not access_token:
raise RuntimeError('Authentication data not found')
yield Mastodon(access_token=access_token, api_base_url=api_base_url)

def upload_file(self, session, filename):
with codecs.open(filename, 'r', encoding=self.encoding) as toot_file:
toot = toot_file.read()
media = []
while toot.startswith('media'):
media_item, toot = toot.split('\n', 1)
media_item = media_item.split()[1]
media.append(media_item)
media_ids = []
try:
for media_item in media:
rsp = session.media_post(media_item)
media_ids.append(rsp['id'])
rsp = session.status_post(status=toot, media_ids=media_ids)
except Exception as ex:
return False, str(ex)
return True, 'OK'


def register(self):
import webbrowser

# get user handle
handle = self.context.params.get(service_name, 'handle')
api_base_url = handle.split('@')[-1]
# get client data
client_id = self.context.params.get('mastodon', 'client_id')
client_secret = self.context.params.get('mastodon', 'client_secret')
if (not client_id) or (not client_secret):
client_id, client_secret = Mastodon.create_app(
'pywws', scopes=['write'], api_base_url=api_base_url)
self.context.params.set('mastodon', 'client_id', client_id)
self.context.params.set('mastodon', 'client_secret', client_secret)
# create api
api = Mastodon(client_id=client_id, client_secret=client_secret,
api_base_url=api_base_url)
# authorise
auth_request_url = api.auth_request_url(scopes=['write'])
if not webbrowser.open(auth_request_url, new=2, autoraise=0):
print('Please use a web browser to open the following URL')
print(auth_request_url)
if sys.version_info[0] >= 3:
input_ = input
else:
input_ = raw_input
code = input_('Please enter the auth code shown in your web browser: ')
code = code.strip()
# log in
access_token = api.log_in(code=code, scopes=['write'])
self.context.params.set('mastodon', 'access_token', access_token)


if __name__ == "__main__":
sys.exit(pywws.service.main(ToService))

0 comments on commit 7f1e07e

Please sign in to comment.