Compare commits
66 commits
Author | SHA1 | Date | |
---|---|---|---|
76e25393d0 | |||
24d68684de | |||
ee600178c4 | |||
705dfe7196 | |||
b185b02073 | |||
5ba3c0db27 | |||
a63aa1fd9c | |||
326e86e088 | |||
62ad4febb1 | |||
ffdb262f94 | |||
a266db1a7e | |||
bdda8d55b8 | |||
ed16a1838b | |||
ce95bbe185 | |||
43b426212c | |||
c75297324e | |||
afbed1f86b | |||
771967c905 | |||
57f3a0f36c | |||
8dc1e20d1b | |||
aa3340f4d7 | |||
f5b07072ad | |||
07a75b73cc | |||
7c438f8e78 | |||
5e872b7288 | |||
cd171f6230 | |||
1d81c15582 | |||
699ea790fa | |||
faa411f02a | |||
b49ed95f05 | |||
6e4283ad26 | |||
83da7e70f9 | |||
1bc0120066 | |||
c4e7e0e90c | |||
642b57c5b5 | |||
6221ed0240 | |||
a7c0af726c | |||
70bd2fbb70 | |||
35e9503b06 | |||
4bfe099883 | |||
11a84f9180 | |||
7c0490a352 | |||
5943d22609 | |||
f2af0b89f0 | |||
d5aac011a1 | |||
273200f8a4 | |||
e3aaba8483 | |||
a6ec889b11 | |||
92c40adcd7 | |||
83c23ffe69 | |||
0b8fae74e4 | |||
5ed3568436 | |||
e453127ddb | |||
f5a3021d81 | |||
fbe512d549 | |||
d75cc3e904 | |||
f3c2d0acf6 | |||
9e734706f7 | |||
962c12f6b0 | |||
ca0a09c7ba | |||
e4c8c916b7 | |||
78390c4140 | |||
b39cb7b5e5 | |||
783eecd9a2 | |||
0de622561b | |||
6c589a7dca |
13 changed files with 657 additions and 178 deletions
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
graft templates
|
||||||
|
include LICENSE
|
||||||
|
include README.md
|
||||||
|
include podman_build.sh
|
||||||
|
include podman_run.sh
|
||||||
|
include Dockerfile
|
54
README.md
54
README.md
|
@ -1,23 +1,59 @@
|
||||||
# mastodon_email_bridge
|
# mastodon_email_bridge
|
||||||
|
|
||||||
|
Simple script to forward your Mastodon Home timeline to your email.
|
||||||
|
|
||||||
## Requirements
|
## 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
|
## Installation
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo python3 setup.py install
|
python -m venv "${HOME}/pyenvs/mastodon_email_bridge"
|
||||||
```
|
source "${HOME}/pyenvs/mastodon_email_bridge/bin/activate"
|
||||||
|
pip install .
|
||||||
### Windows (from PowerShell)
|
mkdir -p "${HOME}/.config/mastodon_email_bridge"
|
||||||
|
cp -r templates "${HOME}/.config/mastodon_email_bridge/"
|
||||||
```powershell
|
|
||||||
& $(where.exe python).split()[0] setup.py install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
Customize the templates (if you know HTML, CSS and Jinja2) and run the command.
|
||||||
mastodon_email_bridge.py [--debug-level|-d CRITICAL|ERROR|WARNING|INFO|DEBUG|NOTSET] # Other parameters
|
|
||||||
```
|
```
|
||||||
|
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
29
install.sh
Executable 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
10
mastodon_email_bridge.sh
Executable 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 "${@}"
|
|
@ -15,10 +15,11 @@ import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import importlib
|
||||||
import click
|
import click
|
||||||
import click_config_file
|
import click_config_file
|
||||||
import requests
|
import requests
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
from jinja2 import Environment, select_autoescape, FileSystemLoader
|
||||||
|
|
||||||
class MastodonEmailBridge:
|
class MastodonEmailBridge:
|
||||||
'''CLass to redirect the Mastodon home timeline to email'''
|
'''CLass to redirect the Mastodon home timeline to email'''
|
||||||
|
@ -49,19 +50,46 @@ class MastodonEmailBridge:
|
||||||
'.mastodon_email_bridge_sent_items.db'
|
'.mastodon_email_bridge_sent_items.db'
|
||||||
)
|
)
|
||||||
self._init_log()
|
self._init_log()
|
||||||
|
self.last_translation_response = None
|
||||||
self._get_sent_posts()
|
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(
|
self.j2env = Environment(
|
||||||
loader=PackageLoader("mastodon_email_bridge"),
|
#loader=PackageLoader("mastodon_email_bridge"),
|
||||||
|
loader=FileSystemLoader(templates_folder, followlinks=True),
|
||||||
autoescape=select_autoescape()
|
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 = requests.Session()
|
||||||
self.session.headers.update({'Authorization': f"Bearer {self.config['token']}"})
|
self.session.headers.update({'Authorization': f"Bearer {self.config['token']}"})
|
||||||
count=1
|
count=1
|
||||||
self._log.debug(
|
self._log.debug(
|
||||||
"Getting URL 'https://%s/api/v1/timelines/home?limit=1'",
|
"Getting URL 'https://%s/api/v1/timelines/home?limit=%s'",
|
||||||
self.config['server']
|
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)
|
next_url = self.process_url(next_url)
|
||||||
while next_url != "" and (count < self.config['limit'] or self.config['limit'] == 0):
|
while next_url != "" and (count < self.config['limit'] or self.config['limit'] == 0):
|
||||||
count = count + 1
|
count = count + 1
|
||||||
|
@ -72,6 +100,9 @@ class MastodonEmailBridge:
|
||||||
|
|
||||||
def _get_sent_posts(self):
|
def _get_sent_posts(self):
|
||||||
self.sent_items = []
|
self.sent_items = []
|
||||||
|
self._log.debug(
|
||||||
|
"Reading sent items from database..."
|
||||||
|
)
|
||||||
self.sqlite = sqlite3.connect(self.config['sent_items_file'])
|
self.sqlite = sqlite3.connect(self.config['sent_items_file'])
|
||||||
self.sqlite.row_factory = sqlite3.Row
|
self.sqlite.row_factory = sqlite3.Row
|
||||||
cur = self.sqlite.cursor()
|
cur = self.sqlite.cursor()
|
||||||
|
@ -80,11 +111,15 @@ class MastodonEmailBridge:
|
||||||
res = cur.execute("SELECT id FROM sent_items")
|
res = cur.execute("SELECT id FROM sent_items")
|
||||||
rows = res.fetchall()
|
rows = res.fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if row[0] in self.sent_items:
|
# if row[0] in self.sent_items:
|
||||||
self._log.warning("Duplicate id in database '%s'", row[0])
|
# self._log.warning("Duplicate id in database '%s'", row[0])
|
||||||
else:
|
# else:
|
||||||
self._log.debug("Found sent item with id '%s' (%s)", row[0], type(row[0]))
|
# self._log.debug("Found sent item with id '%s' (%s)", row[0], type(row[0]))
|
||||||
self.sent_items.append(row[0])
|
self.sent_items.append(row[0])
|
||||||
|
self._log.debug(
|
||||||
|
"Got %s sent items from database",
|
||||||
|
len(self.sent_items)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_url(self, url):
|
def process_url(self, url):
|
||||||
|
@ -93,7 +128,8 @@ class MastodonEmailBridge:
|
||||||
result = self.session.get(url)
|
result = self.session.get(url)
|
||||||
# for header in result.headers:
|
# for header in result.headers:
|
||||||
# self._log.debug("%s = %s", header, result.headers[header])
|
# 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'])
|
self._log.warning("X-RateLimit-Reset: %s", result.headers['X-RateLimit-Reset'])
|
||||||
try:
|
try:
|
||||||
reset_time = time.mktime(
|
reset_time = time.mktime(
|
||||||
|
@ -111,7 +147,22 @@ class MastodonEmailBridge:
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
return url
|
return url
|
||||||
data = result.json()[0]
|
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:
|
if int(data['id']) not in self.sent_items:
|
||||||
self.send_mail(data)
|
self.send_mail(data)
|
||||||
else:
|
else:
|
||||||
|
@ -123,22 +174,40 @@ class MastodonEmailBridge:
|
||||||
next_url = slink[0].replace('<', '').replace('>', '')
|
next_url = slink[0].replace('<', '').replace('>', '')
|
||||||
return next_url
|
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):
|
def send_mail(self, data):
|
||||||
'''Send an email with the post composed'''
|
'''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 = MIMEMultipart('alternative')
|
||||||
msg['Subject'] = f"FediPost from {data['account']['display_name']} ({data['account']['username']})"
|
msg['Subject'] = self._str_template(self.config['subjet_template'], data)
|
||||||
msg['From'] = self.config['sender']
|
msg['From'] = sender
|
||||||
msg['To'] = self.config['recipient']
|
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_template = self.j2env.get_template("new_post.html.j2")
|
||||||
html_source = html_template.render(
|
html_source = html_template.render(
|
||||||
|
imp0rt = importlib.import_module,
|
||||||
data=data,
|
data=data,
|
||||||
json_raw=json.dumps(data, indent=2)
|
json_raw=json.dumps(data, indent=2)
|
||||||
)
|
)
|
||||||
txt_template = self.j2env.get_template("new_post.txt.j2")
|
txt_template = self.j2env.get_template("new_post.txt.j2")
|
||||||
txt_source = txt_template.render(
|
txt_source = txt_template.render(
|
||||||
|
imp0rt = importlib.import_module,
|
||||||
data=data,
|
data=data,
|
||||||
json_raw=json.dumps(data, indent=2)
|
json_raw=json.dumps(data, indent=2)
|
||||||
)
|
)
|
||||||
|
if 'sent_folder' not in self.config:
|
||||||
sent_folder = os.path.join(
|
sent_folder = os.path.join(
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
'HOME',
|
'HOME',
|
||||||
|
@ -149,6 +218,8 @@ class MastodonEmailBridge:
|
||||||
),
|
),
|
||||||
'.mastodon_email_bridge_sent_items'
|
'.mastodon_email_bridge_sent_items'
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
sent_folder = self.config['sent_folder']
|
||||||
if not os.path.exists(sent_folder):
|
if not os.path.exists(sent_folder):
|
||||||
os.mkdir(sent_folder)
|
os.mkdir(sent_folder)
|
||||||
sent_file = os.path.join(sent_folder, data['id'] + '.html')
|
sent_file = os.path.join(sent_folder, data['id'] + '.html')
|
||||||
|
@ -162,15 +233,101 @@ class MastodonEmailBridge:
|
||||||
if self.config['mail_user'] is not None:
|
if self.config['mail_user'] is not None:
|
||||||
conn.login(self.config['mail_user'], self.config['mail_pass'])
|
conn.login(self.config['mail_user'], self.config['mail_pass'])
|
||||||
self._log.debug("Sending email for post with id '%s'...", data['id'])
|
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()
|
conn.quit()
|
||||||
self._log.debug("Adding entry to database...")
|
self._log.debug("Adding entry to database...")
|
||||||
cur = self.sqlite.cursor()
|
cur = self.sqlite.cursor()
|
||||||
|
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()})")
|
cur.execute(f"INSERT INTO sent_items (id, date) VALUES ({data['id']}, {time.time()})")
|
||||||
self.sqlite.commit()
|
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'])
|
self.sent_items.append(data['id'])
|
||||||
return True
|
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):
|
def _init_log(self):
|
||||||
''' Initialize log object '''
|
''' Initialize log object '''
|
||||||
self._log = logging.getLogger("mastodon_email_bridge")
|
self._log = logging.getLogger("mastodon_email_bridge")
|
||||||
|
@ -230,13 +387,24 @@ class MastodonEmailBridge:
|
||||||
help='Mastodon server full qualified name.'
|
help='Mastodon server full qualified name.'
|
||||||
)
|
)
|
||||||
@click.option('--limit', '-L', default=0, help='Mastodon token with read access.')
|
@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('--limit-per-request', '-R', default=40, help='Mastodon token with read access.')
|
||||||
@click.option('--recipient', '-r', required=True, help='Recipient email to get the posts.')
|
@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(
|
@click.option(
|
||||||
'--sender',
|
'--sender',
|
||||||
'-S',
|
'-S',
|
||||||
default='mastodon_email_bridge@example.org',
|
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(
|
@click.option(
|
||||||
'--sent-items-file',
|
'--sent-items-file',
|
||||||
|
@ -263,6 +431,46 @@ class MastodonEmailBridge:
|
||||||
default=465,
|
default=465,
|
||||||
help='SMTP Mail server port to send emails.'
|
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('--log-file', '-l', help="File to store all debug messages.")
|
||||||
# @click.option("--dummy","-n", is_flag=True,
|
# @click.option("--dummy","-n", is_flag=True,
|
||||||
# help="Don't do anything, just show what would be done.")
|
# help="Don't do anything, just show what would be done.")
|
||||||
|
|
|
@ -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>
|
|
|
@ -3,12 +3,12 @@ requires = ["setuptools", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = ""
|
Homepage = "https://codeberg.org/adelgado/mastodon_email_bridge"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mastodon_email_bridge"
|
name = "mastodon_email_bridge"
|
||||||
version = "0.0.1"
|
version = "1.0.1"
|
||||||
description = "Redirect the home timeline to email"
|
description = "Redirect your Mastodon Home timeline to your email"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }]
|
authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }]
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
@ -17,9 +17,11 @@ classifiers = [
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
]
|
]
|
||||||
#keywords = ["vCard", "contacts", "duplicates"]
|
keywords = ["Mastodon", "email", "fediverse", "ActivityPub"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click",
|
"click",
|
||||||
"click_config_file",
|
"click_config_file",
|
||||||
|
"requests",
|
||||||
|
"jinja2",
|
||||||
]
|
]
|
||||||
requires-python = ">=3"
|
requires-python = ">=3"
|
|
@ -1,2 +1,4 @@
|
||||||
click
|
click
|
||||||
click_config_file
|
click_config_file
|
||||||
|
requests
|
||||||
|
jinja2
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = mastodon_email_bridge
|
name = mastodon_email_bridge
|
||||||
version = 0.0.1
|
version = 1.0.1
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
packages = mastodon_email_bridge
|
packages = mastodon_email_bridge
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -14,10 +14,11 @@ setuptools.setup(
|
||||||
version=config['metadata']['version'],
|
version=config['metadata']['version'],
|
||||||
name=config['metadata']['name'],
|
name=config['metadata']['name'],
|
||||||
author_email="ad@susurrando.com",
|
author_email="ad@susurrando.com",
|
||||||
url="",
|
url="https://codeberg.org/adelgado/mastodon_email_bridge",
|
||||||
description="Redirect the home timeline to email",
|
description="Redirect your Mastodon Home timeline to your email",
|
||||||
long_description="README.md",
|
long_description="README.md",
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
license="GPLv3",
|
license="GPLv3",
|
||||||
# keywords=["my", "script", "does", "things"]
|
keywords=["Mastodon", "email", "fediverse", "ActivityPub"],
|
||||||
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
|
272
templates/new_post.html.j2
Normal file
272
templates/new_post.html.j2
Normal 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>
|
|
@ -1,7 +1,13 @@
|
||||||
{{ data['account']['display_name'] }} ({{ data['account']['username'] }})
|
{{ data['account']['display_name'] }} ({{ data['account']['username'] }})
|
||||||
{{ data['created_at'] }}
|
{{ data['created_at'] }}
|
||||||
|
|
||||||
|
{% if data['translated_spoiler'] != "" and data['translated_spoiler'] != null %}
|
||||||
|
{{ data['translated_spoiler'] }}
|
||||||
|
{% endif %}
|
||||||
{{ data['spoiler'] }}
|
{{ data['spoiler'] }}
|
||||||
|
{% if data['translated_content'] != "" and data['translated_content'] != null %}
|
||||||
|
{{ data['translated_content'] }}
|
||||||
|
{% endif %}
|
||||||
{{ data['content'] }}
|
{{ data['content'] }}
|
||||||
{% if data['media_attachments'] %}
|
{% if data['media_attachments'] %}
|
||||||
{% for media in data['media_attachments'] %}
|
{% for media in data['media_attachments'] %}
|
||||||
|
@ -18,7 +24,13 @@
|
||||||
Reblogged from {{ data['reblog']['account']['display_name'] }} ({{ data['reblog']['account']['username'] }})
|
Reblogged from {{ data['reblog']['account']['display_name'] }} ({{ data['reblog']['account']['username'] }})
|
||||||
{{ data['reblog']['created_at'] }}
|
{{ data['reblog']['created_at'] }}
|
||||||
|
|
||||||
|
{% if data['reblog']['translated_spoiler'] != "" and data['reblog']['translated_spoiler'] != null %}
|
||||||
|
{{ data['reblog']['translated_spoiler'] }}
|
||||||
|
{% endif %}
|
||||||
{{ data['reblog']['spoiler'] }}
|
{{ data['reblog']['spoiler'] }}
|
||||||
|
{% if data['reblog']['translated_content'] != "" and data['reblog']['translated_content'] != null %}
|
||||||
|
{{ data['reblog']['translated_content'] }}
|
||||||
|
{% endif %}
|
||||||
{{ data['reblog']['content'] }}
|
{{ data['reblog']['content'] }}
|
||||||
{% if data['reblog']['media_attachments'] %}
|
{% if data['reblog']['media_attachments'] %}
|
||||||
{% for media in data['reblog']['media_attachments'] %}
|
{% for media in data['reblog']['media_attachments'] %}
|
22
wrapper.sh
Normal file
22
wrapper.sh
Normal 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[@]}" "${@}"
|
Loading…
Reference in a new issue