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

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View 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()

View File

@@ -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"])

View 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}")

View File

@@ -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()

View 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)

View File

@@ -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']}")

View 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()

View 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/",
}

File diff suppressed because it is too large Load Diff

View 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))

View 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)

View 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")

View 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,
)
)

View 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)

View File

@@ -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)

View 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")