Commit 8525bd37 authored by Tulir Asokan's avatar Tulir Asokan 🐈

Initial commit

parents
.editorconfig
.codeclimate.yml
*.png
*.md
.venv
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
max_line_length = 99
indent_style = space
[*.{yaml, yml}]
indent_style = space
.venv
*.pyc
*.egg-info
/build
/dist
*.yaml
!example-config.yaml
*.log
*.log.*
*.db
image: docker:stable
stages:
- build
- push
default:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
push latest:
stage: push
only:
- master
variables:
GIT_STRATEGY: none
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
push tag:
stage: push
variables:
GIT_STRATEGY: none
except:
- master
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
FROM docker.io/alpine:3.10
ENV UID=1337 \
GID=1337
RUN apk add --no-cache \
py3-pillow \
py3-aiohttp \
py3-magic \
py3-sqlalchemy \
py3-psycopg2 \
py3-ruamel.yaml \
# Indirect dependencies
#commonmark
py3-future \
#alembic
py3-mako \
py3-dateutil \
py3-markupsafe \
py3-six \
py3-idna \
# Other dependencies
ca-certificates \
su-exec
COPY . /opt/mautrix-twilio
WORKDIR /opt/mautrix-twilio
RUN pip3 install .
VOLUME /data
CMD ["/opt/mautrix-twilio/docker-run.sh"]
This diff is collapsed.
# mautrix-twilio
A Matrix-Twilio relaybot bridge.
## Discussion
Matrix room: [`#twilio:maunium.net`](https://matrix.to/#/#twilio:maunium.net)
[alembic]
script_location = alembic
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import sys
from os.path import abspath, dirname
sys.path.insert(0, dirname(dirname(abspath(__file__))))
from mautrix.util.db import Base
from mautrix_twilio.config import Config
import mautrix_twilio.db
config = context.config
mxtw_config_path = context.get_x_argument(as_dictionary=True).get("config", "config.yaml")
mxtw_config = Config(mxtw_config_path, None, None)
mxtw_config.load()
config.set_main_option("sqlalchemy.url",
mxtw_config.get("appservice.database", "sqlite:///mautrix-twilio.db"))
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""Initial revision
Revision ID: 8e87452589a1
Revises:
Create Date: 2019-09-22 01:10:14.783562
"""
from alembic import op
import sqlalchemy as sa
from mautrix.bridge.db.mx_room_state import PowerLevelType
# revision identifiers, used by Alembic.
revision = '8e87452589a1'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('message',
sa.Column('mxid', sa.String(length=255), nullable=True),
sa.Column('mx_room', sa.String(length=255), nullable=True),
sa.Column('tw_receiver', sa.String(length=127), nullable=False),
sa.Column('twid', sa.String(length=127), nullable=False),
sa.PrimaryKeyConstraint('tw_receiver', 'twid')
)
op.create_table('mx_room_state',
sa.Column('room_id', sa.String(length=255), nullable=False),
sa.Column('power_levels', PowerLevelType(), nullable=True),
sa.PrimaryKeyConstraint('room_id')
)
op.create_table('mx_user_profile',
sa.Column('room_id', sa.String(length=255), nullable=False),
sa.Column('user_id', sa.String(length=255), nullable=False),
sa.Column('membership', sa.Enum('JOIN', 'LEAVE', 'INVITE', 'BAN', 'KNOCK', name='membership'), nullable=False),
sa.Column('displayname', sa.String(), nullable=True),
sa.Column('avatar_url', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('room_id', 'user_id')
)
op.create_table('portal',
sa.Column('twid', sa.String(length=127), nullable=False),
sa.Column('mxid', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('twid')
)
op.create_table('puppet',
sa.Column('twid', sa.String(length=127), nullable=False),
sa.Column('matrix_registered', sa.Boolean(), server_default=sa.text('0'), nullable=False),
sa.PrimaryKeyConstraint('twid')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('puppet')
op.drop_table('portal')
op.drop_table('mx_user_profile')
op.drop_table('mx_room_state')
op.drop_table('message')
# ### end Alembic commands ###
#!/bin/sh
# Define functions.
function fixperms {
chown -R $UID:$GID /data /opt/mautrix-twilio
}
cd /opt/mautrix-twilio
if [ ! -f /data/config.yaml ]; then
cp example-config.yaml /data/config.yaml
echo "Didn't find a config file."
echo "Copied default config file to /data/config.yaml"
echo "Modify that config file to your liking."
echo "Start the container again after that to generate the registration file."
fixperms
exit
fi
# Replace database path in config.
sed -i "s#sqlite:///mautrix-twilio.db#sqlite:////data/mautrix-twilio.db#" /data/config.yaml
# Check that database is in the right state
alembic -x config=/data/config.yaml upgrade head
if [ ! -f /data/registration.yaml ]; then
python3 -m mautrix_twilio -g -c /data/config.yaml -r /data/registration.yaml
echo "Didn't find a registration file."
echo "Generated one for you."
echo "Copy that over to synapses app service directory."
fixperms
exit
fi
fixperms
exec su-exec $UID:$GID python3 -m mautrix_twilio -c /data/config.yaml
# Homeserver details
homeserver:
# The address that this appservice can use to connect to the homeserver.
address: https://matrix.example.com
# The domain of the homeserver (for MXIDs, etc).
domain: example.com
# Whether or not to verify the SSL certificate of the homeserver.
# Only applies if address starts with https://
verify_ssl: true
# Application service host/registration related details
# Changing these values requires regeneration of the registration.
appservice:
# The address that the homeserver can use to connect to this appservice.
address: http://localhost:29322
# The hostname and port where this appservice should listen.
hostname: 0.0.0.0
port: 29322
# The maximum body size of appservice API requests (from the homeserver) in mebibytes
# Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s
max_body_size: 1
# The full URI to the database. SQLite and Postgres are fully supported.
# Other DBMSes supported by SQLAlchemy may or may not work.
# Format examples:
# SQLite: sqlite:///filename.db
# Postgres: postgres://username:password@hostname/dbname
database: sqlite:///mautrix-twilio.db
# The unique ID of this appservice.
id: twilio
# Username of the appservice bot.
bot_username: twiliobot
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
bot_displayname: Twilio bridge bot
bot_avatar: mxc://maunium.net/FYuKJHaCrSeSpvBJfHwgYylP
# Community ID for bridged users (changes registration file) and rooms.
# Must be created manually.
community_id: null
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
# Bridge config
bridge:
# Localpart template of MXIDs for remote users.
# {userid} is replaced with the phone number of the user (international format without +).
username_template: "twilio_whatsapp_{userid}"
# Displayname template for remote users.
# {displayname} is replaced with the phone number of the user (international format without +).
displayname_template: "+{displayname} (WhatsApp)"
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!tw"
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# Initial room state for created rooms.
initial_state:
m.room.power_levels:
events_default: 0
users_default: 0
state_default: 50
events:
m.room.avatar: 0
m.room.name: 0
m.room.topic: 0
# Permissions for using the bridge.
# Permitted values:
# user - Use the bridge with puppeting.
# admin - Use and administrate the bridge.
# Permitted keys:
# * - All Matrix users
# domain - All users on that homeserver
# mxid - Specific user
permissions:
"example.com": "user"
"@admin:example.com": "admin"
# Twilio webhook settings.
twilio:
# Twilio account ID
account_id: AC1082dcd0e9ae51404f6cae3581edfbff
# Twilio phone number to send messages from.
sender_id: whatsapp:+1415550199
# Your Twilio auth token (get from Twilio dashboard front page)
secret: 2035141f21a001604e763c009aa3be4c
# Path prefix for webhook endpoints. Subpaths are /status and /receive.
# Note that the webhook must be put behind a reverse proxy with https.
webhook_path: /twilio
# Python logging configuration.
#
# See section 16.7.2 of the Python documentation for more info:
# https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
logging:
version: 1
formatters:
colored:
(): mautrix.util.color_log.ColorFormatter
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
normal:
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
handlers:
file:
class: logging.handlers.RotatingFileHandler
formatter: normal
filename: ./mautrix-twilio.log
maxBytes: 10485760
backupCount: 10
console:
class: logging.StreamHandler
formatter: colored
loggers:
mau:
level: DEBUG
fbchat:
level: DEBUG
aiohttp:
level: INFO
root:
level: DEBUG
handlers: [file, console]
__version__ = "0.1.0.dev1"
__author__ = "Tulir Asokan <tulir@maunium.net>"
# mautrix-twilio - A Matrix-Twilio relaybot bridge.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.bridge import Bridge
from .config import Config
from .twilio import TwilioHandler, TwilioClient
from .matrix import MatrixHandler
from .sqlstatestore import SQLStateStore
from .context import Context
from .puppet import init as init_puppet
from .portal import init as init_portal
from .user import init as init_user
from .db import init as init_db
from . import __version__
class TwilioBridge(Bridge):
name = "mautrix-twilio"
command = "python -m mautrix-twilio"
description = "A Matrix-Twilio relaybot bridge."
version = __version__
config_class = Config
matrix_class = MatrixHandler
state_store_class = SQLStateStore
config: Config
twilio: TwilioHandler
twilio_client: TwilioClient
def prepare_bridge(self) -> None:
init_db(self.db)
self.twilio_client = TwilioClient(config=self.config, loop=self.loop)
context = Context(az=self.az, config=self.config, twc=self.twilio_client, loop=self.loop)
context.mx = self.matrix = MatrixHandler(self.az, self.config, self.loop)
context.tw = self.twilio = TwilioHandler(context)
init_user(context)
init_portal(context)
init_puppet(context)
self.az.app.add_subapp(self.config["twilio.webhook_path"], self.twilio.app)
TwilioBridge().run()
# mautrix-twilio - A Matrix-Twilio relaybot bridge.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Tuple, List, Any
from mautrix.types import UserID
from mautrix.bridge.config import BaseBridgeConfig, ConfigUpdateHelper
class Config(BaseBridgeConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
super().do_update(helper)
copy, copy_dict = helper.copy, helper.copy_dict
copy("appservice.community_id")
copy("bridge.username_template")
copy("bridge.command_prefix")
copy("bridge.federate_rooms")
copy("bridge.initial_state")
copy_dict("bridge.permissions")
copy("twilio.account_id")
copy("twilio.sender_id")
copy("twilio.secret")
copy("twilio.webhook_path")
def _get_permissions(self, key: str) -> Tuple[bool, bool]:
level = self["bridge.permissions"].get(key, "")
admin = level == "admin"
user = level == "user" or admin
return user, admin
def get_permissions(self, mxid: UserID) -> Tuple[bool, bool]:
permissions = self["bridge.permissions"] or {}
if mxid in permissions:
return self._get_permissions(mxid)
homeserver = mxid[mxid.index(":") + 1:]
if homeserver in permissions:
return self._get_permissions(homeserver)
return self._get_permissions("*")
@property
def namespaces(self) -> Dict[str, List[Dict[str, Any]]]:
homeserver = self["homeserver.domain"]
username_format = self["bridge.username_template"].lower().format(userid=".+")
group_id = ({"group_id": self["appservice.community_id"]}
if self["appservice.community_id"] else {})
return {
"users": [{
"exclusive": True,
"regex": f"@{username_format}:{homeserver}",
**group_id,
}],
}
# mautrix-twilio - A Matrix-Twilio relaybot bridge.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Tuple, TYPE_CHECKING
from asyncio import AbstractEventLoop
from mautrix.appservice import AppService
from .config import Config
if TYPE_CHECKING:
from .matrix import MatrixHandler
from .twilio import TwilioHandler, TwilioClient
class Context:
az: AppService
config: Config
twc: 'TwilioClient'
loop: AbstractEventLoop
mx: Optional['MatrixHandler']
tw: Optional['TwilioHandler']
def __init__(self, az: AppService, config: Config, twc: 'TwilioClient', loop: AbstractEventLoop
) -> None:
self.az = az
self.config = config
self.twc = twc
self.loop = loop
self.mx = None
self.tw = None
@property
def core(self) -> Tuple[AppService, Config, AbstractEventLoop]:
return self.a