Compare commits

..

66 commits
master ... main

Author SHA1 Message Date
76e25393d0 Add wrappers 2025-04-13 09:36:48 +03:00
24d68684de Remove json 2025-02-19 18:44:23 +02:00
ee600178c4 strip response 2025-02-18 15:10:00 +02:00
705dfe7196 translate to html 2025-02-18 14:46:41 +02:00
b185b02073 recursive translate 2025-02-18 14:21:51 +02:00
5ba3c0db27 add comments for both 2025-02-18 11:50:06 +02:00
a63aa1fd9c add comment 2025-02-18 11:04:36 +02:00
326e86e088 separate divs 2025-02-18 11:03:03 +02:00
62ad4febb1 show raw data 2025-02-18 11:00:50 +02:00
ffdb262f94 add divs for texts 2025-02-17 16:47:24 +02:00
a266db1a7e add debug info 2025-02-17 15:45:14 +02:00
bdda8d55b8 add default 2025-02-17 15:40:30 +02:00
ed16a1838b add debug info 2025-02-17 15:30:48 +02:00
ce95bbe185 add more debug data 2025-02-17 15:28:08 +02:00
43b426212c add info to data 2025-02-17 15:25:37 +02:00
c75297324e fix binding 2025-02-17 15:22:54 +02:00
afbed1f86b fix binding 2025-02-17 15:22:00 +02:00
771967c905 translate reblog 2025-02-17 15:21:09 +02:00
57f3a0f36c Fix language style 2025-02-17 14:40:53 +02:00
8dc1e20d1b add counter 2025-02-17 14:32:49 +02:00
aa3340f4d7 add comments 2025-02-17 14:31:24 +02:00
f5b07072ad check for existing record before inserting 2025-02-17 14:28:32 +02:00
07a75b73cc add translated text 2025-02-17 14:17:13 +02:00
7c438f8e78 Add translated texts 2025-02-17 14:15:10 +02:00
5e872b7288 translate replies and reblogs 2025-02-17 10:39:13 +02:00
cd171f6230 fix error handling 2025-02-17 10:37:16 +02:00
1d81c15582 handle none language 2025-02-17 10:32:07 +02:00
699ea790fa add debug 2025-02-17 10:30:32 +02:00
faa411f02a handle error translating 2025-02-17 10:29:50 +02:00
b49ed95f05 don't translate empty string 2025-02-17 10:26:11 +02:00
6e4283ad26 remove extra slash 2025-02-17 10:25:20 +02:00
83da7e70f9 change debug 2025-02-17 10:24:41 +02:00
1bc0120066 add debug 2025-02-17 10:22:20 +02:00
c4e7e0e90c remove check for dupes 2025-02-17 09:56:21 +02:00
642b57c5b5 change debug 2025-02-17 09:55:50 +02:00
6221ed0240 add debug 2025-02-17 09:54:19 +02:00
a7c0af726c add debug 2025-02-17 09:51:30 +02:00
70bd2fbb70 add debug 2025-02-17 09:50:46 +02:00
35e9503b06 remove extra debug 2025-02-17 09:49:18 +02:00
4bfe099883 fix headers 2025-02-17 09:47:58 +02:00
11a84f9180 Fix if 2025-02-17 09:35:46 +02:00
7c0490a352 add debug info 2025-02-16 16:03:31 +02:00
5943d22609 remove subject field 2025-02-16 16:01:42 +02:00
f2af0b89f0 Add translation 2025-02-16 15:52:09 +02:00
d5aac011a1 Add config for folders 2024-09-20 11:08:51 +03:00
273200f8a4 Add date field 2024-09-19 20:11:36 +03:00
e3aaba8483 Add default media object to template 2024-09-03 13:20:58 +03:00
a6ec889b11 Fix subject template 2024-08-30 19:42:41 +03:00
92c40adcd7 Add template subject 2024-08-10 18:03:31 +03:00
83c23ffe69 Fix replies template 2024-08-04 20:01:21 +03:00
0b8fae74e4 Increase version 2024-08-04 09:06:48 +03:00
5ed3568436 Format date 2024-08-04 09:06:35 +03:00
e453127ddb Add import in jinja2 2024-08-04 08:52:38 +03:00
f5a3021d81 update template 2024-08-04 08:25:59 +03:00
fbe512d549 Adjust video 2024-08-03 23:11:34 +03:00
d75cc3e904 Fix sender template 2024-08-03 15:42:58 +03:00
f3c2d0acf6 Update help 2024-08-03 15:13:50 +03:00
9e734706f7 Increase version 2024-08-03 15:11:50 +03:00
962c12f6b0 Add replies from reblogs and use jinja2 template for sender and recipient 2024-08-03 15:11:24 +03:00
ca0a09c7ba Update template 2024-08-03 12:39:53 +03:00
e4c8c916b7 Increase wait between requests to 1min 2024-08-03 12:39:44 +03:00
78390c4140 Update readme 2024-08-03 10:32:03 +03:00
b39cb7b5e5 Increase version 2024-08-03 10:27:22 +03:00
783eecd9a2 Allow to set requests limit 2024-08-03 10:27:05 +03:00
0de622561b Add info about templates 2024-08-02 20:16:34 +03:00
6c589a7dca Fix templates in virtualenv 2024-08-02 20:07:20 +03:00
13 changed files with 657 additions and 178 deletions

