Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC
This commit is contained in:
2
tier_communications_matrix/__init__.py
Normal file
2
tier_communications_matrix/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
16
tier_communications_matrix/__manifest__.py
Normal file
16
tier_communications_matrix/__manifest__.py
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
'name': 'Tier Communications - Matrix',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Discuss',
|
||||
'summary': 'Matrix channel integration for Tier Communications',
|
||||
'author': 'Tier',
|
||||
'depends': ['tier_communications'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/tier_channel_tag_matrix_data.xml',
|
||||
'views/res_users_matrix_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
1
tier_communications_matrix/controllers/__init__.py
Normal file
1
tier_communications_matrix/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import matrix_webhook
|
||||
56
tier_communications_matrix/controllers/matrix_webhook.py
Normal file
56
tier_communications_matrix/controllers/matrix_webhook.py
Normal file
@ -0,0 +1,56 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MatrixWebhookController(http.Controller):
|
||||
"""HTTP controller for incoming Matrix push events.
|
||||
|
||||
Matrix homeserver pushes events to this endpoint.
|
||||
Incoming message → create mail.message in Odoo with 'matrix' channel tag.
|
||||
"""
|
||||
|
||||
@http.route(
|
||||
'/tier_matrix/webhook',
|
||||
type='http',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
csrf=False,
|
||||
)
|
||||
def matrix_webhook(self, **kwargs):
|
||||
"""Receive a Matrix event and create a mail.message in Odoo."""
|
||||
try:
|
||||
payload = json.loads(request.httprequest.data or '{}')
|
||||
except (ValueError, TypeError):
|
||||
_logger.warning('tier_matrix webhook: invalid JSON payload')
|
||||
return request.make_response(
|
||||
json.dumps({'error': 'invalid JSON'}),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
status=400,
|
||||
)
|
||||
|
||||
event_type = payload.get('type', '')
|
||||
if event_type != 'm.room.message':
|
||||
return request.make_response(
|
||||
json.dumps({'status': 'ignored'}),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
)
|
||||
|
||||
try:
|
||||
request.env['tier.channel.matrix.helper'].sudo().channel_receive_message(payload)
|
||||
except Exception:
|
||||
_logger.exception('tier_matrix webhook: error processing event')
|
||||
return request.make_response(
|
||||
json.dumps({'error': 'processing error'}),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
status=500,
|
||||
)
|
||||
|
||||
return request.make_response(
|
||||
json.dumps({'status': 'ok'}),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
)
|
||||
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Activate the matrix channel tag when this module is installed -->
|
||||
<record id="tier_communications.channel_tag_matrix" model="tier.channel.tag">
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
tier_communications_matrix/models/__init__.py
Normal file
2
tier_communications_matrix/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import tier_channel_matrix
|
||||
from . import res_users_matrix
|
||||
19
tier_communications_matrix/models/res_users_matrix.py
Normal file
19
tier_communications_matrix/models/res_users_matrix.py
Normal file
@ -0,0 +1,19 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersMatrix(models.Model):
|
||||
"""Extend res.users with Matrix credentials."""
|
||||
_inherit = 'res.users'
|
||||
|
||||
tier_matrix_homeserver = fields.Char(
|
||||
string='Matrix Homeserver',
|
||||
help='URL Matrix homeserver, например https://matrix.org',
|
||||
)
|
||||
tier_matrix_access_token = fields.Char(
|
||||
string='Matrix Access Token',
|
||||
help='Access token для Matrix бота/пользователя.',
|
||||
)
|
||||
tier_matrix_room_id = fields.Char(
|
||||
string='Matrix Room ID',
|
||||
help='ID комнаты Matrix для переписки, например !roomid:server',
|
||||
)
|
||||
171
tier_communications_matrix/models/tier_channel_matrix.py
Normal file
171
tier_communications_matrix/models/tier_channel_matrix.py
Normal file
@ -0,0 +1,171 @@
|
||||
import uuid
|
||||
import logging
|
||||
import requests
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TierChannelMatrixHelper(models.AbstractModel):
|
||||
"""Helper for Matrix channel operations using per-user credentials from res.users.
|
||||
|
||||
Credentials stored on res.users: tier_matrix_homeserver, tier_matrix_access_token, tier_matrix_room_id.
|
||||
"""
|
||||
_name = 'tier.channel.matrix.helper'
|
||||
_description = 'Matrix Channel Helper'
|
||||
|
||||
def _get_user_matrix_config(self, user=None):
|
||||
if not user:
|
||||
user = self.env.user
|
||||
return {
|
||||
'homeserver_url': user.tier_matrix_homeserver,
|
||||
'access_token': user.tier_matrix_access_token,
|
||||
'room_id': user.tier_matrix_room_id,
|
||||
}
|
||||
|
||||
def channel_authenticate(self, user=None):
|
||||
"""Verify Matrix credentials for the given user."""
|
||||
config = self._get_user_matrix_config(user)
|
||||
if not all([config['homeserver_url'], config['access_token']]):
|
||||
raise UserError(_('Matrix credentials not configured for this user.'))
|
||||
|
||||
url = f"{config['homeserver_url'].rstrip('/')}/_matrix/client/v3/account/whoami"
|
||||
headers = {'Authorization': f"Bearer {config['access_token']}"}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
return True
|
||||
raise UserError(
|
||||
_('Matrix authentication failed (HTTP %s): %s') % (resp.status_code, resp.text)
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise UserError(_('Cannot connect to Matrix homeserver: %s') % str(e))
|
||||
|
||||
# =========================================================================
|
||||
# ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX
|
||||
# =========================================================================
|
||||
|
||||
def channel_send_message(self, message, user=None):
|
||||
"""Send a mail.message to Matrix using user's credentials."""
|
||||
if not user:
|
||||
user = self.env.user
|
||||
|
||||
config = self._get_user_matrix_config(user)
|
||||
|
||||
if not config['homeserver_url']:
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет homeserver URL (tier_matrix_homeserver)"
|
||||
)
|
||||
return
|
||||
if not config['access_token']:
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет access token (tier_matrix_access_token)"
|
||||
)
|
||||
return
|
||||
if not config['room_id']:
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет room ID (tier_matrix_room_id)"
|
||||
)
|
||||
return
|
||||
|
||||
txn_id = str(uuid.uuid4())
|
||||
url = (
|
||||
f"{config['homeserver_url'].rstrip('/')}/_matrix/client/v3/rooms/"
|
||||
f"{config['room_id']}/send/m.room.message/{txn_id}"
|
||||
)
|
||||
headers = {
|
||||
'Authorization': f"Bearer {config['access_token']}",
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
# Убираем HTML теги для Matrix (plain text)
|
||||
import re
|
||||
body_text = re.sub(r'<[^>]+>', '', message.body or '')
|
||||
|
||||
payload = {
|
||||
'msgtype': 'm.text',
|
||||
'body': body_text,
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': message.body or '',
|
||||
}
|
||||
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id}, room={config['room_id']}, txn_id={txn_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
resp = requests.put(url, json=payload, headers=headers, timeout=15)
|
||||
if resp.status_code not in (200, 201):
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — HTTP {resp.status_code}: {resp.text}"
|
||||
)
|
||||
raise UserError(
|
||||
_('Failed to send Matrix message (HTTP %s): %s') % (resp.status_code, resp.text)
|
||||
)
|
||||
event_id = resp.json().get('event_id', '')
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — SUCCESS, event_id={event_id}"
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(
|
||||
f"[tier_matrix] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — ОШИБКА соединения: {e}"
|
||||
)
|
||||
raise UserError(_('Matrix send error: %s') % str(e))
|
||||
|
||||
# =========================================================================
|
||||
# ПРИЁМ ВХОДЯЩЕГО СООБЩЕНИЯ ИЗ MATRIX
|
||||
# =========================================================================
|
||||
|
||||
def channel_receive_message(self, payload):
|
||||
"""Create a mail.message in Odoo from an incoming Matrix event.
|
||||
|
||||
Входящее из Matrix → создаём mail.message с тегом 'matrix'.
|
||||
Это дублирует переписку внутри Odoo.
|
||||
"""
|
||||
body = payload.get('content', {}).get('body', '')
|
||||
sender = payload.get('sender', '')
|
||||
room_id = payload.get('room_id', '')
|
||||
|
||||
print(
|
||||
f"[tier_matrix] channel_receive_message: "
|
||||
f"входящее из Matrix, sender={sender}, room={room_id}, body_len={len(body)}"
|
||||
)
|
||||
|
||||
# Ищем пользователя по room_id чтобы привязать к нужному треду
|
||||
user_with_room = self.env['res.users'].sudo().search(
|
||||
[('tier_matrix_room_id', '=', room_id)], limit=1
|
||||
)
|
||||
|
||||
vals = {
|
||||
'body': body,
|
||||
'message_type': 'email',
|
||||
'email_from': sender,
|
||||
'subject': f'Matrix: {sender}',
|
||||
}
|
||||
|
||||
# Если нашли пользователя с этой комнатой — привязываем к его партнёру
|
||||
if user_with_room and user_with_room.partner_id:
|
||||
vals['author_id'] = False # внешний отправитель
|
||||
|
||||
msg = self.env['mail.message'].sudo().create(vals)
|
||||
|
||||
# Помечаем тегом matrix
|
||||
matrix_tag = self.env['tier.channel.tag'].search([('code', '=', 'matrix')], limit=1)
|
||||
if matrix_tag:
|
||||
msg.channel_tag_ids = [(4, matrix_tag.id)]
|
||||
|
||||
from odoo.addons.tier_communications.models.tier_router import TierRouter
|
||||
TierRouter(self.env).route_incoming(msg)
|
||||
|
||||
print(
|
||||
f"[tier_matrix] channel_receive_message: "
|
||||
f"создано msg_id={msg.id} с тегом matrix"
|
||||
)
|
||||
return msg
|
||||
2
tier_communications_matrix/security/ir.model.access.csv
Normal file
2
tier_communications_matrix/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_tier_channel_matrix_helper_user,tier.channel.matrix.helper user,model_tier_channel_matrix_helper,base.group_user,1,1,1,1
|
||||
|
36
tier_communications_matrix/views/res_users_matrix_views.xml
Normal file
36
tier_communications_matrix/views/res_users_matrix_views.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Add Matrix credentials to Preferences form -->
|
||||
<record id="view_users_form_matrix_preferences" model="ir.ui.view">
|
||||
<field name="name">res.users.form.matrix.preferences</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="after">
|
||||
<group string="Matrix" name="tier_matrix_group">
|
||||
<field name="tier_matrix_homeserver" placeholder="https://matrix.org"/>
|
||||
<field name="tier_matrix_access_token" password="True"/>
|
||||
<field name="tier_matrix_room_id" placeholder="!roomid:server"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add Matrix credentials to full user form -->
|
||||
<record id="view_users_form_matrix_full" model="ir.ui.view">
|
||||
<field name="name">res.users.form.matrix.full</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="inside">
|
||||
<field name="tier_matrix_homeserver" placeholder="https://matrix.org"/>
|
||||
<field name="tier_matrix_access_token" password="True"/>
|
||||
<field name="tier_matrix_room_id" placeholder="!roomid:server"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="tier_channel_matrix_view_list" model="ir.ui.view">
|
||||
<field name="name">tier.channel.matrix.list</field>
|
||||
<field name="model">tier.channel.matrix</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Matrix-каналы">
|
||||
<field name="name"/>
|
||||
<field name="homeserver_url"/>
|
||||
<field name="room_id"/>
|
||||
<field name="credential_scope"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="tier_channel_matrix_view_form" model="ir.ui.view">
|
||||
<field name="name">tier.channel.matrix.form</field>
|
||||
<field name="model">tier.channel.matrix</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Matrix-канал">
|
||||
<header>
|
||||
<button name="channel_authenticate"
|
||||
string="Проверить подключение"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="Название канала"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Подключение">
|
||||
<field name="homeserver_url" placeholder="https://matrix.org"/>
|
||||
<field name="access_token" password="True"/>
|
||||
<field name="room_id" placeholder="!roomid:server"/>
|
||||
</group>
|
||||
<group string="Настройки">
|
||||
<field name="credential_scope"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_tier_channel_matrix" model="ir.actions.act_window">
|
||||
<field name="name">Matrix-каналы</field>
|
||||
<field name="res_model">tier.channel.matrix</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_tier_channel_matrix"
|
||||
name="Matrix-каналы"
|
||||
parent="tier_communications.menu_tier_communications_config"
|
||||
action="action_tier_channel_matrix"
|
||||
sequence="15"
|
||||
groups="base.group_system"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user