mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
158 Commits
anoa/modul
...
michaelk/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b7252178 | ||
|
|
3b082bf3b2 | ||
|
|
b00a8d870c | ||
|
|
8fa09c7479 | ||
|
|
d94873d525 | ||
|
|
f2fa172375 | ||
|
|
8c0ebe3026 | ||
|
|
53dd358c83 | ||
|
|
f5c944c7f2 | ||
|
|
aefb7a1146 | ||
|
|
479b7b1eff | ||
|
|
f04ee0b351 | ||
|
|
164798ec32 | ||
|
|
b7d7d20a38 | ||
|
|
2bb6d85736 | ||
|
|
0f0671e5e0 | ||
|
|
8eb9f37a01 | ||
|
|
ea89e73ebf | ||
|
|
68a9d1fc34 | ||
|
|
feae387576 | ||
|
|
e64f7c0188 | ||
|
|
b85ff4b894 | ||
|
|
4eca8d3fb3 | ||
|
|
aa3dc78f65 | ||
|
|
de874364e7 | ||
|
|
7e94e2ad94 | ||
|
|
e7ec6f78ca | ||
|
|
7a9aa4b81b | ||
|
|
5827e976fe | ||
|
|
44c0661d97 | ||
|
|
85db96cc81 | ||
|
|
3271742905 | ||
|
|
0a23bf442f | ||
|
|
9b13038d05 | ||
|
|
dfe09ec313 | ||
|
|
13bc1e5307 | ||
|
|
70e039c7ae | ||
|
|
2712a9ef8f | ||
|
|
da757b7759 | ||
|
|
59bc7debf0 | ||
|
|
cf68593544 | ||
|
|
9cc95fd0a5 | ||
|
|
82886e4c8f | ||
|
|
08919847c1 | ||
|
|
c3acc45a87 | ||
|
|
ae5bb32ad0 | ||
|
|
7ed3232b08 | ||
|
|
6e7488ce11 | ||
|
|
41585e1340 | ||
|
|
9498cd3e7b | ||
|
|
c7503f8f33 | ||
|
|
9d8baa1595 | ||
|
|
4ff8486f0f | ||
|
|
2669e494e0 | ||
|
|
b6d8a808a4 | ||
|
|
0cb5d34756 | ||
|
|
650761666d | ||
|
|
aa2a4b4b42 | ||
|
|
022469d819 | ||
|
|
45d06c754a | ||
|
|
dbd0821c43 | ||
|
|
0476852fc6 | ||
|
|
1d11d9323d | ||
|
|
261e4f2542 | ||
|
|
11728561f3 | ||
|
|
9d57abcadd | ||
|
|
cb0bbde981 | ||
|
|
abc97bd1de | ||
|
|
ee238254a0 | ||
|
|
0125b5d002 | ||
|
|
fe265fe990 | ||
|
|
7735eee41d | ||
|
|
3d0faa39fb | ||
|
|
fd28d13e19 | ||
|
|
d18731e252 | ||
|
|
81beae30b8 | ||
|
|
11f1bace3c | ||
|
|
1e8cfc9e77 | ||
|
|
488ed3e444 | ||
|
|
c3ec84dbcd | ||
|
|
0783801659 | ||
|
|
9f2fd29c14 | ||
|
|
6372dff771 | ||
|
|
b3e346f40c | ||
|
|
fb47ce3e6a | ||
|
|
debf04556b | ||
|
|
907a62df28 | ||
|
|
41b987cbc5 | ||
|
|
5c74ab4064 | ||
|
|
06820250c9 | ||
|
|
383c4ae59c | ||
|
|
f639ac143d | ||
|
|
ad0424bab0 | ||
|
|
2992125561 | ||
|
|
ef56b6e27c | ||
|
|
53d6245529 | ||
|
|
25e471dac3 | ||
|
|
76fca1730e | ||
|
|
32e4420a66 | ||
|
|
79b2583f1b | ||
|
|
8a24c4eee5 | ||
|
|
f93cb7410d | ||
|
|
50d5a97c1b | ||
|
|
c06932a029 | ||
|
|
3a62cacfb0 | ||
|
|
4d55b16faa | ||
|
|
105709bf32 | ||
|
|
d7fad867fa | ||
|
|
8fddcf703e | ||
|
|
e2adb360eb | ||
|
|
47ed4a4aa7 | ||
|
|
7fafa838ae | ||
|
|
de341bec1b | ||
|
|
643c89d497 | ||
|
|
6554253f48 | ||
|
|
3add16df49 | ||
|
|
dde01efbcb | ||
|
|
22e416b726 | ||
|
|
b4b7c80181 | ||
|
|
5fc3477fd3 | ||
|
|
8743f42b49 | ||
|
|
7285afa4be | ||
|
|
b22a53e357 | ||
|
|
3c446d0a81 | ||
|
|
240e940c3f | ||
|
|
969ed2e49d | ||
|
|
1147ce7e18 | ||
|
|
0d2b7fdcec | ||
|
|
4e12b10c7c | ||
|
|
e654230a51 | ||
|
|
ef5193e0cb | ||
|
|
7b3959c7f3 | ||
|
|
2e4a6c5aab | ||
|
|
e3eb2cfe8b | ||
|
|
5c341c99f6 | ||
|
|
739d3500fe | ||
|
|
0e2d70e101 | ||
|
|
82c4fd7226 | ||
|
|
e446077478 | ||
|
|
d82c89ac22 | ||
|
|
75b25b3f1f | ||
|
|
1df10d8814 | ||
|
|
8f9340d248 | ||
|
|
c5034cd4b0 | ||
|
|
f7f937d051 | ||
|
|
e52b5d94a9 | ||
|
|
d90f27a21f | ||
|
|
03cf9710e3 | ||
|
|
1dcdd8d568 | ||
|
|
4344fb1faf | ||
|
|
846577ebde | ||
|
|
3869981227 | ||
|
|
fa80b492a5 | ||
|
|
c776c52eed | ||
|
|
b424c16f50 | ||
|
|
313a489fc9 | ||
|
|
4b090cb273 | ||
|
|
3f79378d4b |
@@ -26,16 +26,6 @@ steps:
|
||||
- docker#v3.0.1:
|
||||
image: "python:3.6"
|
||||
|
||||
- command:
|
||||
- "python -m pip install tox"
|
||||
- "scripts-dev/check-newsfragment"
|
||||
label: ":newspaper: Newsfile"
|
||||
branches: "!master !develop !release-*"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "python:3.6"
|
||||
propagate-environment: true
|
||||
|
||||
- wait
|
||||
|
||||
- command:
|
||||
|
||||
@@ -43,3 +43,8 @@ prune .buildkite
|
||||
|
||||
exclude jenkins*
|
||||
recursive-exclude jenkins *.sh
|
||||
|
||||
# FIXME: we shouldn't have these templates here
|
||||
recursive-include res/templates-dinsic *.css
|
||||
recursive-include res/templates-dinsic *.html
|
||||
recursive-include res/templates-dinsic *.txt
|
||||
|
||||
@@ -654,9 +654,32 @@ uploads_path: "DATADIR/uploads"
|
||||
#
|
||||
#disable_msisdn_registration: true
|
||||
|
||||
# Derive the user's matrix ID from a type of 3PID used when registering.
|
||||
# This overrides any matrix ID the user proposes when calling /register
|
||||
# The 3PID type should be present in registrations_require_3pid to avoid
|
||||
# users failing to register if they don't specify the right kind of 3pid.
|
||||
#
|
||||
#register_mxid_from_3pid: email
|
||||
|
||||
# Uncomment to set the display name of new users to their email address,
|
||||
# rather than using the default heuristic.
|
||||
#
|
||||
#register_just_use_email_for_display_name: true
|
||||
|
||||
# Mandate that users are only allowed to associate certain formats of
|
||||
# 3PIDs with accounts on this server.
|
||||
#
|
||||
# Use an Identity Server to establish which 3PIDs are allowed to register?
|
||||
# Overrides allowed_local_3pids below.
|
||||
#
|
||||
#check_is_for_allowed_local_3pids: matrix.org
|
||||
#
|
||||
# If you are using an IS you can also check whether that IS registers
|
||||
# pending invites for the given 3PID (and then allow it to sign up on
|
||||
# the platform):
|
||||
#
|
||||
#allow_invited_3pids: False
|
||||
#
|
||||
#allowed_local_3pids:
|
||||
# - medium: email
|
||||
# pattern: '.*@matrix\.org'
|
||||
@@ -665,6 +688,11 @@ uploads_path: "DATADIR/uploads"
|
||||
# - medium: msisdn
|
||||
# pattern: '\+44'
|
||||
|
||||
# If true, stop users from trying to change the 3PIDs associated with
|
||||
# their accounts.
|
||||
#
|
||||
#disable_3pid_changes: False
|
||||
|
||||
# If set, allows registration of standard or admin accounts by anyone who
|
||||
# has the shared secret, even if registration is otherwise disabled.
|
||||
#
|
||||
@@ -702,6 +730,30 @@ uploads_path: "DATADIR/uploads"
|
||||
# - matrix.org
|
||||
# - vector.im
|
||||
|
||||
# If enabled, user IDs, display names and avatar URLs will be replicated
|
||||
# to this server whenever they change.
|
||||
# This is an experimental API currently implemented by sydent to support
|
||||
# cross-homeserver user directories.
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
# directory and to avoid ambiguities.
|
||||
#
|
||||
#disable_set_displayname: False
|
||||
#disable_set_avatar_url: False
|
||||
|
||||
# Users who register on this homeserver will automatically be joined
|
||||
# to these rooms
|
||||
#
|
||||
@@ -975,6 +1027,11 @@ password_config:
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
# search_all_users: false
|
||||
#
|
||||
# # If this is set, user search will be delegated to this ID server instead
|
||||
# # of synapse performing the search itself.
|
||||
# # This is an experimental API.
|
||||
# defer_to_id_server: https://id.example.com
|
||||
|
||||
|
||||
# User Consent configuration
|
||||
|
||||
7
res/templates-dinsic/mail-Vector.css
Normal file
7
res/templates-dinsic/mail-Vector.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.header {
|
||||
border-bottom: 4px solid #e4f7ed ! important;
|
||||
}
|
||||
|
||||
.notif_link a, .footer a {
|
||||
color: #76CFA6 ! important;
|
||||
}
|
||||
156
res/templates-dinsic/mail.css
Normal file
156
res/templates-dinsic/mail.css
Normal file
@@ -0,0 +1,156 @@
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#page {
|
||||
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
|
||||
font-color: #454545;
|
||||
font-size: 12pt;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#inner {
|
||||
width: 640px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 87px;
|
||||
color: #454545;
|
||||
border-bottom: 4px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: right;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.salutation {
|
||||
padding-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.summarytext {
|
||||
}
|
||||
|
||||
.room {
|
||||
width: 100%;
|
||||
color: #454545;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.room_header td {
|
||||
padding-top: 38px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.room_name {
|
||||
vertical-align: middle;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.room_header h2 {
|
||||
margin-top: 0px;
|
||||
margin-left: 75px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.room_avatar {
|
||||
width: 56px;
|
||||
line-height: 0px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.room_avatar img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.notif {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.historical_message .sender_avatar {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* spell out opacity and historical_message class names for Outlook aka Word */
|
||||
.historical_message .sender_name {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.historical_message .message_time {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.historical_message .message_body {
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
.historical_message td,
|
||||
.message td {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sender_avatar {
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sender_avatar img {
|
||||
margin-top: -2px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.sender_name {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
color: #a2a2a2;
|
||||
}
|
||||
|
||||
.message_time {
|
||||
text-align: right;
|
||||
width: 100px;
|
||||
font-size: 11px;
|
||||
color: #a2a2a2;
|
||||
}
|
||||
|
||||
.message_body {
|
||||
}
|
||||
|
||||
.notif_link td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notif_link a, .footer a {
|
||||
color: #454545;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.debug {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
45
res/templates-dinsic/notif.html
Normal file
45
res/templates-dinsic/notif.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% for message in notif.messages %}
|
||||
<tr class="{{ "historical_message" if message.is_historical else "message" }}">
|
||||
<td class="sender_avatar">
|
||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
||||
{% if message.sender_avatar_url %}
|
||||
<img alt="" class="sender_avatar" src="{{ message.sender_avatar_url|mxc_to_http(32,32) }}" />
|
||||
{% else %}
|
||||
{% if message.sender_hash % 3 == 0 %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/76cfa6.png" />
|
||||
{% elif message.sender_hash % 3 == 1 %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/50e2c2.png" />
|
||||
{% else %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/f4c371.png" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="message_contents">
|
||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
||||
<div class="sender_name">{% if message.msgtype == "m.emote" %}*{% endif %} {{ message.sender_name }}</div>
|
||||
{% endif %}
|
||||
<div class="message_body">
|
||||
{% if message.msgtype == "m.text" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.emote" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.notice" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.image" %}
|
||||
<img src="{{ message.image_url|mxc_to_http(640, 480, scale) }}" />
|
||||
{% elif message.msgtype == "m.file" %}
|
||||
<span class="filename">{{ message.body_text_plain }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="message_time">{{ message.ts|format_ts("%H:%M") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="notif_link">
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ notif.link }}">Voir {{ room.title }}</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
16
res/templates-dinsic/notif.txt
Normal file
16
res/templates-dinsic/notif.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
{% for message in notif.messages %}
|
||||
{% if message.msgtype == "m.emote" %}* {% endif %}{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }})
|
||||
{% if message.msgtype == "m.text" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.emote" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.notice" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.image" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.file" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Voir {{ room.title }} à {{ notif.link }}
|
||||
55
res/templates-dinsic/notif_mail.html
Normal file
55
res/templates-dinsic/notif_mail.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style type="text/css">
|
||||
{% include 'mail.css' without context %}
|
||||
{% include "mail-%s.css" % app_name ignore missing without context %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table id="page">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td id="inner">
|
||||
<table class="header">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="salutation">Bonjour {{ user_display_name }},</div>
|
||||
<div class="summarytext">{{ summary_text }}</div>
|
||||
</td>
|
||||
<td class="logo">
|
||||
{% if app_name == "Riot" %}
|
||||
<img src="http://matrix.org/img/riot-logo-email.png" width="83" height="83" alt="[Riot]"/>
|
||||
{% elif app_name == "Vector" %}
|
||||
<img src="http://matrix.org/img/vector-logo-email.png" width="64" height="83" alt="[Vector]"/>
|
||||
{% else %}
|
||||
<img src="http://matrix.org/img/matrix-120x51.png" width="120" height="51" alt="[matrix]"/>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% for room in rooms %}
|
||||
{% include 'room.html' with context %}
|
||||
{% endfor %}
|
||||
<div class="footer">
|
||||
<a href="{{ unsubscribe_link }}">Se désinscrire</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="debug">
|
||||
Sending email at {{ reason.now|format_ts("%c") }} due to activity in room {{ reason.room_name }} because
|
||||
an event was received at {{ reason.received_at|format_ts("%c") }}
|
||||
which is more than {{ "%.1f"|format(reason.delay_before_mail_ms / (60*1000)) }} ({{ reason.delay_before_mail_ms }}) mins ago,
|
||||
{% if reason.last_sent_ts %}
|
||||
and the last time we sent a mail for this room was {{ reason.last_sent_ts|format_ts("%c") }},
|
||||
which is more than {{ "%.1f"|format(reason.throttle_ms / (60*1000)) }} (current throttle_ms) mins ago.
|
||||
{% else %}
|
||||
and we don't have a last time we sent a mail for this room.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
10
res/templates-dinsic/notif_mail.txt
Normal file
10
res/templates-dinsic/notif_mail.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Bonjour {{ user_display_name }},
|
||||
|
||||
{{ summary_text }}
|
||||
|
||||
{% for room in rooms %}
|
||||
{% include 'room.txt' with context %}
|
||||
{% endfor %}
|
||||
|
||||
Vous pouvez désactiver ces notifications en cliquant ici {{ unsubscribe_link }}
|
||||
|
||||
33
res/templates-dinsic/room.html
Normal file
33
res/templates-dinsic/room.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<table class="room">
|
||||
<tr class="room_header">
|
||||
<td class="room_avatar">
|
||||
{% if room.avatar_url %}
|
||||
<img alt="" src="{{ room.avatar_url|mxc_to_http(48,48) }}" />
|
||||
{% else %}
|
||||
{% if room.hash % 3 == 0 %}
|
||||
<img alt="" src="https://vector.im/beta/img/76cfa6.png" />
|
||||
{% elif room.hash % 3 == 1 %}
|
||||
<img alt="" src="https://vector.im/beta/img/50e2c2.png" />
|
||||
{% else %}
|
||||
<img alt="" src="https://vector.im/beta/img/f4c371.png" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="room_name" colspan="2">
|
||||
{{ room.title }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if room.invite %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ room.link }}">Rejoindre la conversation.</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for notif in room.notifs %}
|
||||
{% include 'notif.html' with context %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
9
res/templates-dinsic/room.txt
Normal file
9
res/templates-dinsic/room.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ room.title }}
|
||||
|
||||
{% if room.invite %}
|
||||
Vous avez été invité, rejoignez la conversation en cliquant sur le lien suivant {{ room.link }}
|
||||
{% else %}
|
||||
{% for notif in room.notifs %}
|
||||
{% include 'notif.txt' with context %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -205,6 +205,7 @@ class Auth(object):
|
||||
)
|
||||
|
||||
user_id, app_service = yield self._get_appservice_user_id(request)
|
||||
|
||||
if user_id:
|
||||
request.authenticated_entity = user_id
|
||||
|
||||
@@ -255,39 +256,40 @@ class Auth(object):
|
||||
errcode=Codes.MISSING_TOKEN
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_appservice_user_id(self, request):
|
||||
app_service = self.store.get_app_service_by_token(
|
||||
self.get_access_token_from_request(
|
||||
request, self.TOKEN_NOT_FOUND_HTTP_STATUS
|
||||
)
|
||||
)
|
||||
|
||||
if app_service is None:
|
||||
defer.returnValue((None, None))
|
||||
return(None, None)
|
||||
|
||||
if app_service.ip_range_whitelist:
|
||||
ip_address = IPAddress(self.hs.get_ip_from_request(request))
|
||||
if ip_address not in app_service.ip_range_whitelist:
|
||||
defer.returnValue((None, None))
|
||||
return(None, None)
|
||||
|
||||
if b"user_id" not in request.args:
|
||||
defer.returnValue((app_service.sender, app_service))
|
||||
return(app_service.sender, app_service)
|
||||
|
||||
user_id = request.args[b"user_id"][0].decode('utf8')
|
||||
if app_service.sender == user_id:
|
||||
defer.returnValue((app_service.sender, app_service))
|
||||
return(app_service.sender, app_service)
|
||||
|
||||
if not app_service.is_interested_in_user(user_id):
|
||||
raise AuthError(
|
||||
403,
|
||||
"Application service cannot masquerade as this user."
|
||||
)
|
||||
if not (yield self.store.get_user_by_id(user_id)):
|
||||
raise AuthError(
|
||||
403,
|
||||
"Application service has not registered this user"
|
||||
)
|
||||
defer.returnValue((user_id, app_service))
|
||||
# Let ASes manipulate nonexistent users (e.g. to shadow-register them)
|
||||
# if not (yield self.store.get_user_by_id(user_id)):
|
||||
# raise AuthError(
|
||||
# 403,
|
||||
# "Application service has not registered this user"
|
||||
# )
|
||||
return(user_id, app_service)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_by_access_token(self, token, rights="access"):
|
||||
@@ -520,24 +522,15 @@ class Auth(object):
|
||||
defer.returnValue(user_info)
|
||||
|
||||
def get_appservice_by_req(self, request):
|
||||
try:
|
||||
token = self.get_access_token_from_request(
|
||||
request, self.TOKEN_NOT_FOUND_HTTP_STATUS
|
||||
)
|
||||
service = self.store.get_app_service_by_token(token)
|
||||
if not service:
|
||||
logger.warn("Unrecognised appservice access token.")
|
||||
raise AuthError(
|
||||
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
||||
"Unrecognised access token.",
|
||||
errcode=Codes.UNKNOWN_TOKEN
|
||||
)
|
||||
request.authenticated_entity = service.sender
|
||||
return defer.succeed(service)
|
||||
except KeyError:
|
||||
(user_id, app_service) = self._get_appservice_user_id(request)
|
||||
if not app_service:
|
||||
raise AuthError(
|
||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token."
|
||||
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
||||
"Unrecognised access token.",
|
||||
errcode=Codes.UNKNOWN_TOKEN,
|
||||
)
|
||||
request.authenticated_entity = app_service.sender
|
||||
return app_service
|
||||
|
||||
def is_server_admin(self, user):
|
||||
""" Check if the given user is a local server admin.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2018 New Vector Ltd.
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -75,6 +75,7 @@ class EventTypes(object):
|
||||
RoomAvatar = "m.room.avatar"
|
||||
RoomEncryption = "m.room.encryption"
|
||||
GuestAccess = "m.room.guest_access"
|
||||
Encryption = "m.room.encryption"
|
||||
|
||||
# These are used for validation
|
||||
Message = "m.room.message"
|
||||
|
||||
@@ -265,7 +265,7 @@ class ApplicationService(object):
|
||||
def is_exclusive_room(self, room_id):
|
||||
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
|
||||
|
||||
def get_exlusive_user_regexes(self):
|
||||
def get_exclusive_user_regexes(self):
|
||||
"""Get the list of regexes used to determine if a user is exclusively
|
||||
registered by the AS
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,18 @@ class RegistrationConfig(Config):
|
||||
|
||||
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
|
||||
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
|
||||
self.check_is_for_allowed_local_3pids = config.get(
|
||||
"check_is_for_allowed_local_3pids", None
|
||||
)
|
||||
self.allow_invited_3pids = config.get("allow_invited_3pids", False)
|
||||
|
||||
self.disable_3pid_changes = config.get("disable_3pid_changes", False)
|
||||
|
||||
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||
self.register_mxid_from_3pid = config.get("register_mxid_from_3pid")
|
||||
self.register_just_use_email_for_display_name = config.get(
|
||||
"register_just_use_email_for_display_name", False,
|
||||
)
|
||||
|
||||
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
|
||||
self.trusted_third_party_id_servers = config.get(
|
||||
@@ -53,6 +64,16 @@ class RegistrationConfig(Config):
|
||||
raise ConfigError('Invalid auto_join_rooms entry %s' % (room_alias,))
|
||||
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
|
||||
|
||||
self.disable_set_displayname = config.get("disable_set_displayname", False)
|
||||
self.disable_set_avatar_url = config.get("disable_set_avatar_url", False)
|
||||
|
||||
self.replicate_user_profiles_to = config.get("replicate_user_profiles_to", [])
|
||||
if not isinstance(self.replicate_user_profiles_to, list):
|
||||
self.replicate_user_profiles_to = [self.replicate_user_profiles_to, ]
|
||||
|
||||
self.shadow_server = config.get("shadow_server", None)
|
||||
self.rewrite_identity_server_urls = config.get("rewrite_identity_server_urls", {})
|
||||
|
||||
self.disable_msisdn_registration = (
|
||||
config.get("disable_msisdn_registration", False)
|
||||
)
|
||||
@@ -86,9 +107,32 @@ class RegistrationConfig(Config):
|
||||
#
|
||||
#disable_msisdn_registration: true
|
||||
|
||||
# Derive the user's matrix ID from a type of 3PID used when registering.
|
||||
# This overrides any matrix ID the user proposes when calling /register
|
||||
# The 3PID type should be present in registrations_require_3pid to avoid
|
||||
# users failing to register if they don't specify the right kind of 3pid.
|
||||
#
|
||||
#register_mxid_from_3pid: email
|
||||
|
||||
# Uncomment to set the display name of new users to their email address,
|
||||
# rather than using the default heuristic.
|
||||
#
|
||||
#register_just_use_email_for_display_name: true
|
||||
|
||||
# Mandate that users are only allowed to associate certain formats of
|
||||
# 3PIDs with accounts on this server.
|
||||
#
|
||||
# Use an Identity Server to establish which 3PIDs are allowed to register?
|
||||
# Overrides allowed_local_3pids below.
|
||||
#
|
||||
#check_is_for_allowed_local_3pids: matrix.org
|
||||
#
|
||||
# If you are using an IS you can also check whether that IS registers
|
||||
# pending invites for the given 3PID (and then allow it to sign up on
|
||||
# the platform):
|
||||
#
|
||||
#allow_invited_3pids: False
|
||||
#
|
||||
#allowed_local_3pids:
|
||||
# - medium: email
|
||||
# pattern: '.*@matrix\\.org'
|
||||
@@ -97,6 +141,11 @@ class RegistrationConfig(Config):
|
||||
# - medium: msisdn
|
||||
# pattern: '\\+44'
|
||||
|
||||
# If true, stop users from trying to change the 3PIDs associated with
|
||||
# their accounts.
|
||||
#
|
||||
#disable_3pid_changes: False
|
||||
|
||||
# If set, allows registration of standard or admin accounts by anyone who
|
||||
# has the shared secret, even if registration is otherwise disabled.
|
||||
#
|
||||
@@ -134,6 +183,30 @@ class RegistrationConfig(Config):
|
||||
# - matrix.org
|
||||
# - vector.im
|
||||
|
||||
# If enabled, user IDs, display names and avatar URLs will be replicated
|
||||
# to this server whenever they change.
|
||||
# This is an experimental API currently implemented by sydent to support
|
||||
# cross-homeserver user directories.
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
# directory and to avoid ambiguities.
|
||||
#
|
||||
#disable_set_displayname: False
|
||||
#disable_set_avatar_url: False
|
||||
|
||||
# Users who register on this homeserver will automatically be joined
|
||||
# to these rooms
|
||||
#
|
||||
|
||||
@@ -24,6 +24,7 @@ class UserDirectoryConfig(Config):
|
||||
def read_config(self, config):
|
||||
self.user_directory_search_enabled = True
|
||||
self.user_directory_search_all_users = False
|
||||
self.user_directory_defer_to_id_server = None
|
||||
user_directory_config = config.get("user_directory", None)
|
||||
if user_directory_config:
|
||||
self.user_directory_search_enabled = (
|
||||
@@ -32,6 +33,9 @@ class UserDirectoryConfig(Config):
|
||||
self.user_directory_search_all_users = (
|
||||
user_directory_config.get("search_all_users", False)
|
||||
)
|
||||
self.user_directory_defer_to_id_server = (
|
||||
user_directory_config.get("defer_to_id_server", None)
|
||||
)
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
@@ -50,4 +54,9 @@ class UserDirectoryConfig(Config):
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
# search_all_users: false
|
||||
#
|
||||
# # If this is set, user search will be delegated to this ID server instead
|
||||
# # of synapse performing the search itself.
|
||||
# # This is an experimental API.
|
||||
# defer_to_id_server: https://id.example.com
|
||||
"""
|
||||
|
||||
@@ -46,13 +46,23 @@ class SpamChecker(object):
|
||||
|
||||
return self.spam_checker.check_event_for_spam(event)
|
||||
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, room_id):
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
|
||||
room_id, new_room):
|
||||
"""Checks if a given user may send an invite
|
||||
|
||||
If this method returns false, the invite will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
inviter_userid (str)
|
||||
invitee_userid (str|None): The user ID of the invitee. Is None
|
||||
if this is a third party invite and the 3PID is not bound to a
|
||||
user ID.
|
||||
third_party_invite (dict|None): If a third party invite then is a
|
||||
dict containing the medium and address of the invitee.
|
||||
room_id (str)
|
||||
new_room (bool): Wether the user is being invited to the room as
|
||||
part of a room creation, if so the invitee would have been
|
||||
included in the call to `user_may_create_room`.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may send an invite, otherwise False
|
||||
@@ -60,15 +70,24 @@ class SpamChecker(object):
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id)
|
||||
return self.spam_checker.user_may_invite(
|
||||
inviter_userid, invitee_userid, third_party_invite, room_id, new_room,
|
||||
)
|
||||
|
||||
def user_may_create_room(self, userid):
|
||||
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
|
||||
cloning):
|
||||
"""Checks if a given user may create a room
|
||||
|
||||
If this method returns false, the creation request will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
invite_list (list[str]): List of user IDs that would be invited to
|
||||
the new room.
|
||||
third_party_invite_list (list[dict]): List of third party invites
|
||||
for the new room.
|
||||
cloning (bool): Whether the user is cloning an existing room, e.g.
|
||||
upgrading a room.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may create a room, otherwise False
|
||||
@@ -76,7 +95,9 @@ class SpamChecker(object):
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_create_room(userid)
|
||||
return self.spam_checker.user_may_create_room(
|
||||
userid, invite_list, third_party_invite_list, cloning,
|
||||
)
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
"""Checks if a given user may create a room alias
|
||||
@@ -111,3 +132,21 @@ class SpamChecker(object):
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_publish_room(userid, room_id)
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Checks if a given users is allowed to join a room.
|
||||
|
||||
Is not called when the user creates a room.
|
||||
|
||||
Args:
|
||||
userid (str)
|
||||
room_id (str)
|
||||
is_invited (bool): Whether the user is invited into the room
|
||||
|
||||
Returns:
|
||||
bool: Whether the user may join the room
|
||||
"""
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_join_room(userid, room_id, is_invited)
|
||||
|
||||
@@ -33,6 +33,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
self._device_handler = hs.get_device_handler()
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
self._identity_handler = hs.get_handlers().identity_handler
|
||||
self._profile_handler = hs.get_profile_handler()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
|
||||
# Flag that indicates whether the process to part users from rooms is running
|
||||
@@ -94,6 +95,9 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
|
||||
yield self.store.user_set_password_hash(user_id, None)
|
||||
|
||||
user = UserID.from_string(user_id)
|
||||
yield self._profile_handler.set_active(user, False, False)
|
||||
|
||||
# Add the user to a table of users pending deactivation (ie.
|
||||
# removal from all the rooms they're a member of)
|
||||
yield self.store.add_user_pending_deactivation(user_id)
|
||||
|
||||
@@ -568,6 +568,12 @@ class DeviceListEduUpdater(object):
|
||||
stream_id = result["stream_id"]
|
||||
devices = result["devices"]
|
||||
|
||||
for device in devices:
|
||||
logger.debug(
|
||||
"Handling resync update %r/%r, ID: %r",
|
||||
user_id, device["device_id"], stream_id,
|
||||
)
|
||||
|
||||
# If the remote server has more than ~1000 devices for this user
|
||||
# we assume that something is going horribly wrong (e.g. a bot
|
||||
# that logs in and creates a new device every time it tries to
|
||||
|
||||
@@ -1346,7 +1346,8 @@ class FederationHandler(BaseHandler):
|
||||
raise SynapseError(403, "This server does not accept room invites")
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
event.sender, event.state_key, event.room_id,
|
||||
event.sender, event.state_key, None,
|
||||
room_id=event.room_id, new_room=False,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "This user is not permitted to send invites to this server/user"
|
||||
|
||||
@@ -47,6 +47,7 @@ class IdentityHandler(BaseHandler):
|
||||
self.trust_any_id_server_just_for_testing_do_not_use = (
|
||||
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
|
||||
)
|
||||
self.rewrite_identity_server_urls = hs.config.rewrite_identity_server_urls
|
||||
|
||||
def _should_trust_id_server(self, id_server):
|
||||
if id_server not in self.trusted_id_servers:
|
||||
@@ -84,7 +85,10 @@ class IdentityHandler(BaseHandler):
|
||||
'credentials', id_server
|
||||
)
|
||||
defer.returnValue(None)
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.get_json(
|
||||
"https://%s%s" % (
|
||||
@@ -119,7 +123,10 @@ class IdentityHandler(BaseHandler):
|
||||
client_secret = creds['clientSecret']
|
||||
else:
|
||||
raise SynapseError(400, "No client_secret in creds")
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.post_urlencoded_get_json(
|
||||
"https://%s%s" % (
|
||||
@@ -162,7 +169,6 @@ class IdentityHandler(BaseHandler):
|
||||
# deletion request to.
|
||||
id_server = next(iter(self.trusted_id_servers))
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
|
||||
content = {
|
||||
"mxid": mxid,
|
||||
"threepid": threepid,
|
||||
@@ -182,6 +188,16 @@ class IdentityHandler(BaseHandler):
|
||||
b"Authorization": auth_headers,
|
||||
}
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
#
|
||||
# Note that destination_is has to be the real id_server, not
|
||||
# the server we connect to.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
|
||||
|
||||
try:
|
||||
yield self.http_client.post_json_get_json(
|
||||
url,
|
||||
@@ -213,7 +229,10 @@ class IdentityHandler(BaseHandler):
|
||||
'send_attempt': send_attempt,
|
||||
}
|
||||
params.update(kwargs)
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
"https://%s%s" % (
|
||||
@@ -245,7 +264,10 @@ class IdentityHandler(BaseHandler):
|
||||
'send_attempt': send_attempt,
|
||||
}
|
||||
params.update(kwargs)
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
"https://%s%s" % (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +16,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
from six.moves import range
|
||||
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
@@ -26,6 +31,7 @@ from synapse.api.errors import (
|
||||
)
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.types import UserID, get_domain_from_id
|
||||
from synapse.util.logcontext import run_in_background
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
@@ -40,6 +46,8 @@ class BaseProfileHandler(BaseHandler):
|
||||
subclass MasterProfileHandler
|
||||
"""
|
||||
|
||||
PROFILE_REPLICATE_INTERVAL = 2 * 60 * 1000
|
||||
|
||||
def __init__(self, hs):
|
||||
super(BaseProfileHandler, self).__init__(hs)
|
||||
|
||||
@@ -50,6 +58,84 @@ class BaseProfileHandler(BaseHandler):
|
||||
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
if hs.config.worker_app is None:
|
||||
self.clock.looping_call(
|
||||
self._start_update_remote_profile_cache, self.PROFILE_UPDATE_MS,
|
||||
)
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
reactor.callWhenRunning(self._assign_profile_replication_batches)
|
||||
reactor.callWhenRunning(self._replicate_profiles)
|
||||
# Add a looping call to replicate_profiles: this handles retries
|
||||
# if the replication is unsuccessful when the user updated their
|
||||
# profile.
|
||||
self.clock.looping_call(
|
||||
self._replicate_profiles, self.PROFILE_REPLICATE_INTERVAL
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _assign_profile_replication_batches(self):
|
||||
"""If no profile replication has been done yet, allocate replication batch
|
||||
numbers to each profile to start the replication process.
|
||||
"""
|
||||
logger.info("Assigning profile batch numbers...")
|
||||
total = 0
|
||||
while True:
|
||||
assigned = yield self.store.assign_profile_batch()
|
||||
total += assigned
|
||||
if assigned == 0:
|
||||
break
|
||||
logger.info("Assigned %d profile batch numbers", total)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _replicate_profiles(self):
|
||||
"""If any profile data has been updated and not pushed to the replication targets,
|
||||
replicate it.
|
||||
"""
|
||||
host_batches = yield self.store.get_replication_hosts()
|
||||
latest_batch = yield self.store.get_latest_profile_replication_batch_number()
|
||||
if latest_batch is None:
|
||||
latest_batch = -1
|
||||
for repl_host in self.hs.config.replicate_user_profiles_to:
|
||||
if repl_host not in host_batches:
|
||||
host_batches[repl_host] = -1
|
||||
try:
|
||||
for i in range(host_batches[repl_host] + 1, latest_batch + 1):
|
||||
yield self._replicate_host_profile_batch(repl_host, i)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Exception while replicating to %s: aborting for now", repl_host,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _replicate_host_profile_batch(self, host, batchnum):
|
||||
logger.info("Replicating profile batch %d to %s", batchnum, host)
|
||||
batch_rows = yield self.store.get_profile_batch(batchnum)
|
||||
batch = {
|
||||
UserID(r["user_id"], self.hs.hostname).to_string(): ({
|
||||
"display_name": r["displayname"],
|
||||
"avatar_url": r["avatar_url"],
|
||||
} if r["active"] else None) for r in batch_rows
|
||||
}
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/replicate_profiles" % (host,)
|
||||
body = {
|
||||
"batchnum": batchnum,
|
||||
"batch": batch,
|
||||
"origin_server": self.hs.hostname,
|
||||
}
|
||||
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
|
||||
try:
|
||||
yield self.http_client.post_json_get_json(url, signed_body)
|
||||
yield self.store.update_replication_batch_for_host(host, batchnum)
|
||||
logger.info("Sucessfully replicated profile batch %d to %s", batchnum, host)
|
||||
except Exception:
|
||||
# This will get retried when the looping call next comes around
|
||||
logger.exception("Failed to replicate profile batch %d to %s", batchnum, host)
|
||||
raise
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_profile(self, user_id):
|
||||
target_user = UserID.from_string(user_id)
|
||||
@@ -147,19 +233,30 @@ class BaseProfileHandler(BaseHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_displayname(self, target_user, requester, new_displayname, by_admin=False):
|
||||
"""target_user is the user whose displayname is to be changed;
|
||||
auth_user is the user attempting to make this change."""
|
||||
"""target_user is the UserID whose displayname is to be changed;
|
||||
requester is the authenticated user attempting to make this change."""
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if not by_admin and target_user != requester.user:
|
||||
if not by_admin and requester and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's displayname")
|
||||
|
||||
if not by_admin and self.hs.config.disable_set_displayname:
|
||||
profile = yield self.store.get_profileinfo(target_user.localpart)
|
||||
if profile.display_name:
|
||||
raise SynapseError(400, "Changing displayname is disabled on this server")
|
||||
|
||||
if new_displayname == '':
|
||||
new_displayname = None
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
|
||||
yield self.store.set_profile_displayname(
|
||||
target_user.localpart, new_displayname
|
||||
target_user.localpart, new_displayname, new_batchnum
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
@@ -168,7 +265,37 @@ class BaseProfileHandler(BaseHandler):
|
||||
target_user.to_string(), profile
|
||||
)
|
||||
|
||||
yield self._update_join_states(requester, target_user)
|
||||
if requester:
|
||||
yield self._update_join_states(requester, target_user)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_active(self, target_user, active, hide):
|
||||
"""
|
||||
Sets the 'active' flag on a user profile. If set to false, the user
|
||||
account is considered deactivated or hidden.
|
||||
|
||||
If 'hide' is true, then we interpret active=False as a request to try to
|
||||
hide the user rather than deactivating it. This means withholding the
|
||||
profile from replication (and mark it as inactive) rather than clearing
|
||||
the profile from the HS DB. Note that unlike set_displayname and
|
||||
set_avatar_url, this does *not* perform authorization checks! This is
|
||||
because the only place it's used currently is in account deactivation
|
||||
where we've already done these checks anyway.
|
||||
"""
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
yield self.store.set_profile_active(
|
||||
target_user.localpart, active, hide, new_batchnum
|
||||
)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_avatar_url(self, target_user):
|
||||
@@ -210,8 +337,19 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not by_admin and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's avatar_url")
|
||||
|
||||
if not by_admin and self.hs.config.disable_set_avatar_url:
|
||||
profile = yield self.store.get_profileinfo(target_user.localpart)
|
||||
if profile.avatar_url:
|
||||
raise SynapseError(400, "Changing avatar url is disabled on this server")
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
|
||||
yield self.store.set_profile_avatar_url(
|
||||
target_user.localpart, new_avatar_url
|
||||
target_user.localpart, new_avatar_url, new_batchnum,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
@@ -222,6 +360,9 @@ class BaseProfileHandler(BaseHandler):
|
||||
|
||||
yield self._update_join_states(requester, target_user)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_profile_query(self, args):
|
||||
user = UserID.from_string(args["user_id"])
|
||||
|
||||
@@ -61,6 +61,7 @@ class RegistrationHandler(BaseHandler):
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
self.captcha_client = CaptchaServerHttpClient(hs)
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.identity_handler = self.hs.get_handlers().identity_handler
|
||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||
|
||||
@@ -223,6 +224,11 @@ class RegistrationHandler(BaseHandler):
|
||||
address=address,
|
||||
)
|
||||
|
||||
if default_display_name:
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, default_display_name, by_admin=True,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
profile = yield self.store.get_profileinfo(localpart)
|
||||
yield self.user_directory_handler.handle_local_profile_change(
|
||||
@@ -252,6 +258,11 @@ class RegistrationHandler(BaseHandler):
|
||||
create_profile_with_displayname=default_display_name,
|
||||
address=address,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, default_display_name, by_admin=True,
|
||||
)
|
||||
|
||||
except SynapseError:
|
||||
# if user id is taken, just generate another
|
||||
user = None
|
||||
@@ -330,7 +341,9 @@ class RegistrationHandler(BaseHandler):
|
||||
yield self._auto_join_rooms(user_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def appservice_register(self, user_localpart, as_token):
|
||||
def appservice_register(self, user_localpart, as_token, password, display_name):
|
||||
# FIXME: this should be factored out and merged with normal register()
|
||||
|
||||
user = UserID(user_localpart, self.hs.hostname)
|
||||
user_id = user.to_string()
|
||||
service = self.store.get_app_service_by_token(as_token)
|
||||
@@ -348,12 +361,29 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id, allowed_appservice=service
|
||||
)
|
||||
|
||||
password_hash = ""
|
||||
if password:
|
||||
password_hash = yield self.auth_handler().hash(password)
|
||||
|
||||
display_name = display_name or user.localpart
|
||||
|
||||
yield self.register_with_store(
|
||||
user_id=user_id,
|
||||
password_hash="",
|
||||
password_hash=password_hash,
|
||||
appservice_id=service_id,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
create_profile_with_displayname=display_name,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, display_name, by_admin=True,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
profile = yield self.store.get_profileinfo(user_localpart)
|
||||
yield self.user_directory_handler.handle_local_profile_change(
|
||||
user_id, profile
|
||||
)
|
||||
|
||||
defer.returnValue(user_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -379,6 +409,39 @@ class RegistrationHandler(BaseHandler):
|
||||
else:
|
||||
logger.info("Valid captcha entered from %s", ip)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def register_saml2(self, localpart):
|
||||
"""
|
||||
Registers email_id as SAML2 Based Auth.
|
||||
"""
|
||||
if types.contains_invalid_mxid_characters(localpart):
|
||||
raise SynapseError(
|
||||
400,
|
||||
"User ID can only contain characters a-z, 0-9, or '=_-./'",
|
||||
)
|
||||
yield self.auth.check_auth_blocking()
|
||||
user = UserID(localpart, self.hs.hostname)
|
||||
user_id = user.to_string()
|
||||
|
||||
yield self.check_user_id_not_appservice_exclusive(user_id)
|
||||
token = self.macaroon_gen.generate_access_token(user_id)
|
||||
try:
|
||||
yield self.register_with_store(
|
||||
user_id=user_id,
|
||||
token=token,
|
||||
password_hash=None,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, user.localpart, by_admin=True,
|
||||
)
|
||||
except Exception as e:
|
||||
yield self.store.add_access_token_to_user(user_id, token)
|
||||
# Ignore Registration errors
|
||||
logger.exception(e)
|
||||
defer.returnValue((user_id, token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def register_email(self, threepidCreds):
|
||||
"""
|
||||
@@ -401,7 +464,9 @@ class RegistrationHandler(BaseHandler):
|
||||
logger.info("got threepid with medium '%s' and address '%s'",
|
||||
threepid['medium'], threepid['address'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, threepid['medium'], threepid['address']):
|
||||
if not (
|
||||
yield check_3pid_allowed(self.hs, threepid['medium'], threepid['address'])
|
||||
):
|
||||
raise RegistrationError(
|
||||
403, "Third party identifier is not allowed"
|
||||
)
|
||||
@@ -442,6 +507,39 @@ class RegistrationHandler(BaseHandler):
|
||||
errcode=Codes.EXCLUSIVE
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_register(self, localpart, display_name, auth_result, params):
|
||||
"""Invokes the current registration on another server, using
|
||||
shared secret registration, passing in any auth_results from
|
||||
other registration UI auth flows (e.g. validated 3pids)
|
||||
Useful for setting up shadow/backup accounts on a parallel deployment.
|
||||
"""
|
||||
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/register?access_token=%s" % (
|
||||
shadow_hs_url, as_token,
|
||||
),
|
||||
{
|
||||
# XXX: auth_result is an unspecified extension for shadow registration
|
||||
'auth_result': auth_result,
|
||||
# XXX: another unspecified extension for shadow registration to ensure
|
||||
# that the displayname is correctly set by the masters erver
|
||||
'display_name': display_name,
|
||||
'username': localpart,
|
||||
'password': params.get("password"),
|
||||
'bind_email': params.get("bind_email"),
|
||||
'bind_msisdn': params.get("bind_msisdn"),
|
||||
'device_id': params.get("device_id"),
|
||||
'initial_device_display_name': params.get("initial_device_display_name"),
|
||||
'inhibit_login': False,
|
||||
'access_token': as_token,
|
||||
}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _generate_user_id(self, reseed=False):
|
||||
if reseed or self._next_generated_user_id is None:
|
||||
@@ -528,18 +626,16 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id=user_id,
|
||||
token=token,
|
||||
password_hash=password_hash,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
create_profile_with_displayname=displayname or user.localpart,
|
||||
)
|
||||
if displayname is not None:
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, displayname or user.localpart, by_admin=True,
|
||||
)
|
||||
else:
|
||||
yield self._auth_handler.delete_access_tokens_for_user(user_id)
|
||||
yield self.store.add_access_token_to_user(user_id=user_id, token=token)
|
||||
|
||||
if displayname is not None:
|
||||
logger.info("setting user display name: %s -> %s", user_id, displayname)
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, requester, displayname, by_admin=True,
|
||||
)
|
||||
|
||||
defer.returnValue((user_id, token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
||||
@@ -54,12 +54,14 @@ class RoomCreationHandler(BaseHandler):
|
||||
"history_visibility": "shared",
|
||||
"original_invitees_have_ops": False,
|
||||
"guest_can_join": True,
|
||||
"encryption_alg": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
|
||||
"join_rules": JoinRules.INVITE,
|
||||
"history_visibility": "shared",
|
||||
"original_invitees_have_ops": True,
|
||||
"guest_can_join": True,
|
||||
"encryption_alg": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
RoomCreationPreset.PUBLIC_CHAT: {
|
||||
"join_rules": JoinRules.PUBLIC,
|
||||
@@ -79,6 +81,8 @@ class RoomCreationHandler(BaseHandler):
|
||||
# linearizer to stop two upgrades happening at once
|
||||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
|
||||
|
||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def upgrade_room(self, requester, old_room_id, new_version):
|
||||
"""Replace a room with a new room with a different version
|
||||
@@ -252,7 +256,22 @@ class RoomCreationHandler(BaseHandler):
|
||||
"""
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=[],
|
||||
third_party_invite_list=[],
|
||||
cloning=True,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
creation_content = {
|
||||
@@ -473,7 +492,24 @@ class RoomCreationHandler(BaseHandler):
|
||||
|
||||
yield self.auth.check_auth_blocking(user_id)
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
invite_list = config.get("invite", [])
|
||||
invite_3pid_list = config.get("invite_3pid", [])
|
||||
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=invite_list,
|
||||
third_party_invite_list=invite_3pid_list,
|
||||
cloning=False,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
if ratelimit:
|
||||
@@ -516,7 +552,6 @@ class RoomCreationHandler(BaseHandler):
|
||||
else:
|
||||
room_alias = None
|
||||
|
||||
invite_list = config.get("invite", [])
|
||||
for i in invite_list:
|
||||
try:
|
||||
UserID.from_string(i)
|
||||
@@ -527,8 +562,6 @@ class RoomCreationHandler(BaseHandler):
|
||||
requester,
|
||||
)
|
||||
|
||||
invite_3pid_list = config.get("invite_3pid", [])
|
||||
|
||||
visibility = config.get("visibility", None)
|
||||
is_public = visibility == "public"
|
||||
|
||||
@@ -613,6 +646,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
"invite",
|
||||
ratelimit=False,
|
||||
content=content,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
for invite_3pid in invite_3pid_list:
|
||||
@@ -627,6 +661,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
id_server,
|
||||
requester,
|
||||
txn_id=None,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
result = {"room_id": room_id}
|
||||
@@ -697,6 +732,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
"join",
|
||||
ratelimit=False,
|
||||
content=creator_join_profile,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
# We treat the power levels override specially as this needs to be one
|
||||
@@ -772,6 +808,15 @@ class RoomCreationHandler(BaseHandler):
|
||||
content=content,
|
||||
)
|
||||
|
||||
if "encryption_alg" in config:
|
||||
yield send(
|
||||
etype=EventTypes.Encryption,
|
||||
state_key="",
|
||||
content={
|
||||
'algorithm': config["encryption_alg"],
|
||||
}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _generate_room_id(self, creator_id, is_public):
|
||||
# autogen room IDs and try to create it. We may clash, so just
|
||||
|
||||
@@ -70,6 +70,7 @@ class RoomMemberHandler(object):
|
||||
self.clock = hs.get_clock()
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self._server_notices_mxid = self.config.server_notices_mxid
|
||||
self.rewrite_identity_server_urls = self.config.rewrite_identity_server_urls
|
||||
|
||||
@abc.abstractmethod
|
||||
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
|
||||
@@ -307,8 +308,31 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
require_consent=True,
|
||||
):
|
||||
"""Update a users membership in a room
|
||||
|
||||
Args:
|
||||
requester (Requester)
|
||||
target (UserID)
|
||||
room_id (str)
|
||||
action (str): The "action" the requester is performing against the
|
||||
target. One of join/leave/kick/ban/invite/unban.
|
||||
txn_id (str|None): The transaction ID associated with the request,
|
||||
or None not provided.
|
||||
remote_room_hosts (list[str]|None): List of remote servers to try
|
||||
and join via if server isn't already in the room.
|
||||
third_party_signed (dict|None): The signed object for third party
|
||||
invites.
|
||||
ratelimit (bool): Whether to apply ratelimiting to this request.
|
||||
content (dict|None): Fields to include in the new events content.
|
||||
new_room (bool): Whether these membership changes are happening
|
||||
as part of a room creation (e.g. initial joins and invites)
|
||||
|
||||
Returns:
|
||||
Deferred[FrozenEvent]
|
||||
"""
|
||||
key = (room_id,)
|
||||
|
||||
with (yield self.member_linearizer.queue(key)):
|
||||
@@ -322,6 +346,7 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=third_party_signed,
|
||||
ratelimit=ratelimit,
|
||||
content=content,
|
||||
new_room=new_room,
|
||||
require_consent=require_consent,
|
||||
)
|
||||
|
||||
@@ -339,6 +364,7 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
require_consent=True,
|
||||
):
|
||||
content_specified = bool(content)
|
||||
@@ -400,7 +426,10 @@ class RoomMemberHandler(object):
|
||||
block_invite = True
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), target.to_string(), room_id,
|
||||
requester.user.to_string(), target.to_string(),
|
||||
third_party_invite=None,
|
||||
room_id=room_id,
|
||||
new_room=new_room,
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
block_invite = True
|
||||
@@ -470,8 +499,29 @@ class RoomMemberHandler(object):
|
||||
# so don't really fit into the general auth process.
|
||||
raise AuthError(403, "Guest access not allowed")
|
||||
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to join rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
inviter = yield self._get_inviter(target.to_string(), room_id)
|
||||
if not is_requester_admin:
|
||||
# We assume that if the spam checker allowed the user to create
|
||||
# a room then they're allowed to join it.
|
||||
if not new_room and not self.spam_checker.user_may_join_room(
|
||||
target.to_string(), room_id,
|
||||
is_invited=inviter is not None,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "Not allowed to join this room",
|
||||
)
|
||||
|
||||
if not is_host_in_room:
|
||||
inviter = yield self._get_inviter(target.to_string(), room_id)
|
||||
if inviter and not self.hs.is_mine(inviter):
|
||||
remote_room_hosts.append(inviter.domain)
|
||||
|
||||
@@ -681,7 +731,8 @@ class RoomMemberHandler(object):
|
||||
address,
|
||||
id_server,
|
||||
requester,
|
||||
txn_id
|
||||
txn_id,
|
||||
new_room=False,
|
||||
):
|
||||
if self.config.block_non_admin_invites:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
@@ -697,6 +748,20 @@ class RoomMemberHandler(object):
|
||||
id_server, medium, address
|
||||
)
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), invitee,
|
||||
third_party_invite={
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
},
|
||||
room_id=room_id,
|
||||
new_room=new_room,
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
raise SynapseError(
|
||||
403, "Invites have been disabled on this server",
|
||||
)
|
||||
|
||||
if invitee:
|
||||
yield self.update_membership(
|
||||
requester,
|
||||
@@ -716,6 +781,20 @@ class RoomMemberHandler(object):
|
||||
txn_id=txn_id
|
||||
)
|
||||
|
||||
def _get_id_server_target(self, id_server):
|
||||
"""Looks up an id_server's actual http endpoint
|
||||
|
||||
Args:
|
||||
id_server (str): the server name to lookup.
|
||||
|
||||
Returns:
|
||||
the http endpoint to connect to.
|
||||
"""
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
return self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
return id_server
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _lookup_3pid(self, id_server, medium, address):
|
||||
"""Looks up a 3pid in the passed identity server.
|
||||
@@ -730,8 +809,9 @@ class RoomMemberHandler(object):
|
||||
str: the matrix ID of the 3pid, or None if it is not recognized.
|
||||
"""
|
||||
try:
|
||||
target = self._get_id_server_target(id_server)
|
||||
data = yield self.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,),
|
||||
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, target,),
|
||||
{
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
@@ -753,9 +833,10 @@ class RoomMemberHandler(object):
|
||||
if server_hostname not in data["signatures"]:
|
||||
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||
for key_name, signature in data["signatures"][server_hostname].items():
|
||||
target = self._get_id_server_target(server_hostname)
|
||||
key_data = yield self.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/api/v1/pubkey/%s" %
|
||||
(id_server_scheme, server_hostname, key_name,),
|
||||
(id_server_scheme, target, key_name,),
|
||||
)
|
||||
if "public_key" not in key_data:
|
||||
raise AuthError(401, "No public key named %s from %s" %
|
||||
@@ -892,8 +973,9 @@ class RoomMemberHandler(object):
|
||||
user.
|
||||
"""
|
||||
|
||||
target = self._get_id_server_target(id_server)
|
||||
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
|
||||
id_server_scheme, id_server,
|
||||
id_server_scheme, target,
|
||||
)
|
||||
|
||||
invite_config = {
|
||||
@@ -933,7 +1015,7 @@ class RoomMemberHandler(object):
|
||||
fallback_public_key = {
|
||||
"public_key": data["public_key"],
|
||||
"key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
||||
id_server_scheme, id_server,
|
||||
id_server_scheme, target,
|
||||
),
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -934,7 +934,7 @@ class SyncHandler(object):
|
||||
res = yield self._generate_sync_entry_for_rooms(
|
||||
sync_result_builder, account_data_by_room
|
||||
)
|
||||
newly_joined_rooms, newly_joined_users, _, _ = res
|
||||
newly_joined_rooms, newly_joined_or_invited_users, _, _ = res
|
||||
_, _, newly_left_rooms, newly_left_users = res
|
||||
|
||||
block_all_presence_data = (
|
||||
@@ -943,7 +943,7 @@ class SyncHandler(object):
|
||||
)
|
||||
if self.hs_config.use_presence and not block_all_presence_data:
|
||||
yield self._generate_sync_entry_for_presence(
|
||||
sync_result_builder, newly_joined_rooms, newly_joined_users
|
||||
sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users
|
||||
)
|
||||
|
||||
yield self._generate_sync_entry_for_to_device(sync_result_builder)
|
||||
@@ -951,7 +951,7 @@ class SyncHandler(object):
|
||||
device_lists = yield self._generate_sync_entry_for_device_list(
|
||||
sync_result_builder,
|
||||
newly_joined_rooms=newly_joined_rooms,
|
||||
newly_joined_users=newly_joined_users,
|
||||
newly_joined_or_invited_users=newly_joined_or_invited_users,
|
||||
newly_left_rooms=newly_left_rooms,
|
||||
newly_left_users=newly_left_users,
|
||||
)
|
||||
@@ -1036,7 +1036,8 @@ class SyncHandler(object):
|
||||
@measure_func("_generate_sync_entry_for_device_list")
|
||||
@defer.inlineCallbacks
|
||||
def _generate_sync_entry_for_device_list(self, sync_result_builder,
|
||||
newly_joined_rooms, newly_joined_users,
|
||||
newly_joined_rooms,
|
||||
newly_joined_or_invited_users,
|
||||
newly_left_rooms, newly_left_users):
|
||||
user_id = sync_result_builder.sync_config.user.to_string()
|
||||
since_token = sync_result_builder.since_token
|
||||
@@ -1050,7 +1051,7 @@ class SyncHandler(object):
|
||||
# share a room with?
|
||||
for room_id in newly_joined_rooms:
|
||||
joined_users = yield self.state.get_current_user_in_room(room_id)
|
||||
newly_joined_users.update(joined_users)
|
||||
newly_joined_or_invited_users.update(joined_users)
|
||||
|
||||
for room_id in newly_left_rooms:
|
||||
left_users = yield self.state.get_current_user_in_room(room_id)
|
||||
@@ -1058,7 +1059,7 @@ class SyncHandler(object):
|
||||
|
||||
# TODO: Check that these users are actually new, i.e. either they
|
||||
# weren't in the previous sync *or* they left and rejoined.
|
||||
changed.update(newly_joined_users)
|
||||
changed.update(newly_joined_or_invited_users)
|
||||
|
||||
if not changed and not newly_left_users:
|
||||
defer.returnValue(DeviceLists(
|
||||
@@ -1176,7 +1177,7 @@ class SyncHandler(object):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _generate_sync_entry_for_presence(self, sync_result_builder, newly_joined_rooms,
|
||||
newly_joined_users):
|
||||
newly_joined_or_invited_users):
|
||||
"""Generates the presence portion of the sync response. Populates the
|
||||
`sync_result_builder` with the result.
|
||||
|
||||
@@ -1184,8 +1185,9 @@ class SyncHandler(object):
|
||||
sync_result_builder(SyncResultBuilder)
|
||||
newly_joined_rooms(list): List of rooms that the user has joined
|
||||
since the last sync (or empty if an initial sync)
|
||||
newly_joined_users(list): List of users that have joined rooms
|
||||
since the last sync (or empty if an initial sync)
|
||||
newly_joined_or_invited_users(list): List of users that have joined
|
||||
or been invited to rooms since the last sync (or empty if an initial
|
||||
sync)
|
||||
"""
|
||||
now_token = sync_result_builder.now_token
|
||||
sync_config = sync_result_builder.sync_config
|
||||
@@ -1211,7 +1213,7 @@ class SyncHandler(object):
|
||||
"presence_key", presence_key
|
||||
)
|
||||
|
||||
extra_users_ids = set(newly_joined_users)
|
||||
extra_users_ids = set(newly_joined_or_invited_users)
|
||||
for room_id in newly_joined_rooms:
|
||||
users = yield self.state.get_current_user_in_room(room_id)
|
||||
extra_users_ids.update(users)
|
||||
@@ -1243,7 +1245,8 @@ class SyncHandler(object):
|
||||
|
||||
Returns:
|
||||
Deferred(tuple): Returns a 4-tuple of
|
||||
`(newly_joined_rooms, newly_joined_users, newly_left_rooms, newly_left_users)`
|
||||
`(newly_joined_rooms, newly_joined_or_invited_users,
|
||||
newly_left_rooms, newly_left_users)`
|
||||
"""
|
||||
user_id = sync_result_builder.sync_config.user.to_string()
|
||||
block_all_room_ephemeral = (
|
||||
@@ -1314,8 +1317,8 @@ class SyncHandler(object):
|
||||
|
||||
sync_result_builder.invited.extend(invited)
|
||||
|
||||
# Now we want to get any newly joined users
|
||||
newly_joined_users = set()
|
||||
# Now we want to get any newly joined or invited users
|
||||
newly_joined_or_invited_users = set()
|
||||
newly_left_users = set()
|
||||
if since_token:
|
||||
for joined_sync in sync_result_builder.joined:
|
||||
@@ -1324,19 +1327,22 @@ class SyncHandler(object):
|
||||
)
|
||||
for event in it:
|
||||
if event.type == EventTypes.Member:
|
||||
if event.membership == Membership.JOIN:
|
||||
newly_joined_users.add(event.state_key)
|
||||
if (
|
||||
event.membership == Membership.JOIN or
|
||||
event.membership == Membership.INVITE
|
||||
):
|
||||
newly_joined_or_invited_users.add(event.state_key)
|
||||
else:
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_membership = prev_content.get("membership", None)
|
||||
if prev_membership == Membership.JOIN:
|
||||
newly_left_users.add(event.state_key)
|
||||
|
||||
newly_left_users -= newly_joined_users
|
||||
newly_left_users -= newly_joined_or_invited_users
|
||||
|
||||
defer.returnValue((
|
||||
newly_joined_rooms,
|
||||
newly_joined_users,
|
||||
newly_joined_or_invited_users,
|
||||
newly_left_rooms,
|
||||
newly_left_users,
|
||||
))
|
||||
@@ -1381,7 +1387,7 @@ class SyncHandler(object):
|
||||
where:
|
||||
room_entries is a list [RoomSyncResultBuilder]
|
||||
invited_rooms is a list [InvitedSyncResult]
|
||||
newly_joined rooms is a list[str] of room ids
|
||||
newly_joined_rooms is a list[str] of room ids
|
||||
newly_left_rooms is a list[str] of room ids
|
||||
"""
|
||||
user_id = sync_result_builder.sync_config.user.to_string()
|
||||
@@ -1422,7 +1428,7 @@ class SyncHandler(object):
|
||||
if room_id in sync_result_builder.joined_room_ids and non_joins:
|
||||
# Always include if the user (re)joined the room, especially
|
||||
# important so that device list changes are calculated correctly.
|
||||
# If there are non join member events, but we are still in the room,
|
||||
# If there are non-join member events, but we are still in the room,
|
||||
# then the user must have left and joined
|
||||
newly_joined_rooms.append(room_id)
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
# limitations under the License.
|
||||
|
||||
""" This module contains REST servlets to do with profile: /profile/<paths> """
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.http.servlet import parse_json_object_from_request
|
||||
@@ -21,6 +23,8 @@ from synapse.types import UserID
|
||||
|
||||
from .base import ClientV1RestServlet, client_path_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProfileDisplaynameRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)/displayname")
|
||||
@@ -28,6 +32,7 @@ class ProfileDisplaynameRestServlet(ClientV1RestServlet):
|
||||
def __init__(self, hs):
|
||||
super(ProfileDisplaynameRestServlet, self).__init__(hs)
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, user_id):
|
||||
@@ -59,11 +64,30 @@ class ProfileDisplaynameRestServlet(ClientV1RestServlet):
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, requester, new_name, is_admin)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_displayname(shadow_user.to_string(), content)
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return (200, {})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_displayname(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/displayname?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, user_id, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ProfileAvatarURLRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)/avatar_url")
|
||||
@@ -71,6 +95,7 @@ class ProfileAvatarURLRestServlet(ClientV1RestServlet):
|
||||
def __init__(self, hs):
|
||||
super(ProfileAvatarURLRestServlet, self).__init__(hs)
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, user_id):
|
||||
@@ -101,11 +126,30 @@ class ProfileAvatarURLRestServlet(ClientV1RestServlet):
|
||||
yield self.profile_handler.set_avatar_url(
|
||||
user, requester, new_name, is_admin)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_avatar_url(shadow_user.to_string(), content)
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return (200, {})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_avatar_url(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/avatar_url?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, user_id, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ProfileRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)")
|
||||
|
||||
@@ -666,7 +666,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
|
||||
content["address"],
|
||||
content["id_server"],
|
||||
requester,
|
||||
txn_id
|
||||
txn_id,
|
||||
new_room=False,
|
||||
)
|
||||
defer.returnValue((200, {}))
|
||||
return
|
||||
|
||||
@@ -27,6 +27,7 @@ from synapse.http.servlet import (
|
||||
assert_params_in_dict,
|
||||
parse_json_object_from_request,
|
||||
)
|
||||
from synapse.types import UserID
|
||||
from synapse.util.msisdn import phone_number_to_msisdn
|
||||
from synapse.util.threepids import check_3pid_allowed
|
||||
|
||||
@@ -51,10 +52,10 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
|
||||
'id_server', 'client_secret', 'email', 'send_attempt'
|
||||
])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized on this server",
|
||||
"Your email is not authorized on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -89,7 +90,7 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Account phone numbers are not authorized on this server",
|
||||
@@ -117,6 +118,7 @@ class PasswordRestServlet(RestServlet):
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.datastore = self.hs.get_datastore()
|
||||
self._set_password_handler = hs.get_set_password_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@interactive_auth_handler
|
||||
@defer.inlineCallbacks
|
||||
@@ -135,9 +137,13 @@ class PasswordRestServlet(RestServlet):
|
||||
|
||||
if self.auth.has_access_token(request):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
params = yield self.auth_handler.validate_user_via_ui_auth(
|
||||
requester, body, self.hs.get_ip_from_request(request),
|
||||
)
|
||||
# blindly trust ASes without UI-authing them
|
||||
if requester.app_service:
|
||||
params = body
|
||||
else:
|
||||
params = yield self.auth_handler.validate_user_via_ui_auth(
|
||||
requester, body, self.hs.get_ip_from_request(request),
|
||||
)
|
||||
user_id = requester.user.to_string()
|
||||
else:
|
||||
requester = None
|
||||
@@ -173,11 +179,30 @@ class PasswordRestServlet(RestServlet):
|
||||
user_id, new_password, requester
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_password(params, shadow_user.to_string())
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, _):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_password(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/password?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id,
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class DeactivateAccountRestServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/account/deactivate$")
|
||||
@@ -243,10 +268,10 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
|
||||
['id_server', 'client_secret', 'email', 'send_attempt'],
|
||||
)
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized on this server",
|
||||
"Your email is not authorized on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -280,7 +305,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Account phone numbers are not authorized on this server",
|
||||
@@ -307,7 +332,8 @@ class ThreepidRestServlet(RestServlet):
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.datastore = self.hs.get_datastore()
|
||||
self.datastore = hs.get_datastore()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
@@ -321,27 +347,38 @@ class ThreepidRestServlet(RestServlet):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
body = parse_json_object_from_request(request)
|
||||
if self.hs.config.disable_3pid_changes:
|
||||
raise SynapseError(400, "3PID changes disabled on this server")
|
||||
|
||||
threePidCreds = body.get('threePidCreds')
|
||||
threePidCreds = body.get('three_pid_creds', threePidCreds)
|
||||
if threePidCreds is None:
|
||||
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)
|
||||
# skip validation if this is a shadow 3PID from an AS
|
||||
if not requester.app_service:
|
||||
threePidCreds = body.get('threePidCreds')
|
||||
threePidCreds = body.get('three_pid_creds', threePidCreds)
|
||||
if threePidCreds is None:
|
||||
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
|
||||
|
||||
if not threepid:
|
||||
raise SynapseError(
|
||||
400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED
|
||||
)
|
||||
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)
|
||||
|
||||
for reqd in ['medium', 'address', 'validated_at']:
|
||||
if reqd not in threepid:
|
||||
logger.warn("Couldn't add 3pid: invalid response from ID server")
|
||||
raise SynapseError(500, "Invalid response from ID Server")
|
||||
if not threepid:
|
||||
raise SynapseError(
|
||||
400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED
|
||||
)
|
||||
|
||||
for reqd in ['medium', 'address', 'validated_at']:
|
||||
if reqd not in threepid:
|
||||
logger.warn("Couldn't add 3pid: invalid response from ID server")
|
||||
raise SynapseError(500, "Invalid response from ID Server")
|
||||
else:
|
||||
# XXX: ASes pass in a validated threepid directly to bypass the IS.
|
||||
# This makes the API entirely change shape when we have an AS token;
|
||||
# it really should be an entirely separate API - perhaps
|
||||
# /account/3pid/replicate or something.
|
||||
threepid = body.get('threepid')
|
||||
|
||||
yield self.auth_handler.add_threepid(
|
||||
user_id,
|
||||
@@ -350,7 +387,7 @@ class ThreepidRestServlet(RestServlet):
|
||||
threepid['validated_at'],
|
||||
)
|
||||
|
||||
if 'bind' in body and body['bind']:
|
||||
if not requester.app_service and ('bind' in body and body['bind']):
|
||||
logger.debug(
|
||||
"Binding threepid %s to %s",
|
||||
threepid, user_id
|
||||
@@ -359,19 +396,43 @@ class ThreepidRestServlet(RestServlet):
|
||||
threePidCreds, user_id
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_3pid({'threepid': threepid}, shadow_user.to_string())
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_3pid(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id,
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ThreepidDeleteRestServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/account/3pid/delete$", releases=())
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ThreepidDeleteRestServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
if self.hs.config.disable_3pid_changes:
|
||||
raise SynapseError(400, "3PID changes disabled on this server")
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(body, ['medium', 'address'])
|
||||
|
||||
@@ -389,6 +450,12 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
logger.exception("Failed to remove threepid")
|
||||
raise SynapseError(500, "Failed to remove threepid")
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_3pid_delete(body, shadow_user.to_string())
|
||||
|
||||
if ret:
|
||||
id_server_unbind_result = "success"
|
||||
else:
|
||||
@@ -398,6 +465,19 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
"id_server_unbind_result": id_server_unbind_result,
|
||||
}))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_3pid_delete(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid/delete?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class WhoamiRestServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/account/whoami$")
|
||||
|
||||
@@ -19,6 +19,7 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import AuthError, NotFoundError, SynapseError
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.types import UserID
|
||||
|
||||
from ._base import client_v2_patterns
|
||||
|
||||
@@ -39,6 +40,7 @@ class AccountDataServlet(RestServlet):
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.notifier = hs.get_notifier()
|
||||
self._profile_handler = hs.get_profile_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, user_id, account_data_type):
|
||||
@@ -48,6 +50,11 @@ class AccountDataServlet(RestServlet):
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if account_data_type == "im.vector.hide_profile":
|
||||
user = UserID.from_string(user_id)
|
||||
hide_profile = body.get('hide_profile')
|
||||
yield self._profile_handler.set_active(user, not hide_profile, True)
|
||||
|
||||
max_id = yield self.store.add_account_data_for_user(
|
||||
user_id, account_data_type, body
|
||||
)
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
from hashlib import sha1
|
||||
from string import capwords
|
||||
|
||||
from six import string_types
|
||||
|
||||
@@ -78,10 +80,10 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
|
||||
'id_server', 'client_secret', 'email', 'send_attempt'
|
||||
])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized to register on this server",
|
||||
"Your email is not authorized to register on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -120,7 +122,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Phone numbers are not authorized to register on this server",
|
||||
@@ -249,6 +251,8 @@ class RegisterRestServlet(RestServlet):
|
||||
raise SynapseError(400, "Invalid username")
|
||||
desired_username = body['username']
|
||||
|
||||
desired_display_name = body.get('display_name')
|
||||
|
||||
appservice = None
|
||||
if self.auth.has_access_token(request):
|
||||
appservice = yield self.auth.get_appservice_by_req(request)
|
||||
@@ -272,7 +276,8 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
if isinstance(desired_username, string_types):
|
||||
result = yield self._do_appservice_registration(
|
||||
desired_username, access_token, body
|
||||
desired_username, desired_password, desired_display_name,
|
||||
access_token, body
|
||||
)
|
||||
defer.returnValue((200, result)) # we throw for non 200 responses
|
||||
return
|
||||
@@ -398,7 +403,7 @@ class RegisterRestServlet(RestServlet):
|
||||
medium = auth_result[login_type]['medium']
|
||||
address = auth_result[login_type]['address']
|
||||
|
||||
if not check_3pid_allowed(self.hs, medium, address):
|
||||
if not (yield check_3pid_allowed(self.hs, medium, address)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Third party identifiers (email/phone numbers)" +
|
||||
@@ -406,6 +411,84 @@ class RegisterRestServlet(RestServlet):
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
if self.hs.config.register_mxid_from_3pid:
|
||||
# override the desired_username based on the 3PID if any.
|
||||
# reset it first to avoid folks picking their own username.
|
||||
desired_username = None
|
||||
|
||||
# we should have an auth_result at this point if we're going to progress
|
||||
# to register the user (i.e. we haven't picked up a registered_user_id
|
||||
# from our session store), in which case get ready and gen the
|
||||
# desired_username
|
||||
if auth_result:
|
||||
if (
|
||||
self.hs.config.register_mxid_from_3pid == 'email' and
|
||||
LoginType.EMAIL_IDENTITY in auth_result
|
||||
):
|
||||
address = auth_result[LoginType.EMAIL_IDENTITY]['address']
|
||||
desired_username = synapse.types.strip_invalid_mxid_characters(
|
||||
address.replace('@', '-').lower()
|
||||
)
|
||||
|
||||
# find a unique mxid for the account, suffixing numbers
|
||||
# if needed
|
||||
while True:
|
||||
try:
|
||||
yield self.registration_handler.check_username(
|
||||
desired_username,
|
||||
guest_access_token=guest_access_token,
|
||||
assigned_user_id=registered_user_id,
|
||||
)
|
||||
# if we got this far we passed the check.
|
||||
break
|
||||
except SynapseError as e:
|
||||
if e.errcode == Codes.USER_IN_USE:
|
||||
m = re.match(r'^(.*?)(\d+)$', desired_username)
|
||||
if m:
|
||||
desired_username = m.group(1) + str(
|
||||
int(m.group(2)) + 1
|
||||
)
|
||||
else:
|
||||
desired_username += "1"
|
||||
else:
|
||||
# something else went wrong.
|
||||
break
|
||||
|
||||
if self.hs.config.register_just_use_email_for_display_name:
|
||||
desired_display_name = address
|
||||
else:
|
||||
# XXX: a nasty heuristic to turn an email address into
|
||||
# a displayname, as part of register_mxid_from_3pid
|
||||
parts = address.replace('.', ' ').split('@')
|
||||
org_parts = parts[1].split(' ')
|
||||
|
||||
if org_parts[-2] == "matrix" and org_parts[-1] == "org":
|
||||
org = "Tchap Admin"
|
||||
elif org_parts[-2] == "gouv" and org_parts[-1] == "fr":
|
||||
org = org_parts[-3] if len(org_parts) > 2 else org_parts[-2]
|
||||
else:
|
||||
org = org_parts[-2]
|
||||
|
||||
desired_display_name = (
|
||||
capwords(parts[0]) + " [" + capwords(org) + "]"
|
||||
)
|
||||
elif (
|
||||
self.hs.config.register_mxid_from_3pid == 'msisdn' and
|
||||
LoginType.MSISDN in auth_result
|
||||
):
|
||||
desired_username = auth_result[LoginType.MSISDN]['address']
|
||||
else:
|
||||
raise SynapseError(
|
||||
400, "Cannot derive mxid from 3pid; no recognised 3pid"
|
||||
)
|
||||
|
||||
if desired_username is not None:
|
||||
yield self.registration_handler.check_username(
|
||||
desired_username,
|
||||
guest_access_token=guest_access_token,
|
||||
assigned_user_id=registered_user_id,
|
||||
)
|
||||
|
||||
if registered_user_id is not None:
|
||||
logger.info(
|
||||
"Already registered user ID %r for this session",
|
||||
@@ -417,9 +500,16 @@ class RegisterRestServlet(RestServlet):
|
||||
# NB: This may be from the auth handler and NOT from the POST
|
||||
assert_params_in_dict(params, ["password"])
|
||||
|
||||
desired_username = params.get("username", None)
|
||||
if not self.hs.config.register_mxid_from_3pid:
|
||||
desired_username = params.get("username", None)
|
||||
else:
|
||||
# we keep the original desired_username derived from the 3pid above
|
||||
pass
|
||||
|
||||
guest_access_token = params.get("guest_access_token", None)
|
||||
new_password = params.get("password", None)
|
||||
|
||||
# XXX: don't we need to validate these for length etc like we did on
|
||||
# the ones from the JSON body earlier on in the method?
|
||||
|
||||
if desired_username is not None:
|
||||
desired_username = desired_username.lower()
|
||||
@@ -430,9 +520,10 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
(registered_user_id, _) = yield self.registration_handler.register(
|
||||
localpart=desired_username,
|
||||
password=new_password,
|
||||
password=params.get("password", None),
|
||||
guest_access_token=guest_access_token,
|
||||
generate_token=False,
|
||||
default_display_name=desired_display_name,
|
||||
threepid=threepid,
|
||||
address=client_addr,
|
||||
)
|
||||
@@ -444,6 +535,14 @@ class RegisterRestServlet(RestServlet):
|
||||
):
|
||||
yield self.store.upsert_monthly_active_user(registered_user_id)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
yield self.registration_handler.shadow_register(
|
||||
localpart=desired_username,
|
||||
display_name=desired_display_name,
|
||||
auth_result=auth_result,
|
||||
params=params,
|
||||
)
|
||||
|
||||
# remember that we've now registered that user account, and with
|
||||
# what user ID (since the user may not have specified)
|
||||
self.auth_handler.set_session_data(
|
||||
@@ -471,11 +570,33 @@ class RegisterRestServlet(RestServlet):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_appservice_registration(self, username, as_token, body):
|
||||
def _do_appservice_registration(
|
||||
self, username, password, display_name, as_token, body
|
||||
):
|
||||
|
||||
# FIXME: appservice_register() is horribly duplicated with register()
|
||||
# and they should probably just be combined together with a config flag.
|
||||
user_id = yield self.registration_handler.appservice_register(
|
||||
username, as_token
|
||||
username, as_token, password, display_name
|
||||
)
|
||||
defer.returnValue((yield self._create_registration_details(user_id, body)))
|
||||
result = yield self._create_registration_details(user_id, body)
|
||||
|
||||
auth_result = body.get('auth_result')
|
||||
if auth_result and LoginType.EMAIL_IDENTITY in auth_result:
|
||||
threepid = auth_result[LoginType.EMAIL_IDENTITY]
|
||||
yield self._register_email_threepid(
|
||||
user_id, threepid, result["access_token"],
|
||||
body.get("bind_email")
|
||||
)
|
||||
|
||||
if auth_result and LoginType.MSISDN in auth_result:
|
||||
threepid = auth_result[LoginType.MSISDN]
|
||||
yield self._register_msisdn_threepid(
|
||||
user_id, threepid, result["access_token"],
|
||||
body.get("bind_msisdn")
|
||||
)
|
||||
|
||||
defer.returnValue(result)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_shared_secret_registration(self, username, password, body):
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
@@ -37,6 +39,7 @@ class UserDirectorySearchRestServlet(RestServlet):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
@@ -67,6 +70,14 @@ class UserDirectorySearchRestServlet(RestServlet):
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if self.hs.config.user_directory_defer_to_id_server:
|
||||
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
|
||||
url = "%s/_matrix/identity/api/v1/user_directory/search" % (
|
||||
self.hs.config.user_directory_defer_to_id_server,
|
||||
)
|
||||
resp = yield self.http_client.post_json_get_json(url, signed_body)
|
||||
defer.returnValue((200, resp))
|
||||
|
||||
limit = body.get("limit", 10)
|
||||
limit = min(limit, 50)
|
||||
|
||||
|
||||
0
synapse/rulecheck/__init__.py
Normal file
0
synapse/rulecheck/__init__.py
Normal file
159
synapse/rulecheck/domain_rule_checker.py
Normal file
159
synapse/rulecheck/domain_rule_checker.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from synapse.config._base import ConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainRuleChecker(object):
|
||||
"""
|
||||
A re-implementation of the SpamChecker that prevents users in one domain from
|
||||
inviting users in other domains to rooms, based on a configuration.
|
||||
|
||||
Takes a config in the format:
|
||||
|
||||
spam_checker:
|
||||
module: "rulecheck.DomainRuleChecker"
|
||||
config:
|
||||
domain_mapping:
|
||||
"inviter_domain": [ "invitee_domain_permitted", "other_domain_permitted" ]
|
||||
"other_inviter_domain": [ "invitee_domain_permitted" ]
|
||||
default: False
|
||||
|
||||
# Only let local users join rooms if they were explicitly invited.
|
||||
can_only_join_rooms_with_invite: false
|
||||
|
||||
# Only let local users create rooms if they are inviting only one
|
||||
# other user, and that user matches the rules above.
|
||||
can_only_create_one_to_one_rooms: false
|
||||
|
||||
# Only let local users invite during room creation, regardless of the
|
||||
# domain mapping rules above.
|
||||
can_only_invite_during_room_creation: false
|
||||
|
||||
# Allow third party invites
|
||||
can_invite_by_third_party_id: true
|
||||
|
||||
Don't forget to consider if you can invite users from your own domain.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.domain_mapping = config["domain_mapping"] or {}
|
||||
self.default = config["default"]
|
||||
|
||||
self.can_only_join_rooms_with_invite = config.get(
|
||||
"can_only_join_rooms_with_invite", False,
|
||||
)
|
||||
self.can_only_create_one_to_one_rooms = config.get(
|
||||
"can_only_create_one_to_one_rooms", False,
|
||||
)
|
||||
self.can_only_invite_during_room_creation = config.get(
|
||||
"can_only_invite_during_room_creation", False,
|
||||
)
|
||||
self.can_invite_by_third_party_id = config.get(
|
||||
"can_invite_by_third_party_id", True,
|
||||
)
|
||||
|
||||
def check_event_for_spam(self, event):
|
||||
"""Implements synapse.events.SpamChecker.check_event_for_spam
|
||||
"""
|
||||
return False
|
||||
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
|
||||
room_id, new_room):
|
||||
"""Implements synapse.events.SpamChecker.user_may_invite
|
||||
"""
|
||||
if self.can_only_invite_during_room_creation and not new_room:
|
||||
return False
|
||||
|
||||
if not self.can_invite_by_third_party_id and third_party_invite:
|
||||
return False
|
||||
|
||||
# This is a third party invite (without a bound mxid), so unless we have
|
||||
# banned all third party invites (above) we allow it.
|
||||
if not invitee_userid:
|
||||
return True
|
||||
|
||||
inviter_domain = self._get_domain_from_id(inviter_userid)
|
||||
invitee_domain = self._get_domain_from_id(invitee_userid)
|
||||
|
||||
if inviter_domain not in self.domain_mapping:
|
||||
return self.default
|
||||
|
||||
return invitee_domain in self.domain_mapping[inviter_domain]
|
||||
|
||||
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
|
||||
cloning):
|
||||
"""Implements synapse.events.SpamChecker.user_may_create_room
|
||||
"""
|
||||
|
||||
if cloning:
|
||||
return True
|
||||
|
||||
if not self.can_invite_by_third_party_id and third_party_invite_list:
|
||||
return False
|
||||
|
||||
number_of_invites = len(invite_list) + len(third_party_invite_list)
|
||||
|
||||
if self.can_only_create_one_to_one_rooms and number_of_invites != 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
"""Implements synapse.events.SpamChecker.user_may_create_room_alias
|
||||
"""
|
||||
return True
|
||||
|
||||
def user_may_publish_room(self, userid, room_id):
|
||||
"""Implements synapse.events.SpamChecker.user_may_publish_room
|
||||
"""
|
||||
return True
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Implements synapse.events.SpamChecker.user_may_join_room
|
||||
"""
|
||||
if self.can_only_join_rooms_with_invite and not is_invited:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config):
|
||||
"""Implements synapse.events.SpamChecker.parse_config
|
||||
"""
|
||||
if "default" in config:
|
||||
return config
|
||||
else:
|
||||
raise ConfigError("No default set for spam_config DomainRuleChecker")
|
||||
|
||||
@staticmethod
|
||||
def _get_domain_from_id(mxid):
|
||||
"""Parses a string and returns the domain part of the mxid.
|
||||
|
||||
Args:
|
||||
mxid (str): a valid mxid
|
||||
|
||||
Returns:
|
||||
str: the domain part of the mxid
|
||||
|
||||
"""
|
||||
idx = mxid.find(":")
|
||||
if idx == -1:
|
||||
raise Exception("Invalid ID: %r" % (mxid,))
|
||||
return mxid[idx + 1:]
|
||||
@@ -35,7 +35,7 @@ def _make_exclusive_regex(services_cache):
|
||||
exclusive_user_regexes = [
|
||||
regex.pattern
|
||||
for service in services_cache
|
||||
for regex in service.get_exlusive_user_regexes()
|
||||
for regex in service.get_exclusive_user_regexes()
|
||||
]
|
||||
if exclusive_user_regexes:
|
||||
exclusive_user_regex = "|".join("(" + r + ")" for r in exclusive_user_regexes)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +21,8 @@ from synapse.storage.roommember import ProfileInfo
|
||||
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
BATCH_SIZE = 100
|
||||
|
||||
|
||||
class ProfileWorkerStore(SQLBaseStore):
|
||||
@defer.inlineCallbacks
|
||||
@@ -62,6 +65,55 @@ class ProfileWorkerStore(SQLBaseStore):
|
||||
desc="get_profile_avatar_url",
|
||||
)
|
||||
|
||||
def get_latest_profile_replication_batch_number(self):
|
||||
def f(txn):
|
||||
txn.execute("SELECT MAX(batch) as maxbatch FROM profiles")
|
||||
rows = self.cursor_to_dict(txn)
|
||||
return rows[0]['maxbatch']
|
||||
return self.runInteraction(
|
||||
"get_latest_profile_replication_batch_number", f,
|
||||
)
|
||||
|
||||
def get_profile_batch(self, batchnum):
|
||||
return self._simple_select_list(
|
||||
table="profiles",
|
||||
keyvalues={
|
||||
"batch": batchnum,
|
||||
},
|
||||
retcols=("user_id", "displayname", "avatar_url", "active"),
|
||||
desc="get_profile_batch",
|
||||
)
|
||||
|
||||
def assign_profile_batch(self):
|
||||
def f(txn):
|
||||
sql = (
|
||||
"UPDATE profiles SET batch = "
|
||||
"(SELECT COALESCE(MAX(batch), -1) + 1 FROM profiles) "
|
||||
"WHERE user_id in ("
|
||||
" SELECT user_id FROM profiles WHERE batch is NULL limit ?"
|
||||
")"
|
||||
)
|
||||
txn.execute(sql, (BATCH_SIZE,))
|
||||
return txn.rowcount
|
||||
return self.runInteraction("assign_profile_batch", f)
|
||||
|
||||
def get_replication_hosts(self):
|
||||
def f(txn):
|
||||
txn.execute("SELECT host, last_synced_batch FROM profile_replication_status")
|
||||
rows = self.cursor_to_dict(txn)
|
||||
return {r['host']: r['last_synced_batch'] for r in rows}
|
||||
return self.runInteraction("get_replication_hosts", f)
|
||||
|
||||
def update_replication_batch_for_host(self, host, last_synced_batch):
|
||||
return self._simple_upsert(
|
||||
table="profile_replication_status",
|
||||
keyvalues={"host": host},
|
||||
values={
|
||||
"last_synced_batch": last_synced_batch,
|
||||
},
|
||||
desc="update_replication_batch_for_host",
|
||||
)
|
||||
|
||||
def get_from_remote_profile_cache(self, user_id):
|
||||
return self._simple_select_one(
|
||||
table="remote_profile_cache",
|
||||
@@ -71,27 +123,46 @@ class ProfileWorkerStore(SQLBaseStore):
|
||||
desc="get_from_remote_profile_cache",
|
||||
)
|
||||
|
||||
def create_profile(self, user_localpart):
|
||||
return self._simple_insert(
|
||||
table="profiles",
|
||||
values={"user_id": user_localpart},
|
||||
desc="create_profile",
|
||||
)
|
||||
|
||||
def set_profile_displayname(self, user_localpart, new_displayname):
|
||||
return self._simple_update_one(
|
||||
def set_profile_displayname(self, user_localpart, new_displayname, batchnum):
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
updatevalues={"displayname": new_displayname},
|
||||
values={
|
||||
"displayname": new_displayname,
|
||||
"batch": batchnum,
|
||||
},
|
||||
desc="set_profile_displayname",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
def set_profile_avatar_url(self, user_localpart, new_avatar_url):
|
||||
return self._simple_update_one(
|
||||
def set_profile_avatar_url(self, user_localpart, new_avatar_url, batchnum):
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
updatevalues={"avatar_url": new_avatar_url},
|
||||
values={
|
||||
"avatar_url": new_avatar_url,
|
||||
"batch": batchnum,
|
||||
},
|
||||
desc="set_profile_avatar_url",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
def set_profile_active(self, user_localpart, active, hide, batchnum):
|
||||
values = {
|
||||
"active": int(active),
|
||||
"batch": batchnum,
|
||||
}
|
||||
if not active and not hide:
|
||||
# we are deactivating for real (not in hide mode)
|
||||
# so clear the profile.
|
||||
values["avatar_url"] = None
|
||||
values["displayname"] = None
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
values=values,
|
||||
desc="set_profile_active",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
|
||||
|
||||
36
synapse/storage/schema/delta/48/profiles_batch.sql
Normal file
36
synapse/storage/schema/delta/48/profiles_batch.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
/* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Add a batch number to track changes to profiles and the
|
||||
* order they're made in so we can replicate user profiles
|
||||
* to other hosts as they change
|
||||
*/
|
||||
ALTER TABLE profiles ADD COLUMN batch BIGINT DEFAULT NULL;
|
||||
|
||||
/*
|
||||
* Index on the batch number so we can get profiles
|
||||
* by their batch
|
||||
*/
|
||||
CREATE INDEX profiles_batch_idx ON profiles(batch);
|
||||
|
||||
/*
|
||||
* A table to track what batch of user profiles has been
|
||||
* synced to what profile replication target.
|
||||
*/
|
||||
CREATE TABLE profile_replication_status (
|
||||
host TEXT NOT NULL,
|
||||
last_synced_batch BIGINT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
/* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* A flag saying whether the user owning the profile has been deactivated
|
||||
* This really belongs on the users table, not here, but the users table
|
||||
* stores users by their full user_id and profiles stores them by localpart,
|
||||
* so we can't easily join between the two tables. Plus, the batch number
|
||||
* realy ought to represent data in this table that has changed.
|
||||
*/
|
||||
ALTER TABLE profiles ADD COLUMN active SMALLINT DEFAULT 1 NOT NULL;
|
||||
@@ -231,6 +231,18 @@ def contains_invalid_mxid_characters(localpart):
|
||||
return any(c not in mxid_localpart_allowed_characters for c in localpart)
|
||||
|
||||
|
||||
def strip_invalid_mxid_characters(localpart):
|
||||
"""Removes any invalid characters from an mxid
|
||||
|
||||
Args:
|
||||
localpart (basestring): the localpart to be stripped
|
||||
|
||||
Returns:
|
||||
localpart (basestring): the localpart having been stripped
|
||||
"""
|
||||
return filter(lambda c: c in mxid_localpart_allowed_characters, localpart)
|
||||
|
||||
|
||||
UPPER_CASE_PATTERN = re.compile(b"[A-Z_]")
|
||||
|
||||
# the following is a pattern which matches '=', and bytes which are not allowed in a mxid
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_3pid_allowed(hs, medium, address):
|
||||
"""Checks whether a given format of 3PID is allowed to be used on this HS
|
||||
"""Checks whether a given 3PID is allowed to be used on this HS
|
||||
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
@@ -28,9 +31,35 @@ def check_3pid_allowed(hs, medium, address):
|
||||
address (str): address within that medium (e.g. "wotan@matrix.org")
|
||||
msisdns need to first have been canonicalised
|
||||
Returns:
|
||||
bool: whether the 3PID medium/address is allowed to be added to this HS
|
||||
defered bool: whether the 3PID medium/address is allowed to be added to this HS
|
||||
"""
|
||||
|
||||
if hs.config.check_is_for_allowed_local_3pids:
|
||||
data = yield hs.get_simple_http_client().get_json(
|
||||
"https://%s%s" % (
|
||||
hs.config.check_is_for_allowed_local_3pids,
|
||||
"/_matrix/identity/api/v1/internal-info"
|
||||
),
|
||||
{'medium': medium, 'address': address}
|
||||
)
|
||||
|
||||
# Check for invalid response
|
||||
if 'hs' not in data and 'shadow_hs' not in data:
|
||||
defer.returnValue(False)
|
||||
|
||||
# Check if this user is intended to register for this homeserver
|
||||
if (
|
||||
data.get('hs') != hs.config.server_name
|
||||
and data.get('shadow_hs') != hs.config.server_name
|
||||
):
|
||||
defer.returnValue(False)
|
||||
|
||||
if data.get('requires_invite', False) and not data.get('invited', False):
|
||||
# Requires an invite but hasn't been invited
|
||||
defer.returnValue(False)
|
||||
|
||||
defer.returnValue(True)
|
||||
|
||||
if hs.config.allowed_local_3pids:
|
||||
for constraint in hs.config.allowed_local_3pids:
|
||||
logger.debug(
|
||||
@@ -41,8 +70,8 @@ def check_3pid_allowed(hs, medium, address):
|
||||
medium == constraint['medium'] and
|
||||
re.match(constraint['pattern'], address)
|
||||
):
|
||||
return True
|
||||
defer.returnValue(True)
|
||||
else:
|
||||
return True
|
||||
defer.returnValue(True)
|
||||
|
||||
return False
|
||||
defer.returnValue(False)
|
||||
|
||||
@@ -67,13 +67,13 @@ class ProfileTestCase(unittest.TestCase):
|
||||
self.bob = UserID.from_string("@4567:test")
|
||||
self.alice = UserID.from_string("@alice:remote")
|
||||
|
||||
yield self.store.create_profile(self.frank.localpart)
|
||||
|
||||
self.handler = hs.get_profile_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_my_name(self):
|
||||
yield self.store.set_profile_displayname(self.frank.localpart, "Frank")
|
||||
yield self.store.set_profile_displayname(
|
||||
self.frank.localpart, "Frank", 1,
|
||||
)
|
||||
|
||||
displayname = yield self.handler.get_displayname(self.frank)
|
||||
|
||||
@@ -116,8 +116,7 @@ class ProfileTestCase(unittest.TestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_incoming_fed_query(self):
|
||||
yield self.store.create_profile("caroline")
|
||||
yield self.store.set_profile_displayname("caroline", "Caroline")
|
||||
yield self.store.set_profile_displayname("caroline", "Caroline", 1)
|
||||
|
||||
response = yield self.query_handlers["profile"](
|
||||
{"user_id": "@caroline:test", "field": "displayname"}
|
||||
@@ -128,7 +127,7 @@ class ProfileTestCase(unittest.TestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_get_my_avatar(self):
|
||||
yield self.store.set_profile_avatar_url(
|
||||
self.frank.localpart, "http://my.server/me.png"
|
||||
self.frank.localpart, "http://my.server/me.png", 1,
|
||||
)
|
||||
|
||||
avatar_url = yield self.handler.get_avatar_url(self.frank)
|
||||
|
||||
14
tests/rulecheck/__init__.py
Normal file
14
tests/rulecheck/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
316
tests/rulecheck/test_domainrulecheck.py
Normal file
316
tests/rulecheck/test_domainrulecheck.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
from synapse.rulecheck.domain_rule_checker import DomainRuleChecker
|
||||
|
||||
from tests import unittest
|
||||
from tests.server import make_request, render
|
||||
|
||||
|
||||
class DomainRuleCheckerTestCase(unittest.TestCase):
|
||||
def test_allowed(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertTrue(
|
||||
check.user_may_invite(
|
||||
"test:source_one", "test:target_one", None, "room", False
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
check.user_may_invite(
|
||||
"test:source_one", "test:target_two", None, "room", False
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
check.user_may_invite(
|
||||
"test:source_two", "test:target_two", None, "room", False
|
||||
)
|
||||
)
|
||||
|
||||
def test_disallowed(self):
|
||||
config = {
|
||||
"default": True,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
"source_four": [],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertFalse(
|
||||
check.user_may_invite(
|
||||
"test:source_one", "test:target_three", None, "room", False
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite(
|
||||
"test:source_two", "test:target_three", None, "room", False
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite(
|
||||
"test:source_two", "test:target_one", None, "room", False
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite(
|
||||
"test:source_four", "test:target_one", None, "room", False
|
||||
)
|
||||
)
|
||||
|
||||
def test_default_allow(self):
|
||||
config = {
|
||||
"default": True,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertTrue(
|
||||
check.user_may_invite(
|
||||
"test:source_three", "test:target_one", None, "room", False
|
||||
)
|
||||
)
|
||||
|
||||
def test_default_deny(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertFalse(
|
||||
check.user_may_invite(
|
||||
"test:source_three", "test:target_one", None, "room", False
|
||||
)
|
||||
)
|
||||
|
||||
def test_config_parse(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
self.assertEquals(config, DomainRuleChecker.parse_config(config))
|
||||
|
||||
def test_config_parse_failure(self):
|
||||
config = {
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
}
|
||||
}
|
||||
self.assertRaises(ConfigError, DomainRuleChecker.parse_config, config)
|
||||
|
||||
|
||||
class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
hijack_auth = False
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
config = self.default_config()
|
||||
|
||||
config.spam_checker = (
|
||||
DomainRuleChecker,
|
||||
{
|
||||
"default": True,
|
||||
"domain_mapping": {},
|
||||
"can_only_join_rooms_with_invite": True,
|
||||
"can_only_create_one_to_one_rooms": True,
|
||||
"can_only_invite_during_room_creation": True,
|
||||
"can_invite_by_third_party_id": False,
|
||||
},
|
||||
)
|
||||
|
||||
hs = self.setup_test_homeserver(config=config)
|
||||
return hs
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.admin_user_id = self.register_user("admin_user", "pass", admin=True)
|
||||
self.admin_access_token = self.login("admin_user", "pass")
|
||||
|
||||
self.normal_user_id = self.register_user("normal_user", "pass", admin=False)
|
||||
self.normal_access_token = self.login("normal_user", "pass")
|
||||
|
||||
self.other_user_id = self.register_user("other_user", "pass", admin=False)
|
||||
|
||||
def test_admin_can_create_room(self):
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
def test_normal_user_cannot_create_empty_room(self):
|
||||
channel = self._create_room(self.normal_access_token)
|
||||
assert channel.result["code"] == b"403", channel.result
|
||||
|
||||
def test_normal_user_cannot_create_room_with_multiple_invites(self):
|
||||
channel = self._create_room(
|
||||
self.normal_access_token,
|
||||
content={"invite": [self.other_user_id, self.admin_user_id]},
|
||||
)
|
||||
assert channel.result["code"] == b"403", channel.result
|
||||
|
||||
# Test that it correctly counts both normal and third party invites
|
||||
channel = self._create_room(
|
||||
self.normal_access_token,
|
||||
content={
|
||||
"invite": [self.other_user_id],
|
||||
"invite_3pid": [{"medium": "email", "address": "foo@example.com"}],
|
||||
},
|
||||
)
|
||||
assert channel.result["code"] == b"403", channel.result
|
||||
|
||||
# Test that it correctly rejects third party invites
|
||||
channel = self._create_room(
|
||||
self.normal_access_token,
|
||||
content={
|
||||
"invite": [],
|
||||
"invite_3pid": [{"medium": "email", "address": "foo@example.com"}],
|
||||
},
|
||||
)
|
||||
assert channel.result["code"] == b"403", channel.result
|
||||
|
||||
def test_normal_user_can_room_with_single_invites(self):
|
||||
channel = self._create_room(
|
||||
self.normal_access_token, content={"invite": [self.other_user_id]}
|
||||
)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
def test_cannot_join_public_room(self):
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
room_id = channel.json_body["room_id"]
|
||||
|
||||
self.helper.join(
|
||||
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=403
|
||||
)
|
||||
|
||||
def test_can_join_invited_room(self):
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
room_id = channel.json_body["room_id"]
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.admin_user_id,
|
||||
targ=self.normal_user_id,
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
self.helper.join(
|
||||
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=200
|
||||
)
|
||||
|
||||
def test_cannot_invite(self):
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
room_id = channel.json_body["room_id"]
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.admin_user_id,
|
||||
targ=self.normal_user_id,
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
self.helper.join(
|
||||
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=200
|
||||
)
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.normal_user_id,
|
||||
targ=self.other_user_id,
|
||||
tok=self.normal_access_token,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
def test_cannot_3pid_invite(self):
|
||||
"""Test that unbound 3pid invites get rejected.
|
||||
"""
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
room_id = channel.json_body["room_id"]
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.admin_user_id,
|
||||
targ=self.normal_user_id,
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
self.helper.join(
|
||||
room_id, self.normal_user_id,
|
||||
tok=self.normal_access_token,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.normal_user_id,
|
||||
targ=self.other_user_id,
|
||||
tok=self.normal_access_token,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
"rooms/%s/invite" % (room_id),
|
||||
{
|
||||
"address": "foo@bar.com",
|
||||
"medium": "email",
|
||||
"id_server": "localhost"
|
||||
},
|
||||
access_token=self.normal_access_token,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 403, channel.result["body"])
|
||||
|
||||
def _create_room(self, token, content={}):
|
||||
path = "/_matrix/client/r0/createRoom?access_token=%s" % (token,)
|
||||
|
||||
request, channel = make_request(
|
||||
self.hs.get_reactor(),
|
||||
"POST",
|
||||
path,
|
||||
content=json.dumps(content).encode("utf8"),
|
||||
)
|
||||
render(request, self.resource, self.hs.get_reactor())
|
||||
|
||||
return channel
|
||||
@@ -34,20 +34,19 @@ class ProfileStoreTestCase(unittest.TestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_displayname(self):
|
||||
yield self.store.create_profile(self.u_frank.localpart)
|
||||
|
||||
yield self.store.set_profile_displayname(self.u_frank.localpart, "Frank")
|
||||
yield self.store.set_profile_displayname(
|
||||
self.u_frank.localpart, "Frank", 1,
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
"Frank", (yield self.store.get_profile_displayname(self.u_frank.localpart))
|
||||
"Frank",
|
||||
(yield self.store.get_profile_displayname(self.u_frank.localpart))
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_avatar_url(self):
|
||||
yield self.store.create_profile(self.u_frank.localpart)
|
||||
|
||||
yield self.store.set_profile_avatar_url(
|
||||
self.u_frank.localpart, "http://my.site/here"
|
||||
self.u_frank.localpart, "http://my.site/here", 1,
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
|
||||
@@ -144,6 +144,7 @@ def default_config(name):
|
||||
config.federation_rc_concurrent = 10
|
||||
config.filter_timeline_limit = 5000
|
||||
config.user_directory_search_all_users = False
|
||||
config.replicate_user_profiles_to = []
|
||||
config.user_consent_server_notice_content = None
|
||||
config.block_events_without_consent_error = None
|
||||
config.user_consent_at_registration = False
|
||||
@@ -162,6 +163,8 @@ def default_config(name):
|
||||
config.admin_contact = None
|
||||
config.rc_messages_per_second = 10000
|
||||
config.rc_message_burst_count = 10000
|
||||
config.register_mxid_from_3pid = None
|
||||
config.shadow_server = None
|
||||
config.rc_registration.per_second = 10000
|
||||
config.rc_registration.burst_count = 10000
|
||||
config.rc_login_address.per_second = 10000
|
||||
|
||||
Reference in New Issue
Block a user