snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
4
zilencer/README.md
Normal file
4
zilencer/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# ZILENCER -- The Zulip license manager
|
||||
|
||||
This app is the place for storing state about various deployments of
|
||||
Zulip that exist in the world.
|
||||
0
zilencer/__init__.py
Normal file
0
zilencer/__init__.py
Normal file
178
zilencer/auth.py
Normal file
178
zilencer/auth.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
import sentry_sdk
|
||||
from django.conf import settings
|
||||
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import path
|
||||
from django.urls.resolvers import URLPattern
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from typing_extensions import ParamSpec, override
|
||||
|
||||
from zerver.decorator import get_basic_credentials, process_client
|
||||
from zerver.lib.exceptions import (
|
||||
ErrorCode,
|
||||
JsonableError,
|
||||
RateLimitedError,
|
||||
RemoteServerDeactivatedError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from zerver.lib.rate_limiter import should_rate_limit
|
||||
from zerver.lib.request import RequestNotes
|
||||
from zerver.lib.rest import default_never_cache_responses, get_target_view_function_or_response
|
||||
from zerver.lib.subdomains import get_subdomain
|
||||
from zerver.models import Realm
|
||||
from zilencer.models import (
|
||||
RateLimitedRemoteZulipServer,
|
||||
RemoteZulipServer,
|
||||
get_remote_server_by_uuid,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ParamT = ParamSpec("ParamT")
|
||||
|
||||
REMOTE_SERVER_TAKEOVER_TOKEN_SALT = "remote_server_transfer"
|
||||
REMOTE_SERVER_TAKEOVER_TOKEN_VALIDITY_SECONDS = 10
|
||||
|
||||
|
||||
def generate_registration_transfer_verification_secret(hostname: str) -> str:
|
||||
signer = TimestampSigner(salt=REMOTE_SERVER_TAKEOVER_TOKEN_SALT)
|
||||
secret = base64.b16encode(signer.sign(hostname).encode()).decode()
|
||||
return secret
|
||||
|
||||
|
||||
def validate_registration_transfer_verification_secret(secret: str, hostname: str) -> None:
|
||||
signer = TimestampSigner(salt=REMOTE_SERVER_TAKEOVER_TOKEN_SALT)
|
||||
try:
|
||||
signed_data = base64.b16decode(secret).decode()
|
||||
hostname_from_secret = signer.unsign(
|
||||
signed_data, max_age=REMOTE_SERVER_TAKEOVER_TOKEN_VALIDITY_SECONDS
|
||||
)
|
||||
except SignatureExpired:
|
||||
raise JsonableError(_("The verification secret has expired"))
|
||||
except BadSignature:
|
||||
raise JsonableError(_("The verification secret is invalid"))
|
||||
except binascii.Error:
|
||||
raise JsonableError(_("The verification secret is malformed"))
|
||||
if hostname_from_secret != hostname:
|
||||
raise JsonableError(_("The verification secret is for a different hostname"))
|
||||
|
||||
|
||||
class InvalidZulipServerError(JsonableError):
|
||||
code = ErrorCode.INVALID_ZULIP_SERVER
|
||||
data_fields = ["role"]
|
||||
|
||||
def __init__(self, role: str) -> None:
|
||||
self.role: str = role
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return "Zulip server auth failure: {role} is not registered -- did you run `manage.py register_server`?"
|
||||
|
||||
|
||||
class InvalidZulipServerKeyError(InvalidZulipServerError):
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return "Zulip server auth failure: key does not match role {role}"
|
||||
|
||||
|
||||
def rate_limit_remote_server(
|
||||
request: HttpRequest, remote_server: RemoteZulipServer, domain: str
|
||||
) -> None:
|
||||
if not should_rate_limit(request):
|
||||
return
|
||||
|
||||
try:
|
||||
RateLimitedRemoteZulipServer(remote_server, domain=domain).rate_limit_request(request)
|
||||
except RateLimitedError:
|
||||
logger.warning("Remote server %s exceeded rate limits on domain %s", remote_server, domain)
|
||||
raise
|
||||
|
||||
|
||||
def validate_remote_server(
|
||||
request: HttpRequest,
|
||||
role: str,
|
||||
api_key: str,
|
||||
) -> RemoteZulipServer:
|
||||
log_data = RequestNotes.get_notes(request).log_data
|
||||
assert log_data is not None
|
||||
try:
|
||||
remote_server = get_remote_server_by_uuid(role)
|
||||
except RemoteZulipServer.DoesNotExist:
|
||||
log_data["extra"] = "[invalid-server]"
|
||||
raise InvalidZulipServerError(role)
|
||||
if not constant_time_compare(api_key, remote_server.api_key):
|
||||
log_data["extra"] = "[invalid-server-key]"
|
||||
raise InvalidZulipServerKeyError(role)
|
||||
|
||||
if remote_server.deactivated:
|
||||
log_data["extra"] = "[deactivated-server]"
|
||||
raise RemoteServerDeactivatedError
|
||||
if (
|
||||
get_subdomain(request) != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN
|
||||
and not settings.DEVELOPMENT_DISABLE_PUSH_BOUNCER_DOMAIN_CHECK
|
||||
):
|
||||
# Sometimes we may want to test push bouncer logic in development.
|
||||
log_data["extra"] = "[invalid-domain]"
|
||||
raise JsonableError(_("Invalid subdomain for push notifications bouncer"))
|
||||
RequestNotes.get_notes(request).remote_server = remote_server
|
||||
process_client(request)
|
||||
sentry_sdk.set_user({"server": remote_server.uuid})
|
||||
return remote_server
|
||||
|
||||
|
||||
def authenticated_remote_server_view(
|
||||
view_func: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse],
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view_func(
|
||||
request: HttpRequest, /, *args: ParamT.args, **kwargs: ParamT.kwargs
|
||||
) -> HttpResponse:
|
||||
role, api_key = get_basic_credentials(request)
|
||||
if "@" in role:
|
||||
log_data = RequestNotes.get_notes(request).log_data
|
||||
assert log_data is not None
|
||||
log_data["extra"] = "[non-server-key]"
|
||||
raise JsonableError(_("Must validate with valid Zulip server API key"))
|
||||
try:
|
||||
remote_server = validate_remote_server(request, role, api_key)
|
||||
except JsonableError as e:
|
||||
raise UnauthorizedError(e.msg)
|
||||
|
||||
rate_limit_remote_server(request, remote_server, domain="api_by_remote_server")
|
||||
|
||||
remote_server.last_request_datetime = timezone_now()
|
||||
remote_server.save(update_fields=["last_request_datetime"])
|
||||
|
||||
return view_func(request, remote_server, *args, **kwargs)
|
||||
|
||||
return _wrapped_view_func
|
||||
|
||||
|
||||
@default_never_cache_responses
|
||||
@csrf_exempt
|
||||
def remote_server_dispatch(request: HttpRequest, /, **kwargs: Any) -> HttpResponse:
|
||||
result = get_target_view_function_or_response(request, kwargs)
|
||||
if isinstance(result, HttpResponse):
|
||||
return result
|
||||
target_function, _view_flags = result
|
||||
return authenticated_remote_server_view(target_function)(request, **kwargs)
|
||||
|
||||
|
||||
def remote_server_path(
|
||||
route: str,
|
||||
**handlers: Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse]
|
||||
| tuple[Callable[Concatenate[HttpRequest, RemoteZulipServer, ParamT], HttpResponse], set[str]],
|
||||
) -> URLPattern:
|
||||
return path(route, remote_server_dispatch, handlers)
|
||||
0
zilencer/lib/__init__.py
Normal file
0
zilencer/lib/__init__.py
Normal file
252
zilencer/lib/push_notifications.py
Normal file
252
zilencer/lib/push_notifications.py
Normal file
@@ -0,0 +1,252 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
from aioapns import NotificationRequest
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from firebase_admin import exceptions as firebase_exceptions
|
||||
from firebase_admin import messaging as firebase_messaging
|
||||
from firebase_admin.messaging import UnregisteredError as FCMUnregisteredError
|
||||
|
||||
from zerver.lib.devices import b64decode_token_id_base64, b64encode_token_id_int
|
||||
from zerver.lib.push_notifications import (
|
||||
APNsPushRequest,
|
||||
FCMPushRequest,
|
||||
SendNotificationResponseData,
|
||||
fcm_app,
|
||||
get_apns_context,
|
||||
get_info_from_apns_result,
|
||||
)
|
||||
from zerver.models.realms import Realm
|
||||
from zilencer.models import RemotePushDevice, RemoteRealm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SentPushNotificationResult:
|
||||
successfully_sent_count: int
|
||||
delete_token_ids_base64: list[str]
|
||||
|
||||
|
||||
def send_e2ee_push_notification_apple(
|
||||
apns_requests: list[NotificationRequest],
|
||||
apns_remote_push_devices: list[RemotePushDevice],
|
||||
log_context: str,
|
||||
) -> SentPushNotificationResult:
|
||||
import aioapns
|
||||
|
||||
successfully_sent_count = 0
|
||||
delete_token_ids_base64: list[str] = []
|
||||
apns_context = get_apns_context()
|
||||
|
||||
if apns_context is None:
|
||||
logger.error(
|
||||
"APNs: Dropping push notifications since "
|
||||
"neither APNS_TOKEN_KEY_FILE nor APNS_CERT_FILE is set."
|
||||
)
|
||||
return SentPushNotificationResult(
|
||||
successfully_sent_count=successfully_sent_count,
|
||||
delete_token_ids_base64=delete_token_ids_base64,
|
||||
)
|
||||
|
||||
async def send_all_notifications() -> Iterable[
|
||||
tuple[RemotePushDevice, aioapns.common.NotificationResult | BaseException]
|
||||
]:
|
||||
results = await asyncio.gather(
|
||||
*(apns_context.apns.send_notification(request) for request in apns_requests),
|
||||
return_exceptions=True,
|
||||
)
|
||||
return zip(apns_remote_push_devices, results, strict=False)
|
||||
|
||||
results = apns_context.loop.run_until_complete(send_all_notifications())
|
||||
|
||||
for remote_push_device, result in results:
|
||||
result_info = get_info_from_apns_result(
|
||||
result,
|
||||
remote_push_device,
|
||||
log_context % remote_push_device.token,
|
||||
)
|
||||
|
||||
if result_info.successfully_sent:
|
||||
successfully_sent_count += 1
|
||||
elif result_info.delete_token_id_base64 is not None:
|
||||
remote_push_device.expired_time = timezone_now()
|
||||
remote_push_device.save(update_fields=["expired_time"])
|
||||
delete_token_ids_base64.append(result_info.delete_token_id_base64)
|
||||
|
||||
return SentPushNotificationResult(
|
||||
successfully_sent_count=successfully_sent_count,
|
||||
delete_token_ids_base64=delete_token_ids_base64,
|
||||
)
|
||||
|
||||
|
||||
def send_e2ee_push_notification_android(
|
||||
fcm_requests: list[firebase_messaging.Message],
|
||||
fcm_remote_push_devices: list[RemotePushDevice],
|
||||
log_context: str,
|
||||
) -> SentPushNotificationResult:
|
||||
successfully_sent_count = 0
|
||||
delete_token_ids_base64: list[str] = []
|
||||
|
||||
if fcm_app is None:
|
||||
logger.error("FCM: Dropping push notifications since ANDROID_FCM_CREDENTIALS_PATH is unset")
|
||||
return SentPushNotificationResult(
|
||||
successfully_sent_count=successfully_sent_count,
|
||||
delete_token_ids_base64=delete_token_ids_base64,
|
||||
)
|
||||
|
||||
try:
|
||||
batch_response = firebase_messaging.send_each(fcm_requests, app=fcm_app)
|
||||
except firebase_exceptions.FirebaseError:
|
||||
logger.warning("Error while pushing to FCM", exc_info=True)
|
||||
return SentPushNotificationResult(
|
||||
successfully_sent_count=successfully_sent_count,
|
||||
delete_token_ids_base64=delete_token_ids_base64,
|
||||
)
|
||||
|
||||
for idx, response in enumerate(batch_response.responses):
|
||||
# We enumerate to have idx to track which token the response
|
||||
# corresponds to. send_each() preserves the order of the messages,
|
||||
# so this works.
|
||||
|
||||
remote_push_device = fcm_remote_push_devices[idx]
|
||||
token = remote_push_device.token
|
||||
|
||||
if response.success:
|
||||
successfully_sent_count += 1
|
||||
logger.info(
|
||||
"FCM: Sent message with ID: %s %s", response.message_id, log_context % token
|
||||
)
|
||||
else:
|
||||
error = response.exception
|
||||
if isinstance(error, FCMUnregisteredError):
|
||||
remote_push_device.expired_time = timezone_now()
|
||||
remote_push_device.save(update_fields=["expired_time"])
|
||||
delete_token_ids_base64.append(b64encode_token_id_int(remote_push_device.token_id))
|
||||
|
||||
logger.info("FCM: Removing %s due to %s", token, error.code)
|
||||
else:
|
||||
logger.warning(
|
||||
"FCM: Delivery failed %s: %s:%s",
|
||||
log_context % token,
|
||||
error.__class__,
|
||||
error,
|
||||
)
|
||||
|
||||
return SentPushNotificationResult(
|
||||
successfully_sent_count=successfully_sent_count,
|
||||
delete_token_ids_base64=delete_token_ids_base64,
|
||||
)
|
||||
|
||||
|
||||
def send_e2ee_push_notifications(
|
||||
push_requests: list[APNsPushRequest | FCMPushRequest],
|
||||
*,
|
||||
realm: Realm | None = None,
|
||||
remote_realm: RemoteRealm | None = None,
|
||||
) -> SendNotificationResponseData:
|
||||
assert (realm is None) ^ (remote_realm is None)
|
||||
|
||||
import aioapns
|
||||
|
||||
token_ids_base64 = set()
|
||||
token_ids_int = set()
|
||||
for push_request in push_requests:
|
||||
token_ids_base64.add(push_request.token_id)
|
||||
token_ids_int.add(b64decode_token_id_base64(push_request.token_id))
|
||||
|
||||
remote_push_devices = RemotePushDevice.objects.filter(
|
||||
token_id__in=token_ids_int,
|
||||
realm=realm,
|
||||
remote_realm=remote_realm,
|
||||
expired_time__isnull=True,
|
||||
)
|
||||
token_id_base64_to_remote_push_device = {
|
||||
b64encode_token_id_int(remote_push_device.token_id): remote_push_device
|
||||
for remote_push_device in remote_push_devices
|
||||
}
|
||||
unexpired_token_ids_base64 = set(token_id_base64_to_remote_push_device.keys())
|
||||
|
||||
# Token IDs which should be deleted on server.
|
||||
# Either the token ID is invalid or the token
|
||||
# associated has been marked invalid/expired by APNs/FCM.
|
||||
delete_token_ids_base64 = list(
|
||||
filter(
|
||||
lambda token_id_base64: token_id_base64 not in unexpired_token_ids_base64,
|
||||
token_ids_base64,
|
||||
)
|
||||
)
|
||||
|
||||
apns_requests = []
|
||||
apns_remote_push_devices: list[RemotePushDevice] = []
|
||||
|
||||
fcm_requests = []
|
||||
fcm_remote_push_devices: list[RemotePushDevice] = []
|
||||
|
||||
for push_request in push_requests:
|
||||
token_id_base64 = push_request.token_id
|
||||
if token_id_base64 not in unexpired_token_ids_base64:
|
||||
continue
|
||||
|
||||
remote_push_device = token_id_base64_to_remote_push_device[token_id_base64]
|
||||
if remote_push_device.token_kind == RemotePushDevice.TokenKind.APNS:
|
||||
assert isinstance(push_request, APNsPushRequest)
|
||||
apns_requests.append(
|
||||
aioapns.NotificationRequest(
|
||||
apns_topic=remote_push_device.ios_app_id,
|
||||
device_token=remote_push_device.token,
|
||||
message=asdict(push_request.payload),
|
||||
priority=push_request.http_headers.apns_priority,
|
||||
push_type=push_request.http_headers.apns_push_type,
|
||||
)
|
||||
)
|
||||
apns_remote_push_devices.append(remote_push_device)
|
||||
else:
|
||||
assert isinstance(push_request, FCMPushRequest)
|
||||
fcm_payload = dict(
|
||||
# FCM only allows string values, so we stringify push_key_id.
|
||||
push_key_id=str(push_request.payload.push_key_id),
|
||||
encrypted_data=push_request.payload.encrypted_data,
|
||||
)
|
||||
fcm_requests.append(
|
||||
firebase_messaging.Message(
|
||||
data=fcm_payload,
|
||||
token=remote_push_device.token,
|
||||
android=firebase_messaging.AndroidConfig(priority=push_request.fcm_priority),
|
||||
)
|
||||
)
|
||||
fcm_remote_push_devices.append(remote_push_device)
|
||||
|
||||
if realm is not None:
|
||||
log_context = f"to (realm={realm.uuid}, device=%s)"
|
||||
else:
|
||||
assert remote_realm is not None
|
||||
log_context = f"to (remote_realm={remote_realm.uuid}, device=%s)"
|
||||
|
||||
apple_successfully_sent_count = 0
|
||||
if len(apns_requests) > 0:
|
||||
sent_push_notification_result = send_e2ee_push_notification_apple(
|
||||
apns_requests,
|
||||
apns_remote_push_devices,
|
||||
log_context,
|
||||
)
|
||||
apple_successfully_sent_count = sent_push_notification_result.successfully_sent_count
|
||||
delete_token_ids_base64.extend(sent_push_notification_result.delete_token_ids_base64)
|
||||
|
||||
android_successfully_sent_count = 0
|
||||
if len(fcm_requests) > 0:
|
||||
sent_push_notification_result = send_e2ee_push_notification_android(
|
||||
fcm_requests,
|
||||
fcm_remote_push_devices,
|
||||
log_context,
|
||||
)
|
||||
android_successfully_sent_count = sent_push_notification_result.successfully_sent_count
|
||||
delete_token_ids_base64.extend(sent_push_notification_result.delete_token_ids_base64)
|
||||
|
||||
return {
|
||||
"apple_successfully_sent_count": apple_successfully_sent_count,
|
||||
"android_successfully_sent_count": android_successfully_sent_count,
|
||||
"delete_token_ids": delete_token_ids_base64,
|
||||
}
|
||||
77
zilencer/lib/remote_counts.py
Normal file
77
zilencer/lib/remote_counts.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from psycopg2.sql import SQL, Literal
|
||||
|
||||
from zilencer.models import RemoteInstallationCount, RemoteZulipServer
|
||||
|
||||
|
||||
class MissingDataError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def compute_max_monthly_messages(remote_server: RemoteZulipServer) -> int:
|
||||
# Calculate the maximum amount of messages that the server had within a month.
|
||||
# out of the last 3 months.
|
||||
|
||||
# We would like to just check whether we have current data for the
|
||||
# actual property we care about
|
||||
# ('messages_sent:message_type:day'). But because our analytics
|
||||
# tables have implicit zeros, that can't distinguish missing data
|
||||
# from days with no messages. So we filter on `active_users_audit`
|
||||
# instead, which will never be zero for an initialized server.
|
||||
if not RemoteInstallationCount.objects.filter(
|
||||
server=remote_server,
|
||||
property="active_users_audit:is_bot:day",
|
||||
end_time__lte=timezone_now() - timedelta(days=3),
|
||||
).exists():
|
||||
raise MissingDataError
|
||||
|
||||
query = SQL(
|
||||
"""
|
||||
WITH server_message_stats_daily AS -- Up to 4 rows per day for different subgroups
|
||||
(
|
||||
SELECT
|
||||
r.end_time,
|
||||
r.value AS message_count
|
||||
FROM
|
||||
zilencer_remoteinstallationcount r
|
||||
WHERE
|
||||
r.property = 'messages_sent:message_type:day'
|
||||
AND end_time >= CURRENT_TIMESTAMP(0) - INTERVAL '90 days'
|
||||
AND r.server_id = {server_id}
|
||||
),
|
||||
server_message_stats_monthly AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN current_timestamp(0) - end_time <= INTERVAL '30 days' THEN 0
|
||||
WHEN current_timestamp(0) - end_time <= INTERVAL '60 days' THEN 1
|
||||
WHEN current_timestamp(0) - end_time <= INTERVAL '90 days' THEN 2
|
||||
END AS billing_month,
|
||||
SUM(message_count) AS message_count
|
||||
FROM
|
||||
server_message_stats_daily
|
||||
GROUP BY
|
||||
1
|
||||
),
|
||||
server_max_monthly_messages AS (
|
||||
SELECT
|
||||
MAX(message_count) AS message_count
|
||||
FROM
|
||||
server_message_stats_monthly
|
||||
WHERE
|
||||
billing_month IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
-- Return zeros, rather than nulls,
|
||||
-- for reporting servers with zero messages.
|
||||
COALESCE(server_max_monthly_messages.message_count, 0) AS message_count
|
||||
FROM
|
||||
server_max_monthly_messages;
|
||||
"""
|
||||
).format(server_id=Literal(remote_server.id))
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchone()[0]
|
||||
return int(result)
|
||||
0
zilencer/management/__init__.py
Normal file
0
zilencer/management/__init__.py
Normal file
0
zilencer/management/commands/__init__.py
Normal file
0
zilencer/management/commands/__init__.py
Normal file
156
zilencer/management/commands/add_mock_conversation.py
Normal file
156
zilencer/management/commands/add_mock_conversation.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.message_send import do_send_messages, internal_prep_stream_message
|
||||
from zerver.actions.reactions import do_add_reaction
|
||||
from zerver.actions.streams import bulk_add_subscriptions
|
||||
from zerver.actions.user_settings import do_change_avatar_fields
|
||||
from zerver.lib.emoji import get_emoji_data
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.streams import ensure_stream
|
||||
from zerver.lib.upload import upload_avatar_image
|
||||
from zerver.models import Message, UserProfile
|
||||
from zerver.models.realms import get_realm
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Add a mock conversation to the development environment.
|
||||
|
||||
Usage: ./manage.py add_mock_conversation
|
||||
|
||||
After running the script:
|
||||
|
||||
From browser (ideally on high resolution screen):
|
||||
* Refresh to get the rendered tweet
|
||||
* Check that the whale emoji reaction comes before the thumbs_up emoji reaction
|
||||
* Remove the blue box (it's a box shadow on .selected_message .messagebox-content;
|
||||
inspecting the selected element will find it fairly quickly)
|
||||
* Change the color of the stream to #a6c7e5
|
||||
* Shrink screen till the mypy link only just fits
|
||||
* Take screenshot that does not include the timestamps or bottom edge
|
||||
|
||||
From image editing program:
|
||||
* Remove mute (and edit) icons from recipient bar
|
||||
"""
|
||||
|
||||
def set_avatar(self, user: UserProfile, filename: str) -> None:
|
||||
with open(filename, "rb") as f:
|
||||
upload_avatar_image(f, user, content_type="image/png")
|
||||
do_change_avatar_fields(user, UserProfile.AVATAR_FROM_USER, acting_user=None)
|
||||
|
||||
def add_message_formatting_conversation(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
stream = ensure_stream(realm, "zulip features", acting_user=None)
|
||||
|
||||
UserProfile.objects.filter(email__contains="stage").delete()
|
||||
starr = do_create_user(
|
||||
"1@stage.example.com", "password", realm, "Ada Starr", acting_user=None
|
||||
)
|
||||
self.set_avatar(starr, "static/images/characters/starr.png")
|
||||
fisher = do_create_user(
|
||||
"2@stage.example.com", "password", realm, "Bel Fisher", acting_user=None
|
||||
)
|
||||
self.set_avatar(fisher, "static/images/characters/fisher.png")
|
||||
twitter_bot = do_create_user(
|
||||
"3@stage.example.com",
|
||||
"password",
|
||||
realm,
|
||||
"Twitter Bot",
|
||||
bot_type=UserProfile.DEFAULT_BOT,
|
||||
acting_user=None,
|
||||
)
|
||||
self.set_avatar(twitter_bot, "static/images/characters/coral.png")
|
||||
|
||||
bulk_add_subscriptions(
|
||||
realm, [stream], list(UserProfile.objects.filter(realm=realm)), acting_user=None
|
||||
)
|
||||
|
||||
staged_messages: list[dict[str, Any]] = [
|
||||
{
|
||||
"sender": starr,
|
||||
"content": "Hey @**Bel Fisher**, check out Zulip's Markdown formatting! "
|
||||
"You can have:\n* bulleted lists\n * with sub-bullets too\n"
|
||||
"* **bold**, *italic*, and ~~strikethrough~~ text\n"
|
||||
"* LaTeX for mathematical formulas, both inline -- $$O(n^2)$$ -- and displayed:\n"
|
||||
"```math\n\\int_a^b f(t)\\, dt=F(b)-F(a)\n```",
|
||||
},
|
||||
{
|
||||
"sender": fisher,
|
||||
"content": "My favorite is the syntax highlighting for code blocks\n"
|
||||
"```python\ndef fib(n: int) -> int:\n # returns the n-th Fibonacci number\n"
|
||||
" return fib(n-1) + fib(n-2)\n```",
|
||||
},
|
||||
{
|
||||
"sender": starr,
|
||||
"content": "I think you forgot your base case there, Bel :laughing:\n"
|
||||
"```quote\n```python\ndef fib(n: int) -> int:\n # returns the n-th Fibonacci number\n"
|
||||
" return fib(n-1) + fib(n-2)\n```\n```",
|
||||
},
|
||||
{
|
||||
"sender": fisher,
|
||||
"content": "I'm also a big fan of inline link, tweet, video, and image previews. "
|
||||
"Check out this picture of seahorse[](/static/images/characters/seahorse.png)!",
|
||||
},
|
||||
{
|
||||
"sender": starr,
|
||||
"content": "I just set up a custom linkifier, "
|
||||
"so `#1234` becomes [#1234](github.com/zulip/zulip/1234), "
|
||||
"a link to the corresponding GitHub issue.",
|
||||
},
|
||||
{
|
||||
"sender": twitter_bot,
|
||||
"content": "https://twitter.com/gvanrossum/status/786661035637772288",
|
||||
},
|
||||
{
|
||||
"sender": fisher,
|
||||
"content": "Oops, the Twitter bot I set up shouldn't be posting here. Let me go fix that.",
|
||||
},
|
||||
]
|
||||
|
||||
messages = [
|
||||
internal_prep_stream_message(
|
||||
message["sender"],
|
||||
stream,
|
||||
"message formatting",
|
||||
message["content"],
|
||||
)
|
||||
for message in staged_messages
|
||||
]
|
||||
|
||||
message_ids = [
|
||||
sent_message_result.message_id for sent_message_result in do_send_messages(messages)
|
||||
]
|
||||
|
||||
preview_message = Message.objects.get(
|
||||
id__in=message_ids, content__icontains="image previews"
|
||||
)
|
||||
whale = get_emoji_data(realm.id, "whale")
|
||||
do_add_reaction(starr, preview_message, "whale", whale.emoji_code, whale.reaction_type)
|
||||
|
||||
twitter_message = Message.objects.get(id__in=message_ids, content__icontains="gvanrossum")
|
||||
# Setting up a twitter integration in dev is a decent amount of work. If you need
|
||||
# to update this tweet, either copy the format below, or send the link to the tweet
|
||||
# to chat.zulip.org and ask an admin of that server to get you the rendered_content.
|
||||
twitter_message.rendered_content = (
|
||||
"<p><a>https://twitter.com/gvanrossum/status/786661035637772288</a></p>\n"
|
||||
'<div class="inline-preview-twitter"><div class="twitter-tweet">'
|
||||
'<a><img class="twitter-avatar" '
|
||||
'src="https://pbs.twimg.com/profile_images/424495004/GuidoAvatar_bigger.jpg"></a>'
|
||||
"<p>Great blog post about Zulip's use of mypy: "
|
||||
"<a>http://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/</a></p>"
|
||||
"<span>- Guido van Rossum (@gvanrossum)</span></div></div>"
|
||||
)
|
||||
twitter_message.save(update_fields=["rendered_content"])
|
||||
|
||||
# Put a short pause between the whale reaction and this, so that the
|
||||
# thumbs_up shows up second
|
||||
thumbs_up = get_emoji_data(realm.id, "thumbs_up")
|
||||
do_add_reaction(
|
||||
starr, preview_message, "thumbs_up", thumbs_up.emoji_code, thumbs_up.reaction_type
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: str) -> None:
|
||||
self.add_message_formatting_conversation()
|
||||
@@ -0,0 +1,37 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand, abort_unless_locked
|
||||
from zerver.lib.message import maybe_update_first_visible_message_id
|
||||
from zerver.models import Realm
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Calculate the value of first visible message ID and store it in cache"""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
self.add_realm_args(parser)
|
||||
parser.add_argument(
|
||||
"--lookback-hours",
|
||||
type=int,
|
||||
help="Period a bit larger than that of the cron job that runs "
|
||||
"this command so that the lookback periods are sure to overlap.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
@override
|
||||
@abort_unless_locked
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
target_realm = self.get_realm(options)
|
||||
|
||||
if target_realm is None:
|
||||
realms: Iterable[Realm] = Realm.objects.all().iterator()
|
||||
else:
|
||||
realms = [target_realm]
|
||||
|
||||
for realm in realms:
|
||||
maybe_update_first_visible_message_id(realm, options["lookback_hours"])
|
||||
37
zilencer/management/commands/compare_messages.py
Normal file
37
zilencer/management/commands/compare_messages.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from django.core.management.base import CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """
|
||||
Compare rendered messages from files.
|
||||
Usage: ./manage.py compare_messages <dump1> <dump2>
|
||||
"""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument("dump1", help="First file to compare")
|
||||
parser.add_argument("dump2", help="Second file to compare")
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
total_count = 0
|
||||
changed_count = 0
|
||||
with open(options["dump1"]) as dump1, open(options["dump2"]) as dump2:
|
||||
for line1, line2 in zip(dump1, dump2, strict=False):
|
||||
m1 = orjson.loads(line1)
|
||||
m2 = orjson.loads(line2)
|
||||
total_count += 1
|
||||
if m1["id"] != m2["id"]:
|
||||
self.stderr.write("Inconsistent messages dump")
|
||||
break
|
||||
if m1["content"] != m2["content"]:
|
||||
changed_count += 1
|
||||
self.stdout.write("Changed message id: {id}".format(id=m1["id"]))
|
||||
self.stdout.write(f"Total messages: {total_count}")
|
||||
self.stdout.write(f"Changed messages: {changed_count}")
|
||||
@@ -0,0 +1,15 @@
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.lib.stripe import downgrade_small_realms_behind_on_payments_as_needed
|
||||
from zerver.lib.management import ZulipBaseCommand, abort_unless_locked
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = "Downgrade small realms that are running behind on payments"
|
||||
|
||||
@override
|
||||
@abort_unless_locked
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
downgrade_small_realms_behind_on_payments_as_needed()
|
||||
32
zilencer/management/commands/invoice_plans.py
Normal file
32
zilencer/management/commands/invoice_plans.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import CommandError, CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand, abort_unless_locked
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import invoice_plans_as_needed
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Generates invoices for customers if needed."""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
if settings.DEVELOPMENT:
|
||||
parser.add_argument("--date", type=datetime.datetime.fromisoformat)
|
||||
|
||||
@override
|
||||
@abort_unless_locked
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
if not settings.BILLING_ENABLED:
|
||||
raise CommandError("Billing is not enabled!")
|
||||
|
||||
for_date = None
|
||||
if settings.DEVELOPMENT and options["date"]:
|
||||
for_date = options["date"].replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
invoice_plans_as_needed(for_date)
|
||||
@@ -0,0 +1,86 @@
|
||||
import configparser
|
||||
import json
|
||||
from argparse import ArgumentParser
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from nacl.encoding import Base64Encoder
|
||||
from nacl.public import PrivateKey
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """
|
||||
Add or remove a key pair from the `push_registration_encryption_keys` map.
|
||||
|
||||
Usage:
|
||||
./manage.py manage_push_registration_encryption_keys --add
|
||||
./manage.py manage_push_registration_encryption_keys --remove-key <public-key>
|
||||
"""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--add",
|
||||
action="store_true",
|
||||
help="Add a new key pair to the `push_registration_encryption_keys` map.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remove-key",
|
||||
metavar="PUBLIC_KEY",
|
||||
help="Remove the key pair associated with the given public key from the `push_registration_encryption_keys` map.",
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
if not options["add"] and options["remove_key"] is None:
|
||||
print("Error: Please provide either --add or --remove-key <public-key>.")
|
||||
return
|
||||
|
||||
if settings.DEVELOPMENT:
|
||||
SECRETS_FILENAME = "zproject/dev-secrets.conf"
|
||||
else:
|
||||
SECRETS_FILENAME = "/etc/zulip/zulip-secrets.conf"
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(SECRETS_FILENAME)
|
||||
push_registration_encryption_keys: dict[str, str] = json.loads(
|
||||
config.get("secrets", "push_registration_encryption_keys", fallback="{}")
|
||||
)
|
||||
|
||||
added_key_pair: tuple[str, str] | None = None
|
||||
if options["add"]:
|
||||
# Generate a new key-pair and store.
|
||||
private_key = PrivateKey.generate()
|
||||
private_key_str = Base64Encoder.encode(bytes(private_key)).decode("utf-8")
|
||||
public_key_str = Base64Encoder.encode(bytes(private_key.public_key)).decode("utf-8")
|
||||
push_registration_encryption_keys[public_key_str] = private_key_str
|
||||
added_key_pair = (public_key_str, private_key_str)
|
||||
|
||||
if options["remove_key"] is not None:
|
||||
# Remove the key-pair for the given public key.
|
||||
remove_key = options["remove_key"]
|
||||
if remove_key not in push_registration_encryption_keys:
|
||||
print("Error: No key pair found for the given public key.")
|
||||
return
|
||||
|
||||
del push_registration_encryption_keys[remove_key]
|
||||
|
||||
config.set(
|
||||
"secrets",
|
||||
"push_registration_encryption_keys",
|
||||
json.dumps(push_registration_encryption_keys),
|
||||
)
|
||||
with open(SECRETS_FILENAME, "w") as secrets_file:
|
||||
config.write(secrets_file)
|
||||
|
||||
if added_key_pair is not None:
|
||||
public_key_str, private_key_str = added_key_pair
|
||||
print("Added a new key pair:")
|
||||
print(f"- Public key: {public_key_str}")
|
||||
print(f"- Private key: {private_key_str}")
|
||||
|
||||
if options["remove_key"] is not None:
|
||||
print(f"Removed the key pair for public key: {options['remove_key']}")
|
||||
22
zilencer/management/commands/mark_all_messages_unread.py
Normal file
22
zilencer/management/commands/mark_all_messages_unread.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from typing import Any
|
||||
|
||||
import bmemcached
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import F
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.models import UserMessage
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Script to mark all messages as unread."""
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
assert settings.DEVELOPMENT
|
||||
UserMessage.objects.all().update(flags=F("flags").bitand(~UserMessage.flags.read))
|
||||
_cache = cache._cache # type: ignore[attr-defined] # not in stubs
|
||||
assert isinstance(_cache, bmemcached.Client)
|
||||
_cache.flush_all()
|
||||
586
zilencer/management/commands/populate_billing_realms.py
Normal file
586
zilencer/management/commands/populate_billing_realms.py
Normal file
@@ -0,0 +1,586 @@
|
||||
import contextlib
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from django.core.management.base import CommandParser
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
RemoteRealmBillingSession,
|
||||
RemoteServerBillingSession,
|
||||
UpgradeRequest,
|
||||
add_months,
|
||||
sign_string,
|
||||
)
|
||||
from corporate.models.customers import Customer
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
from corporate.models.plans import CustomerPlan
|
||||
from scripts.lib.zulip_tools import TIMESTAMP_FORMAT
|
||||
from zerver.actions.create_realm import do_create_realm
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.streams import bulk_add_subscriptions
|
||||
from zerver.apps import flush_cache
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.remote_server import get_realms_info_for_push_bouncer
|
||||
from zerver.lib.streams import create_stream_if_needed
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
from zerver.models.realms import get_realm
|
||||
from zilencer.models import (
|
||||
RemoteRealm,
|
||||
RemoteRealmBillingUser,
|
||||
RemoteServerBillingUser,
|
||||
RemoteZulipServer,
|
||||
RemoteZulipServerAuditLog,
|
||||
)
|
||||
from zilencer.views import update_remote_realm_data_for_server
|
||||
from zproject.config import get_secret
|
||||
|
||||
current_time = timezone_now().strftime(TIMESTAMP_FORMAT)
|
||||
communicate_with_stripe = get_secret("stripe_secret_key") is not None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomerProfile:
|
||||
unique_id: str
|
||||
billing_schedule: int = CustomerPlan.BILLING_SCHEDULE_ANNUAL
|
||||
tier: int | None = None
|
||||
new_plan_tier: int | None = None
|
||||
automanage_licenses: bool = False
|
||||
status: int = CustomerPlan.ACTIVE
|
||||
sponsorship_pending: bool = False
|
||||
is_sponsored: bool = False
|
||||
card: str = ""
|
||||
charge_automatically: bool = True
|
||||
is_remote_realm: bool = False
|
||||
is_remote_server: bool = False
|
||||
renewal_date: str = current_time
|
||||
# Use (timezone_now() + timedelta(minutes=1)).strftime(TIMESTAMP_FORMAT) as `end_date` for testing.
|
||||
# `invoice_plan` is not implemented yet for remote servers and realms so no payment is generated in stripe.
|
||||
end_date: str = "2030-10-10-01-10-10"
|
||||
remote_server_plan_start_date: str = "billing_cycle_end_date"
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = "Populate database with different types of realms that can exist."
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument(
|
||||
"--only-remote-server",
|
||||
action="store_true",
|
||||
help="Whether to only run for remote servers",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--only-remote-realm",
|
||||
action="store_true",
|
||||
help="Whether to only run for remote realms",
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
# Create a realm for each plan type
|
||||
|
||||
customer_profiles = [
|
||||
# NOTE: The unique_id has to be less than 40 characters.
|
||||
CustomerProfile(unique_id="sponsorship-pending", sponsorship_pending=True),
|
||||
CustomerProfile(
|
||||
unique_id="annual-free",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="annual-standard",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="annual-plus",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_PLUS,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="monthly-free",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="monthly-standard",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="monthly-plus",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_PLUS,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="downgrade-end-of-cycle",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
status=CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="standard-automanage-licenses",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
automanage_licenses=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="standard-automatic-card",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
card="pm_card_visa",
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="standard-invoice-payment",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
charge_automatically=False,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="standard-switch-to-annual-eoc",
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
status=CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="sponsored",
|
||||
is_sponsored=True,
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
# Customer plan might not exist for sponsored realms.
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="free-trial",
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
status=CustomerPlan.FREE_TRIAL,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="legacy-server",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="legacy-server-upgrade-scheduled",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY,
|
||||
status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END,
|
||||
new_plan_tier=CustomerPlan.TIER_SELF_HOSTED_BASIC,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-server",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-server-free-trial",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
status=CustomerPlan.FREE_TRIAL,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-server-sponsorship-pending",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
sponsorship_pending=True,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="self-hosted-server-sponsorship-pending",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BASE,
|
||||
sponsorship_pending=True,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="server-sponsored",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_COMMUNITY,
|
||||
is_sponsored=True,
|
||||
is_remote_server=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="legacy-remote-realm",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="free-tier-remote-realm",
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-remote-realm",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-remote-free-trial",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
status=CustomerPlan.FREE_TRIAL,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="business-remote-sponsorship-pending",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
sponsorship_pending=True,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="remote-sponsorship-pending",
|
||||
sponsorship_pending=True,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="remote-realm-sponsored",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_COMMUNITY,
|
||||
is_sponsored=True,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="basic-remote-realm",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BASIC,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
CustomerProfile(
|
||||
unique_id="basic-remote-free-trial",
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BASIC,
|
||||
status=CustomerPlan.FREE_TRIAL,
|
||||
is_remote_realm=True,
|
||||
),
|
||||
]
|
||||
|
||||
servers = []
|
||||
remote_realms = []
|
||||
for customer_profile in customer_profiles:
|
||||
if customer_profile.is_remote_server and not options.get("only_remote_realm"):
|
||||
server_conf = populate_remote_server(customer_profile)
|
||||
servers.append(server_conf)
|
||||
elif customer_profile.is_remote_realm and not options.get("only_remote_server"):
|
||||
remote_realm_conf = populate_remote_realms(customer_profile)
|
||||
remote_realms.append(remote_realm_conf)
|
||||
elif not options.get("only_remote_server") and not options.get("only_remote_realm"):
|
||||
populate_realm(customer_profile)
|
||||
|
||||
print("-" * 40)
|
||||
for server in servers:
|
||||
for key, value in server.items():
|
||||
print(f"{key}: {value}")
|
||||
print("-" * 40)
|
||||
|
||||
for remote_realm_conf in remote_realms:
|
||||
for key, value in remote_realm_conf.items():
|
||||
print(f"{key}: {value}")
|
||||
print("-" * 40)
|
||||
|
||||
|
||||
def add_card_to_customer(customer: Customer) -> None:
|
||||
if not communicate_with_stripe:
|
||||
return
|
||||
|
||||
assert customer.stripe_customer_id is not None
|
||||
# Set the Stripe API key
|
||||
stripe.api_key = get_secret("stripe_secret_key")
|
||||
|
||||
# Create a card payment method and attach it to the customer
|
||||
payment_method = stripe.PaymentMethod.create(
|
||||
type="card",
|
||||
card={"token": "tok_visa"},
|
||||
)
|
||||
|
||||
# Attach the payment method to the customer
|
||||
stripe.PaymentMethod.attach(payment_method.id, customer=customer.stripe_customer_id)
|
||||
|
||||
# Set the default payment method for the customer
|
||||
stripe.Customer.modify(
|
||||
customer.stripe_customer_id,
|
||||
invoice_settings={"default_payment_method": payment_method.id},
|
||||
)
|
||||
|
||||
|
||||
def create_plan_for_customer(customer: Customer, customer_profile: CustomerProfile) -> None:
|
||||
assert customer_profile.tier is not None
|
||||
if customer_profile.status == CustomerPlan.FREE_TRIAL:
|
||||
# 2 months free trial.
|
||||
next_invoice_date = add_months(timezone_now(), 2)
|
||||
else:
|
||||
months = 12
|
||||
if customer_profile.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
months = 1
|
||||
next_invoice_date = add_months(timezone_now(), months)
|
||||
|
||||
customer_plan = CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
billing_cycle_anchor=timezone_now(),
|
||||
billing_schedule=customer_profile.billing_schedule,
|
||||
tier=customer_profile.tier,
|
||||
price_per_license=1200,
|
||||
automanage_licenses=customer_profile.automanage_licenses,
|
||||
status=customer_profile.status,
|
||||
charge_automatically=customer_profile.charge_automatically,
|
||||
next_invoice_date=next_invoice_date,
|
||||
)
|
||||
|
||||
LicenseLedger.objects.create(
|
||||
licenses=25,
|
||||
licenses_at_next_renewal=25,
|
||||
event_time=timezone_now(),
|
||||
is_renewal=True,
|
||||
plan=customer_plan,
|
||||
)
|
||||
|
||||
|
||||
def populate_realm(customer_profile: CustomerProfile) -> Realm | None:
|
||||
unique_id = customer_profile.unique_id
|
||||
if customer_profile.is_remote_realm:
|
||||
plan_type = Realm.PLAN_TYPE_SELF_HOSTED
|
||||
elif customer_profile.is_sponsored:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
||||
elif customer_profile.tier is None:
|
||||
plan_type = Realm.PLAN_TYPE_LIMITED
|
||||
elif customer_profile.tier == CustomerPlan.TIER_CLOUD_STANDARD:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD
|
||||
elif customer_profile.tier == CustomerPlan.TIER_CLOUD_PLUS:
|
||||
plan_type = Realm.PLAN_TYPE_PLUS
|
||||
else:
|
||||
raise AssertionError("Unexpected tier!")
|
||||
plan_name = Realm.ALL_PLAN_TYPES[plan_type]
|
||||
|
||||
# Delete existing realm with this name
|
||||
with contextlib.suppress(Realm.DoesNotExist):
|
||||
get_realm(unique_id).delete()
|
||||
# Because we just deleted a bunch of objects in the database
|
||||
# directly (rather than deleting individual objects in Django,
|
||||
# in which case our post_save hooks would have flushed the
|
||||
# individual objects from memcached for us), we need to flush
|
||||
# memcached in order to ensure deleted objects aren't still
|
||||
# present in the memcached cache.
|
||||
flush_cache(None)
|
||||
|
||||
realm = do_create_realm(
|
||||
string_id=unique_id,
|
||||
name=unique_id,
|
||||
description=unique_id,
|
||||
plan_type=plan_type,
|
||||
)
|
||||
|
||||
# Create a user with billing access
|
||||
full_name = f"{plan_name}-admin"
|
||||
email = f"{full_name}@zulip.com"
|
||||
user = do_create_user(
|
||||
email,
|
||||
full_name,
|
||||
realm,
|
||||
full_name,
|
||||
role=UserProfile.ROLE_REALM_OWNER,
|
||||
acting_user=None,
|
||||
tos_version=settings.TERMS_OF_SERVICE_VERSION,
|
||||
)
|
||||
|
||||
stream, _ = create_stream_if_needed(
|
||||
realm,
|
||||
"all",
|
||||
)
|
||||
|
||||
bulk_add_subscriptions(realm, [stream], [user], acting_user=None)
|
||||
|
||||
if customer_profile.is_remote_realm:
|
||||
# Remote realm billing data on their local server is irrelevant.
|
||||
return realm
|
||||
|
||||
if customer_profile.sponsorship_pending or customer_profile.is_sponsored:
|
||||
# plan_type is already set correctly above for sponsored realms.
|
||||
customer = Customer.objects.create(
|
||||
realm=realm,
|
||||
sponsorship_pending=customer_profile.sponsorship_pending,
|
||||
)
|
||||
return realm
|
||||
|
||||
if customer_profile.tier is None:
|
||||
return realm
|
||||
|
||||
billing_session = RealmBillingSession(user)
|
||||
if communicate_with_stripe:
|
||||
# This attaches stripe_customer_id to customer.
|
||||
customer = billing_session.update_or_create_stripe_customer()
|
||||
assert customer.stripe_customer_id is not None
|
||||
else:
|
||||
customer = billing_session.update_or_create_customer()
|
||||
|
||||
if customer_profile.card:
|
||||
add_card_to_customer(customer)
|
||||
|
||||
create_plan_for_customer(customer, customer_profile)
|
||||
return realm
|
||||
|
||||
|
||||
def populate_remote_server(customer_profile: CustomerProfile) -> dict[str, str]:
|
||||
unique_id = customer_profile.unique_id
|
||||
|
||||
if (
|
||||
customer_profile.is_sponsored
|
||||
and customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_COMMUNITY
|
||||
):
|
||||
plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY
|
||||
elif customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||
plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY
|
||||
elif customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
||||
plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS
|
||||
elif customer_profile.tier is CustomerPlan.TIER_SELF_HOSTED_BASE:
|
||||
plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED
|
||||
else:
|
||||
raise AssertionError("Unexpected tier!")
|
||||
|
||||
server_uuid = str(uuid.uuid4())
|
||||
api_key = server_uuid
|
||||
hostname = f"{unique_id}.example.com"
|
||||
|
||||
# Delete existing remote server.
|
||||
RemoteZulipServer.objects.filter(hostname=hostname).delete()
|
||||
flush_cache(None)
|
||||
|
||||
remote_server = RemoteZulipServer.objects.create(
|
||||
uuid=server_uuid,
|
||||
api_key=api_key,
|
||||
hostname=f"{unique_id}.example.com",
|
||||
contact_email=f"{unique_id}@example.com",
|
||||
plan_type=plan_type,
|
||||
# TODO: Save property audit log data for server.
|
||||
last_audit_log_update=timezone_now(),
|
||||
)
|
||||
|
||||
RemoteZulipServerAuditLog.objects.create(
|
||||
event_type=AuditLogEventType.REMOTE_SERVER_CREATED,
|
||||
server=remote_server,
|
||||
event_time=remote_server.last_updated,
|
||||
)
|
||||
|
||||
billing_user = RemoteServerBillingUser.objects.create(
|
||||
full_name="Server user",
|
||||
remote_server=remote_server,
|
||||
email=f"{unique_id}@example.com",
|
||||
)
|
||||
billing_session = RemoteServerBillingSession(remote_server, billing_user)
|
||||
if customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||
# Create customer plan for these servers for temporary period.
|
||||
renewal_date = datetime.strptime(customer_profile.renewal_date, TIMESTAMP_FORMAT).replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
billing_session.create_complimentary_access_plan(renewal_date, end_date)
|
||||
|
||||
if not communicate_with_stripe:
|
||||
# We need to communicate with stripe to upgrade here.
|
||||
return {
|
||||
"unique_id": unique_id,
|
||||
"server_uuid": server_uuid,
|
||||
"api_key": api_key,
|
||||
"ERROR": "Need to communicate with stripe to populate this profile.",
|
||||
}
|
||||
|
||||
# Scheduled server to upgrade to business plan.
|
||||
if customer_profile.status == CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END:
|
||||
# This attaches stripe_customer_id to customer.
|
||||
customer = billing_session.update_or_create_stripe_customer()
|
||||
add_card_to_customer(customer)
|
||||
seat_count = 30
|
||||
signed_seat_count, salt = sign_string(str(seat_count))
|
||||
upgrade_request = UpgradeRequest(
|
||||
billing_modality="charge_automatically",
|
||||
schedule="annual",
|
||||
signed_seat_count=signed_seat_count,
|
||||
salt=salt,
|
||||
license_management="automatic",
|
||||
licenses=seat_count,
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
remote_server_plan_start_date=customer_profile.remote_server_plan_start_date,
|
||||
)
|
||||
billing_session.do_upgrade(upgrade_request)
|
||||
|
||||
elif customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
||||
if communicate_with_stripe:
|
||||
# This attaches stripe_customer_id to customer.
|
||||
customer = billing_session.update_or_create_stripe_customer()
|
||||
assert customer.stripe_customer_id is not None
|
||||
else:
|
||||
customer = billing_session.update_or_create_customer()
|
||||
add_card_to_customer(customer)
|
||||
create_plan_for_customer(customer, customer_profile)
|
||||
|
||||
if customer_profile.sponsorship_pending:
|
||||
billing_session.update_customer_sponsorship_status(True)
|
||||
elif customer_profile.is_sponsored:
|
||||
billing_session.do_change_plan_type(tier=None, is_sponsored=True)
|
||||
|
||||
return {
|
||||
"unique_id": unique_id,
|
||||
"server_uuid": server_uuid,
|
||||
"api_key": api_key,
|
||||
}
|
||||
|
||||
|
||||
def populate_remote_realms(customer_profile: CustomerProfile) -> dict[str, str]:
|
||||
# Delete existing remote realm.
|
||||
RemoteRealm.objects.filter(name=customer_profile.unique_id).delete()
|
||||
flush_cache(None)
|
||||
|
||||
local_realm = populate_realm(customer_profile)
|
||||
assert local_realm is not None
|
||||
|
||||
remote_server_uuid = settings.ZULIP_ORG_ID
|
||||
assert remote_server_uuid is not None
|
||||
remote_server = RemoteZulipServer.objects.filter(
|
||||
uuid=remote_server_uuid,
|
||||
).first()
|
||||
|
||||
if remote_server is None:
|
||||
raise AssertionError("Remote server not found! Please run manage.py register_server")
|
||||
|
||||
update_remote_realm_data_for_server(remote_server, get_realms_info_for_push_bouncer())
|
||||
|
||||
remote_realm = RemoteRealm.objects.get(uuid=local_realm.uuid)
|
||||
user = UserProfile.objects.filter(realm=local_realm).first()
|
||||
assert user is not None
|
||||
billing_user = RemoteRealmBillingUser.objects.create(
|
||||
full_name=user.full_name,
|
||||
remote_realm=remote_realm,
|
||||
user_uuid=user.uuid,
|
||||
email=user.email,
|
||||
)
|
||||
billing_session = RemoteRealmBillingSession(remote_realm, billing_user)
|
||||
# TODO: Save property audit log data for server.
|
||||
remote_realm.server.last_audit_log_update = timezone_now()
|
||||
remote_realm.server.save(update_fields=["last_audit_log_update"])
|
||||
if communicate_with_stripe:
|
||||
# This attaches stripe_customer_id to customer.
|
||||
customer = billing_session.update_or_create_stripe_customer()
|
||||
assert customer.stripe_customer_id is not None
|
||||
else:
|
||||
customer = billing_session.update_or_create_customer()
|
||||
add_card_to_customer(customer)
|
||||
if customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||
renewal_date = datetime.strptime(customer_profile.renewal_date, TIMESTAMP_FORMAT).replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
billing_session.create_complimentary_access_plan(renewal_date, end_date)
|
||||
elif customer_profile.tier is not None:
|
||||
billing_session.do_change_plan_type(
|
||||
tier=customer_profile.tier, is_sponsored=customer_profile.is_sponsored
|
||||
)
|
||||
if not customer_profile.is_sponsored:
|
||||
create_plan_for_customer(customer, customer_profile)
|
||||
|
||||
if customer_profile.sponsorship_pending:
|
||||
billing_session.update_customer_sponsorship_status(True)
|
||||
|
||||
return {
|
||||
"unique_id": customer_profile.unique_id,
|
||||
"login_url": local_realm.url + "/self-hosted-billing/",
|
||||
}
|
||||
1567
zilencer/management/commands/populate_db.py
Normal file
1567
zilencer/management/commands/populate_db.py
Normal file
File diff suppressed because it is too large
Load Diff
34
zilencer/management/commands/print_initial_password.py
Normal file
34
zilencer/management/commands/print_initial_password.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from argparse import ArgumentParser
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.initial_password import initial_password
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = "Print the initial password and API key for accounts as created by populate_db"
|
||||
|
||||
fmt = "%-30s %-16s %-32s"
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"emails",
|
||||
metavar="<email>",
|
||||
nargs="*",
|
||||
help="email of user to show password and API key for",
|
||||
)
|
||||
self.add_realm_args(parser)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: str) -> None:
|
||||
realm = self.get_realm(options)
|
||||
print(self.fmt % ("email", "password", "API key"))
|
||||
for email in options["emails"]:
|
||||
if "@" not in email:
|
||||
print(f"ERROR: {email} does not look like an email address")
|
||||
continue
|
||||
user = self.get_user(email, realm)
|
||||
print(self.fmt % (email, initial_password(email), user.api_key))
|
||||
68
zilencer/management/commands/profile_request.py
Normal file
68
zilencer/management/commands/profile_request.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import cProfile
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.core.management.base import CommandParser
|
||||
from django.http import HttpRequest, HttpResponseBase
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.request import RequestNotes
|
||||
from zerver.lib.test_helpers import HostRequestMock
|
||||
from zerver.middleware import LogRequests
|
||||
from zerver.models import UserMessage
|
||||
from zerver.views.message_fetch import get_messages_backend
|
||||
|
||||
|
||||
class MockSession(SessionBase):
|
||||
def __init__(self) -> None:
|
||||
self.modified = False
|
||||
|
||||
|
||||
def profile_request(request: HttpRequest, num_before: int, num_after: int) -> HttpResponseBase:
|
||||
def get_response(request: HttpRequest) -> HttpResponseBase:
|
||||
return prof.runcall(
|
||||
get_messages_backend,
|
||||
request,
|
||||
request.user,
|
||||
num_before=num_before,
|
||||
num_after=num_after,
|
||||
apply_markdown=True,
|
||||
)
|
||||
|
||||
prof = cProfile.Profile()
|
||||
with tempfile.NamedTemporaryFile(prefix="profile.data.", delete=False) as stats_file:
|
||||
response = LogRequests(get_response)(request)
|
||||
assert isinstance(response, HttpResponseBase) # async responses not supported here for now
|
||||
prof.dump_stats(stats_file.name)
|
||||
logging.info("Profiling data written to %s", stats_file.name)
|
||||
return response
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument("email", metavar="<email>", help="Email address of the user")
|
||||
self.add_realm_args(parser)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
realm = self.get_realm(options)
|
||||
user = self.get_user(options["email"], realm)
|
||||
anchor = UserMessage.objects.filter(user_profile=user).order_by("-message")[200].message_id
|
||||
mock_request = HostRequestMock(
|
||||
post_data={
|
||||
"anchor": anchor,
|
||||
"num_before": 1200,
|
||||
"num_after": 200,
|
||||
},
|
||||
user_profile=user,
|
||||
meta_data={"REMOTE_ADDR": "127.0.0.1"},
|
||||
path="/",
|
||||
)
|
||||
mock_request.session = MockSession()
|
||||
RequestNotes.get_notes(mock_request).log_data = None
|
||||
|
||||
profile_request(mock_request, num_before=1200, num_after=200)
|
||||
89
zilencer/management/commands/queue_rate.py
Normal file
89
zilencer/management/commands/queue_rate.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import csv
|
||||
from timeit import timeit
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.queue import SimpleQueueClient, queue_json_publish_rollback_unsafe
|
||||
from zerver.worker.test import BatchNoopWorker, NoopWorker
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Times the overhead of enqueuing and dequeuing messages from RabbitMQ."""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument(
|
||||
"--count", help="Number of messages to enqueue", default=10000, type=int
|
||||
)
|
||||
parser.add_argument("--reps", help="Iterations of enqueue/dequeue", default=1, type=int)
|
||||
parser.add_argument("--batch", help="Enables batch dequeuing", action="store_true")
|
||||
parser.add_argument("--csv", help="Path to CSV output", default="rabbitmq-timings.csv")
|
||||
parser.add_argument(
|
||||
"--prefetches",
|
||||
help="Limits the prefetch size; RabbitMQ defaults to unbounded (0)",
|
||||
default=[0],
|
||||
nargs="+",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slow",
|
||||
help="Which request numbers should take 60s (1-based)",
|
||||
action="append",
|
||||
type=int,
|
||||
default=[],
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
print("Purging queue...")
|
||||
queue = SimpleQueueClient()
|
||||
queue_name = "noop_batch" if options["batch"] else "noop"
|
||||
queue.ensure_queue(queue_name, lambda channel: channel.queue_purge("noop"))
|
||||
count = options["count"]
|
||||
reps = options["reps"]
|
||||
|
||||
with open(options["csv"], "w", newline="") as csvfile:
|
||||
writer = csv.DictWriter(
|
||||
csvfile, fieldnames=["Queue size", "Queue type", "Prefetch", "Rate"]
|
||||
)
|
||||
writer.writeheader()
|
||||
|
||||
for prefetch in options["prefetches"]:
|
||||
print(f"Queue size {count}, prefetch {prefetch}...")
|
||||
worker: NoopWorker | BatchNoopWorker = NoopWorker(count, options["slow"])
|
||||
if options["batch"]:
|
||||
worker = BatchNoopWorker(count, options["slow"])
|
||||
if prefetch > 0 and prefetch < worker.batch_size:
|
||||
print(
|
||||
f" Skipping, as prefetch {prefetch} is less than batch size {worker.batch_size}"
|
||||
)
|
||||
continue
|
||||
worker.setup()
|
||||
|
||||
assert worker.q is not None
|
||||
assert worker.q.channel is not None
|
||||
worker.q.channel.basic_qos(prefetch_count=prefetch)
|
||||
|
||||
total_time = 0.0
|
||||
for i in range(1, reps + 1):
|
||||
worker.consumed = 0
|
||||
timeit(
|
||||
lambda: queue_json_publish_rollback_unsafe(queue_name, {}),
|
||||
number=count,
|
||||
)
|
||||
duration = timeit(worker.start, number=1)
|
||||
print(f" {i}/{reps}: {count}/{duration}s = {count / duration}/s")
|
||||
total_time += duration
|
||||
writer.writerow(
|
||||
{
|
||||
"Queue size": count,
|
||||
"Queue type": queue_name,
|
||||
"Prefetch": prefetch,
|
||||
"Rate": count / duration,
|
||||
}
|
||||
)
|
||||
csvfile.flush()
|
||||
print(f" Overall: {reps * count}/{total_time}s = {(reps * count) / total_time}/s")
|
||||
69
zilencer/management/commands/render_messages.py
Normal file
69
zilencer/management/commands/render_messages.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from django.core.management.base import CommandParser
|
||||
from django.db.models import QuerySet
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.markdown import render_message_markdown
|
||||
from zerver.models import Message
|
||||
|
||||
|
||||
def queryset_iterator(queryset: QuerySet[Message], chunksize: int = 5000) -> Iterator[Message]:
|
||||
queryset = queryset.order_by("id")
|
||||
while queryset.exists():
|
||||
for row in queryset[:chunksize]:
|
||||
msg_id = row.id
|
||||
yield row
|
||||
queryset = queryset.filter(id__gt=msg_id)
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """
|
||||
Render messages to a file.
|
||||
Usage: ./manage.py render_messages <destination> [--amount=10000]
|
||||
"""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument("destination", help="Destination file path")
|
||||
parser.add_argument("--amount", default=100000, help="Number of messages to render")
|
||||
parser.add_argument("--latest_id", default=0, help="Last message id to render")
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
dest_dir = os.path.realpath(os.path.dirname(options["destination"]))
|
||||
amount = int(options["amount"])
|
||||
latest = int(options["latest_id"]) or Message.objects.latest("id").id
|
||||
self.stdout.write(f"Latest message id: {latest}")
|
||||
if not os.path.exists(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
with open(options["destination"], "wb") as result:
|
||||
messages = Message.objects.filter(id__gt=latest - amount, id__lte=latest).order_by("id")
|
||||
for message in queryset_iterator(messages):
|
||||
content = message.content
|
||||
# In order to ensure that the output of this tool is
|
||||
# consistent across the time, even if messages are
|
||||
# edited, we always render the original content
|
||||
# version, extracting it from the edit history if
|
||||
# necessary.
|
||||
if message.edit_history:
|
||||
history = orjson.loads(message.edit_history)
|
||||
history = sorted(history, key=lambda i: i["timestamp"])
|
||||
for entry in history:
|
||||
if "prev_content" in entry:
|
||||
content = entry["prev_content"]
|
||||
break
|
||||
result.write(
|
||||
orjson.dumps(
|
||||
{
|
||||
"id": message.id,
|
||||
"content": render_message_markdown(message, content),
|
||||
},
|
||||
option=orjson.OPT_APPEND_NEWLINE,
|
||||
)
|
||||
)
|
||||
32
zilencer/management/commands/rundjangoserver.py
Normal file
32
zilencer/management/commands/rundjangoserver.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
|
||||
from django.core.management.commands.runserver import Command as DjangoCommand
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
def output_styler(style_func: Callable[[str], str]) -> Callable[[str], str]:
|
||||
# Might fail to suppress the date line around midnight, but, whatever.
|
||||
date_prefix = datetime.now().astimezone().strftime("%B %d, %Y - ")
|
||||
|
||||
@wraps(style_func)
|
||||
def _wrapped_style_func(message: str) -> str:
|
||||
if message == "Performing system checks...\n\n" or message.startswith(
|
||||
("System check identified no issues", date_prefix)
|
||||
):
|
||||
message = ""
|
||||
elif "Quit the server with " in message:
|
||||
message = (
|
||||
"Django process (re)started. " + message[message.index("Quit the server with ") :]
|
||||
)
|
||||
return style_func(message)
|
||||
|
||||
return _wrapped_style_func
|
||||
|
||||
|
||||
class Command(DjangoCommand):
|
||||
@override
|
||||
def inner_run(self, *args: object, **options: object) -> None:
|
||||
self.stdout.style_func = output_styler(self.stdout.style_func)
|
||||
super().inner_run(*args, **options)
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import CommandError, CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import RealmBillingSession
|
||||
from corporate.models.plans import CustomerPlan
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
self.add_realm_args(parser)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
realm = self.get_realm(options)
|
||||
|
||||
if not realm:
|
||||
raise CommandError("No realm found.")
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
billing_session = RealmBillingSession(realm=realm)
|
||||
billing_session.do_change_plan_to_new_tier(new_plan_tier=CustomerPlan.TIER_CLOUD_PLUS)
|
||||
33
zilencer/management/commands/sync_api_key.py
Normal file
33
zilencer/management/commands/sync_api_key.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.models import UserProfile
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models.users import get_user_by_delivery_email
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """Sync your API key from ~/.zuliprc into your development instance"""
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
config_file = os.path.join(os.environ["HOME"], ".zuliprc")
|
||||
if not os.path.exists(config_file):
|
||||
raise RuntimeError("No ~/.zuliprc found")
|
||||
config = ConfigParser()
|
||||
with open(config_file) as f:
|
||||
config.read_file(f, config_file)
|
||||
api_key = config.get("api", "key")
|
||||
email = config.get("api", "email")
|
||||
|
||||
try:
|
||||
realm = get_realm("zulip")
|
||||
user_profile = get_user_by_delivery_email(email, realm)
|
||||
user_profile.api_key = api_key
|
||||
user_profile.save(update_fields=["api_key"])
|
||||
except UserProfile.DoesNotExist:
|
||||
print(f"User {email} does not exist; not syncing API key")
|
||||
28
zilencer/migrations/0001_initial.py
Normal file
28
zilencer/migrations/0001_initial.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Deployment",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("api_key", models.CharField(max_length=32, null=True)),
|
||||
("base_api_url", models.CharField(max_length=128)),
|
||||
("base_site_url", models.CharField(max_length=128)),
|
||||
("realms", models.ManyToManyField(related_name="_deployments", to="zerver.Realm")),
|
||||
],
|
||||
options={},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,595 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-13 20:11
|
||||
|
||||
import django.core.serializers.json
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [
|
||||
("zilencer", "0001_initial"),
|
||||
("zilencer", "0002_remote_zulip_server"),
|
||||
("zilencer", "0003_add_default_for_remotezulipserver_last_updated_field"),
|
||||
("zilencer", "0004_remove_deployment_model"),
|
||||
("zilencer", "0005_remotepushdevicetoken_fix_uniqueness"),
|
||||
("zilencer", "0006_customer"),
|
||||
("zilencer", "0007_remotezulipserver_fix_uniqueness"),
|
||||
("zilencer", "0008_customer_billing_user"),
|
||||
("zilencer", "0009_plan"),
|
||||
("zilencer", "0010_billingprocessor"),
|
||||
("zilencer", "0011_customer_has_billing_relationship"),
|
||||
("zilencer", "0012_coupon"),
|
||||
("zilencer", "0013_remove_customer_billing_user"),
|
||||
("zilencer", "0014_cleanup_pushdevicetoken"),
|
||||
("zilencer", "0015_delete_billing"),
|
||||
("zilencer", "0016_remote_counts"),
|
||||
("zilencer", "0017_installationcount_indexes"),
|
||||
("zilencer", "0018_remoterealmauditlog"),
|
||||
("zilencer", "0019_remotezulipserver_plan_type"),
|
||||
("zilencer", "0020_remotezulipserverauditlog"),
|
||||
("zilencer", "0021_alter_remotezulipserver_uuid"),
|
||||
("zilencer", "0022_remotezulipserver_create_audit_log_backfill"),
|
||||
("zilencer", "0023_remotezulipserver_deactivated"),
|
||||
("zilencer", "0024_remotepushdevicetoken_user_uuid"),
|
||||
("zilencer", "0025_alter_remotepushdevicetoken_user_id_drop_index"),
|
||||
("zilencer", "0026_auditlog_models_extra_data_json"),
|
||||
("zilencer", "0027_backfill_remote_realmauditlog_extradata_to_json_field"),
|
||||
("zilencer", "0028_rename_extradatajson_remoteauditlog_extra_data"),
|
||||
("zilencer", "0029_update_remoterealm_indexes"),
|
||||
("zilencer", "0030_alter_remoteinstallationcount_remote_id"),
|
||||
("zilencer", "0031_alter_remoteinstallationcount_remote_id_and_more"),
|
||||
("zilencer", "0032_remotepushdevicetoken_backfill_ios_app_id"),
|
||||
("zilencer", "0033_remoterealm"),
|
||||
("zilencer", "0034_remoterealmauditlog_remote_realm_and_more"),
|
||||
("zilencer", "0035_remoterealmcount_remote_realm_and_more"),
|
||||
("zilencer", "0036_remotezulipserver_last_version"),
|
||||
("zilencer", "0037_alter_remoteinstallationcount_unique_together_and_more"),
|
||||
("zilencer", "0038_unique_server_remote_id"),
|
||||
("zilencer", "0039_remoterealm_org_type"),
|
||||
("zilencer", "0040_remoterealm_authentication_methods_remoterealm_name"),
|
||||
("zilencer", "0041_remotezulipserver_org_type"),
|
||||
("zilencer", "0042_alter_remoterealmauditlog_realm_id"),
|
||||
("zilencer", "0043_remotepushdevicetoken_remote_realm"),
|
||||
("zilencer", "0044_remoterealmbillinguser"),
|
||||
("zilencer", "0045_remoterealmauditlog_zilencer_remoterealmauditlog_server_realm_and_more"),
|
||||
("zilencer", "0046_remotezulipserver_last_audit_log_update"),
|
||||
("zilencer", "0047_preregistrationremoteserverbillinguser_and_more"),
|
||||
("zilencer", "0048_remotezulipserver_last_api_feature_level"),
|
||||
("zilencer", "0049_alter_remoterealmbillinguser_unique_together_and_more"),
|
||||
("zilencer", "0050_preregistrationremoterealmbillinguser_created_user_and_more"),
|
||||
("zilencer", "0051_remoterealm_is_system_bot_realm"),
|
||||
("zilencer", "0052_alter_remoterealm_plan_type_and_more"),
|
||||
("zilencer", "0053_remoterealmauditlog_acting_remote_user_and_more"),
|
||||
("zilencer", "0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more"),
|
||||
("zilencer", "0055_remoteserverbillinguser_tos_version"),
|
||||
("zilencer", "0056_remoterealm_realm_locally_deleted"),
|
||||
("zilencer", "0057_remoterealm_last_request_timestamp_and_more"),
|
||||
("zilencer", "0058_remoteinstallationcount_add_mobile_pushes_forwarded_index"),
|
||||
("zilencer", "0059_remoterealmauditlog_add_synced_billing_event_type_index"),
|
||||
(
|
||||
"zilencer",
|
||||
"0060_remove_remoterealmcount_unique_remote_realm_installation_count_and_more",
|
||||
),
|
||||
("zilencer", "0061_clean_count_tables"),
|
||||
("zilencer", "0062_alter_remoteinstallationcount_id_and_more"),
|
||||
("zilencer", "0063_convert_ids_to_bigints"),
|
||||
("zilencer", "0064_remotezulipserver_last_merge_base"),
|
||||
]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteZulipServer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(unique=True)),
|
||||
("api_key", models.CharField(max_length=64)),
|
||||
("hostname", models.CharField(max_length=128)),
|
||||
("contact_email", models.EmailField(blank=True, max_length=254)),
|
||||
("last_updated", models.DateTimeField(auto_now=True, verbose_name="last updated")),
|
||||
("plan_type", models.PositiveSmallIntegerField(default=100)),
|
||||
("deactivated", models.BooleanField(default=False)),
|
||||
("last_version", models.CharField(max_length=128, null=True)),
|
||||
(
|
||||
"org_type",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, "Unspecified"),
|
||||
(10, "Business"),
|
||||
(20, "Open-source project"),
|
||||
(30, "Education (non-profit)"),
|
||||
(35, "Education (for-profit)"),
|
||||
(40, "Research"),
|
||||
(50, "Event or conference"),
|
||||
(60, "Non-profit (registered)"),
|
||||
(70, "Government"),
|
||||
(80, "Political group"),
|
||||
(90, "Community"),
|
||||
(100, "Personal"),
|
||||
(1000, "Other"),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
),
|
||||
("last_audit_log_update", models.DateTimeField(null=True)),
|
||||
("last_api_feature_level", models.PositiveIntegerField(null=True)),
|
||||
("last_request_datetime", models.DateTimeField(null=True)),
|
||||
("last_merge_base", models.CharField(max_length=128, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealm",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(unique=True)),
|
||||
("uuid_owner_secret", models.TextField()),
|
||||
("host", models.TextField()),
|
||||
("last_updated", models.DateTimeField(auto_now=True, verbose_name="last updated")),
|
||||
("registration_deactivated", models.BooleanField(default=False)),
|
||||
("realm_deactivated", models.BooleanField(default=False)),
|
||||
("realm_date_created", models.DateTimeField()),
|
||||
("plan_type", models.PositiveSmallIntegerField(db_index=True, default=100)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
(
|
||||
"org_type",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, "Unspecified"),
|
||||
(10, "Business"),
|
||||
(20, "Open-source project"),
|
||||
(30, "Education (non-profit)"),
|
||||
(35, "Education (for-profit)"),
|
||||
(40, "Research"),
|
||||
(50, "Event or conference"),
|
||||
(60, "Non-profit (registered)"),
|
||||
(70, "Government"),
|
||||
(80, "Political group"),
|
||||
(90, "Community"),
|
||||
(100, "Personal"),
|
||||
(1000, "Other"),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
),
|
||||
("authentication_methods", models.JSONField(default=dict)),
|
||||
("name", models.TextField(default="")),
|
||||
("is_system_bot_realm", models.BooleanField(default=False)),
|
||||
("realm_locally_deleted", models.BooleanField(default=False)),
|
||||
("last_request_datetime", models.DateTimeField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteServerBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("full_name", models.TextField(default="")),
|
||||
(
|
||||
"remote_server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
("date_joined", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("last_login", models.DateTimeField(null=True)),
|
||||
("enable_maintenance_release_emails", models.BooleanField(default=True)),
|
||||
("enable_major_release_emails", models.BooleanField(default=True)),
|
||||
("tos_version", models.TextField(default="-1")),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("remote_server", "email")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("user_uuid", models.UUIDField()),
|
||||
("full_name", models.TextField(default="")),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("tos_version", models.TextField(default="-1")),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
("date_joined", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("last_login", models.DateTimeField(null=True)),
|
||||
("enable_maintenance_release_emails", models.BooleanField(default=True)),
|
||||
("enable_major_release_emails", models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("remote_realm", "user_uuid")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteInstallationCount",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("property", models.CharField(max_length=32)),
|
||||
("subgroup", models.CharField(max_length=16, null=True)),
|
||||
("end_time", models.DateTimeField()),
|
||||
("value", models.BigIntegerField()),
|
||||
("remote_id", models.IntegerField(null=True)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": set(),
|
||||
"indexes": [
|
||||
models.Index(
|
||||
condition=models.Q(("property", "mobile_pushes_forwarded::day")),
|
||||
fields=["server_id", "end_time"],
|
||||
name="zilencer_remoteinstallationcount_server_end_time_mobile_pushes_forwarded",
|
||||
)
|
||||
],
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("server", "property", "subgroup", "end_time"),
|
||||
name="unique_remote_installation_count",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("server", "property", "end_time"),
|
||||
name="unique_remote_installation_count_null_subgroup",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("remote_id__isnull", False)),
|
||||
fields=("server", "remote_id"),
|
||||
name="unique_remote_installation_count_server_id_remote_id",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmCount",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("property", models.CharField(max_length=32)),
|
||||
("subgroup", models.CharField(max_length=16, null=True)),
|
||||
("end_time", models.DateTimeField()),
|
||||
("value", models.BigIntegerField()),
|
||||
("realm_id", models.IntegerField(null=True)),
|
||||
("remote_id", models.IntegerField(null=True)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="zilencer.remoterealm",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": set(),
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["property", "end_time"],
|
||||
name="zilencer_remoterealmcount_property_end_time_506a0b38_idx",
|
||||
),
|
||||
models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server", "realm_id"],
|
||||
name="zilencer_remoterealmcount_server_realm",
|
||||
),
|
||||
models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server"],
|
||||
name="zilencer_remoterealmcount_server",
|
||||
),
|
||||
],
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("remote_id__isnull", False)),
|
||||
fields=("server", "remote_id"),
|
||||
name="unique_remote_realm_installation_count_server_id_remote_id",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("server", "realm_id", "property", "subgroup", "end_time"),
|
||||
name="unique_server_realm_installation_count",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("server", "realm_id", "property", "end_time"),
|
||||
name="unique_server_realm_installation_count_null_subgroup",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("remote_realm_id", "property", "subgroup", "end_time"),
|
||||
name="unique_remote_realm_installation_count",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("remote_realm_id", "property", "end_time"),
|
||||
name="unique_remote_realm_installation_count_null_subgroup",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PreregistrationRemoteRealmBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("user_uuid", models.UUIDField()),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("status", models.IntegerField(default=0)),
|
||||
("next_page", models.TextField(null=True)),
|
||||
("uri_scheme", models.TextField()),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoterealmbillinguser",
|
||||
),
|
||||
),
|
||||
("date_joined", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PreregistrationRemoteServerBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("status", models.IntegerField(default=0)),
|
||||
("next_page", models.TextField(null=True)),
|
||||
(
|
||||
"remote_server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoteserverbillinguser",
|
||||
),
|
||||
),
|
||||
("date_joined", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemotePushDeviceToken",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("user_id", models.BigIntegerField(null=True)),
|
||||
("kind", models.PositiveSmallIntegerField(choices=[(1, "apns"), (2, "gcm")])),
|
||||
("token", models.CharField(db_index=True, max_length=4096)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
("ios_app_id", models.TextField(null=True)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
("user_uuid", models.UUIDField(null=True)),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoterealm",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {
|
||||
("server", "user_id", "kind", "token"),
|
||||
("server", "user_uuid", "kind", "token"),
|
||||
},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("realm_id", models.IntegerField(blank=True, null=True)),
|
||||
("remote_id", models.IntegerField(null=True)),
|
||||
("event_time", models.DateTimeField(db_index=True)),
|
||||
("backfilled", models.BooleanField(default=False)),
|
||||
("event_type", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
(
|
||||
"extra_data",
|
||||
models.JSONField(
|
||||
default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||
),
|
||||
),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="zilencer.remoterealm",
|
||||
),
|
||||
),
|
||||
(
|
||||
"acting_remote_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoterealmbillinguser",
|
||||
),
|
||||
),
|
||||
(
|
||||
"acting_support_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
fields=("server", "remote_id"),
|
||||
name="zilencer_remoterealmauditlog_server_remote",
|
||||
)
|
||||
],
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["server", "realm_id", "remote_id"],
|
||||
name="zilencer_remoterealmauditlog_server_realm_remote",
|
||||
),
|
||||
models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server", "realm_id"],
|
||||
name="zilencer_remoterealmauditlog_server_realm",
|
||||
),
|
||||
models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server"],
|
||||
name="zilencer_remoterealmauditlog_server",
|
||||
),
|
||||
models.Index(
|
||||
condition=models.Q(
|
||||
("event_type__in", [101, 102, 103, 104, 105, 201, 202, 229])
|
||||
),
|
||||
fields=["remote_realm_id", "id"],
|
||||
name="zilencer_remoterealmauditlog_synced_billing_events",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteZulipServerAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("event_time", models.DateTimeField(db_index=True)),
|
||||
("backfilled", models.BooleanField(default=False)),
|
||||
("event_type", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
(
|
||||
"extra_data",
|
||||
models.JSONField(
|
||||
default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||
),
|
||||
),
|
||||
(
|
||||
"acting_remote_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoteserverbillinguser",
|
||||
),
|
||||
),
|
||||
(
|
||||
"acting_support_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
50
zilencer/migrations/0002_remote_zulip_server.py
Normal file
50
zilencer/migrations/0002_remote_zulip_server.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemotePushDeviceToken",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
serialize=False, auto_created=True, verbose_name="ID", primary_key=True
|
||||
),
|
||||
),
|
||||
("user_id", models.BigIntegerField()),
|
||||
("kind", models.PositiveSmallIntegerField(choices=[(1, "apns"), (2, "gcm")])),
|
||||
("token", models.CharField(unique=True, max_length=4096)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
("ios_app_id", models.TextField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteZulipServer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
serialize=False, auto_created=True, verbose_name="ID", primary_key=True
|
||||
),
|
||||
),
|
||||
("uuid", models.CharField(unique=True, max_length=36)),
|
||||
("api_key", models.CharField(max_length=64)),
|
||||
("hostname", models.CharField(unique=True, max_length=128)),
|
||||
("contact_email", models.EmailField(max_length=254, blank=True)),
|
||||
("last_updated", models.DateTimeField(verbose_name="last updated")),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="server",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.RemoteZulipServer"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 1.10.5 on 2017-05-16 00:03
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0002_remote_zulip_server"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_updated",
|
||||
field=models.DateTimeField(auto_now=True, verbose_name="last updated"),
|
||||
),
|
||||
]
|
||||
18
zilencer/migrations/0004_remove_deployment_model.py
Normal file
18
zilencer/migrations/0004_remove_deployment_model.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 1.11.5 on 2017-10-12 06:27
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0003_add_default_for_remotezulipserver_last_updated_field"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="deployment",
|
||||
name="realms",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Deployment",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 1.11.5 on 2017-10-19 04:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0004_remove_deployment_model"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="token",
|
||||
field=models.CharField(db_index=True, max_length=4096),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="user_id",
|
||||
field=models.BigIntegerField(db_index=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remotepushdevicetoken",
|
||||
unique_together={("server", "token")},
|
||||
),
|
||||
]
|
||||
32
zilencer/migrations/0006_customer.py
Normal file
32
zilencer/migrations/0006_customer.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 1.11.6 on 2018-01-13 11:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0005_remotepushdevicetoken_fix_uniqueness"),
|
||||
("zerver", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Customer",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("stripe_customer_id", models.CharField(max_length=255, unique=True)),
|
||||
(
|
||||
"realm",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zerver.Realm"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
zilencer/migrations/0007_remotezulipserver_fix_uniqueness.py
Normal file
17
zilencer/migrations/0007_remotezulipserver_fix_uniqueness.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 1.11.11 on 2018-04-30 06:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0006_customer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserver",
|
||||
name="hostname",
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
]
|
||||
22
zilencer/migrations/0008_customer_billing_user.py
Normal file
22
zilencer/migrations/0008_customer_billing_user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 1.11.11 on 2018-04-12 01:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("zilencer", "0007_remotezulipserver_fix_uniqueness"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="customer",
|
||||
name="billing_user",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
||||
25
zilencer/migrations/0009_plan.py
Normal file
25
zilencer/migrations/0009_plan.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 1.11.11 on 2018-04-12 01:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0008_customer_billing_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Plan",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("nickname", models.CharField(max_length=40, unique=True)),
|
||||
("stripe_plan_id", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
39
zilencer/migrations/0010_billingprocessor.py
Normal file
39
zilencer/migrations/0010_billingprocessor.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 1.11.14 on 2018-08-13 23:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0182_set_initial_value_is_private_flag"),
|
||||
("zilencer", "0009_plan"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="BillingProcessor",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("state", models.CharField(max_length=20)),
|
||||
("last_modified", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"log_row",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zerver.RealmAuditLog"
|
||||
),
|
||||
),
|
||||
(
|
||||
"realm",
|
||||
models.OneToOneField(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.Realm"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 1.11.14 on 2018-08-14 01:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0010_billingprocessor"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="customer",
|
||||
name="has_billing_relationship",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
25
zilencer/migrations/0012_coupon.py
Normal file
25
zilencer/migrations/0012_coupon.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 1.11.14 on 2018-08-23 05:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0011_customer_has_billing_relationship"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Coupon",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("percent_off", models.SmallIntegerField(unique=True)),
|
||||
("stripe_coupon_id", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
16
zilencer/migrations/0013_remove_customer_billing_user.py
Normal file
16
zilencer/migrations/0013_remove_customer_billing_user.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 1.11.14 on 2018-08-22 06:31
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0012_coupon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="customer",
|
||||
name="billing_user",
|
||||
),
|
||||
]
|
||||
16
zilencer/migrations/0014_cleanup_pushdevicetoken.py
Normal file
16
zilencer/migrations/0014_cleanup_pushdevicetoken.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 1.11.14 on 2018-10-10 22:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0013_remove_customer_billing_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remotepushdevicetoken",
|
||||
unique_together={("server", "user_id", "kind", "token")},
|
||||
),
|
||||
]
|
||||
36
zilencer/migrations/0015_delete_billing.py
Normal file
36
zilencer/migrations/0015_delete_billing.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 1.11.14 on 2018-09-25 12:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0014_cleanup_pushdevicetoken"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="billingprocessor",
|
||||
name="log_row",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="billingprocessor",
|
||||
name="realm",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Coupon",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="customer",
|
||||
name="realm",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Plan",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="BillingProcessor",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Customer",
|
||||
),
|
||||
]
|
||||
73
zilencer/migrations/0016_remote_counts.py
Normal file
73
zilencer/migrations/0016_remote_counts.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Generated by Django 1.11.18 on 2019-02-02 06:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0015_delete_billing"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteInstallationCount",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("property", models.CharField(max_length=32)),
|
||||
("subgroup", models.CharField(max_length=16, null=True)),
|
||||
("end_time", models.DateTimeField()),
|
||||
("value", models.BigIntegerField()),
|
||||
("remote_id", models.IntegerField(db_index=True)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.RemoteZulipServer"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmCount",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("property", models.CharField(max_length=32)),
|
||||
("subgroup", models.CharField(max_length=16, null=True)),
|
||||
("end_time", models.DateTimeField()),
|
||||
("value", models.BigIntegerField()),
|
||||
("realm_id", models.IntegerField(db_index=True)),
|
||||
("remote_id", models.IntegerField(db_index=True)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.RemoteZulipServer"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remoterealmcount",
|
||||
unique_together={("server", "realm_id", "property", "subgroup", "end_time")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="remoterealmcount",
|
||||
index=models.Index(
|
||||
fields=["property", "end_time"],
|
||||
name="zilencer_remoterealmcount_property_end_time_506a0b38_idx",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remoteinstallationcount",
|
||||
unique_together={("server", "property", "subgroup", "end_time")},
|
||||
),
|
||||
]
|
||||
26
zilencer/migrations/0017_installationcount_indexes.py
Normal file
26
zilencer/migrations/0017_installationcount_indexes.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 1.11.20 on 2019-04-23 20:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0016_remote_counts"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="remoteinstallationcount",
|
||||
index=models.Index(
|
||||
fields=["server", "remote_id"],
|
||||
name="zilencer_remoteinstallat_server_id_remote_id_f72e4c30_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="remoterealmcount",
|
||||
index=models.Index(
|
||||
fields=["server", "remote_id"],
|
||||
name="zilencer_remoterealmcount_server_id_remote_id_de1573d8_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
36
zilencer/migrations/0018_remoterealmauditlog.py
Normal file
36
zilencer/migrations/0018_remoterealmauditlog.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 1.11.24 on 2019-10-03 00:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0017_installationcount_indexes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("realm_id", models.IntegerField(db_index=True)),
|
||||
("remote_id", models.IntegerField(db_index=True)),
|
||||
("event_time", models.DateTimeField(db_index=True)),
|
||||
("backfilled", models.BooleanField(default=False)),
|
||||
("extra_data", models.TextField(null=True)),
|
||||
("event_type", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.RemoteZulipServer"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
zilencer/migrations/0019_remotezulipserver_plan_type.py
Normal file
17
zilencer/migrations/0019_remotezulipserver_plan_type.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.9 on 2021-12-01 16:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0018_remoterealmauditlog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="plan_type",
|
||||
field=models.PositiveSmallIntegerField(default=1),
|
||||
),
|
||||
]
|
||||
37
zilencer/migrations/0020_remotezulipserverauditlog.py
Normal file
37
zilencer/migrations/0020_remotezulipserverauditlog.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.2.9 on 2021-12-06 18:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0019_remotezulipserver_plan_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteZulipServerAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("event_time", models.DateTimeField(db_index=True)),
|
||||
("backfilled", models.BooleanField(default=False)),
|
||||
("extra_data", models.TextField(null=True)),
|
||||
("event_type", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
17
zilencer/migrations/0021_alter_remotezulipserver_uuid.py
Normal file
17
zilencer/migrations/0021_alter_remotezulipserver_uuid.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.9 on 2021-12-22 10:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0020_remotezulipserverauditlog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserver",
|
||||
name="uuid",
|
||||
field=models.UUIDField(unique=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
|
||||
|
||||
def backfill_remote_zulip_server_creation_log_events(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
RemoteZulipServer = apps.get_model("zilencer", "RemoteZulipServer")
|
||||
RemoteZulipServerAuditLog = apps.get_model("zilencer", "RemoteZulipServerAuditLog")
|
||||
RemoteZulipServerAuditLog.REMOTE_SERVER_CREATED = 10215
|
||||
|
||||
objects_to_create = []
|
||||
for remote_server in RemoteZulipServer.objects.all().iterator():
|
||||
entry = RemoteZulipServerAuditLog(
|
||||
server=remote_server,
|
||||
event_type=RemoteZulipServerAuditLog.REMOTE_SERVER_CREATED,
|
||||
event_time=remote_server.last_updated,
|
||||
backfilled=True,
|
||||
)
|
||||
objects_to_create.append(entry)
|
||||
RemoteZulipServerAuditLog.objects.bulk_create(objects_to_create)
|
||||
|
||||
|
||||
def reverse_code(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
||||
RemoteZulipServerAuditLog = apps.get_model("zilencer", "RemoteZulipServerAuditLog")
|
||||
RemoteZulipServerAuditLog.REMOTE_SERVER_CREATED = 10215
|
||||
RemoteZulipServerAuditLog.objects.filter(
|
||||
event_type=RemoteZulipServerAuditLog.REMOTE_SERVER_CREATED, backfilled=True
|
||||
).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0021_alter_remotezulipserver_uuid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
backfill_remote_zulip_server_creation_log_events,
|
||||
reverse_code=reverse_code,
|
||||
elidable=True,
|
||||
)
|
||||
]
|
||||
17
zilencer/migrations/0023_remotezulipserver_deactivated.py
Normal file
17
zilencer/migrations/0023_remotezulipserver_deactivated.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.9 on 2021-12-15 17:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0022_remotezulipserver_create_audit_log_backfill"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="deactivated",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
29
zilencer/migrations/0024_remotepushdevicetoken_user_uuid.py
Normal file
29
zilencer/migrations/0024_remotepushdevicetoken_user_uuid.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2.9 on 2021-12-27 21:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0023_remotezulipserver_deactivated"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="user_id",
|
||||
field=models.BigIntegerField(db_index=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="user_uuid",
|
||||
field=models.UUIDField(null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remotepushdevicetoken",
|
||||
unique_together={
|
||||
("server", "user_uuid", "kind", "token"),
|
||||
("server", "user_id", "kind", "token"),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-10 12:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0024_remotepushdevicetoken_user_uuid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="user_id",
|
||||
field=models.BigIntegerField(null=True),
|
||||
),
|
||||
]
|
||||
27
zilencer/migrations/0026_auditlog_models_extra_data_json.py
Normal file
27
zilencer/migrations/0026_auditlog_models_extra_data_json.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.0.7 on 2022-09-30 20:25
|
||||
|
||||
import django
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0025_alter_remotepushdevicetoken_user_id_drop_index"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="extra_data_json",
|
||||
field=models.JSONField(
|
||||
default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
name="extra_data_json",
|
||||
field=models.JSONField(
|
||||
default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,152 @@
|
||||
# Generated by Django 4.0.7 on 2022-09-30 20:30
|
||||
|
||||
import ast
|
||||
from collections.abc import Callable
|
||||
|
||||
import orjson
|
||||
from django.db import migrations, transaction
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.db.models import F, JSONField, Model
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
# This migration is mostly the same as
|
||||
# backfill_realmauditlog_extradata_to_json_field in zerver.
|
||||
|
||||
OLD_VALUE = "1"
|
||||
NEW_VALUE = "2"
|
||||
USER_FULL_NAME_CHANGED = 124
|
||||
REALM_DISCOUNT_CHANGED = 209
|
||||
BATCH_SIZE = 5000
|
||||
|
||||
OVERWRITE_TEMPLATE = """Audit log entry with id {id} has extra_data_json been inconsistently overwritten.
|
||||
The old value is:
|
||||
{old_value}
|
||||
The new value is:
|
||||
{new_value}
|
||||
"""
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def do_bulk_backfill_extra_data(
|
||||
audit_log_model: type[Model], id_lower_bound: int, id_upper_bound: int
|
||||
) -> None:
|
||||
inconsistent_extra_data_json: list[tuple[int, str, object, object]] = []
|
||||
# A dict converted with str() will start with a open bracket followed by a
|
||||
# single quote, as opposed to a JSON-encoded value, which will use a
|
||||
# _double_ quote. We use this to filter out those entries with malformed
|
||||
# extra_data to be handled later. This should only update rows with
|
||||
# extra_data populated with orjson.dumps.
|
||||
|
||||
# The first query below checks for entries that would have extra_data_json
|
||||
# being overwritten by the migration with a value inconsistent with its
|
||||
# previous value.
|
||||
inconsistent_extra_data_json.extend(
|
||||
audit_log_model._default_manager.filter(
|
||||
extra_data__isnull=False, id__range=(id_lower_bound, id_upper_bound)
|
||||
)
|
||||
.annotate(new_extra_data_json=Cast("extra_data", output_field=JSONField()))
|
||||
.exclude(extra_data__startswith="{'")
|
||||
.exclude(extra_data_json={})
|
||||
.exclude(extra_data_json=F("new_extra_data_json"))
|
||||
.values_list("id", "extra_data", "extra_data_json", "new_extra_data_json")
|
||||
)
|
||||
(
|
||||
audit_log_model._default_manager.filter(
|
||||
extra_data__isnull=False,
|
||||
id__range=(id_lower_bound, id_upper_bound),
|
||||
extra_data_json__inconsistent_old_extra_data__isnull=True,
|
||||
)
|
||||
.exclude(extra_data__startswith="{'")
|
||||
.update(extra_data_json=Cast("extra_data", output_field=JSONField()))
|
||||
)
|
||||
|
||||
python_valued_audit_log_entries = audit_log_model._default_manager.filter(
|
||||
extra_data__startswith="{'",
|
||||
id__range=(id_lower_bound, id_upper_bound),
|
||||
extra_data_json__inconsistent_old_extra_data__isnull=True,
|
||||
)
|
||||
for audit_log_entry in python_valued_audit_log_entries:
|
||||
# extra_data for entries that store dict stringified with builtins.str()
|
||||
# are converted back with ast.literal_eval for safety and efficiency.
|
||||
old_value = audit_log_entry.extra_data_json # type: ignore[attr-defined] # The migration cannot depend on zerver.models, which contains the real type of the RealmAuditLog model, so it cannot be properly typed.
|
||||
new_value = ast.literal_eval(audit_log_entry.extra_data) # type: ignore[attr-defined] # Explained above.
|
||||
if old_value not in ({}, new_value):
|
||||
inconsistent_extra_data_json.append(
|
||||
(audit_log_entry.id, audit_log_entry.extra_data, old_value, new_value) # type: ignore[attr-defined] # Explained above.
|
||||
)
|
||||
audit_log_entry.extra_data_json = new_value # type: ignore[attr-defined] # Explained above.
|
||||
audit_log_model._default_manager.bulk_update(
|
||||
python_valued_audit_log_entries, fields=["extra_data_json"]
|
||||
)
|
||||
|
||||
if inconsistent_extra_data_json:
|
||||
audit_log_entries = []
|
||||
for (
|
||||
audit_log_entry_id,
|
||||
old_extra_data,
|
||||
old_extra_data_json,
|
||||
new_extra_data_json,
|
||||
) in inconsistent_extra_data_json:
|
||||
audit_log_entry = audit_log_model._default_manager.get(id=audit_log_entry_id)
|
||||
assert isinstance(old_extra_data_json, dict)
|
||||
if "inconsistent_old_extra_data" in old_extra_data_json:
|
||||
# Skip entries that have been backfilled and detected as
|
||||
# anomalies before.
|
||||
continue
|
||||
assert isinstance(new_extra_data_json, dict)
|
||||
audit_log_entry.extra_data_json = { # type: ignore[attr-defined] # Explained above.
|
||||
**new_extra_data_json,
|
||||
"inconsistent_old_extra_data": old_extra_data,
|
||||
"inconsistent_old_extra_data_json": old_extra_data_json,
|
||||
}
|
||||
audit_log_entries.append(audit_log_entry)
|
||||
print(
|
||||
OVERWRITE_TEMPLATE.format(
|
||||
id=audit_log_entry_id,
|
||||
old_value=orjson.dumps(old_extra_data_json).decode(),
|
||||
new_value=orjson.dumps(new_extra_data_json).decode(),
|
||||
)
|
||||
)
|
||||
audit_log_model._default_manager.bulk_update(audit_log_entries, fields=["extra_data_json"])
|
||||
|
||||
|
||||
def backfill_extra_data(model_name: str) -> Callable[[StateApps, BaseDatabaseSchemaEditor], None]:
|
||||
def inner(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
||||
audit_log_model = apps.get_model("zilencer", model_name)
|
||||
if not audit_log_model.objects.filter(extra_data__isnull=False).exists():
|
||||
return
|
||||
|
||||
audit_log_entries = audit_log_model.objects.filter(extra_data__isnull=False)
|
||||
id_lower_bound = audit_log_entries.earliest("id").id
|
||||
id_upper_bound = audit_log_entries.latest("id").id
|
||||
while id_lower_bound <= id_upper_bound:
|
||||
do_bulk_backfill_extra_data(
|
||||
audit_log_model, id_lower_bound, min(id_lower_bound + BATCH_SIZE, id_upper_bound)
|
||||
)
|
||||
id_lower_bound += BATCH_SIZE + 1
|
||||
|
||||
do_bulk_backfill_extra_data(audit_log_model, id_lower_bound, id_upper_bound)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0026_auditlog_models_extra_data_json"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
backfill_extra_data("RemoteRealmAuditLog"),
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
migrations.RunPython(
|
||||
backfill_extra_data("RemoteZulipServerAuditLog"),
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.0.7 on 2022-10-02 17:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0027_backfill_remote_realmauditlog_extradata_to_json_field"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="extra_data",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
name="extra_data",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="remoterealmauditlog",
|
||||
old_name="extra_data_json",
|
||||
new_name="extra_data",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
old_name="extra_data_json",
|
||||
new_name="extra_data",
|
||||
),
|
||||
]
|
||||
45
zilencer/migrations/0029_update_remoterealm_indexes.py
Normal file
45
zilencer/migrations/0029_update_remoterealm_indexes.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-13 05:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0028_rename_extradatajson_remoteauditlog_extra_data"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="realm_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmcount",
|
||||
name="realm_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmcount",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmauditlog",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("server", "remote_id"), name="zilencer_remoterealmauditlog_server_remote"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="remoterealmauditlog",
|
||||
index=models.Index(
|
||||
fields=["server", "realm_id", "remote_id"],
|
||||
name="zilencer_remoterealmauditlog_server_realm_remote",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-20 00:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0029_update_remoterealm_indexes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoteinstallationcount",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-23 20:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0030_alter_remoteinstallationcount_remote_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoteinstallationcount",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmcount",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""
|
||||
Previous versions of zilencer dropped the ios_app_id on the floor
|
||||
when registering a push token, and then effectively assumed
|
||||
the value was "org.zulip.Zulip".
|
||||
|
||||
We're going to start actually using that parameter. To preserve
|
||||
the existing behavior for existing tokens, backfill the value.
|
||||
"""
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0031_alter_remoteinstallationcount_remote_id_and_more"),
|
||||
]
|
||||
|
||||
FORMERLY_IMPLICIT_IOS_APP_ID = "org.zulip.Zulip"
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
sql=[
|
||||
(
|
||||
"""
|
||||
UPDATE zilencer_remotepushdevicetoken
|
||||
SET ios_app_id = %s
|
||||
WHERE kind = 1 AND ios_app_id IS NULL
|
||||
""",
|
||||
[FORMERLY_IMPLICIT_IOS_APP_ID],
|
||||
)
|
||||
],
|
||||
# The updated table is still valid with the old schema;
|
||||
# so to reverse, a no-op suffices.
|
||||
reverse_sql=[],
|
||||
elidable=True,
|
||||
),
|
||||
]
|
||||
38
zilencer/migrations/0033_remoterealm.py
Normal file
38
zilencer/migrations/0033_remoterealm.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-02 23:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0032_remotepushdevicetoken_backfill_ios_app_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealm",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(unique=True)),
|
||||
("uuid_owner_secret", models.TextField()),
|
||||
("host", models.TextField()),
|
||||
("last_updated", models.DateTimeField(auto_now=True, verbose_name="last updated")),
|
||||
("registration_deactivated", models.BooleanField(default=False)),
|
||||
("realm_deactivated", models.BooleanField(default=False)),
|
||||
("realm_date_created", models.DateTimeField()),
|
||||
("plan_type", models.PositiveSmallIntegerField(db_index=True, default=1)),
|
||||
(
|
||||
"server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-08 23:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0033_remoterealm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="remote_realm",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="remote_id",
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-09 17:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0034_remoterealmauditlog_remote_realm_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealmcount",
|
||||
name="remote_realm",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmcount",
|
||||
name="realm_id",
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
]
|
||||
17
zilencer/migrations/0036_remotezulipserver_last_version.py
Normal file
17
zilencer/migrations/0036_remotezulipserver_last_version.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-17 20:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0035_remoterealmcount_remote_realm_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_version",
|
||||
field=models.CharField(max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.db.models import Count, Min
|
||||
|
||||
|
||||
def clear_duplicate_counts(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
||||
"""Clean up duplicated RemoteRealmCount and RemoteInstallationCount rows.
|
||||
|
||||
This is the equivalent of analytics' 0015_clear_duplicate_counts
|
||||
migration -- but it also has additional duplicates if there are
|
||||
multiple servers submitting information with the same UUID.
|
||||
|
||||
We drop the behaviour of rolling up and updating the value to the
|
||||
sum, since the active_users_log:is_bot:day field has a subgroup
|
||||
(and is thus not affected by the bug), and the few cases for
|
||||
`invites_sent::day` seem more likely to be re-submissions of the
|
||||
same data, not duplicates to roll up.
|
||||
|
||||
We must do this step before switching the non-unique indexes to be
|
||||
unique, as there are currently violations.
|
||||
|
||||
"""
|
||||
count_tables = dict(
|
||||
realm=apps.get_model("zilencer", "RemoteRealmCount"),
|
||||
installation=apps.get_model("zilencer", "RemoteInstallationCount"),
|
||||
)
|
||||
|
||||
for name, count_table in count_tables.items():
|
||||
value = ["realm_id", "server_id", "property", "end_time"]
|
||||
if name == "installation":
|
||||
value = ["server_id", "property", "end_time"]
|
||||
duplicated_rows = (
|
||||
count_table.objects.filter(subgroup=None)
|
||||
.values(*value)
|
||||
.annotate(Count("id"), Min("id"))
|
||||
.filter(id__count__gt=1)
|
||||
)
|
||||
|
||||
for duplicated_row in duplicated_rows:
|
||||
duplicated_row.pop("id__count")
|
||||
first_id = duplicated_row.pop("id__min")
|
||||
count_table.objects.filter(**duplicated_row, id__gt=first_id).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0036_remotezulipserver_last_version"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
clear_duplicate_counts, reverse_code=migrations.RunPython.noop, elidable=True
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoteinstallationcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("server", "property", "subgroup", "end_time"),
|
||||
name="unique_remote_installation_count",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoteinstallationcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("server", "property", "end_time"),
|
||||
name="unique_remote_installation_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("server", "realm_id", "property", "subgroup", "end_time"),
|
||||
name="unique_remote_realm_installation_count",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("server", "realm_id", "property", "end_time"),
|
||||
name="unique_remote_realm_installation_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remoteinstallationcount",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remoterealmcount",
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
34
zilencer/migrations/0038_unique_server_remote_id.py
Normal file
34
zilencer/migrations/0038_unique_server_remote_id.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0037_alter_remoteinstallationcount_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="remoteinstallationcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("remote_id__isnull", False)),
|
||||
fields=("server", "remote_id"),
|
||||
name="unique_remote_installation_count_server_id_remote_id",
|
||||
),
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="remoteinstallationcount",
|
||||
name="zilencer_remoteinstallat_server_id_remote_id_f72e4c30_idx",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("remote_id__isnull", False)),
|
||||
fields=("server", "remote_id"),
|
||||
name="unique_remote_realm_installation_count_server_id_remote_id",
|
||||
),
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="remoterealmcount",
|
||||
name="zilencer_remoterealmcount_server_id_remote_id_de1573d8_idx",
|
||||
),
|
||||
]
|
||||
34
zilencer/migrations/0039_remoterealm_org_type.py
Normal file
34
zilencer/migrations/0039_remoterealm_org_type.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-27 00:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0038_unique_server_remote_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="org_type",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, "Unspecified"),
|
||||
(10, "Business"),
|
||||
(20, "Open-source project"),
|
||||
(30, "Education (non-profit)"),
|
||||
(35, "Education (for-profit)"),
|
||||
(40, "Research"),
|
||||
(50, "Event or conference"),
|
||||
(60, "Non-profit (registered)"),
|
||||
(70, "Government"),
|
||||
(80, "Political group"),
|
||||
(90, "Community"),
|
||||
(100, "Personal"),
|
||||
(1000, "Other"),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-29 22:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0039_remoterealm_org_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="authentication_methods",
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="name",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
]
|
||||
34
zilencer/migrations/0041_remotezulipserver_org_type.py
Normal file
34
zilencer/migrations/0041_remotezulipserver_org_type.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-29 05:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0040_remoterealm_authentication_methods_remoterealm_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="org_type",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, "Unspecified"),
|
||||
(10, "Business"),
|
||||
(20, "Open-source project"),
|
||||
(30, "Education (non-profit)"),
|
||||
(35, "Education (for-profit)"),
|
||||
(40, "Research"),
|
||||
(50, "Event or conference"),
|
||||
(60, "Non-profit (registered)"),
|
||||
(70, "Government"),
|
||||
(80, "Political group"),
|
||||
(90, "Community"),
|
||||
(100, "Personal"),
|
||||
(1000, "Other"),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-30 11:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0041_remotezulipserver_org_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="realm_id",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-01 15:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0042_alter_remoterealmauditlog_realm_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="remote_realm",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
]
|
||||
34
zilencer/migrations/0044_remoterealmbillinguser.py
Normal file
34
zilencer/migrations/0044_remoterealmbillinguser.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-05 01:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0043_remotepushdevicetoken_remote_realm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemoteRealmBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("user_uuid", models.UUIDField()),
|
||||
("full_name", models.TextField(default="")),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("tos_version", models.TextField(default="-1")),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-05 19:33
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0044_remoterealmbillinguser"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name="remoterealmauditlog",
|
||||
index=models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server", "realm_id"],
|
||||
name="zilencer_remoterealmauditlog_server_realm",
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="remoterealmauditlog",
|
||||
index=models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server"],
|
||||
name="zilencer_remoterealmauditlog_server",
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="remoterealmcount",
|
||||
index=models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server", "realm_id"],
|
||||
name="zilencer_remoterealmcount_server_realm",
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="remoterealmcount",
|
||||
index=models.Index(
|
||||
condition=models.Q(("remote_realm__isnull", True)),
|
||||
fields=["server"],
|
||||
name="zilencer_remoterealmcount_server",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-06 18:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0045_remoterealmauditlog_zilencer_remoterealmauditlog_server_realm_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_audit_log_update",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-08 19:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0046_remotezulipserver_last_audit_log_update"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PreregistrationRemoteServerBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("status", models.IntegerField(default=0)),
|
||||
("next_page", models.TextField(null=True)),
|
||||
(
|
||||
"remote_server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RemoteServerBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("full_name", models.TextField(default="")),
|
||||
(
|
||||
"remote_server",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remotezulipserver"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("remote_server", "email")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-09 18:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0047_preregistrationremoteserverbillinguser_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_api_feature_level",
|
||||
field=models.PositiveIntegerField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-10 01:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0048_remotezulipserver_last_api_feature_level"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remoterealmbillinguser",
|
||||
unique_together={("remote_realm", "user_uuid")},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PreregistrationRemoteRealmBillingUser",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("user_uuid", models.UUIDField()),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("status", models.IntegerField(default=0)),
|
||||
("next_page", models.TextField(null=True)),
|
||||
("uri_scheme", models.TextField()),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-11 12:55
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0049_alter_remoterealmbillinguser_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="preregistrationremoterealmbillinguser",
|
||||
name="created_user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoterealmbillinguser",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="preregistrationremoterealmbillinguser",
|
||||
name="date_joined",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="preregistrationremoteserverbillinguser",
|
||||
name="created_user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoteserverbillinguser",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="preregistrationremoteserverbillinguser",
|
||||
name="date_joined",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="date_joined",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="last_login",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="date_joined",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="last_login",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
17
zilencer/migrations/0051_remoterealm_is_system_bot_realm.py
Normal file
17
zilencer/migrations/0051_remoterealm_is_system_bot_realm.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-11 20:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0050_preregistrationremoterealmbillinguser_created_user_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="is_system_bot_realm",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-13 23:20
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
|
||||
PLAN_TYPE_SELF_HOSTED = 1
|
||||
PLAN_TYPE_SELF_MANAGED = 100
|
||||
|
||||
|
||||
def renumber_plan_types(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
||||
RemoteZulipServer = apps.get_model("zilencer", "RemoteZulipServer")
|
||||
RemoteRealm = apps.get_model("zilencer", "RemoteRealm")
|
||||
RemoteRealm.objects.filter(plan_type=PLAN_TYPE_SELF_HOSTED).update(
|
||||
plan_type=PLAN_TYPE_SELF_MANAGED
|
||||
)
|
||||
RemoteZulipServer.objects.filter(plan_type=PLAN_TYPE_SELF_HOSTED).update(
|
||||
plan_type=PLAN_TYPE_SELF_MANAGED
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0051_remoterealm_is_system_bot_realm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoterealm",
|
||||
name="plan_type",
|
||||
field=models.PositiveSmallIntegerField(db_index=True, default=PLAN_TYPE_SELF_MANAGED),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserver",
|
||||
name="plan_type",
|
||||
field=models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED),
|
||||
),
|
||||
migrations.RunPython(
|
||||
renumber_plan_types, reverse_code=migrations.RunPython.noop, elidable=True
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-14 14:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("zilencer", "0052_alter_remoterealm_plan_type_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="acting_remote_user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoterealmbillinguser",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="acting_support_user",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
name="acting_remote_user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="zilencer.remoteserverbillinguser",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
name="acting_support_user",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-14 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0053_remoterealmauditlog_acting_remote_user_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="enable_maintenance_release_emails",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="enable_major_release_emails",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="enable_maintenance_release_emails",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="enable_major_release_emails",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-14 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="tos_version",
|
||||
field=models.TextField(default="-1"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-15 13:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0055_remoteserverbillinguser_tos_version"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="realm_locally_deleted",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-25 00:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0056_remoterealm_realm_locally_deleted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remoterealm",
|
||||
name="last_request_datetime",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_request_datetime",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-01 13:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0057_remoterealm_last_request_timestamp_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="remoteinstallationcount",
|
||||
index=models.Index(
|
||||
condition=models.Q(("property", "mobile_pushes_forwarded::day")),
|
||||
fields=["server_id", "end_time"],
|
||||
name="zilencer_remoteinstallationcount_server_end_time_mobile_pushes_forwarded",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-20 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0058_remoteinstallationcount_add_mobile_pushes_forwarded_index"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="remoterealmauditlog",
|
||||
index=models.Index(
|
||||
condition=models.Q(("event_type__in", [101, 102, 103, 104, 105, 201, 202, 229])),
|
||||
fields=["remote_realm_id", "id"],
|
||||
name="zilencer_remoterealmauditlog_synced_billing_events",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
# Generated by Django 5.0.5 on 2024-05-06 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0059_remoterealmauditlog_add_synced_billing_event_type_index"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
# Django does not provide a RenameConstraint operation.
|
||||
# "Constraints" are created as indexes in PostgreSQL, so
|
||||
# we rename the underlying indexes.
|
||||
database_operations=[
|
||||
migrations.RunSQL(
|
||||
sql=(
|
||||
"ALTER INDEX unique_remote_realm_installation_count "
|
||||
"RENAME TO unique_server_realm_installation_count"
|
||||
),
|
||||
reverse_sql=(
|
||||
"ALTER INDEX unique_server_realm_installation_count "
|
||||
"RENAME TO unique_remote_realm_installation_count"
|
||||
),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql=(
|
||||
"ALTER INDEX unique_remote_realm_installation_count_null_subgroup "
|
||||
"RENAME TO unique_server_realm_installation_count_null_subgroup"
|
||||
),
|
||||
reverse_sql=(
|
||||
"ALTER INDEX unique_server_realm_installation_count_null_subgroup "
|
||||
"RENAME TO unique_remote_realm_installation_count_null_subgroup"
|
||||
),
|
||||
),
|
||||
],
|
||||
state_operations=[
|
||||
migrations.RemoveConstraint(
|
||||
model_name="remoterealmcount",
|
||||
name="unique_remote_realm_installation_count",
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="remoterealmcount",
|
||||
name="unique_remote_realm_installation_count_null_subgroup",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("server", "realm_id", "property", "subgroup", "end_time"),
|
||||
name="unique_server_realm_installation_count",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("server", "realm_id", "property", "end_time"),
|
||||
name="unique_server_realm_installation_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", False)),
|
||||
fields=("remote_realm_id", "property", "subgroup", "end_time"),
|
||||
name="unique_remote_realm_installation_count",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remoterealmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("subgroup__isnull", True)),
|
||||
fields=("remote_realm_id", "property", "end_time"),
|
||||
name="unique_remote_realm_installation_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
]
|
||||
30
zilencer/migrations/0061_clean_count_tables.py
Normal file
30
zilencer/migrations/0061_clean_count_tables.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.db import migrations
|
||||
|
||||
# This matches analytics.lib.counts.LOGGING_COUNT_STAT_PROPERTIES_NOT_SENT_TO_BOUNCER
|
||||
IGNORED = (
|
||||
"invites_sent::day",
|
||||
"mobile_pushes_sent::day",
|
||||
"active_users_log:is_bot:day",
|
||||
"active_users:is_bot:day",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
elidable = True
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"zilencer",
|
||||
"0060_remove_remoterealmcount_unique_remote_realm_installation_count_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
[
|
||||
("DELETE FROM zilencer_remoterealmcount WHERE property IN %s", (IGNORED,)),
|
||||
("DELETE FROM zilencer_remoteinstallationcount WHERE property IN %s", (IGNORED,)),
|
||||
],
|
||||
elidable=True,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0061_clean_count_tables"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remoteinstallationcount",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmcount",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
||||
75
zilencer/migrations/0063_convert_ids_to_bigints.py
Normal file
75
zilencer/migrations/0063_convert_ids_to_bigints.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0062_alter_remoteinstallationcount_id_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="preregistrationremoterealmbillinguser",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="preregistrationremoteserverbillinguser",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevicetoken",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealm",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmauditlog",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoterealmbillinguser",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remoteserverbillinguser",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserver",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="remotezulipserverauditlog",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-23 00:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0063_convert_ids_to_bigints"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="remotezulipserver",
|
||||
name="last_merge_base",
|
||||
field=models.CharField(max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
47
zilencer/migrations/0065_remotepushdevice.py
Normal file
47
zilencer/migrations/0065_remotepushdevice.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.2.3 on 2025-06-24 07:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0725_realmuserdefault_web_left_sidebar_unreads_count_summary_and_more"),
|
||||
("zilencer", "0001_squashed_0064_remotezulipserver_last_merge_base"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RemotePushDevice",
|
||||
fields=[
|
||||
("device_id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||
("token", models.CharField(max_length=4096)),
|
||||
("token_kind", models.PositiveSmallIntegerField(choices=[(1, "APNs"), (2, "FCM")])),
|
||||
("push_account_id", models.BigIntegerField()),
|
||||
("expired_time", models.DateTimeField(null=True)),
|
||||
("ios_app_id", models.TextField(null=True)),
|
||||
(
|
||||
"realm",
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
|
||||
),
|
||||
),
|
||||
(
|
||||
"remote_realm",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="zilencer.remoterealm",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
fields=("push_account_id", "token"),
|
||||
name="unique_remote_push_device_push_account_id_token",
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-09 13:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0065_remotepushdevice"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="remotepushdevice",
|
||||
name="token_kind",
|
||||
field=models.CharField(choices=[("apns", "APNs"), ("fcm", "FCM")], max_length=4),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,141 @@
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0066_alter_remotepushdevice_token_kind"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# This parallels zerver/migrations/0740 but must account for
|
||||
# the user_id / user_uuid split.
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
WITH dups AS (
|
||||
SELECT server_id, user_id, kind, LOWER(token) AS token, MAX(last_updated) AS max_last_updated
|
||||
FROM zilencer_remotepushdevicetoken
|
||||
WHERE kind = 1
|
||||
AND user_uuid IS NULL
|
||||
GROUP BY server_id, user_id, kind, LOWER(token)
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
UPDATE zilencer_remotepushdevicetoken
|
||||
SET last_updated = dups.max_last_updated
|
||||
FROM dups
|
||||
WHERE zilencer_remotepushdevicetoken.server_id = dups.server_id
|
||||
AND zilencer_remotepushdevicetoken.user_id = dups.user_id
|
||||
AND zilencer_remotepushdevicetoken.user_uuid IS NULL
|
||||
AND zilencer_remotepushdevicetoken.kind = dups.kind
|
||||
AND LOWER(zilencer_remotepushdevicetoken.token) = dups.token
|
||||
"""
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
WITH dups AS (
|
||||
SELECT server_id, user_id, kind, LOWER(token) AS token, MIN(id) AS min_id
|
||||
FROM zilencer_remotepushdevicetoken
|
||||
WHERE kind = 1
|
||||
AND user_uuid IS NULL
|
||||
GROUP BY server_id, user_id, kind, LOWER(token)
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
DELETE FROM zilencer_remotepushdevicetoken
|
||||
USING dups
|
||||
WHERE zilencer_remotepushdevicetoken.server_id = dups.server_id
|
||||
AND zilencer_remotepushdevicetoken.user_id = dups.user_id
|
||||
AND zilencer_remotepushdevicetoken.user_uuid IS NULL
|
||||
AND zilencer_remotepushdevicetoken.kind = dups.kind
|
||||
AND LOWER(zilencer_remotepushdevicetoken.token) = dups.token
|
||||
AND zilencer_remotepushdevicetoken.id != dups.min_id
|
||||
"""
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remotepushdevicetoken",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("server_id"),
|
||||
models.F("user_id"),
|
||||
models.F("kind"),
|
||||
django.db.models.functions.text.Lower(models.F("token")),
|
||||
condition=models.Q(("kind", 1)),
|
||||
name="zilencer_remotepushdevicetoken_apns_server_user_id_kind_token",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remotepushdevicetoken",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("server_id"),
|
||||
models.F("user_id"),
|
||||
models.F("kind"),
|
||||
models.F("token"),
|
||||
condition=models.Q(("kind", 2)),
|
||||
name="zilencer_remotepushdevicetoken_fcm_server_user_id_kind_token",
|
||||
),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
WITH dups AS (
|
||||
SELECT server_id, user_uuid, kind, LOWER(token) AS token, MAX(last_updated) AS max_last_updated
|
||||
FROM zilencer_remotepushdevicetoken
|
||||
WHERE kind = 1
|
||||
AND user_id IS NULL
|
||||
GROUP BY server_id, user_uuid, kind, LOWER(token)
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
UPDATE zilencer_remotepushdevicetoken
|
||||
SET last_updated = dups.max_last_updated
|
||||
FROM dups
|
||||
WHERE zilencer_remotepushdevicetoken.server_id = dups.server_id
|
||||
AND zilencer_remotepushdevicetoken.user_uuid = dups.user_uuid
|
||||
AND zilencer_remotepushdevicetoken.user_id IS NULL
|
||||
AND zilencer_remotepushdevicetoken.kind = dups.kind
|
||||
AND LOWER(zilencer_remotepushdevicetoken.token) = dups.token
|
||||
"""
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
WITH dups AS (
|
||||
SELECT server_id, user_uuid, kind, LOWER(token) AS token, MIN(id) AS min_id
|
||||
FROM zilencer_remotepushdevicetoken
|
||||
WHERE kind = 1
|
||||
AND user_id IS NULL
|
||||
GROUP BY server_id, user_uuid, kind, LOWER(token)
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
DELETE FROM zilencer_remotepushdevicetoken
|
||||
USING dups
|
||||
WHERE zilencer_remotepushdevicetoken.server_id = dups.server_id
|
||||
AND zilencer_remotepushdevicetoken.user_uuid = dups.user_uuid
|
||||
AND zilencer_remotepushdevicetoken.user_id IS NULL
|
||||
AND zilencer_remotepushdevicetoken.kind = dups.kind
|
||||
AND LOWER(zilencer_remotepushdevicetoken.token) = dups.token
|
||||
AND zilencer_remotepushdevicetoken.id != dups.min_id
|
||||
"""
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remotepushdevicetoken",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("server_id"),
|
||||
models.F("user_uuid"),
|
||||
models.F("kind"),
|
||||
django.db.models.functions.text.Lower(models.F("token")),
|
||||
condition=models.Q(("kind", 1)),
|
||||
name="zilencer_remotepushdevicetoken_apns_server_uuid_kind_token",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remotepushdevicetoken",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("server_id"),
|
||||
models.F("user_uuid"),
|
||||
models.F("kind"),
|
||||
models.F("token"),
|
||||
condition=models.Q(("kind", 2)),
|
||||
name="zilencer_remotepushdevicetoken_fcm_server_uuid_kind_token",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="remotepushdevicetoken",
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-02 17:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0779_device"),
|
||||
("zilencer", "0067_remotepushdevicetoken_apns_case_insensitive"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="remotepushdevice",
|
||||
name="unique_remote_push_device_push_account_id_token",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="remotepushdevice",
|
||||
name="token_id",
|
||||
field=models.BigIntegerField(default=None),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="remotepushdevice",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("token_id", "realm", "remote_realm"),
|
||||
name="unique_remote_push_device_token_id_realm_remote_realm",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-03 16:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0068_add_remotepushdevice_token_id_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="remotepushdevice",
|
||||
name="push_account_id",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-08 16:02
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently, RemoveIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zilencer", "0069_remove_remotepushdevice_push_account_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
RemoveIndexConcurrently(
|
||||
model_name="remoterealmauditlog",
|
||||
name="zilencer_remoterealmauditlog_synced_billing_events",
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="remoterealmauditlog",
|
||||
index=models.Index(
|
||||
condition=models.Q(
|
||||
(
|
||||
"event_type__in",
|
||||
[101, 102, 103, 104, 105, 201, 202, 229, 725],
|
||||
)
|
||||
),
|
||||
fields=["remote_realm_id", "id"],
|
||||
name="zilencer_remoterealmauditlog_synced_billing_events",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
zilencer/migrations/__init__.py
Normal file
0
zilencer/migrations/__init__.py
Normal file
683
zilencer/models.py
Normal file
683
zilencer/models.py
Normal file
@@ -0,0 +1,683 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Literal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Max, Q, QuerySet, UniqueConstraint
|
||||
from django.db.models.functions import Lower
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.models import BaseCount
|
||||
from zerver.lib.rate_limiter import RateLimitedObject
|
||||
from zerver.models import AbstractPushDeviceToken, AbstractRealmAuditLog, Realm, UserProfile
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
|
||||
|
||||
def get_remote_server_by_uuid(uuid: str) -> "RemoteZulipServer":
|
||||
try:
|
||||
return RemoteZulipServer.objects.get(uuid=uuid)
|
||||
except ValidationError:
|
||||
raise RemoteZulipServer.DoesNotExist
|
||||
|
||||
|
||||
class RemoteZulipServer(models.Model):
|
||||
"""Each object corresponds to a single remote Zulip server that is
|
||||
registered for the Mobile Push Notifications Service via
|
||||
`manage.py register_server`.
|
||||
"""
|
||||
|
||||
UUID_LENGTH = 36
|
||||
API_KEY_LENGTH = 64
|
||||
HOSTNAME_MAX_LENGTH = 128
|
||||
VERSION_MAX_LENGTH = 128
|
||||
MERGE_BASE_MAX_LENGTH = 128
|
||||
|
||||
# The unique UUID (`zulip_org_id`) and API key (`zulip_org_key`)
|
||||
# for this remote server registration.
|
||||
uuid = models.UUIDField(unique=True)
|
||||
api_key = models.CharField(max_length=API_KEY_LENGTH)
|
||||
|
||||
# The hostname and contact details are not verified/trusted. Thus,
|
||||
# they primarily exist so that we can communicate with the
|
||||
# maintainer of a server about abuse problems.
|
||||
hostname = models.CharField(max_length=HOSTNAME_MAX_LENGTH)
|
||||
contact_email = models.EmailField(blank=True, null=False)
|
||||
last_updated = models.DateTimeField("last updated", auto_now=True)
|
||||
last_request_datetime = models.DateTimeField(null=True)
|
||||
last_version = models.CharField(max_length=VERSION_MAX_LENGTH, null=True)
|
||||
last_merge_base = models.CharField(max_length=MERGE_BASE_MAX_LENGTH, null=True)
|
||||
last_api_feature_level = models.PositiveIntegerField(null=True)
|
||||
|
||||
# Whether the server registration has been deactivated.
|
||||
deactivated = models.BooleanField(default=False)
|
||||
|
||||
# Plan types for self-hosted customers
|
||||
#
|
||||
# We reserve PLAN_TYPE_SELF_HOSTED=Realm.PLAN_TYPE_SELF_HOSTED for
|
||||
# self-hosted installations that aren't using the notifications
|
||||
# service.
|
||||
#
|
||||
# The other values align with, e.g., CustomerPlan.TIER_SELF_HOSTED_BASE
|
||||
PLAN_TYPE_SELF_HOSTED = 1
|
||||
PLAN_TYPE_SELF_MANAGED = 100
|
||||
PLAN_TYPE_SELF_MANAGED_LEGACY = 101
|
||||
PLAN_TYPE_COMMUNITY = 102
|
||||
PLAN_TYPE_BASIC = 103
|
||||
PLAN_TYPE_BUSINESS = 104
|
||||
PLAN_TYPE_ENTERPRISE = 105
|
||||
|
||||
# The current billing plan for the remote server, similar to Realm.plan_type.
|
||||
plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED)
|
||||
|
||||
# This is not synced with the remote server, but only filled for sponsorship requests.
|
||||
org_type = models.PositiveSmallIntegerField(
|
||||
default=Realm.ORG_TYPES["unspecified"]["id"],
|
||||
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
|
||||
)
|
||||
|
||||
# The last time 'RemoteRealmAuditlog' was updated for this server.
|
||||
last_audit_log_update = models.DateTimeField(null=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.hostname} {str(self.uuid)[0:12]}"
|
||||
|
||||
def format_requester_for_logs(self) -> str:
|
||||
return "zulip-server:" + str(self.uuid)
|
||||
|
||||
def get_remote_server_billing_users(self) -> QuerySet["RemoteServerBillingUser"]:
|
||||
return RemoteServerBillingUser.objects.filter(
|
||||
remote_server=self,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
class RemotePushDeviceToken(AbstractPushDeviceToken):
|
||||
"""Class for storing legacy non-E2EE mobile push
|
||||
tokens. Deprecated in favor of RemotePushDevice, which is designed
|
||||
for E2EE mobile notifications.
|
||||
|
||||
Like PushDeviceToken, but for a device connected to a remote server.
|
||||
"""
|
||||
|
||||
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
# The user id on the remote server for this device
|
||||
user_id = models.BigIntegerField(null=True)
|
||||
user_uuid = models.UUIDField(null=True)
|
||||
|
||||
remote_realm = models.ForeignKey("RemoteRealm", on_delete=models.SET_NULL, null=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
# These indexes rely on the property that in Postgres,
|
||||
# NULL != NULL in the context of unique indexes, so multiple
|
||||
# rows with the same values in these columns can exist
|
||||
# if one of them is NULL.
|
||||
models.UniqueConstraint(
|
||||
"server_id",
|
||||
"user_id",
|
||||
"kind",
|
||||
Lower(F("token")),
|
||||
name="zilencer_remotepushdevicetoken_apns_server_user_id_kind_token",
|
||||
condition=Q(kind=1),
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
"server_id",
|
||||
"user_id",
|
||||
"kind",
|
||||
"token",
|
||||
name="zilencer_remotepushdevicetoken_fcm_server_user_id_kind_token",
|
||||
condition=Q(kind=2),
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
"server_id",
|
||||
"user_uuid",
|
||||
"kind",
|
||||
Lower(F("token")),
|
||||
name="zilencer_remotepushdevicetoken_apns_server_uuid_kind_token",
|
||||
condition=Q(kind=1),
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
"server_id",
|
||||
"user_uuid",
|
||||
"kind",
|
||||
"token",
|
||||
name="zilencer_remotepushdevicetoken_fcm_server_uuid_kind_token",
|
||||
condition=Q(kind=2),
|
||||
),
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.server!r} {self.user_id}"
|
||||
|
||||
|
||||
class RemoteRealm(models.Model):
|
||||
"""
|
||||
Each object corresponds to a single remote Realm that is using the
|
||||
Mobile Push Notifications Service via `manage.py register_server`.
|
||||
"""
|
||||
|
||||
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
|
||||
# The unique UUID and secret for this realm.
|
||||
uuid = models.UUIDField(unique=True)
|
||||
uuid_owner_secret = models.TextField()
|
||||
|
||||
# Value obtained's from the remote server's realm.host.
|
||||
host = models.TextField()
|
||||
|
||||
name = models.TextField(default="")
|
||||
|
||||
is_system_bot_realm = models.BooleanField(default=False)
|
||||
|
||||
authentication_methods = models.JSONField(default=dict)
|
||||
|
||||
org_type = models.PositiveSmallIntegerField(
|
||||
default=Realm.ORG_TYPES["unspecified"]["id"],
|
||||
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
|
||||
)
|
||||
|
||||
# The fields below are analogous to RemoteZulipServer fields.
|
||||
|
||||
last_updated = models.DateTimeField("last updated", auto_now=True)
|
||||
last_request_datetime = models.DateTimeField(null=True)
|
||||
|
||||
# Whether the realm registration has been deactivated.
|
||||
registration_deactivated = models.BooleanField(default=False)
|
||||
# Whether the realm has been deactivated on the remote server.
|
||||
realm_deactivated = models.BooleanField(default=False)
|
||||
# Whether we believe the remote server deleted this realm
|
||||
# from the database.
|
||||
realm_locally_deleted = models.BooleanField(default=False)
|
||||
|
||||
# When the realm was created on the remote server.
|
||||
realm_date_created = models.DateTimeField()
|
||||
|
||||
# Plan types for self-hosted customers
|
||||
#
|
||||
# We reserve PLAN_TYPE_SELF_HOSTED=Realm.PLAN_TYPE_SELF_HOSTED for
|
||||
# self-hosted installations that aren't using the notifications
|
||||
# service.
|
||||
#
|
||||
# The other values align with, e.g., CustomerPlan.TIER_SELF_HOSTED_BASE
|
||||
PLAN_TYPE_SELF_HOSTED = 1
|
||||
PLAN_TYPE_SELF_MANAGED = 100
|
||||
PLAN_TYPE_SELF_MANAGED_LEGACY = 101
|
||||
PLAN_TYPE_COMMUNITY = 102
|
||||
PLAN_TYPE_BASIC = 103
|
||||
PLAN_TYPE_BUSINESS = 104
|
||||
PLAN_TYPE_ENTERPRISE = 105
|
||||
plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED, db_index=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.host} {str(self.uuid)[0:12]}"
|
||||
|
||||
def get_remote_realm_billing_users(self) -> QuerySet["RemoteRealmBillingUser"]:
|
||||
return RemoteRealmBillingUser.objects.filter(
|
||||
remote_realm=self,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
class AbstractRemoteRealmBillingUser(models.Model):
|
||||
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE)
|
||||
|
||||
# The .uuid of the UserProfile on the remote server
|
||||
user_uuid = models.UUIDField()
|
||||
email = models.EmailField()
|
||||
|
||||
date_joined = models.DateTimeField(default=timezone_now)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class RemoteRealmBillingUser(AbstractRemoteRealmBillingUser):
|
||||
full_name = models.TextField(default="")
|
||||
|
||||
last_login = models.DateTimeField(null=True)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
|
||||
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
|
||||
|
||||
enable_major_release_emails = models.BooleanField(default=True)
|
||||
enable_maintenance_release_emails = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
("remote_realm", "user_uuid"),
|
||||
]
|
||||
|
||||
|
||||
class PreregistrationRemoteRealmBillingUser(AbstractRemoteRealmBillingUser):
|
||||
# status: whether an object has been confirmed.
|
||||
# if confirmed, set to confirmation.settings.STATUS_USED
|
||||
status = models.IntegerField(default=0)
|
||||
|
||||
# These are for carrying certain information that's originally
|
||||
# in an IdentityDict across the confirmation link flow. These
|
||||
# values will be restored in the final, fully authenticated IdentityDict.
|
||||
next_page = models.TextField(null=True)
|
||||
uri_scheme = models.TextField()
|
||||
|
||||
created_user = models.ForeignKey(RemoteRealmBillingUser, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
class AbstractRemoteServerBillingUser(models.Model):
|
||||
remote_server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
|
||||
email = models.EmailField()
|
||||
|
||||
date_joined = models.DateTimeField(default=timezone_now)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class RemoteServerBillingUser(AbstractRemoteServerBillingUser):
|
||||
full_name = models.TextField(default="")
|
||||
|
||||
last_login = models.DateTimeField(null=True)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
|
||||
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
|
||||
|
||||
enable_major_release_emails = models.BooleanField(default=True)
|
||||
enable_maintenance_release_emails = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
("remote_server", "email"),
|
||||
]
|
||||
|
||||
|
||||
class PreregistrationRemoteServerBillingUser(AbstractRemoteServerBillingUser):
|
||||
# status: whether an object has been confirmed.
|
||||
# if confirmed, set to confirmation.settings.STATUS_USED
|
||||
status = models.IntegerField(default=0)
|
||||
|
||||
next_page = models.TextField(null=True)
|
||||
|
||||
created_user = models.ForeignKey(RemoteServerBillingUser, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
RemoteServerDeactivationReasonType = Literal[
|
||||
"owner_request",
|
||||
"tos_violation",
|
||||
]
|
||||
|
||||
|
||||
class RemoteZulipServerAuditLog(AbstractRealmAuditLog):
|
||||
"""Audit data associated with a remote Zulip server (not specific to a
|
||||
realm). Used primarily for tracking registration and billing
|
||||
changes for self-hosted customers.
|
||||
|
||||
In contrast with RemoteRealmAuditLog, which has a copy of data
|
||||
that is generated on the client Zulip server, this table is the
|
||||
authoritative storage location for the server's history.
|
||||
"""
|
||||
|
||||
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
|
||||
acting_remote_user = models.ForeignKey(
|
||||
RemoteServerBillingUser, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
event_type_name = AuditLogEventType(self.event_type).name
|
||||
return f"{event_type_name} {self.event_time} (id={self.id}): {self.server!r}"
|
||||
|
||||
|
||||
class RemoteRealmAuditLog(AbstractRealmAuditLog):
|
||||
"""Synced audit data from a remote Zulip server, used primarily for
|
||||
billing. See RealmAuditLog and AbstractRealmAuditLog for details.
|
||||
"""
|
||||
|
||||
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
|
||||
# With modern Zulip servers, we can link to the RemoteRealm object.
|
||||
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE, null=True)
|
||||
# For pre-8.0 servers, we might only have the realm ID and thus no
|
||||
# RemoteRealm object yet. We will eventually be able to drop this
|
||||
# column once all self-hosted servers have upgraded in favor of
|
||||
# just using the foreign key everywhere.
|
||||
realm_id = models.IntegerField(null=True, blank=True)
|
||||
# The remote_id field lets us deduplicate data from the remote server
|
||||
remote_id = models.IntegerField(null=True)
|
||||
|
||||
acting_remote_user = models.ForeignKey(
|
||||
RemoteRealmBillingUser, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
event_type_name = AuditLogEventType(self.event_type).name
|
||||
return f"{event_type_name} {self.event_time} (id={self.id}): {self.server!r}"
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["server", "remote_id"],
|
||||
name="zilencer_remoterealmauditlog_server_remote",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["server", "realm_id", "remote_id"],
|
||||
name="zilencer_remoterealmauditlog_server_realm_remote",
|
||||
),
|
||||
models.Index(
|
||||
fields=["server", "realm_id"],
|
||||
condition=Q(remote_realm__isnull=True),
|
||||
name="zilencer_remoterealmauditlog_server_realm",
|
||||
),
|
||||
models.Index(
|
||||
fields=["server"],
|
||||
condition=Q(remote_realm__isnull=True),
|
||||
name="zilencer_remoterealmauditlog_server",
|
||||
),
|
||||
models.Index(
|
||||
fields=["remote_realm_id", "id"],
|
||||
condition=Q(event_type__in=AbstractRealmAuditLog.SYNCED_BILLING_EVENTS),
|
||||
name="zilencer_remoterealmauditlog_synced_billing_events",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class BaseRemoteCount(BaseCount):
|
||||
server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||
# The remote_id field is the id value of the corresponding *Count object
|
||||
# on the remote server.
|
||||
# It lets us deduplicate data from the remote server.
|
||||
# Note: Some counts don't come from the remote server, but rather
|
||||
# are stats we track on the bouncer server itself, pertaining to the remote server.
|
||||
# E.g. mobile_pushes_received::day. Such counts will set this field to None.
|
||||
remote_id = models.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class RemoteInstallationCount(BaseRemoteCount):
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
fields=["server", "property", "subgroup", "end_time"],
|
||||
condition=Q(subgroup__isnull=False),
|
||||
name="unique_remote_installation_count",
|
||||
),
|
||||
UniqueConstraint(
|
||||
fields=["server", "property", "end_time"],
|
||||
condition=Q(subgroup__isnull=True),
|
||||
name="unique_remote_installation_count_null_subgroup",
|
||||
),
|
||||
UniqueConstraint(
|
||||
fields=["server", "remote_id"],
|
||||
# As noted above, remote_id may be null, so we only
|
||||
# enforce uniqueness if it isn't. This is not
|
||||
# technically necessary, since null != null, but it
|
||||
# makes the property more explicit.
|
||||
condition=Q(remote_id__isnull=False),
|
||||
name="unique_remote_installation_count_server_id_remote_id",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["server_id", "end_time"],
|
||||
condition=Q(property="mobile_pushes_forwarded::day"),
|
||||
name="zilencer_remoteinstallationcount_server_end_time_mobile_pushes_forwarded",
|
||||
)
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.property} {self.subgroup} {self.value}"
|
||||
|
||||
|
||||
# We can't subclass RealmCount because we only have a realm_id here, not a foreign key.
|
||||
class RemoteRealmCount(BaseRemoteCount):
|
||||
realm_id = models.IntegerField(null=True)
|
||||
# Certain RemoteRealmCount will be counts tracked on the bouncer server directly, about
|
||||
# stats pertaining to a realm on a remote server. For such objects, we will link to
|
||||
# the corresponding RemoteRealm object that the remote server registered with us.
|
||||
# In the future we may be able to link all RemoteRealmCount objects to a RemoteRealm,
|
||||
# including the RemoteRealmCount objects are results of just syncing the RealmCount
|
||||
# table from the remote server.
|
||||
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE, null=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
# These two constraints come from the information as
|
||||
# provided by the remote server, for rows they provide.
|
||||
UniqueConstraint(
|
||||
fields=["server", "realm_id", "property", "subgroup", "end_time"],
|
||||
condition=Q(subgroup__isnull=False),
|
||||
name="unique_server_realm_installation_count",
|
||||
),
|
||||
UniqueConstraint(
|
||||
fields=["server", "realm_id", "property", "end_time"],
|
||||
condition=Q(subgroup__isnull=True),
|
||||
name="unique_server_realm_installation_count_null_subgroup",
|
||||
),
|
||||
# These two constraints come from our internal
|
||||
# record-keeping, which has a RemoteRealm object.
|
||||
UniqueConstraint(
|
||||
fields=["remote_realm_id", "property", "subgroup", "end_time"],
|
||||
condition=Q(subgroup__isnull=False),
|
||||
name="unique_remote_realm_installation_count",
|
||||
),
|
||||
UniqueConstraint(
|
||||
fields=["remote_realm_id", "property", "end_time"],
|
||||
condition=Q(subgroup__isnull=True),
|
||||
name="unique_remote_realm_installation_count_null_subgroup",
|
||||
),
|
||||
UniqueConstraint(
|
||||
fields=["server", "remote_id"],
|
||||
# As with RemoteInstallationCount above, remote_id may
|
||||
# be null; since null != null, this condition is not
|
||||
# strictly necessary, but serves to make the property
|
||||
# more explicit.
|
||||
condition=Q(remote_id__isnull=False),
|
||||
name="unique_remote_realm_installation_count_server_id_remote_id",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["property", "end_time"],
|
||||
name="zilencer_remoterealmcount_property_end_time_506a0b38_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["server", "realm_id"],
|
||||
condition=Q(remote_realm__isnull=True),
|
||||
name="zilencer_remoterealmcount_server_realm",
|
||||
),
|
||||
models.Index(
|
||||
fields=["server"],
|
||||
condition=Q(remote_realm__isnull=True),
|
||||
name="zilencer_remoterealmcount_server",
|
||||
),
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.server!r} {self.realm_id} {self.property} {self.subgroup} {self.value}"
|
||||
|
||||
|
||||
class RateLimitedRemoteZulipServer(RateLimitedObject):
|
||||
def __init__(
|
||||
self, remote_server: RemoteZulipServer, domain: str = "api_by_remote_server"
|
||||
) -> None:
|
||||
# Remote servers can only make API requests regarding push notifications
|
||||
# which requires ZILENCER_ENABLED and of course can't happen on API endpoints
|
||||
# inside Tornado.
|
||||
assert not settings.RUNNING_INSIDE_TORNADO
|
||||
assert settings.ZILENCER_ENABLED
|
||||
|
||||
self.uuid = str(remote_server.uuid)
|
||||
self.domain = domain
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
def key(self) -> str:
|
||||
return f"{type(self).__name__}:<{self.uuid}>:{self.domain}"
|
||||
|
||||
@override
|
||||
def rules(self) -> list[tuple[int, int]]:
|
||||
return settings.RATE_LIMITING_RULES[self.domain]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RemoteCustomerUserCount:
|
||||
guest_user_count: int
|
||||
non_guest_user_count: int
|
||||
|
||||
|
||||
def get_remote_customer_user_count(
|
||||
audit_logs: list[RemoteRealmAuditLog],
|
||||
) -> RemoteCustomerUserCount:
|
||||
guest_count = 0
|
||||
non_guest_count = 0
|
||||
for log in audit_logs:
|
||||
humans_count_dict = log.extra_data[RemoteRealmAuditLog.ROLE_COUNT][
|
||||
RemoteRealmAuditLog.ROLE_COUNT_HUMANS
|
||||
]
|
||||
for role_type in UserProfile.ROLE_TYPES:
|
||||
if role_type == UserProfile.ROLE_GUEST:
|
||||
guest_count += humans_count_dict.get(str(role_type), 0)
|
||||
else:
|
||||
non_guest_count += humans_count_dict.get(str(role_type), 0)
|
||||
|
||||
return RemoteCustomerUserCount(
|
||||
non_guest_user_count=non_guest_count, guest_user_count=guest_count
|
||||
)
|
||||
|
||||
|
||||
def get_remote_server_guest_and_non_guest_count(
|
||||
server_id: int, event_time: datetime | None = None
|
||||
) -> RemoteCustomerUserCount:
|
||||
# For each realm hosted on the server, find the latest audit log
|
||||
# entry indicating the number of active users in that realm.
|
||||
realm_last_audit_log_ids = (
|
||||
RemoteRealmAuditLog.objects.filter(
|
||||
server_id=server_id,
|
||||
event_type__in=RemoteRealmAuditLog.SYNCED_BILLING_EVENTS,
|
||||
event_time__lte=timezone_now() if event_time is None else event_time,
|
||||
)
|
||||
# Important: extra_data is empty for some pre-2020 audit logs
|
||||
# prior to the introduction of realm_user_count_by_role
|
||||
# logging. Meanwhile, modern Zulip servers using
|
||||
# bulk_create_users to create the users in the system bot
|
||||
# realm also generate such audit logs. Such audit logs should
|
||||
# never be the latest in a normal realm.
|
||||
.exclude(extra_data={})
|
||||
.values("realm_id")
|
||||
.annotate(max_id=Max("id"))
|
||||
.values_list("max_id", flat=True)
|
||||
)
|
||||
|
||||
realm_audit_logs = RemoteRealmAuditLog.objects.filter(id__in=list(realm_last_audit_log_ids))
|
||||
|
||||
# Now we add up the user counts from the different realms.
|
||||
user_count = get_remote_customer_user_count(list(realm_audit_logs))
|
||||
return user_count
|
||||
|
||||
|
||||
def get_remote_realm_guest_and_non_guest_count(
|
||||
remote_realm: RemoteRealm, event_time: datetime | None = None
|
||||
) -> RemoteCustomerUserCount:
|
||||
latest_audit_log = (
|
||||
RemoteRealmAuditLog.objects.filter(
|
||||
remote_realm=remote_realm,
|
||||
event_type__in=RemoteRealmAuditLog.SYNCED_BILLING_EVENTS,
|
||||
event_time__lte=timezone_now() if event_time is None else event_time,
|
||||
)
|
||||
# Important: extra_data is empty for some pre-2020 audit logs
|
||||
# prior to the introduction of realm_user_count_by_role
|
||||
# logging. Meanwhile, modern Zulip servers using
|
||||
# bulk_create_users to create the users in the system bot
|
||||
# realm also generate such audit logs. Such audit logs should
|
||||
# never be the latest in a normal realm.
|
||||
.exclude(extra_data={})
|
||||
.last()
|
||||
)
|
||||
|
||||
if latest_audit_log is not None:
|
||||
assert latest_audit_log is not None
|
||||
user_count = get_remote_customer_user_count([latest_audit_log])
|
||||
else:
|
||||
user_count = RemoteCustomerUserCount(guest_user_count=0, non_guest_user_count=0)
|
||||
return user_count
|
||||
|
||||
|
||||
def has_stale_audit_log(server: RemoteZulipServer) -> bool:
|
||||
if server.last_audit_log_update is None:
|
||||
return True
|
||||
|
||||
if timezone_now() - server.last_audit_log_update > timedelta(days=2):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class RemotePushDevice(models.Model):
|
||||
"""Core bouncer server table storing registrations to receive
|
||||
mobile push notifications via the bouncer server.
|
||||
|
||||
Each row corresponds to an install of the app registered
|
||||
to receive mobile push notifications for a specific realm.
|
||||
"""
|
||||
|
||||
# Unique identifier assigned by the bouncer for this registration.
|
||||
device_id = models.BigAutoField(primary_key=True)
|
||||
|
||||
# Realm that the account is associated with. Both Cloud and
|
||||
# self-hosted are supported; exactly one of these will be null.
|
||||
realm = models.ForeignKey(Realm, on_delete=models.CASCADE, null=True)
|
||||
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE, null=True)
|
||||
|
||||
# The token that uniquely identifies the app instance to the
|
||||
# FCM/APNs servers. There will be duplicates in this table when a
|
||||
# device is logged into multiple Zulip realms.
|
||||
#
|
||||
# FCM and APNs don't specify a maximum token length, so we only enforce
|
||||
# that they're at most the maximum FCM / APNs payload size of 4096 bytes.
|
||||
token = models.CharField(max_length=4096)
|
||||
# ID to reference the `token`.
|
||||
token_id = models.BigIntegerField()
|
||||
|
||||
class TokenKind(models.TextChoices):
|
||||
APNS = "apns", "APNs"
|
||||
FCM = "fcm", "FCM"
|
||||
|
||||
token_kind = models.CharField(max_length=4, choices=TokenKind.choices)
|
||||
|
||||
# If the token is expired, the date when the bouncer learned it
|
||||
# was expired via an error from the FCM/APNs server. Used to
|
||||
# support delayed deletion. Null if the bouncer believes this
|
||||
# token is still valid.
|
||||
expired_time = models.DateTimeField(null=True)
|
||||
|
||||
# Contains the app id of the device if and only if token_kind is APNs.
|
||||
ios_app_id = models.TextField(null=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
# The unique index created is used by a query in
|
||||
# 'do_register_remote_push_device'.
|
||||
fields=["token_id", "realm", "remote_realm"],
|
||||
name="unique_remote_push_device_token_id_realm_remote_realm",
|
||||
),
|
||||
]
|
||||
71
zilencer/urls.py
Normal file
71
zilencer/urls.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from typing import Any
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
|
||||
from corporate.views.remote_billing_page import remote_realm_billing_entry
|
||||
from zilencer.auth import remote_server_path
|
||||
from zilencer.views import (
|
||||
deactivate_remote_server,
|
||||
register_remote_push_device,
|
||||
register_remote_push_device_for_e2ee_push_notification,
|
||||
register_remote_server,
|
||||
remote_server_check_analytics,
|
||||
remote_server_notify_push,
|
||||
remote_server_post_analytics,
|
||||
remote_server_send_e2ee_push_notification,
|
||||
remote_server_send_test_notification,
|
||||
transfer_remote_server_registration,
|
||||
unregister_all_remote_push_devices,
|
||||
unregister_remote_push_device,
|
||||
verify_registration_transfer_challenge_ack_endpoint,
|
||||
)
|
||||
|
||||
i18n_urlpatterns: Any = []
|
||||
|
||||
# Zilencer views following the REST API style
|
||||
# The endpoints marked "intentionally_undocumented" are part of the older system
|
||||
# for sending non-E2EE push notifications, and will be removed in the future.
|
||||
push_bouncer_patterns = [
|
||||
remote_server_path(
|
||||
"remotes/push/register", POST=(register_remote_push_device, {"intentionally_undocumented"})
|
||||
),
|
||||
remote_server_path(
|
||||
"remotes/push/e2ee/register", POST=register_remote_push_device_for_e2ee_push_notification
|
||||
),
|
||||
remote_server_path(
|
||||
"remotes/push/unregister",
|
||||
POST=(unregister_remote_push_device, {"intentionally_undocumented"}),
|
||||
),
|
||||
remote_server_path(
|
||||
"remotes/push/unregister/all",
|
||||
POST=(unregister_all_remote_push_devices, {"intentionally_undocumented"}),
|
||||
),
|
||||
remote_server_path(
|
||||
"remotes/push/notify", POST=(remote_server_notify_push, {"intentionally_undocumented"})
|
||||
),
|
||||
remote_server_path("remotes/push/e2ee/notify", POST=remote_server_send_e2ee_push_notification),
|
||||
remote_server_path(
|
||||
"remotes/push/test_notification",
|
||||
POST=(remote_server_send_test_notification, {"intentionally_undocumented"}),
|
||||
),
|
||||
# Push signup doesn't use the REST API, since there's no auth.
|
||||
path("remotes/server/register", register_remote_server),
|
||||
path("remotes/server/register/transfer", transfer_remote_server_registration),
|
||||
path(
|
||||
"remotes/server/register/verify_challenge",
|
||||
verify_registration_transfer_challenge_ack_endpoint,
|
||||
),
|
||||
remote_server_path("remotes/server/deactivate", POST=deactivate_remote_server),
|
||||
# For receiving table data used in analytics and billing
|
||||
remote_server_path("remotes/server/analytics", POST=remote_server_post_analytics),
|
||||
remote_server_path("remotes/server/analytics/status", GET=remote_server_check_analytics),
|
||||
]
|
||||
|
||||
billing_patterns = [remote_server_path("remotes/server/billing", POST=remote_realm_billing_entry)]
|
||||
|
||||
v1_api_bouncer_patterns = push_bouncer_patterns + billing_patterns
|
||||
|
||||
urlpatterns = [
|
||||
path("api/v1/", include(v1_api_bouncer_patterns)),
|
||||
]
|
||||
1892
zilencer/views.py
Normal file
1892
zilencer/views.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user