6
MANIFEST.in Normal file
View file

@ -0,0 +1,6 @@
graft templates
include LICENSE
include README.md
include podman_build.sh
include podman_run.sh
include Dockerfile

View file

@ -1,23 +1,59 @@
# mastodon_email_bridge
Simple script to forward your Mastodon Home timeline to your email.
## Requirements
You need to obtain an application token with read access and provide it with the --token parameter.
Check the requirements.txt file but the installation should take care of everything.
## Installation
### Linux
```bash
sudo python3 setup.py install
```
### Windows (from PowerShell)
```powershell
& $(where.exe python).split()[0] setup.py install
python -m venv "${HOME}/pyenvs/mastodon_email_bridge"
source "${HOME}/pyenvs/mastodon_email_bridge/bin/activate"
pip install .
mkdir -p "${HOME}/.config/mastodon_email_bridge"
cp -r templates "${HOME}/.config/mastodon_email_bridge/"
```
## Usage
```bash
mastodon_email_bridge.py [--debug-level|-d CRITICAL|ERROR|WARNING|INFO|DEBUG|NOTSET] # Other parameters
Customize the templates (if you know HTML, CSS and Jinja2) and run the command.
```
Usage: mastodon_email_bridge.py [OPTIONS]
Options:
-d, --debug-level [CRITICAL|ERROR|WARNING|INFO|DEBUG|NOTSET]
Set the debug level for the standard output.
-t, --token TEXT Mastodon token with read access. [required]
-s, --server TEXT Mastodon server full qualified name.
-L, --limit INTEGER Mastodon token with read access.
-R, --limit-per-request INTEGER
Mastodon token with read access.
-w, --wait INTEGER Seconds to wait between requests to avoid
rate limits.
-r, --recipient TEXT Recipient email to get the posts. This can be a Jinja2 template.
[required]
-S, --sender TEXT Sender email thant send the posts. This can be a Jinja2 template.
-f, --sent-items-file TEXT File to store the IDs of post already sent
by email.
-m, --mail-server TEXT SMTP Mail server to send emails.
-u, --mail-user TEXT Username for SMTP Mail server to send
emails.
-P, --mail-pass TEXT User password for SMTP Mail server to send
emails.
-p, --mail-server-port INTEGER SMTP Mail server port to send emails.
-t, --subjet-template TEXT Jinja2 template for the subject of the
emails.
-l, --log-file TEXT File to store all debug messages.
--config FILE Read configuration from FILE.
--help Show this message and exit.
```
## Notes
- Clean the folder ~/.mastodon_email_bridge_sent_items every now and then, but you can check the generated HTML files to test new templates.
- The posts that have been sent by email are stored in an SQLite3 database in ~/.mastodon_email_bridge_sent_items.db if you want a post to be sent again you can remove it from there and run again the script.

29
install.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
destination="/usr/local/bin"
while [ $# -gt 0 ]
do
case "$1" in
"--help"|"-h"|"-?")
usage
exit 0
;;
"--destination"|"-d")
shift
destination="${1}"
shift
;;
*)
echo "Ignoring unknwon parameter '${1}'"
shift
;;
esac
done
if [ ! -e "${HOME}/.config/mastodon_email_bridge.conf" ]; then
touch "${HOME}/.config/mastodon_email_bridge.conf"
fi
chmod go-rwx "${HOME}/.config/mastodon_email_bridge.conf"
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
sed "s#__src_folder__#${script_dir}#g" wrapper.sh > "${destination}/mastodon_email_bridge.sh"
chmod +x "${destination}/mastodon_email_bridge.sh"

10
mastodon_email_bridge.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ ! -d "${script_dir}/.venv" ]; then
python -m venv "$script_dir/.venv"
fi
# shellcheck disable=1091
source "$script_dir/.venv/bin/activate"
pip install -r "$script_dir/requirements.txt" > /dev/null
pip install "$script_dir/" > /dev/null
mastodon_email_bridge.py "${@}"

View file

@ -15,10 +15,11 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import sqlite3
import importlib
import click
import click_config_file
import requests
from jinja2 import Environment, PackageLoader, select_autoescape
from jinja2 import Environment, select_autoescape, FileSystemLoader
class MastodonEmailBridge:
'''CLass to redirect the Mastodon home timeline to email'''
@ -49,19 +50,46 @@ class MastodonEmailBridge:
'.mastodon_email_bridge_sent_items.db'
)
self._init_log()
self.last_translation_response = None
self._get_sent_posts()
if 'templates_folder' in self.config:
templates_folder=self.config['templates_folder']
else:
templates_folder = os.path.join(
os.environ.get(
'HOME',
os.environ.get(
'USERPROFILE',
os.getcwd()
)
),
'.config',
'mastodon_email_bridge',
'templates'
)
if not os.path.exists(templates_folder):
os.mkdir(templates_folder)
self.j2env = Environment(
loader=PackageLoader("mastodon_email_bridge"),
#loader=PackageLoader("mastodon_email_bridge"),
loader=FileSystemLoader(templates_folder, followlinks=True),
autoescape=select_autoescape()
)
self.translate_session = requests.Session()
headers = {
'accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
}
self.translate_session.headers.update(headers)
self.session = requests.Session()
self.session.headers.update({'Authorization': f"Bearer {self.config['token']}"})
count=1
self._log.debug(
"Getting URL 'https://%s/api/v1/timelines/home?limit=1'",
self.config['server']
"Getting URL 'https://%s/api/v1/timelines/home?limit=%s'",
self.config['server'],
self.config['limit_per_request']
)
next_url = f"https://{self.config['server']}/api/v1/timelines/home?limit=1"
next_url = f"https://{self.config['server']}/api/v1/timelines/home?limit={self.config['limit_per_request']}"
next_url = self.process_url(next_url)
while next_url != "" and (count < self.config['limit'] or self.config['limit'] == 0):
count = count + 1
@ -72,6 +100,9 @@ class MastodonEmailBridge:
def _get_sent_posts(self):
self.sent_items = []
self._log.debug(
"Reading sent items from database..."
)
self.sqlite = sqlite3.connect(self.config['sent_items_file'])
self.sqlite.row_factory = sqlite3.Row
cur = self.sqlite.cursor()
@ -80,11 +111,15 @@ class MastodonEmailBridge:
res = cur.execute("SELECT id FROM sent_items")
rows = res.fetchall()
for row in rows:
if row[0] in self.sent_items:
self._log.warning("Duplicate id in database '%s'", row[0])
else:
self._log.debug("Found sent item with id '%s' (%s)", row[0], type(row[0]))
# if row[0] in self.sent_items:
# self._log.warning("Duplicate id in database '%s'", row[0])
# else:
# self._log.debug("Found sent item with id '%s' (%s)", row[0], type(row[0]))
self.sent_items.append(row[0])
self._log.debug(
"Got %s sent items from database",
len(self.sent_items)
)
return True
def process_url(self, url):
@ -93,7 +128,8 @@ class MastodonEmailBridge:
result = self.session.get(url)
# for header in result.headers:
# self._log.debug("%s = %s", header, result.headers[header])
if 'X-RateLimit-Remaining' in result.headers and int(result.headers['X-RateLimit-Remaining']) < 10:
if ('X-RateLimit-Remaining' in result.headers
and int(result.headers['X-RateLimit-Remaining']) < 10):
self._log.warning("X-RateLimit-Reset: %s", result.headers['X-RateLimit-Reset'])
try:
reset_time = time.mktime(
@ -111,44 +147,79 @@ class MastodonEmailBridge:
error
)
return url
data = result.json()[0]
if int(data['id']) not in self.sent_items:
self.send_mail(data)
else:
self._log.debug("Skipping post %s that was sent in the past", data['id'])
if 'Link' in result.headers:
for link in result.headers['Link'].split(', '):
slink = link.split('; ')
if slink[1] == 'rel="next"':
next_url = slink[0].replace('<', '').replace('>', '')
for data in result.json():
data['meb_reply_to'] = []
if data['in_reply_to_id']:
self._log.debug(
"This post is a reply to '%s', fetching it",
data['in_reply_to_id']
)
data['meb_reply_to'].append(self._translate_data(self.get_post(data['in_reply_to_id'])))
if data['reblog'] and data['reblog']['in_reply_to_id']:
data['reblog'] = self._translate_data(data['reblog'])
self._log.debug(
"This post is a reblog of a reply to '%s', fetching it",
data['reblog']['in_reply_to_id']
)
data['meb_reply_to'].append(self._translate_data(self.get_post(data['reblog']['in_reply_to_id'])))
data = self._translate_data(data)
if int(data['id']) not in self.sent_items:
self.send_mail(data)
else:
self._log.debug("Skipping post %s that was sent in the past", data['id'])
if 'Link' in result.headers:
for link in result.headers['Link'].split(', '):
slink = link.split('; ')
if slink[1] == 'rel="next"':
next_url = slink[0].replace('<', '').replace('>', '')
return next_url
def get_post(self, post_id):
'''Get a single post'''
post = {}
self._log.debug(
"Getting post URL 'https://%s/api/v1/statuses/%s'...",
self.config['server'],
post_id
)
result = self.session.get(f"https://{self.config['server']}/api/v1/statuses/{post_id}")
post = result.json()
return post
def send_mail(self, data):
'''Send an email with the post composed'''
sender = self._str_template(self.config['sender'], data)
recipient = self._str_template(self.config['recipient'], data)
msg = MIMEMultipart('alternative')
msg['Subject'] = f"FediPost from {data['account']['display_name']} ({data['account']['username']})"
msg['From'] = self.config['sender']
msg['To'] = self.config['recipient']
msg['Subject'] = self._str_template(self.config['subjet_template'], data)
msg['From'] = sender
msg['Date'] = time.strftime('%a, %d %b %Y %H:%M:%S %z')
msg['To'] = recipient
html_template = self.j2env.get_template("new_post.html.j2")
html_source = html_template.render(
imp0rt = importlib.import_module,
data=data,
json_raw=json.dumps(data, indent=2)
)
txt_template = self.j2env.get_template("new_post.txt.j2")
txt_source = txt_template.render(
imp0rt = importlib.import_module,
data=data,
json_raw=json.dumps(data, indent=2)
)
sent_folder = os.path.join(
os.environ.get(
'HOME',
if 'sent_folder' not in self.config:
sent_folder = os.path.join(
os.environ.get(
'USERPROFILE',
os.getcwd()
)
),
'.mastodon_email_bridge_sent_items'
)
'HOME',
os.environ.get(
'USERPROFILE',
os.getcwd()
)
),
'.mastodon_email_bridge_sent_items'
)
else:
sent_folder = self.config['sent_folder']
if not os.path.exists(sent_folder):
os.mkdir(sent_folder)
sent_file = os.path.join(sent_folder, data['id'] + '.html')
@ -162,15 +233,101 @@ class MastodonEmailBridge:
if self.config['mail_user'] is not None:
conn.login(self.config['mail_user'], self.config['mail_pass'])
self._log.debug("Sending email for post with id '%s'...", data['id'])
conn.sendmail(self.config['sender'], self.config['recipient'], msg.as_string())
conn.sendmail(sender, recipient, msg.as_string())
conn.quit()
self._log.debug("Adding entry to database...")
cur = self.sqlite.cursor()
cur.execute(f"INSERT INTO sent_items (id, date) VALUES ({data['id']}, {time.time()})")
self.sqlite.commit()
res = cur.execute("SELECT id FROM sent_items WHERE id = ?", [(data['id'])])
rows = res.fetchall()
if len(rows) == 0:
cur.execute(f"INSERT INTO sent_items (id, date) VALUES ({data['id']}, {time.time()})")
self.sqlite.commit()
else:
self._log.warning(
"There was at least one record already with the same id %s, check if another instance of this script is running.",
data['id']
)
self.sent_items.append(data['id'])
return True
def _str_template(self, template_string, data):
template = self.j2env.from_string(template_string)
result = template.render(data=data)
return result
def _translate_data(self, data):
if self.config['libretranslate_url'] is None or self.config['libretranslate_url'] == '':
self._log.debug(
"Not translating data because no LibreTranslate URL was specified"
)
return data
new_data = data
if (
'language' not in data or
data['language'] is None
):
source_language = 'auto'
else:
source_language = data['language']
data['source_language'] = source_language
data['destination_language'] = self.config['destination_language']
if source_language in self.config['not_translate_language']:
return data
fields_to_translate = [
'spoiler',
'content',
'description'
]
counter = 0
for field in fields_to_translate:
if field in data:
counter += 1
new_data[f"translated_{field}"] = self._translate(
data[field],
source_language=source_language
)
new_data[f"translated_{field}_response"] = self.last_translation_response
self._log.debug(
"Total of %s fields translated",
counter
)
for item in data:
if isinstance(item, dict):
item = self._translate_data(item)
return new_data
def _translate(self, text, source_language='auto', destination_language=None):
if text == '':
return ''
if destination_language is None:
destination_language = self.config['destination_language']
data = {
"q": text,
"source": source_language,
"target": destination_language,
"api_key": self.config['libretranslate_token'],
"format": "html",
}
response = self.translate_session.post(
url=f"{self.config['libretranslate_url']}",
data=data,
)
self.last_translation_response = None
try:
self.last_translation_response = response.json()
translation = self.last_translation_response['translatedText'].strip(' ').strip('\n')
except Exception as error:
self._log.error(
"Error translating '%s' from '%s' to '%s'. %s. Response content: %s",
text,
source_language,
destination_language,
error,
response.content,
)
return text
return translation
def _init_log(self):
''' Initialize log object '''
self._log = logging.getLogger("mastodon_email_bridge")
@ -230,13 +387,24 @@ class MastodonEmailBridge:
help='Mastodon server full qualified name.'
)
@click.option('--limit', '-L', default=0, help='Mastodon token with read access.')
@click.option('--wait', '-w', default=5, help='Seconds to wait between requests to avoid rate limits.')
@click.option('--recipient', '-r', required=True, help='Recipient email to get the posts.')
@click.option('--limit-per-request', '-R', default=40, help='Mastodon token with read access.')
@click.option(
'--wait',
'-w',
default=60,
help='Seconds to wait between requests to avoid rate limits.'
)
@click.option(
'--recipient',
'-r',
required=True,
help='Recipient email to get the posts. This can be a Jinja2 template.'
)
@click.option(
'--sender',
'-S',
default='mastodon_email_bridge@example.org',
help='Sender email thant send the posts.'
help='Sender email thant send the posts. This can be a Jinja2 template.'
)
@click.option(
'--sent-items-file',
@ -263,6 +431,46 @@ class MastodonEmailBridge:
default=465,
help='SMTP Mail server port to send emails.'
)
@click.option(
'--subjet-template',
'-t',
default='{{ data["account"]["display_name"] }} ({{ data["account"]["username"] }}) {% if data["in_reply_to_id"] %}replied {% else %}posted{% endif %}{% for tag in data["tags"] %} #{{ tag["name"] }}{% endfor %}',
help='Jinja2 template for the subject of the emails.'
)
@click.option(
'--sent-folder',
'-F',
help='Folder to store generated HTML files to be sent.'
)
@click.option(
'--templates-folder',
'-T',
help='Folder with the templates to generate HTML and text files to be sent.'
)
@click.option(
'--libretranslate-url',
'-U',
help='LibreTranslate instance URL to use like: https://translate.example.org/translate'
)
@click.option(
'--libretranslate-token',
'-o',
default='',
help='LibreTranslate token to use'
)
@click.option(
'--not-translate-language',
'-n',
multiple=True,
help='Languages that you don\'t want to translate with LibreTranslate'
)
@click.option(
'--destination-language',
'-D',
default='en',
help='Language destination for the translations'
)
@click.option('--log-file', '-l', help="File to store all debug messages.")
# @click.option("--dummy","-n", is_flag=True,
# help="Don't do anything, just show what would be done.")

View file

@ -1,121 +0,0 @@
<!doctype html>
<html lang="{{ data['language'] }}">
<head>
<meta charset="utf-8"/>
</head>
<style>
body { background-color: black; color: #999;}
p { }
div { margin: 1%; }
/* unvisited link */
a:link {
color: blueviolet;
}
/* visited link */
a:visited {
color: #040;
}
/* mouse over link */
a:hover {
color: hotpink;
}
/* selected link */
a:active {
color: blue;
}
</style>
<BODY>
<!-- account bloc -->
<DIV>
<A HREF="{{ data['account']['url'] }}" TARGET="_blank"></A>
<IMG ALT="{{ data['account']['display_name'] }} avatar image" SRC="{{ data['account']['avatar_static'] }}" STYLE="width:64px;height:64px;margin:1%;float: left;">
<B>{{ data['account']['display_name'] }} ({{ data['account']['username'] }})</B>
</A>
</DIV>
<!-- creation_date -->
<DIV STYLE='font-size: 0.75em;'>
{{ data['created_at'] }}
</DIV>
<!-- content block -->
<DIV STYLE='font-size: 1.5em;'>
<!-- spoiler -->
<DIV CLASS='item-spoiler'>
{{ data['spoiler'] }}
</DIV>
<!-- item-content -->
<DIV CLASS='item-content' STYLE="margin:5%;">
{{ data['content'] }}
<!-- media -->
{% if data['media_attachments'] %}
{% for media in data['media_attachments'] %}
<DIV STYLE="margin:2%;">
{% if media['type'] == 'image' %}
<IMG SRC="{{ media['preview_url'] }}" ALT="{{ media['description'] }}">
{% elif media['type'] == 'video' %}
<video controls width="100%">
<source src="{{ media['url'] }}" type="video/webm" />
<A HREF="{{ media['url'] }}">Download video</A>
</video>
{% elif media['type'] == 'audio' %}
<audio controls src="{{ media['url'] }}"></audio>
<A HREF="{{ media['url'] }}">Download audio</A>
{% endif %}
</DIV>
{% endfor %}
{% endif %}
{% if data['reblog'] %}
<!-- reblog -->
<DIV STYLE="margin:5%;">
<!-- reblog-account -->
<DIV>
<A HREF="{{ data['reblog']['account']['url'] }}" TARGET="_blank"></A>
<IMG ALT="{{ data['reblog']['account']['display_name'] }} avatar image" SRC="{{ data['reblog']['account']['avatar_static'] }}" STYLE='width:64px;height:64px;margin:1%;float: left;'>
<B>{{ data['reblog']['account']['display_name'] }} ({{ data['reblog']['account']['username'] }})</B>
</A>
</DIV>
<!-- reblog_creation_date -->
<DIV STYLE='font-size: 0.75em;'>
{{ data['reblog']['created_at'] }}
</DIV>
<!-- reblog_content_bloc -->
<DIV STYLE='font-size: 1.5em;'>
<!-- reblog_spoiler -->
<DIV CLASS='reblog-spoiler'>
{{ data['reblog']['spoiler'] }}
</DIV>
<!-- reblog_content -->
<DIV CLASS='reblog-content'>
{{ data['reblog']['content'] }}
<!-- media -->
{% if data['reblog']['media_attachments'] %}
{% for media in data['reblog']['media_attachments'] %}
{% if media['type'] == 'image' %}
<IMG SRC="{{ media['preview_url'] }}" ALT="{{ media['description'] }}">
{% elif media['type'] == 'video' %}
<video controls width="100%">
<source src="{{ media['url'] }}" type="video/webm" />
<A HREF="{{ media['url'] }}">Download video</A>
</video>
{% elif media['type'] == 'audio' %}
<audio controls src="{{ media['url'] }}"></audio>
<A HREF="{{ media['url'] }}">Download audio</A>
{% endif %}
{% endfor %}
{% endif %}
</DIV>
</DIV>
</DIV>
{% endif %}
</DIV>
</DIV>
<!-- URL -->
<DIV>
<A TARGET="_blank" HREF="{{ data['url'] }}">Post original page</A>
</DIV>
{# <!-- card -->{{ data['card'] }} #}
<!-- Raw JSON data -->
<DIV STYLE="margin-top:15%;font-size:0.75em;">
Raw JSON data:
<PRE>{{ json_raw }}</PRE>
</DIV>
</BODY>

View file

@ -3,12 +3,12 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project.urls]
Homepage = ""
Homepage = "https://codeberg.org/adelgado/mastodon_email_bridge"
[project]
name = "mastodon_email_bridge"
version = "0.0.1"
description = "Redirect the home timeline to email"
version = "1.0.1"
description = "Redirect your Mastodon Home timeline to your email"
readme = "README.md"
authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }]
license = { file = "LICENSE" }
@ -17,9 +17,11 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
#keywords = ["vCard", "contacts", "duplicates"]
keywords = ["Mastodon", "email", "fediverse", "ActivityPub"]
dependencies = [
"click",
"click_config_file",
"requests",
"jinja2",
]
requires-python = ">=3"
requires-python = ">=3"

View file

@ -1,2 +1,4 @@
click
click_config_file
click_config_file
requests
jinja2

View file

@ -1,6 +1,6 @@
[metadata]
name = mastodon_email_bridge
version = 0.0.1
version = 1.0.1
[options]
packages = mastodon_email_bridge

View file

@ -14,10 +14,11 @@ setuptools.setup(
version=config['metadata']['version'],
name=config['metadata']['name'],
author_email="ad@susurrando.com",
url="",
description="Redirect the home timeline to email",
url="https://codeberg.org/adelgado/mastodon_email_bridge",
description="Redirect your Mastodon Home timeline to your email",
long_description="README.md",
long_description_content_type="text/markdown",
license="GPLv3",
# keywords=["my", "script", "does", "things"]
keywords=["Mastodon", "email", "fediverse", "ActivityPub"],
include_package_data=True,
)

272
templates/new_post.html.j2 Normal file
View file

@ -0,0 +1,272 @@
{% set time = imp0rt( 'time' ) %}
<!doctype html>
<html lang="{{ data['language'] }}">
<head>
<meta charset="utf-8"/>
</head>
<style>
body { background-color: black; color: #999;}
p { }
div {
margin: 1%;
{# border-style: dashed; #}
}
/* unvisited link */
a:link {
color: blueviolet;
}
/* visited link */
a:visited {
color: #040;
}
/* mouse over link */
a:hover {
color: hotpink;
}
/* selected link */
a:active {
color: blue;
}
</style>
<BODY>
<P>(Post language {{ data['language'] }})</P>
{% if data['url'] != "" %}
<!-- URL -->
<A TARGET="_blank" HREF="{{ data['url'] }}">Post original page</A>
{% endif %}
<!-- creation_date -->
<P STYLE='font-size: 12px;'>
{% set created_date = time.strptime(data['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ') %}
{{ time.strftime('%Y-%m-%d %H:%M:%S %zUTC', created_date) }}
</P>
<!-- account bloc -->
<TABLE>
<TR>
<TD>
<A HREF="{{ data['account']['url'] }}" TARGET="_blank">
<IMG ALT="{{ data['account']['display_name'] }} avatar image" SRC="{{ data['account']['avatar_static'] }}" STYLE="width:64px;height:64px;margin:1%;float: left;">
</A>
</TD>
<TD>
<A HREF="{{ data['account']['url'] }}" TARGET="_blank">
<B>{{ data['account']['display_name'] }} ({{ data['account']['acct'] }})</B>
</A>
</TD>
</TR>
</TABLE>
<!-- content block -->
<DIV STYLE='font-size: 24px;'>
{% if data['spoiler'] != "" and data['spoiler'] != null %}
{% if data['translated_spoiler'] != "" and data['translated_spoiler'] != null %}
<!-- translated spoiler -->
(Translated spoiler)
<DIV CLASS='item-spoiler'>
{{ data['translated_spoiler'] }}
</DIV>
(Original spoiler)
{% endif %}
<!-- spoiler -->
<DIV CLASS='item-spoiler'>
{{ data['spoiler'] }}
</DIV>
{% endif %}
<!-- item-content -->
<DIV CLASS='item-content' STYLE="margin:0.5%;background-color:#111;padding:0.5%;">
{% if data['translated_content'] != '' and data['translated_content'] != null %}
<!-- translated_content -->
(Translated content)
<DIV>
{{ data['translated_content'] }}
</DIV>
(Original content)
{% else %}
<!-- no translated_content present -->
{% endif %}
<DIV>
{{ data['content'] }}
</DIV>
<!-- media -->
{% if data['media_attachments'] %}
{% for media in data['media_attachments'] %}
{% if media['type'] == 'image' -%}
<IMG SRC="{{ media['preview_url'] }}" ALT="{{ media['description'] }}">
{% elif media['type'] == 'video' or media['type'] == 'gifv' -%}
<video controls height="50%">
<source src="{{ media['url'] }}" type="video/webm" />
<A HREF="{{ media['url'] }}">Download video</A>
</video>
{% elif media['type'] == 'audio' -%}
<audio controls src="{{ media['url'] }}"></audio>
<A HREF="{{ media['url'] }}">Download audio</A>
{% else %}
<object data="{{ media['url'] }}"></object>
{%- endif %}
{% endfor %}
{% endif %}
{% if data['meb_reply_to'] %}
{% for reply in data['meb_reply_to'] %}
<!-- reply -->
<A TARGET="_blank" HREF="{{ reply['url'] }}">Reply original page</A>
<!-- creation_date -->
<P STYLE='font-size: 12px;'>
{% set reply_created_date = time.strptime(reply['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ') %}
{{ time.strftime('%Y-%m-%d %H:%M:%S %zUTC', reply_created_date) }}
</P>
<DIV STYLE="margin:1%;">
<!-- reply-account -->
<TABLE>
<TR>
<TD>
<A HREF="{{ reply['account']['url'] }}" TARGET="_blank">
<IMG ALT="{{ reply['account']['display_name'] }} avatar image" SRC="{{ reply['account']['avatar_static'] }}" STYLE="width:64px;height:64px;margin:1%;float: left;">
</A>
</TD>
<TD>
<A HREF="{{ reply['account']['url'] }}" TARGET="_blank">
<B>{{ reply['account']['display_name'] }} ({{ reply['account']['acct'] }})</B>
</A>
</TD>
</TR>
</TABLE>
<!-- reply_content_bloc -->
<DIV STYLE='font-size: 24px;'>
<!-- reply_spoiler -->
<DIV CLASS='reply-spoiler'>
{% if reply['translated_spoiler'] != '' and reply['translated_spoiler'] != null %}
<!-- translated_reply_spoiler --->
(Translated spoiler)
<DIV>
{{ reply['translated_spoiler'] }}
</DIV>
(Original spoiler)
{% endif %}
<DIV>
{{ reply['spoiler'] }}
</DIV>
</DIV>
<!-- reply_content -->
<DIV CLASS='reply-content'>
{% if reply['translated_content'] != '' and reply['translated_content'] != null %}
<!-- translated_reply_content --->
(Translated content)
<DIV>
{{ reply['translated_content'] }}
</DIV>
(Original content)
{% else %}
<!-- no translated_reply_content -->
{% endif %}
<DIV>
{{ reply['content'] }}
</DIV>
<!-- media -->
{% if reply['media_attachments'] %}
{% for media in reply['media_attachments'] %}
{% if media['type'] == 'image' %}
<IMG SRC="{{ media['preview_url'] }}" ALT="{{ media['description'] }}">
{% elif media['type'] == 'video' %}
<video controls width="100%">
<source src="{{ media['url'] }}" type="video/webm" />
<A HREF="{{ media['url'] }}">Download video</A>
</video>
{% elif media['type'] == 'audio' %}
<audio controls src="{{ media['url'] }}"></audio>
<A HREF="{{ media['url'] }}">Download audio</A>
{% else %}
<object data="{{ media['url'] }}"></object>
{% endif %}
{% endfor %}
{% endif %}
</DIV>
</DIV>
</DIV>
{% endfor %}
{% endif %}
{% if data['reblog'] %}
<!-- reblog -->
<DIV STYLE="margin:1%;">
<!-- reply -->
<A TARGET="_blank" HREF="{{ data['reblog']['url'] }}">Reply original page</A>
<!-- creation_date -->
<P STYLE='font-size: 12px;'>
{% set reblog_created_date = time.strptime(data['reblog']['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ') %}
{{ time.strftime('%Y-%m-%d %H:%M:%S %zUTC', reblog_created_date) }}
</P>
<!-- reblog-account -->
<TABLE>
<TR>
<TD>
<A HREF="{{ data['reblog']['account']['url'] }}" TARGET="_blank">
<IMG ALT="{{ data['reblog']['account']['display_name'] }} avatar image" SRC="{{ data['reblog']['account']['avatar_static'] }}" STYLE="width:64px;height:64px;margin:1%;float: left;">
</A>
</TD>
<TD>
<A HREF="{{ data['reblog']['account']['url'] }}" TARGET="_blank">
<B>{{ data['reblog']['account']['display_name'] }} ({{ data['reblog']['account']['acct'] }})</B>
</A>
</TD>
</TR>
</TABLE>
<!-- reblog_content_bloc -->
<DIV STYLE='font-size: 24px;'>
<!-- reblog_spoiler -->
{% if data['reblog']['translated_spoiler'] != '' and data['reblog']['translated_spoiler'] != null %}
<!-- translated_reblog_spoiler --->
(Translated spoiler)
<DIV CLASS='reblog-spoiler'>
{{ data['reblog']['translated_spoiler'] }}
</DIV>
(Original spoiler)
{% else %}
<!-- no translated_spoiler for reblog -->
{% endif %}
<DIV CLASS='reblog-spoiler'>
{{ data['reblog']['spoiler'] }}
</DIV>
<!-- reblog_content -->
<DIV CLASS='reblog-content'>
{% if data['reblog']['translated_content'] != '' and data['reblog']['translated_content'] != null %}
<!-- translated_reblog_content --->
(Translated content)
<DIV>
{{ data['reblog']['translated_content'] }}
</DIV>
(Original content)
{% else %}
<!-- no translated_content for reblog -->
{% endif %}
<DIV>
{{ data['reblog']['content'] }}
</DIV>
<!-- media -->
{% if data['reblog']['media_attachments'] %}
{% for media in data['reblog']['media_attachments'] %}
{% if media['type'] == 'image' %}
<IMG SRC="{{ media['preview_url'] }}" ALT="{{ media['description'] }}">
{% elif media['type'] == 'video' %}
<video controls width="100%">
<source src="{{ media['url'] }}" type="video/webm" />
<A HREF="{{ media['url'] }}">Download video</A>
</video>
{% elif media['type'] == 'audio' %}
<audio controls src="{{ media['url'] }}"></audio>
<A HREF="{{ media['url'] }}">Download audio</A>
{% endif %}
{% endfor %}
{% endif %}
</DIV>
</DIV>
</DIV>
{% endif %}
</DIV>
</DIV>
{# <!-- card -->{{ data['card'] }} #}
<!-- Raw JSON data
<DIV STYLE="margin-top:15%;font-size: 12px;">
Raw JSON data:
<PRE>{{ json_raw }}</PRE>
</DIV>
-->
</BODY>

View file

@ -1,7 +1,13 @@
{{ data['account']['display_name'] }} ({{ data['account']['username'] }})
{{ data['created_at'] }}
{% if data['translated_spoiler'] != "" and data['translated_spoiler'] != null %}
{{ data['translated_spoiler'] }}
{% endif %}
{{ data['spoiler'] }}
{% if data['translated_content'] != "" and data['translated_content'] != null %}
{{ data['translated_content'] }}
{% endif %}
{{ data['content'] }}
{% if data['media_attachments'] %}
{% for media in data['media_attachments'] %}
@ -18,7 +24,13 @@
Reblogged from {{ data['reblog']['account']['display_name'] }} ({{ data['reblog']['account']['username'] }})
{{ data['reblog']['created_at'] }}
{% if data['reblog']['translated_spoiler'] != "" and data['reblog']['translated_spoiler'] != null %}
{{ data['reblog']['translated_spoiler'] }}
{% endif %}
{{ data['reblog']['spoiler'] }}
{% if data['reblog']['translated_content'] != "" and data['reblog']['translated_content'] != null %}
{{ data['reblog']['translated_content'] }}
{% endif %}
{{ data['reblog']['content'] }}
{% if data['reblog']['media_attachments'] %}
{% for media in data['reblog']['media_attachments'] %}

22
wrapper.sh Normal file
View file

@ -0,0 +1,22 @@
#!/bin/bash
if [ -z "${HOME}" ]; then
if [ "$(whoami)" == "root" ]; then
HOME="/root"
else
HOME=$(grep "$(whoami)" /etc/passwd | awk 'BEGIN {FS=":"} {print($6)}')
fi
fi
CONFIG_FILE="${HOME}/.config/mastodon_email_bridge.conf"
cd "__src_folder__" || exit 1
if [ -r "${CONFIG_FILE}" ]; then
perms=$(stat -c %A "${CONFIG_FILE}")
if [ "${perms:4:6}" != '------' ]; then
echo "Permissions too open for config file '${CONFIG_FILE}' ($perms). Remove all permissions to group and others."
exit 1
fi
config=(--config "${CONFIG_FILE}")
else
config=()
fi
"__src_folder__/mastodon_email_bridge.sh" "${config[@]}" "${@}"