2019-01-31 00:39:02 +01:00
|
|
|
import datetime
|
2019-04-23 22:32:12 +02:00
|
|
|
import logging
|
2020-06-11 00:54:34 +02:00
|
|
|
from typing import Any, Dict, List, Optional, Union
|
2013-10-17 22:55:09 +02:00
|
|
|
|
2018-05-04 01:40:46 +02:00
|
|
|
from django.core.exceptions import ValidationError
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.core.validators import URLValidator, validate_email
|
2018-10-11 00:53:13 +02:00
|
|
|
from django.db import IntegrityError, transaction
|
2018-09-25 12:24:11 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2017-11-16 00:55:49 +01:00
|
|
|
from django.utils import timezone
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.utils.translation import ugettext as err_
|
2018-01-13 19:38:13 +01:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
|
2019-01-31 00:39:02 +01:00
|
|
|
from analytics.lib.counts import COUNT_STATS
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.decorator import InvalidZulipServerKeyError, require_post
|
2017-10-28 00:07:31 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.push_notifications import (
|
|
|
|
send_android_push_notification,
|
|
|
|
send_apple_push_notification,
|
|
|
|
)
|
2017-11-16 00:55:49 +01:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2016-10-27 23:55:31 +02:00
|
|
|
from zerver.lib.response import json_error, json_success
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.validator import (
|
|
|
|
check_bool,
|
|
|
|
check_capped_string,
|
|
|
|
check_dict_only,
|
|
|
|
check_float,
|
|
|
|
check_int,
|
|
|
|
check_list,
|
|
|
|
check_none_or,
|
|
|
|
check_string,
|
|
|
|
check_string_fixed_length,
|
|
|
|
)
|
2018-09-25 12:24:11 +02:00
|
|
|
from zerver.models import UserProfile
|
2017-07-07 18:23:36 +02:00
|
|
|
from zerver.views.push_notifications import validate_token
|
2020-06-11 00:54:34 +02:00
|
|
|
from zilencer.models import (
|
|
|
|
RemoteInstallationCount,
|
|
|
|
RemotePushDeviceToken,
|
|
|
|
RemoteRealmAuditLog,
|
|
|
|
RemoteRealmCount,
|
|
|
|
RemoteZulipServer,
|
|
|
|
)
|
|
|
|
|
2018-07-13 13:33:05 +02:00
|
|
|
|
2019-08-10 00:30:35 +02:00
|
|
|
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> RemoteZulipServer:
|
2017-05-08 14:25:40 +02:00
|
|
|
if not isinstance(entity, RemoteZulipServer):
|
2018-02-15 20:50:37 +01:00
|
|
|
raise JsonableError(err_("Must validate with valid Zulip server API key"))
|
2019-08-10 00:30:35 +02:00
|
|
|
return entity
|
2017-05-08 14:25:40 +02:00
|
|
|
|
2017-10-27 12:57:54 +02:00
|
|
|
def validate_bouncer_token_request(entity: Union[UserProfile, RemoteZulipServer],
|
2019-11-13 06:54:30 +01:00
|
|
|
token: str, kind: int) -> RemoteZulipServer:
|
2017-07-07 18:29:45 +02:00
|
|
|
if kind not in [RemotePushDeviceToken.APNS, RemotePushDeviceToken.GCM]:
|
2018-02-15 20:50:37 +01:00
|
|
|
raise JsonableError(err_("Invalid token type"))
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_entity(entity)
|
2017-07-07 18:23:36 +02:00
|
|
|
validate_token(token, kind)
|
2019-08-10 00:30:35 +02:00
|
|
|
return server
|
2017-05-08 14:25:40 +02:00
|
|
|
|
2018-05-04 01:40:46 +02:00
|
|
|
@csrf_exempt
|
|
|
|
@require_post
|
|
|
|
@has_request_variables
|
|
|
|
def register_remote_server(
|
|
|
|
request: HttpRequest,
|
|
|
|
zulip_org_id: str=REQ(str_validator=check_string_fixed_length(RemoteZulipServer.UUID_LENGTH)),
|
|
|
|
zulip_org_key: str=REQ(str_validator=check_string_fixed_length(RemoteZulipServer.API_KEY_LENGTH)),
|
|
|
|
hostname: str=REQ(str_validator=check_capped_string(RemoteZulipServer.HOSTNAME_MAX_LENGTH)),
|
|
|
|
contact_email: str=REQ(str_validator=check_string),
|
|
|
|
new_org_key: Optional[str]=REQ(str_validator=check_string_fixed_length(
|
|
|
|
RemoteZulipServer.API_KEY_LENGTH), default=None),
|
|
|
|
) -> HttpResponse:
|
|
|
|
# REQ validated the the field lengths, but we still need to
|
|
|
|
# validate the format of these fields.
|
|
|
|
try:
|
|
|
|
# TODO: Ideally we'd not abuse the URL validator this way
|
|
|
|
url_validator = URLValidator()
|
|
|
|
url_validator('http://' + hostname)
|
|
|
|
except ValidationError:
|
|
|
|
raise JsonableError(_('%s is not a valid hostname') % (hostname,))
|
|
|
|
|
|
|
|
try:
|
|
|
|
validate_email(contact_email)
|
|
|
|
except ValidationError as e:
|
|
|
|
raise JsonableError(e.message)
|
|
|
|
|
|
|
|
remote_server, created = RemoteZulipServer.objects.get_or_create(
|
|
|
|
uuid=zulip_org_id,
|
|
|
|
defaults={'hostname': hostname, 'contact_email': contact_email,
|
|
|
|
'api_key': zulip_org_key})
|
|
|
|
|
|
|
|
if not created:
|
|
|
|
if remote_server.api_key != zulip_org_key:
|
2018-05-04 18:04:01 +02:00
|
|
|
raise InvalidZulipServerKeyError(zulip_org_id)
|
2018-05-04 01:40:46 +02:00
|
|
|
else:
|
|
|
|
remote_server.hostname = hostname
|
|
|
|
remote_server.contact_email = contact_email
|
|
|
|
if new_org_key is not None:
|
|
|
|
remote_server.api_key = new_org_key
|
|
|
|
remote_server.save()
|
|
|
|
|
|
|
|
return json_success({'created': created})
|
|
|
|
|
2016-10-27 23:55:31 +02:00
|
|
|
@has_request_variables
|
2018-04-29 00:06:26 +02:00
|
|
|
def register_remote_push_device(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
2019-11-13 08:17:49 +01:00
|
|
|
user_id: int=REQ(validator=check_int), token: str=REQ(),
|
2017-12-20 20:56:11 +01:00
|
|
|
token_kind: int=REQ(validator=check_int),
|
2018-05-11 01:38:41 +02:00
|
|
|
ios_app_id: Optional[str]=None) -> HttpResponse:
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_bouncer_token_request(entity, token, token_kind)
|
2016-10-27 23:55:31 +02:00
|
|
|
|
2018-10-11 00:53:13 +02:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
|
|
|
RemotePushDeviceToken.objects.create(
|
|
|
|
user_id=user_id,
|
|
|
|
server=server,
|
|
|
|
kind=token_kind,
|
|
|
|
token=token,
|
|
|
|
ios_app_id=ios_app_id,
|
|
|
|
# last_updated is to be renamed to date_created.
|
|
|
|
last_updated=timezone.now())
|
|
|
|
except IntegrityError:
|
|
|
|
pass
|
2016-10-27 23:55:31 +02:00
|
|
|
|
|
|
|
return json_success()
|
|
|
|
|
|
|
|
@has_request_variables
|
2018-04-29 00:07:47 +02:00
|
|
|
def unregister_remote_push_device(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
2019-11-13 06:54:30 +01:00
|
|
|
token: str=REQ(),
|
2017-12-20 20:56:11 +01:00
|
|
|
token_kind: int=REQ(validator=check_int),
|
2019-11-13 08:17:49 +01:00
|
|
|
user_id: int=REQ(validator=check_int),
|
2018-05-11 01:38:41 +02:00
|
|
|
ios_app_id: Optional[str]=None) -> HttpResponse:
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_bouncer_token_request(entity, token, token_kind)
|
2016-10-27 23:55:31 +02:00
|
|
|
deleted = RemotePushDeviceToken.objects.filter(token=token,
|
|
|
|
kind=token_kind,
|
2018-10-12 20:18:07 +02:00
|
|
|
user_id=user_id,
|
2016-10-27 23:55:31 +02:00
|
|
|
server=server).delete()
|
|
|
|
if deleted[0] == 0:
|
2018-02-15 20:50:37 +01:00
|
|
|
return json_error(err_("Token does not exist"))
|
2016-10-27 23:55:31 +02:00
|
|
|
|
|
|
|
return json_success()
|
2017-05-08 13:48:16 +02:00
|
|
|
|
2019-11-19 03:12:54 +01:00
|
|
|
@has_request_variables
|
|
|
|
def unregister_all_remote_push_devices(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
user_id: int=REQ(validator=check_int)) -> HttpResponse:
|
|
|
|
server = validate_entity(entity)
|
|
|
|
RemotePushDeviceToken.objects.filter(user_id=user_id,
|
|
|
|
server=server).delete()
|
|
|
|
return json_success()
|
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
@has_request_variables
|
2017-12-20 20:56:11 +01:00
|
|
|
def remote_server_notify_push(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
payload: Dict[str, Any]=REQ(argument_type='body')) -> HttpResponse:
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_entity(entity)
|
2017-05-09 10:31:47 +02:00
|
|
|
|
|
|
|
user_id = payload['user_id']
|
|
|
|
gcm_payload = payload['gcm_payload']
|
|
|
|
apns_payload = payload['apns_payload']
|
2018-11-29 21:37:40 +01:00
|
|
|
gcm_options = payload.get('gcm_options', {})
|
2017-05-09 10:31:47 +02:00
|
|
|
|
|
|
|
android_devices = list(RemotePushDeviceToken.objects.filter(
|
|
|
|
user_id=user_id,
|
|
|
|
kind=RemotePushDeviceToken.GCM,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
server=server,
|
2017-05-09 10:31:47 +02:00
|
|
|
))
|
|
|
|
|
|
|
|
apple_devices = list(RemotePushDeviceToken.objects.filter(
|
|
|
|
user_id=user_id,
|
|
|
|
kind=RemotePushDeviceToken.APNS,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
server=server,
|
2017-05-09 10:31:47 +02:00
|
|
|
))
|
|
|
|
|
|
|
|
if android_devices:
|
2018-11-29 21:37:40 +01:00
|
|
|
send_android_push_notification(android_devices, gcm_payload, gcm_options, remote=True)
|
2017-05-09 10:31:47 +02:00
|
|
|
|
|
|
|
if apple_devices:
|
2018-05-21 20:20:23 +02:00
|
|
|
send_apple_push_notification(user_id, apple_devices, apns_payload, remote=True)
|
2017-05-09 10:31:47 +02:00
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
return json_success()
|
2019-01-31 00:39:02 +01:00
|
|
|
|
2019-10-03 01:54:36 +02:00
|
|
|
def validate_incoming_table_data(server: RemoteZulipServer, model: Any,
|
|
|
|
rows: List[Dict[str, Any]], is_count_stat: bool=False) -> None:
|
2019-01-31 00:39:02 +01:00
|
|
|
last_id = get_last_id_from_server(server, model)
|
2019-10-03 01:54:36 +02:00
|
|
|
for row in rows:
|
|
|
|
if is_count_stat and row['property'] not in COUNT_STATS:
|
|
|
|
raise JsonableError(_("Invalid property %s") % (row['property'],))
|
|
|
|
if row['id'] <= last_id:
|
2019-01-31 00:39:02 +01:00
|
|
|
raise JsonableError(_("Data is out of order."))
|
2019-10-03 01:54:36 +02:00
|
|
|
last_id = row['id']
|
|
|
|
|
|
|
|
def batch_create_table_data(server: RemoteZulipServer, model: Any,
|
|
|
|
row_objects: Union[List[RemoteRealmCount],
|
|
|
|
List[RemoteInstallationCount]]) -> None:
|
|
|
|
BATCH_SIZE = 1000
|
|
|
|
while len(row_objects) > 0:
|
|
|
|
try:
|
|
|
|
model.objects.bulk_create(row_objects[:BATCH_SIZE])
|
|
|
|
except IntegrityError:
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.warning(
|
|
|
|
"Invalid data saving %s for server %s/%s",
|
|
|
|
model._meta.db_table, server.hostname, server.uuid,
|
|
|
|
)
|
2019-10-03 01:54:36 +02:00
|
|
|
raise JsonableError(_("Invalid data."))
|
|
|
|
row_objects = row_objects[BATCH_SIZE:]
|
2019-01-31 00:39:02 +01:00
|
|
|
|
|
|
|
@has_request_variables
|
|
|
|
def remote_server_post_analytics(request: HttpRequest,
|
|
|
|
entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
realm_counts: List[Dict[str, Any]]=REQ(
|
|
|
|
validator=check_list(check_dict_only([
|
|
|
|
('property', check_string),
|
|
|
|
('realm', check_int),
|
|
|
|
('id', check_int),
|
|
|
|
('end_time', check_float),
|
|
|
|
('subgroup', check_none_or(check_string)),
|
|
|
|
('value', check_int),
|
|
|
|
]))),
|
|
|
|
installation_counts: List[Dict[str, Any]]=REQ(
|
|
|
|
validator=check_list(check_dict_only([
|
|
|
|
('property', check_string),
|
|
|
|
('id', check_int),
|
|
|
|
('end_time', check_float),
|
|
|
|
('subgroup', check_none_or(check_string)),
|
|
|
|
('value', check_int),
|
2019-10-03 02:01:36 +02:00
|
|
|
]))),
|
|
|
|
realmauditlog_rows: Optional[List[Dict[str, Any]]]=REQ(
|
|
|
|
validator=check_list(check_dict_only([
|
|
|
|
('id', check_int),
|
|
|
|
('realm', check_int),
|
|
|
|
('event_time', check_float),
|
|
|
|
('backfilled', check_bool),
|
|
|
|
('extra_data', check_none_or(check_string)),
|
|
|
|
('event_type', check_int),
|
|
|
|
])), default=None)) -> HttpResponse:
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_entity(entity)
|
2019-01-31 00:39:02 +01:00
|
|
|
|
2019-10-03 01:54:36 +02:00
|
|
|
validate_incoming_table_data(server, RemoteRealmCount, realm_counts, True)
|
|
|
|
validate_incoming_table_data(server, RemoteInstallationCount, installation_counts, True)
|
2019-10-03 02:01:36 +02:00
|
|
|
if realmauditlog_rows is not None:
|
|
|
|
validate_incoming_table_data(server, RemoteRealmAuditLog, realmauditlog_rows)
|
2019-10-03 01:54:36 +02:00
|
|
|
|
|
|
|
row_objects = [RemoteRealmCount(
|
|
|
|
property=row['property'],
|
|
|
|
realm_id=row['realm'],
|
|
|
|
remote_id=row['id'],
|
|
|
|
server=server,
|
2020-06-05 06:55:20 +02:00
|
|
|
end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=datetime.timezone.utc),
|
2019-10-03 01:54:36 +02:00
|
|
|
subgroup=row['subgroup'],
|
|
|
|
value=row['value']) for row in realm_counts]
|
|
|
|
batch_create_table_data(server, RemoteRealmCount, row_objects)
|
|
|
|
|
|
|
|
row_objects = [RemoteInstallationCount(
|
|
|
|
property=row['property'],
|
|
|
|
remote_id=row['id'],
|
|
|
|
server=server,
|
2020-06-05 06:55:20 +02:00
|
|
|
end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=datetime.timezone.utc),
|
2019-10-03 01:54:36 +02:00
|
|
|
subgroup=row['subgroup'],
|
|
|
|
value=row['value']) for row in installation_counts]
|
|
|
|
batch_create_table_data(server, RemoteInstallationCount, row_objects)
|
2019-01-31 00:39:02 +01:00
|
|
|
|
2019-10-03 02:01:36 +02:00
|
|
|
if realmauditlog_rows is not None:
|
|
|
|
row_objects = [RemoteRealmAuditLog(
|
|
|
|
realm_id=row['realm'],
|
|
|
|
remote_id=row['id'],
|
|
|
|
server=server,
|
2020-06-05 06:55:20 +02:00
|
|
|
event_time=datetime.datetime.fromtimestamp(row['event_time'], tz=datetime.timezone.utc),
|
2019-10-03 02:01:36 +02:00
|
|
|
backfilled=row['backfilled'],
|
|
|
|
extra_data=row['extra_data'],
|
|
|
|
event_type=row['event_type']) for row in realmauditlog_rows]
|
|
|
|
batch_create_table_data(server, RemoteRealmAuditLog, row_objects)
|
|
|
|
|
2019-01-31 00:39:02 +01:00
|
|
|
return json_success()
|
|
|
|
|
|
|
|
def get_last_id_from_server(server: RemoteZulipServer, model: Any) -> int:
|
|
|
|
last_count = model.objects.filter(server=server).order_by("remote_id").last()
|
|
|
|
if last_count is not None:
|
|
|
|
return last_count.remote_id
|
|
|
|
return 0
|
|
|
|
|
|
|
|
@has_request_variables
|
|
|
|
def remote_server_check_analytics(request: HttpRequest,
|
|
|
|
entity: Union[UserProfile, RemoteZulipServer]) -> HttpResponse:
|
2019-08-10 00:30:35 +02:00
|
|
|
server = validate_entity(entity)
|
2019-01-31 00:39:02 +01:00
|
|
|
|
|
|
|
result = {
|
|
|
|
'last_realm_count_id': get_last_id_from_server(server, RemoteRealmCount),
|
|
|
|
'last_installation_count_id': get_last_id_from_server(
|
|
|
|
server, RemoteInstallationCount),
|
2019-10-03 02:01:36 +02:00
|
|
|
'last_realmauditlog_id': get_last_id_from_server(
|
|
|
|
server, RemoteRealmAuditLog),
|
2019-01-31 00:39:02 +01:00
|
|
|
}
|
|
|
|
return json_success(result)
|