Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f48928a
feat: add msc3911 config option
itsoyou Aug 12, 2025
ec5ec76
feat: add msc3911 config option (#87)
itsoyou Aug 19, 2025
0120afb
feat: Introduce preliminary database infrastructure for storage and r…
jason-famedly Aug 12, 2025
1742606
MSC3911: AP2 - Database preparations and storage helpers (#91)
jason-famedly Aug 22, 2025
c5e1337
feat: add new media create and upload endpoints
itsoyou Aug 12, 2025
4456cf3
AP2: New create and upload endpoints to create restricted media (#89)
nico-famedly Aug 28, 2025
955928e
chore: update workflow for nightly image
itsoyou Aug 22, 2025
5529009
feat: msc3911[AP3] - Update methods for sending events to allow attac…
jason-famedly Aug 25, 2025
0a0cf05
MSC3911 AP3: Update methods for sending events to allow attaching med…
nico-famedly Sep 3, 2025
b1ce63d
fix: Ensure that failure to persist an Event does not incorrectly set…
jason-famedly Sep 3, 2025
3c65f59
MSC3911: Follow up to AP3 - Atomic persistence of media restrictions …
jason-famedly Sep 3, 2025
ed40073
feat: ap4 support attaching media for profile updates
itsoyou Aug 27, 2025
0dfb823
chore: make validate_media_url_and_retrieve_media_info reusable
itsoyou Sep 3, 2025
7c596a1
chore: make update profile idempotent
itsoyou Sep 3, 2025
bb9e33e
chore: update validation
itsoyou Sep 4, 2025
020eb8e
chore: validate avatar url in handler
itsoyou Sep 4, 2025
59fec66
MSC3911 AP4: Update methods for profile updates to support attaching …
nico-famedly Sep 4, 2025
64483d6
fix: remove unused repository
itsoyou Sep 4, 2025
af22810
AP4 fix: remove unused repository (#100)
nico-famedly Sep 4, 2025
510a4d5
feat: add copy api
itsoyou Aug 29, 2025
65bfb2b
chore: add more test
itsoyou Sep 4, 2025
c9aee37
feat: msc3911[AP5] - Update room creation handler to recognize and at…
jason-famedly Sep 2, 2025
6196a5e
chore: use default_max_timeout_ms
itsoyou Sep 5, 2025
4caf346
MSC3911 AP5: Updating the room creation handler to recognize attachin…
jason-famedly Sep 5, 2025
dbd189e
MSC3911 AP9: Media copy endpoint (#95)
nico-famedly Sep 8, 2025
bc93a34
feat: ap8 expose restrictions over federation
itsoyou Sep 8, 2025
c281798
chore: namespace the json response
itsoyou Sep 10, 2025
e4b8ea6
MSC3911 AP8: Expose restrictions over federation (#101)
nico-famedly Sep 10, 2025
a05b032
MSC3911 AP7: Permission checks for download and thumbnail endpoints
jason-famedly Sep 4, 2025
f025b32
MSC3911 AP7: Permission checks for download and thumbnail endpoints (…
jason-famedly Sep 10, 2025
dbf7649
feat: add media permission check on copy endpoint
itsoyou Sep 9, 2025
f0e6b47
MSC3911 AP9: Media copy endpoint follow up (#105)
nico-famedly Sep 11, 2025
700d11e
MSC3911 AP7.5: Accepting restrictions received from federation
jason-famedly Sep 10, 2025
7978e54
MSC3911 AP7.5: Establishing restrictions from remote federation media…
nico-famedly Sep 11, 2025
5fc0264
MSC3911 AP10: Ensure backwards compatibility
jason-famedly Sep 11, 2025
0f192bd
chore: Abstract media handling functions to allow using them without …
itsoyou Sep 12, 2025
2272621
chore: Abstract media handling functions to allow for HTTP replicatio…
nico-famedly Sep 13, 2025
bcda2a9
MSC3911 AP10: Ensure backwards compatibility (#107)
nico-famedly Sep 13, 2025
243eb4a
MSC3911: AP6 Automatic copy and attach media when updating member events
itsoyou Sep 12, 2025
cedef23
MSC3911: AP6 Automatic copy and attach media when updating member eve…
jason-famedly Sep 15, 2025
ac8750d
Swap out the 'get_state_for_events()' method to retrieve state mappings
jason-famedly Sep 16, 2025
4b4154f
fix: Avoid partial room creation due to errors in room avatars with r…
jason-famedly Sep 17, 2025
3cf58b6
chore: Add some debug logging so media that is not visible has a disc…
jason-famedly Sep 17, 2025
1b253f2
chore: Add some debug logging so media that is not visible has a disc…
jason-famedly Sep 18, 2025
f283804
fix(MSC3911): Use different state retrieval function for graceful han…
nico-famedly Sep 18, 2025
3dda03a
fix(MSC3911): Avoid partial room creation due to errors in room avata…
nico-famedly Sep 18, 2025
bc9dc1e
fix: reorganize profile avatar validation to account for remote media
jason-famedly Sep 22, 2025
edc2047
fix(MSC3911): reorganize profile avatar validation to account for rem…
jason-famedly Sep 23, 2025
e950d0c
fix(MSC3911): Correct where to find remote media at on the local file…
jason-famedly Sep 23, 2025
b0b8a10
fix(MSC3911): Correct where to find remote media at on the local file…
jason-famedly Sep 25, 2025
815ff8f
fix(MSC3911): Handle profile avatars that are unknown gracefully
jason-famedly Oct 20, 2025
d713ba0
fix(MSC3911): Handle profile avatars that are unknown gracefully (#148)
jason-famedly Oct 29, 2025
ca89fbf
Setup Complement testing infrastructure in preparation of media worke…
jason-famedly Sep 16, 2025
f65fb91
Pin installed versions of python to 3.13 for linting and format check…
jason-famedly Nov 3, 2025
0ae5375
chore(MSC3911): Complement (#139)
jason-famedly Nov 18, 2025
3baaa1e
chore(msc3911): Some test cleanups
jason-famedly Oct 27, 2025
cf795a5
chore(MSC3911): Various clean ups for msc3911 related unit tests (#190)
jason-famedly Dec 5, 2025
3cd87fb
feat: automatic pending media deletion
itsoyou Sep 18, 2025
595e5ed
chore: add msc3911 config class
itsoyou Dec 1, 2025
b792a25
MSC3911: ap? automatic pending media deletion (#130)
itsoyou Dec 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ci/scripts/checkout_complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ for BRANCH_NAME in "$GITHUB_HEAD_REF" "$GITHUB_BASE_REF" "${GITHUB_REF#refs/head
continue
fi

(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
(wget -O - "https://github.com/famedly/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
done
22 changes: 21 additions & 1 deletion .github/workflows/docker-famedly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ name: Docker
on:
push:
tags: ["v*.*.*_*"]
workflow_dispatch:
# Manually trigger to build and push docker image with modules to nightly Harbor registry.
inputs:
tag:
description: 'Provide tag name with work package. Tag must only contain ASCII letters, digits, underscores, periods, or dashes.'
required: true
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -59,6 +66,18 @@ jobs:
outputs:
build_matrix: ${{ steps.get-matrix.outputs.build_matrix }}

validate_image_tag:
# Validate the tag input for workflow_dispatch.
if: ${{ github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
steps:
- name: Validate Image Tag
run: |
if ! [[ "${{ github.event.inputs.tag }}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Error: tag must only contain ASCII letters, digits, underscores, periods, or dashes."
exit 1
fi

production-build:
if: ${{ !cancelled() && !failure() }} # Allow for stopping the build job
needs:
Expand All @@ -70,7 +89,7 @@ jobs:
with:
push: ${{ github.event_name != 'pull_request' }} # Always build, don't publish on pull requests
registry_user: ${{ vars.REGISTRY_USER }}
registry: registry.famedly.net/docker-oss
registry: ${{ github.event_name == 'workflow_dispatch' && 'registry.famedly.net/docker-nightly' || 'registry.famedly.net/docker-oss' }}
image_name: synapse
file: docker/Dockerfile-famedly
# Notice that there is a leading 'sha-' in front of the actual sha, as that is
Expand All @@ -82,5 +101,6 @@ jobs:
# Tag the production image used for famedly deployments.
tags: |
type=ref,event=tag,suffix=-${{ matrix.job.mod_pack_name }}
type=raw,enable=${{ github.event_name == 'workflow_dispatch' }},value=${{ matrix.job.mod_pack_name }}-${{ github.event.inputs.tag }}
flavor: latest=false
secrets: inherit
9 changes: 6 additions & 3 deletions .github/workflows/famedly-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- uses: Swatinem/rust-cache@68b3cb7503c78e67dae8373749990a220eb65352
- uses: matrix-org/setup-python-poetry@v2
with:
python-version: "3.x"
python-version: "3.13"
poetry-version: "2.1.1"
extras: "all"
- run: poetry run scripts-dev/generate_sample_config.sh --check
Expand All @@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
python-version: "3.13"
- run: .ci/scripts/check_lockfile.py

lint:
Expand All @@ -63,6 +63,7 @@ jobs:
uses: matrix-org/setup-python-poetry@v2
with:
poetry-version: "2.1.1"
python-version: "3.13"
install-project: "false"

- name: Run ruff check
Expand Down Expand Up @@ -91,6 +92,7 @@ jobs:
# https://github.com/matrix-org/synapse/pull/15376#issuecomment-1498983775
# To make CI green, err towards caution and install the project.
install-project: "true"
python-version: "3.13"
poetry-version: "2.1.1"

# Cribbed from
Expand Down Expand Up @@ -124,6 +126,7 @@ jobs:
- uses: matrix-org/setup-python-poetry@v2
with:
poetry-version: "2.1.1"
python-version: "3.13"
extras: "all"
- run: poetry run scripts-dev/check_pydantic_models.py

Expand Down Expand Up @@ -161,7 +164,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
python-version: "3.13"
- run: "pip install rstcheck"
- run: "rstcheck --report-level=WARNING README.rst"

Expand Down
6 changes: 6 additions & 0 deletions docker/complement/conf/workers-shared-extra.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ experimental_features:
msc3984_appservice_key_query: true
# Invite filtering
msc4155_enabled: true
# Media Attachment
msc3911:
enabled: true
block_unrestricted_media_upload: false
purge_pending_unattached_media: true
pending_media_cleanup_interval_ms: 86400000 # 1 day

server_notices:
system_mxid_localpart: _server
Expand Down
18 changes: 16 additions & 2 deletions docker/configure_workers_and_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,15 @@
},
"media_repository": {
"app": "synapse.app.generic_worker",
"listener_resources": ["media", "client"],
"listener_resources": ["media", "client", "replication"],
"endpoint_patterns": [
"^/_matrix/media/",
"^/_synapse/admin/v1/purge_media_cache$",
"^/_synapse/admin/v1/room/.*/media.*$",
"^/_synapse/admin/v1/user/.*/media.*$",
"^/_synapse/admin/v1/media/.*$",
"^/_synapse/admin/v1/quarantine_media/.*$",
"^/_matrix/client/v1/media/.*$",
"^/_matrix/client/(v1|unstable/.*)/media/.*$",
"^/_matrix/federation/v1/media/.*$",
],
# The first configured media worker will run the media background jobs
Expand Down Expand Up @@ -448,6 +448,9 @@ def add_worker_roles_to_shared_config(
if "federation_sender" in worker_types_set:
shared_config.setdefault("federation_sender_instances", []).append(worker_name)

if "media_repository" in worker_types_set:
shared_config.setdefault("media_repo_instances", []).append(worker_name)

# Update the list of stream writers. It's convenient that the name of the worker
# type is the same as the stream to write. Iterate over the whole list in case there
# is more than one.
Expand All @@ -468,6 +471,17 @@ def add_worker_roles_to_shared_config(
"host": "localhost",
"port": worker_port,
}
if worker == "media_repository":
# Just like for stream_writers, media workers now need to be on the instance_map
if os.environ.get("SYNAPSE_USE_UNIX_SOCKET", False):
instance_map[worker_name] = {
"path": f"/run/worker.{worker_port}",
}
else:
instance_map[worker_name] = {
"host": "localhost",
"port": worker_port,
}


def merge_worker_template_configs(
Expand Down
20 changes: 20 additions & 0 deletions rust/src/events/internal_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@ enum EventInternalMetadataData {
TxnId(Box<str>),
TokenId(i64),
DeviceId(Box<str>),
MediaReferences(Vec<String>),
}

impl EventInternalMetadataData {
/// Convert the field to its name and python object.
fn to_python_pair<'a>(&self, py: Python<'a>) -> (&'a Bound<'a, PyString>, Bound<'a, PyAny>) {
match self {
EventInternalMetadataData::MediaReferences(o) => (
pyo3::intern!(py, "media_references"),
o.into_pyobject(py).unwrap().into_any(),
),
EventInternalMetadataData::OutOfBandMembership(o) => (
pyo3::intern!(py, "out_of_band_membership"),
o.into_pyobject(py)
Expand Down Expand Up @@ -128,6 +133,11 @@ impl EventInternalMetadataData {
let key_str: PyBackedStr = key.extract()?;

let e = match &*key_str {
"media_references" => EventInternalMetadataData::MediaReferences(
value
.extract()
.with_context(|| format!("'{key_str}' has invalid type"))?,
),
"out_of_band_membership" => EventInternalMetadataData::OutOfBandMembership(
value
.extract()
Expand Down Expand Up @@ -469,4 +479,14 @@ impl EventInternalMetadata {
fn set_device_id(&mut self, obj: String) {
set_property!(self, DeviceId, obj.into_boxed_str());
}

/// The media references for the restrictions being set for this event, if any.
#[getter]
fn get_media_references(&self) -> Option<&Vec<String>> {
get_property_opt!(self, MediaReferences)
}
#[setter]
fn set_media_references(&mut self, obj: Vec<String>) {
set_property!(self, MediaReferences, obj);
}
}
1 change: 1 addition & 0 deletions scripts-dev/complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ test_packages=(
./tests/msc3967
./tests/msc4140
./tests/msc4155
./tests/msc3911
)

# Enable dirty runs, so tests will reuse the same container where possible.
Expand Down
7 changes: 7 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,13 @@ def __init__(self, msg: str):
super().__init__(HTTPStatus.BAD_REQUEST, msg, Codes.BAD_JSON)


class UnauthorizedRequestAPICallError(SynapseError):
"""Error raised when a request was not allowed due to authorization"""

def __init__(self, msg: str):
super().__init__(HTTPStatus.FORBIDDEN, msg, Codes.UNAUTHORIZED)


class InvalidProxyCredentialsError(SynapseError):
"""Error raised when the proxy credentials are invalid."""

Expand Down
20 changes: 20 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,22 @@ class MSC3866Config:
require_approval_for_new_accounts: bool = False


@attr.s(auto_attribs=True, frozen=True, slots=True)
class MSC3911Config:
"""Configuration for MSC3911 (Linking Media to Events)"""

# MSC3911 is enabled
enabled: bool = False

# Disable the current media create and upload endpoints
block_unrestricted_media_upload: bool = False

# Delete pending media that is older than certain interval and not attached to any events.
purge_pending_unattached_media: bool = False
# This configures how often the cleanup loop run in milliseconds
pending_media_cleanup_interval_ms: int = 24 * 60 * 60 * 1000 # 1 day


class ExperimentalConfig(Config):
"""Config section for enabling experimental features"""

Expand Down Expand Up @@ -588,3 +604,7 @@ def read_config(
# MSC4306: Thread Subscriptions
# (and MSC4308: sliding sync extension for thread subscriptions)
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)

# MSC3911: Linking Media to Events
raw_msc3911_config = experimental.get("msc3911", {})
self.msc3911 = MSC3911Config(**raw_msc3911_config)
2 changes: 1 addition & 1 deletion synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class HomeServerConfig(RootConfig):
DatabaseConfig,
LoggingConfig,
RatelimitConfig,
WorkerConfig,
ContentRepositoryConfig,
OembedConfig,
CaptchaConfig,
Expand Down Expand Up @@ -103,7 +104,6 @@ class HomeServerConfig(RootConfig):
RoomDirectoryConfig,
ThirdPartyRulesConfig,
TracerConfig,
WorkerConfig,
RedisConfig,
ExperimentalConfig,
BackgroundUpdateConfig,
Expand Down
43 changes: 35 additions & 8 deletions synapse/config/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,46 @@ class ContentRepositoryConfig(Config):
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
# Only enable the media repo if either the media repo is enabled or the
# current worker app is the media repo.
if (
config.get("enable_media_repo", True) is False
and config.get("worker_app") != "synapse.app.media_repository"
):
self.can_load_media_repo = False
return

workers_doing_media_duty = self.root.worker.workers_doing_media_duty
# It is expected by many places in Synapse and its unit tests that either there
# are no media repos, or only one, or every worker is one. If we have our new
# proper list, use that to decide if we are supposed to be handling these duties
if not workers_doing_media_duty:
if (
config.get("enable_media_repo", True) is False
and config.get("worker_app") != "synapse.app.media_repository"
):
self.can_load_media_repo = False
return
self.can_load_media_repo = True

else:
if self.root.worker.instance_name not in workers_doing_media_duty:
self.can_load_media_repo = False
return
self.can_load_media_repo = True

# Whether this instance should be the one to run the background jobs to
# e.g clean up old URL previews.
self.media_instance_running_background_jobs = config.get(
"media_instance_running_background_jobs",
# We prefer the first worker that is on the list to be responsible. However,
# backwards compatible the old setting and allow it to override the list.
#
# The URLPreviewer is the only thing that cares about this. Refactoring this to
# not stomp all over it's own feet doing the same work twice(maybe a mod on
# sha256?) and then it would not matter which media worker does the job.
media_instance_running_background_jobs_from_list = None
if self.root.worker.workers_doing_media_duty:
media_instance_running_background_jobs_from_list = (
self.root.worker.workers_doing_media_duty[0]
)
media_instance_running_background_jobs = config.get(
"media_instance_running_background_jobs"
)

self.media_instance_running_background_jobs = (
media_instance_running_background_jobs
or media_instance_running_background_jobs_from_list
)

self.max_upload_size = self.parse_size(config.get("max_upload_size", "50M"))
Expand Down
10 changes: 10 additions & 0 deletions synapse/config/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,16 @@ def read_config(
self.instance_map[instance]
)

self.workers_doing_media_duty = config.get("media_repo_instances", [])
# I would rather do this bit below, but the behavior of Synapse is rather lax.
# Documented what I mean in config/repository.py
# self.workers_doing_media_duty = self._worker_names_performing_this_duty(
# config,
# "enable_media_repo",
# "synapse.app.media_repository",
# "media_repo_instances",
# )

def _should_this_worker_perform_duty(
self,
config: Dict[str, Any],
Expand Down
4 changes: 4 additions & 0 deletions synapse/federation/transport/server/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
)
from synapse.http.site import SynapseRequest
from synapse.media._base import DEFAULT_MAX_TIMEOUT_MS, MAXIMUM_ALLOWED_MAX_TIMEOUT_MS
from synapse.media.media_repository import MediaRepository
from synapse.media.thumbnailer import ThumbnailProvider
from synapse.types import JsonDict
from synapse.util import SYNAPSE_VERSION
Expand Down Expand Up @@ -78,6 +79,7 @@ def __init__(
):
super().__init__(hs, authenticator, ratelimiter, server_name)
self.handler = hs.get_federation_server()
self.enable_restricted_media = hs.config.experimental.msc3911.enabled


class FederationSendServlet(BaseFederationServerServlet):
Expand Down Expand Up @@ -850,6 +852,7 @@ def __init__(
super().__init__(hs, authenticator, ratelimiter, server_name)
self.media_repo = self.hs.get_media_repository()
self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
assert isinstance(self.media_repo, MediaRepository)
self.thumbnail_provider = ThumbnailProvider(
hs, self.media_repo, self.media_repo.media_storage
)
Expand Down Expand Up @@ -879,6 +882,7 @@ async def on_GET(
await self.thumbnail_provider.respond_local_thumbnail(
request, media_id, width, height, method, m_type, max_timeout_ms, True
)
assert isinstance(self.media_repo, MediaRepository)
self.media_repo.mark_recently_accessed(None, media_id)


Expand Down
Loading