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:
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")
|
||||
Reference in New Issue
Block a user