Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrix reporter #421

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Expand Up @@ -42,6 +42,7 @@ Optional dependencies (install via `python3 -m pip install <packagename>`):

* Pushover reporter: [chump](https://github.com/karanlyons/chump/)
* Pushbullet reporter: [pushbullet.py](https://github.com/randomchars/pushbullet.py)
* Matrix reporter: [matrix_client](https://github.com/matrix-org/matrix-python-sdk), [markdown2](https://github.com/trentm/python-markdown2)
* Stdout reporter with color on Windows: [colorama](https://github.com/tartley/colorama)
* "browser" job kind: [requests-html](https://html.python-requests.org)
* Unit testing: [pycodestyle](http://pycodestyle.pycqa.org/en/latest/)
Expand Down Expand Up @@ -250,6 +251,43 @@ a channel, you'll get a webhook URL, copy it into the configuration as seen abov
You can use the command `urlwatch --test-slack` to test if the Slack integration works.


MATRIX
------

You can have notifications sent to you through the Matrix protocol.

To achieve this, you first need to register a Matrix account for the bot on any homeserver.

You then need to acquire an access token and room ID, using the following instructions adapted from [this guide](https://t2bot.io/docs/access_tokens/):

1. Open [Riot.im](https://riot.im/app/) in a private browsing window
2. Register/Log in as your bot, using its user ID and password.
3. Set the display name and avatar, if desired.
4. In the settings page, scroll down to the bottom and click Access Token: \<click to reveal\>.
5. Copy the highlighted text to your configuration.
6. Join the room that you wish to send notifications to.
7. Go to the Room Settings (gear icon) and copy the *Internal Room ID* from the bottom.
8. Close the private browsing window **but do not log out, as this invalidates the Access Token**.

Here is a sample configuration:

```yaml
matrix:
homeserver: https://matrix.org
access_token: "YOUR_TOKEN_HERE"
room_id: "!roomroomroom:matrix.org"
```

You will probably want to use the following configuration for the `markdown` reporter, if you intend to post change
notifications to a public Matrix room, as the messages quickly become noisy:

```yaml
markdown:
details: false
footer: false
minimal: true
```

BROWSER
-------

Expand Down
119 changes: 118 additions & 1 deletion lib/urlwatch/reporters.py
Expand Up @@ -57,8 +57,18 @@
except ImportError:
Pushbullet = None

logger = logging.getLogger(__name__)
try:
import matrix_client.api
except ImportError:
matrix_client = None

try:
# markdown2 is an optional dependency which provides better formatting for Matrix.
from markdown2 import Markdown
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This optional dependency should be mentioned in the README.md file.

except ImportError:
Markdown = None

logger = logging.getLogger(__name__)

# Regular expressions that match the added/removed markers of GNU wdiff output
WDIFF_ADDED_RE = r'[{][+].*?[+][}]'
Expand Down Expand Up @@ -614,3 +624,110 @@ def submit_to_slack(self, webhook_url, text):

def chunkstring(self, string, length):
return (string[0 + i:length + i] for i in range(0, len(string), length))


class MarkdownReporter(ReporterBase):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the MarkdownReporter share some/more code with the TextReporter? Seems like there's some code duplication there now.

def submit(self):
cfg = self.report.config['report']['markdown']
show_details = cfg['details']
show_footer = cfg['footer']

if cfg['minimal']:
for job_state in self.report.get_filtered_job_states(self.job_states):
pretty_name = job_state.job.pretty_name()
location = job_state.job.get_location()
if pretty_name != location:
location = '%s (%s)' % (pretty_name, location)
yield '* ' + ': '.join((job_state.verb.upper(), location))
return

summary = []
details = []
for job_state in self.report.get_filtered_job_states(self.job_states):
summary_part, details_part = self._format_output(job_state)
summary.extend(summary_part)
details.extend(details_part)

if summary:
yield from ('%d. %s' % (idx + 1, line) for idx, line in enumerate(summary))
yield ''

if show_details:
yield from details

if summary and show_footer:
yield from ('--- ',
'%s %s, %s ' % (urlwatch.pkgname, urlwatch.__version__, urlwatch.__copyright__),
'Website: %s ' % (urlwatch.__url__,),
'watched %d URLs in %d seconds' % (len(self.job_states), self.duration.seconds))

def _format_content(self, job_state):
if job_state.verb == 'error':
return job_state.traceback.strip()

if job_state.verb == 'unchanged':
return job_state.old_data

if job_state.old_data in (None, job_state.new_data):
return None

return self.unified_diff(job_state)

def _format_output(self, job_state):
summary_part = []
details_part = []

pretty_name = job_state.job.pretty_name()
location = job_state.job.get_location()
if pretty_name != location:
location = '%s (%s)' % (pretty_name, location)

pretty_summary = ': '.join((job_state.verb.upper(), pretty_name))
summary = ': '.join((job_state.verb.upper(), location))
content = self._format_content(job_state)

summary_part.append(pretty_summary)

details_part.append('### ' + summary)
if content is not None:
details_part.extend(('', '```', content, '```', ''))
details_part.extend(('', ''))

return summary_part, details_part


class MatrixReporter(MarkdownReporter):
"""Custom Matrix reporter"""
MAX_LENGTH = 4096

__kind__ = 'matrix'

def submit(self):
homeserver_url = self.config['homeserver']
access_token = self.config['access_token']
room_id = self.config['room_id']

body_markdown = '\n'.join(super().submit())

if not body_markdown:
logger.debug('Not calling Matrix API (no changes)')
return

client_api = matrix_client.api.MatrixHttpApi(homeserver_url, access_token)

if Markdown is not None:
body_html = Markdown().convert(body_markdown)

client_api.send_message_event(
room_id,
"m.room.message",
content={
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": body_markdown,
"formatted_body": body_html
}
)
else:
logger.debug('Not formatting as Markdown; dependency on markdown2 not met?')
client_api.send_message(room_id, body_markdown)
12 changes: 12 additions & 0 deletions lib/urlwatch/storage.py
Expand Up @@ -59,6 +59,12 @@
'minimal': False,
},

'markdown': {
'details': True,
'footer': True,
'minimal': False,
},

'html': {
'diff': 'unified', # "unified" or "table"
},
Expand Down Expand Up @@ -106,6 +112,12 @@
'enabled': False,
'webhook_url': '',
},
'matrix': {
'enabled': False,
'homeserver': '',
'access_token': '',
'room_id': '',
},
'mailgun': {
'enabled': False,
'region': 'us',
Expand Down