zulip/web/src/people.ts

1725 lines
50 KiB
TypeScript
Raw Normal View History

2020-08-20 21:33:00 +02:00
import md5 from "blueimp-md5";
import assert from "minimalistic-assert";
import * as typeahead from "../shared/src/typeahead";
import * as blueslip from "./blueslip";
2020-08-20 21:33:00 +02:00
import {FoldDict} from "./fold_dict";
import {$t} from "./i18n";
import * as message_user_ids from "./message_user_ids";
import * as muted_users from "./muted_users";
import {page_params} from "./page_params";
import * as reload_state from "./reload_state";
import * as settings_config from "./settings_config";
import * as settings_data from "./settings_data";
import * as timerender from "./timerender";
import type {DisplayRecipientUser, Message, MessageWithBooleans} from "./types";
import {user_settings} from "./user_settings";
2020-08-20 21:33:00 +02:00
import * as util from "./util";
export type ProfileData = {
value: string;
rendered_value?: string;
};
export type User = {
user_id: number;
delivery_email: string | null;
email: string;
full_name: string;
date_joined: string;
is_active: boolean;
is_owner: boolean;
is_admin: boolean;
is_guest: boolean;
is_moderator: boolean;
is_billing_admin: boolean;
role: number;
timezone: string;
avatar_url?: string | null;
avatar_version: number;
profile_data: Record<number, ProfileData>;
is_missing_server_data?: boolean; // used for fake user objects.
is_inaccessible_user?: boolean; // used for inaccessible user objects.
} & (
| {
is_bot: false;
bot_type: null;
}
| {
is_bot: true;
bot_type: number;
bot_owner_id: number | null;
}
);
export type SenderInfo = User & {
avatar_url_small: string;
is_muted: boolean;
};
// This type is generated by the `compose_typeahead.broadcast_mentions` function.
export type PseudoMentionUser = {
special_item_text: string;
email: string;
pm_recipient_count: number;
full_name: string;
is_broadcast: true;
idx: number;
};
export type CrossRealmBot = User & {
is_system_bot: boolean;
};
export type PeopleParams = {
realm_users: User[];
realm_non_active_users: User[];
cross_realm_bots: CrossRealmBot[];
};
let people_dict: FoldDict<User>;
let people_by_name_dict: FoldDict<User>;
let people_by_user_id_dict: Map<number, User>;
let active_user_dict: Map<number, User>;
let non_active_user_dict: Map<number, User>;
let cross_realm_dict: Map<number, CrossRealmBot>;
let pm_recipient_count_dict: Map<number, number>;
let duplicate_full_name_data: FoldDict<Set<number>>;
let my_user_id: number;
export let INACCESSIBLE_USER_NAME: string;
// We have an init() function so that our automated tests
// can easily clear data.
export function init(): void {
// The following three dicts point to the same objects
// (all people we've seen), but people_dict can have duplicate
// keys related to email changes. We want to deprecate
// people_dict over time and always do lookups by user_id.
people_dict = new FoldDict();
people_by_name_dict = new FoldDict();
people_by_user_id_dict = new Map();
// The next dictionary includes all active users (human/user)
// in our realm, but it excludes non-active users and
// cross-realm bots.
active_user_dict = new Map();
non_active_user_dict = new Map();
cross_realm_dict = new Map(); // keyed by user_id
pm_recipient_count_dict = new Map();
// This maintains a set of ids of people with same full names.
duplicate_full_name_data = new FoldDict();
INACCESSIBLE_USER_NAME = $t({defaultMessage: "Unknown user"});
2020-08-20 21:33:00 +02:00
}
// WE INITIALIZE DATA STRUCTURES HERE!
2020-08-20 21:33:00 +02:00
init();
export function split_to_ints(lst: string): number[] {
return lst.split(",").map((s) => Number.parseInt(s, 10));
}
export function get_users_from_ids(user_ids: number[]): User[] {
return user_ids.map((user_id) => get_by_user_id(user_id));
}
// Use this function only when you are sure that user_id is valid.
export function get_by_user_id(user_id: number): User {
const person = people_by_user_id_dict.get(user_id);
assert(person, `Unknown user_id in get_by_user_id: ${user_id}`);
return person;
}
// This is type unsafe version of get_by_user_id for the callers that expects undefined values.
export function maybe_get_user_by_id(user_id: number, ignore_missing = false): User | undefined {
if (!people_by_user_id_dict.has(user_id) && !ignore_missing) {
blueslip.error("Unknown user_id in maybe_get_user_by_id", {user_id});
return undefined;
}
return people_by_user_id_dict.get(user_id);
2020-08-20 21:33:00 +02:00
}
export function validate_user_ids(user_ids: number[]): number[] {
const good_ids = [];
const bad_ids = [];
for (const user_id of user_ids) {
if (people_by_user_id_dict.has(user_id)) {
good_ids.push(user_id);
} else {
bad_ids.push(user_id);
}
}
if (bad_ids.length > 0) {
blueslip.warn("We have untracked user_ids", {bad_ids});
}
return good_ids;
}
export function get_by_email(email: string): User | undefined {
const person = people_dict.get(email);
if (!person) {
return undefined;
}
if (person.email.toLowerCase() !== email.toLowerCase()) {
blueslip.warn(
"Obsolete email passed to get_by_email: " + email + " new email = " + person.email,
);
}
return person;
2020-08-20 21:33:00 +02:00
}
export function get_bot_owner_user(user: User & {is_bot: true}): User | undefined {
const owner_id = user.bot_owner_id;
if (owner_id === undefined || owner_id === null) {
// This is probably a cross-realm bot.
return undefined;
}
return get_user_by_id_assert_valid(owner_id);
2020-08-20 21:33:00 +02:00
}
export function can_admin_user(user: User): boolean {
return (
(user.is_bot && user.bot_owner_id && user.bot_owner_id === page_params.user_id) ||
is_my_user_id(user.user_id)
);
}
export function id_matches_email_operand(user_id: number, email: string): boolean {
2020-08-20 21:33:00 +02:00
const person = get_by_email(email);
if (!person) {
// The user may type bad data into the search bar, so
// we don't complain too loud here.
blueslip.debug("User email operand unknown: " + email);
return false;
}
return person.user_id === user_id;
2020-08-20 21:33:00 +02:00
}
export function update_email(user_id: number, new_email: string): void {
const person = get_by_user_id(user_id);
person.email = new_email;
people_dict.set(new_email, person);
// For legacy reasons we don't delete the old email
// keys in our dictionaries, so that reverse lookups
// still work correctly.
2020-08-20 21:33:00 +02:00
}
export function get_visible_email(user: User): string {
if (user.delivery_email) {
return user.delivery_email;
}
return user.email;
2020-08-20 21:33:00 +02:00
}
export function get_user_id(email: string): number | undefined {
2020-08-20 21:33:00 +02:00
const person = get_by_email(email);
2016-10-30 15:22:24 +01:00
if (person === undefined) {
blueslip.error("Unknown email for get_user_id", {email});
return undefined;
2016-10-30 15:22:24 +01:00
}
const user_id = person.user_id;
2016-10-30 15:22:24 +01:00
if (!user_id) {
blueslip.error("No user_id found for email", {email});
return undefined;
2016-10-30 15:22:24 +01:00
}
return user_id;
2020-08-20 21:33:00 +02:00
}
2016-10-30 15:22:24 +01:00
export function is_known_user_id(user_id: number): boolean {
2017-03-26 19:32:54 +02:00
/*
We may get a user_id from mention syntax that we don't
know about if a user includes some random number in
the mention syntax by manually typing it instead of
selecting some user from typeahead.
2017-03-26 19:32:54 +02:00
*/
return people_by_user_id_dict.has(user_id);
2020-08-20 21:33:00 +02:00
}
2017-03-26 19:32:54 +02:00
export function is_known_user(user: User): boolean {
return user && is_known_user_id(user.user_id);
}
function sort_numerically(user_ids: number[]): number[] {
user_ids.sort((a, b) => a - b);
return user_ids;
}
export function huddle_string(message: Message): string | undefined {
if (message.type !== "private") {
return undefined;
}
assert(
typeof message.display_recipient !== "string",
"Private messages should have list of recipients",
);
let user_ids = message.display_recipient.map((recip) => recip.id);
user_ids = user_ids.filter(
(user_id) => user_id && people_by_user_id_dict.has(user_id) && !is_my_user_id(user_id),
);
if (user_ids.length <= 1) {
return undefined;
}
user_ids = sort_numerically(user_ids);
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function user_ids_string_to_emails_string(user_ids_string: string): string | undefined {
const user_ids = split_to_ints(user_ids_string);
let emails = util.try_parse_as_truthy(
user_ids.map((user_id) => {
const person = people_by_user_id_dict.get(user_id);
return person?.email;
}),
);
if (emails === undefined) {
blueslip.warn("Unknown user ids: " + user_ids_string);
return undefined;
}
emails = emails.map((email) => email.toLowerCase());
emails.sort();
return emails.join(",");
2020-08-20 21:33:00 +02:00
}
export function user_ids_string_to_ids_array(user_ids_string: string): number[] {
const user_ids = user_ids_string.length === 0 ? [] : user_ids_string.split(",");
const ids = user_ids.map(Number);
return ids;
2020-08-20 21:33:00 +02:00
}
export function get_participants_from_user_ids_string(user_ids_string: string): Set<number> {
// Convert to set to ensure there are no duplicate ids.
const user_ids = new Set(user_ids_string_to_ids_array(user_ids_string));
// For group or 1:1 direct messages, the user_ids_string contains
// just the other user, so we need to add ourselves if not already
// present. For a direct message to oneself, the current user is
// already present, in user_ids_string, so we don't need to add it
// which is take care of by user_ids being a `Set`.
user_ids.add(my_user_id);
return user_ids;
}
export function emails_strings_to_user_ids_array(emails_string: string): number[] | undefined {
2020-08-20 21:33:00 +02:00
const user_ids_string = emails_strings_to_user_ids_string(emails_string);
if (user_ids_string === undefined) {
return undefined;
}
2020-08-20 21:33:00 +02:00
const user_ids_array = user_ids_string_to_ids_array(user_ids_string);
return user_ids_array;
2020-08-20 21:33:00 +02:00
}
export function reply_to_to_user_ids_string(emails_string: string): string | undefined {
// This is basically emails_strings_to_user_ids_string
// without blueslip warnings, since it can be called with
// invalid data.
const emails = emails_string.split(",");
let user_ids = util.try_parse_as_truthy(
emails.map((email) => {
const person = get_by_email(email);
return person?.user_id;
}),
);
if (user_ids === undefined) {
return undefined;
}
user_ids = sort_numerically(user_ids);
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function emails_to_full_names_string(emails: string[]): string {
return emails
.map((email) => {
email = email.trim();
const person = get_by_email(email);
if (person !== undefined) {
return person.full_name;
}
return email;
})
.join(", ");
}
export function get_user_time(user_id: number): string | undefined {
const user_timezone = get_by_user_id(user_id)!.timezone;
if (user_timezone) {
try {
return new Date().toLocaleTimeString(user_settings.default_language, {
...timerender.get_format_options_for_type(
"time",
user_settings.twenty_four_hour_time,
),
timeZone: user_timezone,
});
} catch (error) {
blueslip.warn(`Error formatting time in ${user_timezone}: ${String(error)}`);
}
}
return undefined;
2020-08-20 21:33:00 +02:00
}
export function get_user_type(user_id: number): string | undefined {
2020-08-20 21:33:00 +02:00
const user_profile = get_by_user_id(user_id);
return settings_config.user_role_map.get(user_profile.role);
2020-08-20 21:33:00 +02:00
}
export function emails_strings_to_user_ids_string(emails_string: string): string | undefined {
const emails = emails_string.split(",");
2020-08-20 21:33:00 +02:00
return email_list_to_user_ids_string(emails);
}
export function email_list_to_user_ids_string(emails: string[]): string | undefined {
let user_ids = util.try_parse_as_truthy(
emails.map((email) => {
const person = get_by_email(email);
return person?.user_id;
}),
);
if (user_ids === undefined) {
blueslip.warn("Unknown emails", {emails});
return undefined;
}
user_ids = sort_numerically(user_ids);
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function get_full_names_for_poll_option(user_ids: number[]): string {
return get_display_full_names(user_ids).join(", ");
2020-08-20 21:33:00 +02:00
}
2018-02-23 16:16:55 +01:00
export function get_display_full_name(user_id: number): string {
const person = get_user_by_id_assert_valid(user_id);
if (muted_users.is_user_muted(user_id)) {
if (should_add_guest_user_indicator(user_id)) {
return $t({defaultMessage: "Muted user (guest)"});
}
return $t({defaultMessage: "Muted user"});
}
if (should_add_guest_user_indicator(user_id)) {
return $t({defaultMessage: "{name} (guest)"}, {name: person.full_name});
}
return person.full_name;
}
export function get_display_full_names(user_ids: number[]): string[] {
return user_ids.map((user_id) => get_display_full_name(user_id));
}
export function get_full_name(user_id: number): string {
const person = get_by_user_id(user_id);
return person.full_name;
2020-08-20 21:33:00 +02:00
}
function _calc_user_and_other_ids(user_ids_string: string): {
user_ids: number[];
other_ids: number[];
} {
const user_ids = split_to_ints(user_ids_string);
const other_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
return {user_ids, other_ids};
}
export function get_recipients(user_ids_string: string): string {
// See message_store.get_pm_full_names() for a similar function.
const {other_ids} = _calc_user_and_other_ids(user_ids_string);
if (other_ids.length === 0) {
// direct message with oneself
2020-08-20 21:33:00 +02:00
return my_full_name();
}
const names = get_display_full_names(other_ids).sort();
return names.join(", ");
2020-08-20 21:33:00 +02:00
}
export function pm_reply_user_string(message: Message): string | undefined {
2020-08-20 21:33:00 +02:00
const user_ids = pm_with_user_ids(message);
if (!user_ids) {
return undefined;
}
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function pm_reply_to(message: Message): string | undefined {
2020-08-20 21:33:00 +02:00
const user_ids = pm_with_user_ids(message);
if (!user_ids) {
return undefined;
}
const emails = user_ids.map((user_id) => {
const person = people_by_user_id_dict.get(user_id);
if (!person) {
blueslip.error("Unknown user id in message", {user_id});
return "?";
}
return person.email;
});
emails.sort();
const reply_to = emails.join(",");
return reply_to;
2020-08-20 21:33:00 +02:00
}
function sorted_other_user_ids(user_ids: number[]): number[] {
2017-08-01 15:35:07 +02:00
// This excludes your own user id unless you're the only user
// (i.e. you sent a message to yourself).
2020-08-20 21:33:00 +02:00
const other_user_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
if (other_user_ids.length >= 1) {
user_ids = other_user_ids;
} else {
user_ids = [my_user_id];
}
user_ids = sort_numerically(user_ids);
return user_ids;
2017-08-01 15:35:07 +02:00
}
export function concat_huddle(user_ids: number[], user_id: number): string {
/*
We assume user_ids and user_id have already
been validated by the caller.
The only logic we're encapsulating here is
how to encode huddles.
*/
const sorted_ids = sort_numerically([...user_ids, user_id]);
return sorted_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function pm_lookup_key_from_user_ids(user_ids: number[]): string {
2017-08-01 15:51:56 +02:00
/*
The server will sometimes include our own user id
in keys for direct messages, but we only want our
user id if we sent a direct message to ourself.
2017-08-01 15:51:56 +02:00
*/
user_ids = sorted_other_user_ids(user_ids);
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
2017-08-01 15:51:56 +02:00
export function pm_lookup_key(user_ids_string: string): string {
const user_ids = split_to_ints(user_ids_string);
return pm_lookup_key_from_user_ids(user_ids);
}
export function all_user_ids_in_pm(message: Message): number[] | undefined {
if (message.type !== "private") {
return undefined;
2018-10-18 22:05:28 +02:00
}
assert(
typeof message.display_recipient !== "string",
"Private messages should have list of recipients",
);
2018-10-18 22:05:28 +02:00
if (message.display_recipient.length === 0) {
blueslip.error("Empty recipient list in message");
return undefined;
2018-10-18 22:05:28 +02:00
}
let user_ids = message.display_recipient.map((recip) => recip.id);
2018-10-18 22:05:28 +02:00
user_ids = sort_numerically(user_ids);
return user_ids;
2020-08-20 21:33:00 +02:00
}
2018-10-18 22:05:28 +02:00
export function pm_with_user_ids(
message: Message & {reply_to?: string; url?: string},
): number[] | undefined {
if (message.type !== "private") {
return undefined;
2017-08-01 15:35:07 +02:00
}
assert(
typeof message.display_recipient !== "string",
"Private messages should have list of recipients",
);
2017-08-01 15:35:07 +02:00
if (message.display_recipient.length === 0) {
blueslip.error("Empty recipient list in message");
return undefined;
2017-08-01 15:35:07 +02:00
}
const user_ids = message.display_recipient.map((recip) => recip.id);
2017-08-01 15:35:07 +02:00
return sorted_other_user_ids(user_ids);
2020-08-20 21:33:00 +02:00
}
export function pm_perma_link(message: Message): string | undefined {
2020-08-20 21:33:00 +02:00
const user_ids = all_user_ids_in_pm(message);
2018-10-18 22:05:28 +02:00
if (!user_ids) {
return undefined;
2018-10-18 22:05:28 +02:00
}
let suffix;
2018-10-18 22:05:28 +02:00
if (user_ids.length >= 3) {
suffix = "group";
2018-10-18 22:05:28 +02:00
} else {
suffix = "dm";
2018-10-18 22:05:28 +02:00
}
const slug = user_ids.join(",") + "-" + suffix;
const url = "#narrow/dm/" + slug;
return url;
2020-08-20 21:33:00 +02:00
}
2018-10-18 22:05:28 +02:00
export function pm_with_url(message: Message): string | undefined {
2020-08-20 21:33:00 +02:00
const user_ids = pm_with_user_ids(message);
if (!user_ids) {
return undefined;
}
let suffix;
if (user_ids.length > 1) {
suffix = "group";
} else {
const person = maybe_get_user_by_id(user_ids[0]);
if (person?.full_name) {
suffix = person.full_name.replaceAll(/[ "%/<>`\p{C}]+/gu, "-");
} else {
blueslip.error("Unknown people in message");
suffix = "unk";
}
}
const slug = user_ids.join(",") + "-" + suffix;
const url = "#narrow/dm/" + slug;
return url;
2020-08-20 21:33:00 +02:00
}
export function update_email_in_reply_to(
reply_to: string,
user_id: number,
new_email: string,
): string {
// We try to replace an old email with a new email in a reply_to,
// but we try to avoid changing the reply_to if we don't have to,
// and we don't warn on any errors.
let emails = reply_to.split(",");
const persons = util.try_parse_as_truthy(emails.map((email) => people_dict.get(email.trim())));
if (persons === undefined) {
return reply_to;
}
const needs_patch = persons.some((person) => person.user_id === user_id);
if (!needs_patch) {
return reply_to;
}
emails = persons.map((person) => {
if (person.user_id === user_id) {
return new_email;
}
return person.email;
});
return emails.join(",");
2020-08-20 21:33:00 +02:00
}
export function pm_with_operand_ids(operand: string): number[] | undefined {
let emails = operand.split(",");
emails = emails.map((email) => email.trim());
let persons = util.try_parse_as_truthy(emails.map((email) => people_dict.get(email)));
if (persons === undefined) {
return undefined;
}
// If your email is included in a group direct message with other people,
// then ignore it.
if (persons.length > 1) {
const my_user = people_by_user_id_dict.get(my_user_id);
persons = persons.filter((person) => person !== my_user);
}
let user_ids = persons.map((person) => person.user_id);
user_ids = sort_numerically(user_ids);
return user_ids;
2020-08-20 21:33:00 +02:00
}
export function emails_to_slug(emails_string: string): string | undefined {
2020-08-20 21:33:00 +02:00
let slug = reply_to_to_user_ids_string(emails_string);
if (!slug) {
return undefined;
}
slug += "-";
const emails = emails_string.split(",");
if (emails.length === 1) {
const person = get_by_email(emails[0]);
assert(person !== undefined, "Unknown person in emails_to_slug");
const name = person.full_name;
slug += name.replaceAll(/[ "%/<>`\p{C}]+/gu, "-");
} else {
slug += "group";
}
return slug;
2020-08-20 21:33:00 +02:00
}
export function slug_to_emails(slug: string): string | undefined {
/*
It's not super important to be flexible about
direct message related slugs, since you would
rarely post them to the web, but we do want
to support reasonable variations:
99-alice@example.com
99
Our canonical version is 99-alice@example.com,
and we only care about the "99" prefix.
*/
const m = /^([\d,]+)(-.*)?/.exec(slug);
if (m) {
let user_ids_string = m[1];
2020-08-20 21:33:00 +02:00
user_ids_string = exclude_me_from_string(user_ids_string);
return user_ids_string_to_emails_string(user_ids_string);
}
/* istanbul ignore next */
return undefined;
2020-08-20 21:33:00 +02:00
}
export function exclude_me_from_string(user_ids_string: string): string {
// Exclude me from a user_ids_string UNLESS I'm the
// only one in it.
let user_ids = split_to_ints(user_ids_string);
if (user_ids.length <= 1) {
// We either have a message to ourself, an empty
// slug, or a message to somebody else where we weren't
// part of the slug.
return user_ids.join(",");
}
2020-08-20 21:33:00 +02:00
user_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
return user_ids.join(",");
2020-08-20 21:33:00 +02:00
}
export function format_small_avatar_url(raw_url: string): string {
const url = new URL(raw_url, location.origin);
url.search += (url.search ? "&" : "") + "s=50";
return url.href;
2020-08-20 21:33:00 +02:00
}
export function sender_is_bot(message: Message): boolean {
if (message.sender_id) {
2020-08-20 21:33:00 +02:00
const person = get_by_user_id(message.sender_id);
return person.is_bot;
}
return false;
2020-08-20 21:33:00 +02:00
}
export function sender_is_guest(message: Message): boolean {
if (message.sender_id) {
2020-08-20 21:33:00 +02:00
const person = get_by_user_id(message.sender_id);
return person.is_guest;
}
return false;
2020-08-20 21:33:00 +02:00
}
export function user_is_bot(user_id: number): boolean {
const user = get_by_user_id(user_id);
return user.is_bot;
}
export function should_add_guest_user_indicator(user_id: number): boolean {
if (!page_params.realm_enable_guest_user_indicator) {
return false;
}
const user = get_by_user_id(user_id);
return user.is_guest;
}
export function user_can_direct_message(recipient_ids_string: string): boolean {
// Common function for checking if a user can send a direct
// message to the target user (or group of users) represented by a
// user ids string.
// Regardless of policy, we allow sending direct messages to bots.
const recipient_ids = user_ids_string_to_ids_array(recipient_ids_string);
if (recipient_ids.length === 1 && user_is_bot(recipient_ids[0])) {
return true;
}
if (
page_params.realm_private_message_policy ===
settings_config.private_message_policy_values.disabled.code
) {
return false;
}
return true;
}
function gravatar_url_for_email(email: string): string {
const hash = md5(email.toLowerCase());
const avatar_url = "https://secure.gravatar.com/avatar/" + hash + "?d=identicon";
2020-08-20 21:33:00 +02:00
const small_avatar_url = format_small_avatar_url(avatar_url);
return small_avatar_url;
}
export function small_avatar_url_for_person(person: User): string {
if (person.avatar_url) {
2020-08-20 21:33:00 +02:00
return format_small_avatar_url(person.avatar_url);
}
if (person.avatar_url === null) {
return gravatar_url_for_email(person.email);
}
return format_small_avatar_url(`/avatar/${person.user_id}`);
2020-08-20 21:33:00 +02:00
}
function medium_gravatar_url_for_email(email: string): string {
const hash = md5(email.toLowerCase());
const avatar_url = "https://secure.gravatar.com/avatar/" + hash + "?d=identicon";
const url = new URL(avatar_url, location.origin);
url.search += (url.search ? "&" : "") + "s=500";
return url.href;
}
export function medium_avatar_url_for_person(person: User): string {
/* Unlike the small avatar URL case, we don't generally have a
* medium avatar URL included in person objects. So only have the
* gravatar and server endpoints here. */
if (person.avatar_url === null) {
return medium_gravatar_url_for_email(person.email);
}
// We need to attach a version to the URL as a cache-breaker so that the browser
// will update the image in real time when user uploads a new avatar.
//
// TODO: Newly created users sometimes are first learned about via
// the report_late_add code path; these are missing the avatar_version
// metadata. Long term, we should clean up that possibility, but
// until it is, we fallback to using a version number of 0.
return `/avatar/${person.user_id}/medium?version=${person.avatar_version ?? 0}`;
}
export function sender_info_for_recent_view_row(sender_ids: number[]): SenderInfo[] {
const senders_info = [];
for (const id of sender_ids) {
// TODO: Better handling for optional values w/o the assertion.
const person = get_by_user_id(id)!;
const sender: SenderInfo = {
...person,
avatar_url_small: small_avatar_url_for_person(person),
is_muted: muted_users.is_user_muted(id),
};
senders_info.push(sender);
}
return senders_info;
2020-08-20 21:33:00 +02:00
}
export function small_avatar_url(message: Message): string {
2017-01-21 20:29:39 +01:00
// Try to call this function in all places where we need 25px
// avatar images, so that the browser can help
// us avoid unnecessary network trips. (For user-uploaded avatars,
// the s=25 parameter is essentially ignored, but it's harmless.)
//
// We actually request these at s=50, so that we look better
// on retina displays.
let person;
if (message.sender_id) {
// We should always have message.sender_id, except for in the
// tutorial, where it's ok to fall back to the URL in the fake
// messages.
person = maybe_get_user_by_id(message.sender_id);
}
// The first time we encounter a sender in a message, we may
// not have person.avatar_url set, but if we do, then use that.
if (person?.avatar_url) {
2020-08-20 21:33:00 +02:00
return small_avatar_url_for_person(person);
}
// Try to get info from the message if we didn't have a `person` object
// or if the avatar was missing. We do this verbosely to avoid false
// positives on line coverage (we don't do branch checking).
if (message.avatar_url) {
2020-08-20 21:33:00 +02:00
return format_small_avatar_url(message.avatar_url);
}
if (person && person.avatar_url === undefined) {
// If we don't have an avatar_url at all, we use `GET
// /avatar/{user_id}` endpoint to obtain avatar url. This is
// required to take advantage of the user_avatar_url_field_optional
// optimization, which saves a huge amount of network traffic on
// servers with 10,000s of user accounts.
return format_small_avatar_url(`/avatar/${person.user_id}`);
}
// For computing the user's email, we first trust the person
// object since that is updated via our real-time sync system, but
// if unavailable, we use the sender email.
let email;
if (person) {
email = person.email;
} else {
email = message.sender_email;
}
return gravatar_url_for_email(email);
2020-08-20 21:33:00 +02:00
}
2017-01-21 20:29:39 +01:00
export function is_valid_email_for_compose(email: string): boolean {
2020-08-20 21:33:00 +02:00
if (is_cross_realm_email(email)) {
return true;
}
2020-08-20 21:33:00 +02:00
const person = get_by_email(email);
if (!person) {
return false;
}
// we allow deactivated users in compose so that
// one can attempt to reply to threads that contained them.
return true;
2020-08-20 21:33:00 +02:00
}
export function is_valid_bulk_emails_for_compose(emails: string[]): boolean {
// Returns false if at least one of the emails is invalid.
return emails.every((email) => {
2020-08-20 21:33:00 +02:00
if (!is_valid_email_for_compose(email)) {
return false;
}
return true;
});
2020-08-20 21:33:00 +02:00
}
export function is_active_user_for_popover(user_id: number): boolean {
// For popover menus, we include cross-realm bots as active
// users.
if (cross_realm_dict.get(user_id)) {
return true;
}
if (active_user_dict.has(user_id)) {
return true;
}
// TODO: We can report errors here once we start loading
// deactivated users at page-load time. For now just warn.
if (!people_by_user_id_dict.has(user_id)) {
blueslip.warn("Unexpectedly invalid user_id in user popover query", {user_id});
}
return false;
2020-08-20 21:33:00 +02:00
}
export function is_current_user_only_owner(): boolean {
if (!page_params.is_owner || page_params.is_bot) {
return false;
}
let active_owners = 0;
for (const person of active_user_dict.values()) {
if (person.is_owner && !person.is_bot) {
active_owners += 1;
}
if (active_owners > 1) {
return false;
}
}
return true;
}
export function filter_all_persons(pred: (person: User) => boolean): User[] {
const ret = [];
for (const person of people_by_user_id_dict.values()) {
if (pred(person)) {
ret.push(person);
}
}
return ret;
2020-08-20 21:33:00 +02:00
}
2016-11-01 19:36:50 +01:00
export function filter_all_users(pred: (person: User) => boolean): User[] {
const ret = [];
for (const person of active_user_dict.values()) {
if (pred(person)) {
ret.push(person);
}
}
return ret;
2020-08-20 21:33:00 +02:00
}
export function get_realm_users(): User[] {
// includes humans and bots from your realm
return [...active_user_dict.values()];
2020-08-20 21:33:00 +02:00
}
2016-11-01 19:45:53 +01:00
export function get_realm_active_human_users(): User[] {
// includes ONLY humans from your realm
const humans = [];
for (const user of active_user_dict.values()) {
if (!user.is_bot) {
humans.push(user);
}
}
return humans;
}
export function get_realm_active_human_user_ids(): number[] {
const human_ids = [];
for (const user of active_user_dict.values()) {
if (!user.is_bot) {
human_ids.push(user.user_id);
}
}
return human_ids;
2020-08-20 21:33:00 +02:00
}
export function get_non_active_human_ids(): number[] {
const human_ids = [];
for (const user of non_active_user_dict.values()) {
if (!user.is_bot) {
human_ids.push(user.user_id);
}
}
return human_ids;
2020-08-20 21:33:00 +02:00
}
export function get_bot_ids(): number[] {
const bot_ids = [];
for (const user of people_by_user_id_dict.values()) {
if (user.is_bot) {
bot_ids.push(user.user_id);
}
}
return bot_ids;
}
export function get_active_human_count(): number {
let count = 0;
for (const person of active_user_dict.values()) {
if (!person.is_bot) {
count += 1;
}
}
return count;
2020-08-20 21:33:00 +02:00
}
export function get_active_user_ids(): number[] {
// This includes active users and active bots.
return [...active_user_dict.keys()];
2020-08-20 21:33:00 +02:00
}
export function get_non_active_realm_users(): User[] {
return [...non_active_user_dict.values()];
2020-08-20 21:33:00 +02:00
}
export function is_cross_realm_email(email: string): boolean {
2020-08-20 21:33:00 +02:00
const person = get_by_email(email);
if (!person) {
return false;
}
return cross_realm_dict.has(person.user_id);
2020-08-20 21:33:00 +02:00
}
export function get_recipient_count(person: User | PseudoMentionUser): number {
// We can have fake person objects like the "all"
// pseudo-person in at-mentions. They will have
// the pm_recipient_count on the object itself.
if ("pm_recipient_count" in person) {
return person.pm_recipient_count;
}
/*
For searching in the search bar, we will
have true `person` objects with `user_id`.
Likewise, we'll have user_id if we are
tab-completing a user to send a direct message
to (but we only get called if we're not
currently in a stream view).
Finally, we'll have user_id if we are adding
people to a stream (w/typeahead).
*/
const count = pm_recipient_count_dict.get(person.user_id);
return count ?? 0;
2020-08-20 21:33:00 +02:00
}
export function incr_recipient_count(user_id: number): void {
const old_count = pm_recipient_count_dict.get(user_id) ?? 0;
pm_recipient_count_dict.set(user_id, old_count + 1);
2020-08-20 21:33:00 +02:00
}
export function clear_recipient_counts_for_testing(): void {
pm_recipient_count_dict.clear();
}
export function set_recipient_count_for_testing(user_id: number, count: number): void {
pm_recipient_count_dict.set(user_id, count);
}
export function get_message_people(): User[] {
/*
message_people are roughly the people who have
actually sent messages that are currently
showing up on your feed. These people
are important--we give them preference
over other users in certain search
suggestions, since non-message-people are
presumably either not very active or
possibly subscribed to streams you don't
care about.
message_people also includes people whom
you have sent direct messages, but look at
the message_store code to see the precise
semantics
*/
const message_people = util.try_parse_as_truthy(
message_user_ids
.user_ids()
.map((user_id) => people_by_user_id_dict.get(user_id))
.filter(Boolean),
);
return message_people ?? [];
2020-08-20 21:33:00 +02:00
}
export function get_active_message_people(): User[] {
2020-08-20 21:33:00 +02:00
const message_people = get_message_people();
const active_message_people = message_people.filter((item) =>
active_user_dict.has(item.user_id),
);
return active_message_people;
2020-08-20 21:33:00 +02:00
}
export function get_people_for_search_bar(query: string): User[] {
2020-08-20 21:33:00 +02:00
const pred = build_person_matcher(query);
2020-08-20 21:33:00 +02:00
const message_people = get_message_people();
const small_results = message_people.filter((item) => pred(item));
if (small_results.length >= 5) {
return small_results;
}
2020-08-20 21:33:00 +02:00
return filter_all_persons(pred);
}
export function build_termlet_matcher(termlet: string): (user: User) => boolean {
termlet = termlet.trim();
const is_ascii = /^[a-z]+$/.test(termlet);
return function (user: User): boolean {
let full_name = user.full_name;
if (is_ascii) {
// Only ignore diacritics if the query is plain ascii
full_name = typeahead.remove_diacritics(full_name);
}
const names = full_name.toLowerCase().split(" ");
return names.some((name) => name.startsWith(termlet));
};
2020-08-20 21:33:00 +02:00
}
export function build_person_matcher(query: string): (user: User) => boolean {
query = query.trim();
const termlets = query.toLowerCase().split(/\s+/);
const termlet_matchers = termlets.map((termlet) => build_termlet_matcher(termlet));
return function (user: User): boolean {
const email = user.email.toLowerCase();
if (email.startsWith(query)) {
return true;
}
return termlet_matchers.every((matcher) => matcher(user));
};
2020-08-20 21:33:00 +02:00
}
export function filter_people_by_search_terms(
users: User[],
search_terms: string[],
): Map<number, User> {
const filtered_users = new Map();
// Build our matchers outside the loop to avoid some
// search overhead that is not user-specific.
2020-08-20 21:33:00 +02:00
const matchers = search_terms.map((search_term) => build_person_matcher(search_term));
// Loop through users and populate filtered_users only
// if they include search_terms
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const user of users) {
2020-08-20 21:33:00 +02:00
const person = get_by_email(user.email);
// Get person object (and ignore errors)
if (!person?.full_name) {
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
continue;
}
// Return user emails that include search terms
const match = matchers.some((matcher) => matcher(user));
if (match) {
filtered_users.set(person.user_id, true);
}
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
return filtered_users;
2020-08-20 21:33:00 +02:00
}
export const is_valid_full_name_and_user_id = (full_name: string, user_id: number): boolean => {
const person = people_by_user_id_dict.get(user_id);
if (!person) {
return false;
}
return person.full_name === full_name;
};
export const get_actual_name_from_user_id = (user_id: number): string | undefined => {
/*
If you are dealing with user-entered data, you
should validate the user_id BEFORE calling
this function.
*/
const person = people_by_user_id_dict.get(user_id);
if (!person) {
blueslip.error("Unknown user_id", {user_id});
return undefined;
}
return person.full_name;
};
export function get_user_id_from_name(full_name: string): number | undefined {
// get_user_id_from_name('Alice Smith') === 42
/*
This function is intended to be called
with a full name that is user-entered, such
a full name from a user mention.
We will only return a **unique** user_id
here. For duplicate names, our UI should
force users to disambiguate names with a
user_id and then call is_valid_full_name_and_user_id
to make sure the combo is valid. This is
exactly what we do with mentions.
*/
const person = people_by_name_dict.get(full_name);
if (!person) {
return undefined;
}
2020-08-20 21:33:00 +02:00
if (is_duplicate_full_name(full_name)) {
return undefined;
}
return person.user_id;
2020-08-20 21:33:00 +02:00
}
export function track_duplicate_full_name(
full_name: string,
user_id: number,
to_remove?: boolean,
): void {
let ids: Set<number>;
if (duplicate_full_name_data.has(full_name)) {
// TODO: Better handling for optional values w/o the assertion.
ids = duplicate_full_name_data.get(full_name)!;
} else {
ids = new Set();
}
if (!to_remove && user_id) {
ids.add(user_id);
}
if (to_remove && user_id) {
ids.delete(user_id);
}
duplicate_full_name_data.set(full_name, ids);
2020-08-20 21:33:00 +02:00
}
export function is_duplicate_full_name(full_name: string): boolean {
const ids = duplicate_full_name_data.get(full_name);
return ids !== undefined && ids.size > 1;
2020-08-20 21:33:00 +02:00
}
export function get_mention_syntax(full_name: string, user_id: number, silent: boolean): string {
let mention = "";
if (silent) {
mention += "@_**";
} else {
mention += "@**";
}
mention += full_name;
if (!user_id) {
blueslip.warn("get_mention_syntax called without user_id.");
}
if (
(is_duplicate_full_name(full_name) || full_name_matches_wildcard_mention(full_name)) &&
user_id
) {
mention += `|${user_id}`;
}
mention += "**";
return mention;
2020-08-20 21:33:00 +02:00
}
function full_name_matches_wildcard_mention(full_name: string): boolean {
return ["all", "everyone", "stream"].includes(full_name);
}
export function _add_user(person: User): void {
/*
This is common code to add any user, even
users who may be deactivated or outside
our realm (like cross-realm bots).
*/
person.is_moderator = false;
if (person.role === settings_config.user_role_values.moderator.code) {
person.is_moderator = true;
}
if (person.user_id) {
people_by_user_id_dict.set(person.user_id, person);
} else {
// We eventually want to lock this down completely
// and report an error and not update other the data
// structures here, but we have a lot of edge cases
// with cross-realm bots, zephyr users, etc., deactivated
// users, where we are probably fine for now not to
// find them via user_id lookups.
blueslip.warn("No user_id provided", {email: person.email});
}
2020-08-20 21:33:00 +02:00
track_duplicate_full_name(person.full_name, person.user_id);
people_dict.set(person.email, person);
people_by_name_dict.set(person.full_name, person);
2020-08-20 21:33:00 +02:00
}
export function add_active_user(person: User): void {
active_user_dict.set(person.user_id, person);
2020-08-20 21:33:00 +02:00
_add_user(person);
non_active_user_dict.delete(person.user_id);
2020-08-20 21:33:00 +02:00
}
export const is_person_active = (user_id: number): boolean => {
if (!people_by_user_id_dict.has(user_id)) {
blueslip.error("No user found", {user_id});
}
if (cross_realm_dict.has(user_id)) {
return true;
}
return active_user_dict.has(user_id);
};
export function add_cross_realm_user(person: CrossRealmBot): void {
if (!people_dict.has(person.email)) {
2020-08-20 21:33:00 +02:00
_add_user(person);
}
cross_realm_dict.set(person.user_id, person);
2020-08-20 21:33:00 +02:00
}
export function deactivate(person: User): void {
// We don't fully remove a person from all of our data
// structures, because deactivated users can be part
// of somebody's direct message list.
active_user_dict.delete(person.user_id);
non_active_user_dict.set(person.user_id, person);
2020-08-20 21:33:00 +02:00
}
export function report_late_add(user_id: number, email: string): void {
// If the events system is not running, then it is expected that
// we will fetch messages from the server that were sent by users
// who don't exist in our users data set. This can happen because
// we're in the middle of a reload (and thus stopped our event
// queue polling) or because we are a spectator and never had an
// event queue in the first place.
if (reload_state.is_in_progress() || page_params.is_spectator) {
blueslip.log("Added user late", {user_id, email});
} else if (!settings_data.user_can_access_all_other_users()) {
blueslip.log("Message was sent by an inaccessible user", {user_id});
} else {
blueslip.error("Added user late", {user_id, email});
}
2020-08-20 21:33:00 +02:00
}
export function make_user(user_id: number, email: string, full_name: string): User {
// Used to create fake user objects for users who we see via some
// API call, such as fetching a message sent by the user, before
// we receive a full user object for the user via the events
// system.
//
// This function is an ugly hack in that it makes up a lot of
// values, but usually thesefake user objects only persist for
// less than a second before being replaced by a real user when
// the events system receives the user-created event for the new
// or newly visible user.
return {
user_id,
email,
full_name,
role: settings_config.user_role_values.member.code,
is_active: true,
is_admin: false,
is_owner: false,
is_guest: false,
is_bot: false,
is_moderator: false,
is_billing_admin: false,
// We explicitly don't set `avatar_url` for fake person objects so that fallback code
// will ask the server or compute a gravatar URL only once we need the avatar URL,
// it's important for performance that we not hash every user's email to get gravatar URLs.
avatar_url: undefined,
avatar_version: 0,
timezone: "",
date_joined: "",
delivery_email: null,
profile_data: {},
bot_type: null,
// This may lead to cases where this field is set to
// true for an accessible user also and such user would
// not be shown in the right sidebar for some time till
// the user's correct data is received from the server.
is_inaccessible_user: !settings_data.user_can_access_all_other_users(),
// This property allows us to distinguish actual server person
// objects from fake person objects generated by this function.
is_missing_server_data: true,
};
}
export function add_inaccessible_user(user_id: number): User {
const email = "user" + user_id + "@" + page_params.realm_bot_domain;
const unknown_user = make_user(user_id, email, INACCESSIBLE_USER_NAME);
_add_user(unknown_user);
return unknown_user;
}
export function get_user_by_id_assert_valid(
user_id: number,
allow_missing_user: boolean = !settings_data.user_can_access_all_other_users(),
): User {
if (!allow_missing_user) {
return get_by_user_id(user_id);
}
let person = maybe_get_user_by_id(user_id, true);
if (person === undefined) {
person = add_inaccessible_user(user_id);
}
return person;
}
function get_involved_people(message: MessageWithBooleans): DisplayRecipientUser[] {
let involved_people: DisplayRecipientUser[];
switch (message.type) {
case "stream":
involved_people = [
{
full_name: message.sender_full_name,
id: message.sender_id,
email: message.sender_email,
is_mirror_dummy: false,
},
];
break;
case "private":
assert(
typeof message.display_recipient !== "string",
"Private messages should have list of recipients",
);
involved_people = message.display_recipient;
break;
default:
involved_people = [];
}
return involved_people;
}
export function extract_people_from_message(message: MessageWithBooleans): void {
const involved_people = get_involved_people(message);
// Add new people involved in this message to the people list
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const person of involved_people) {
if (person.unknown_local_echo_user) {
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
continue;
}
const user_id = person.id;
if (people_by_user_id_dict.has(user_id)) {
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
continue;
}
2020-08-20 21:33:00 +02:00
report_late_add(user_id, person.email);
_add_user(make_user(user_id, person.email, person.full_name));
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
2020-08-20 21:33:00 +02:00
}
function safe_lower(s?: string | null): string {
return (s ?? "").toLowerCase();
}
export function matches_user_settings_search(person: User, value: string): boolean {
const email = person.delivery_email;
return safe_lower(person.full_name).includes(value) || safe_lower(email).includes(value);
2020-08-20 21:33:00 +02:00
}
export function filter_for_user_settings_search(persons: User[], query: string): User[] {
/*
TODO: For large realms, we can optimize this a couple
different ways. For realms that don't show
emails, we can make a simpler filter predicate
that works solely with full names. And we can
also consider two-pass filters that try more
stingy criteria first, such as exact prefix
matches, before widening the search.
See #13554 for more context.
*/
2020-08-20 21:33:00 +02:00
return persons.filter((person) => matches_user_settings_search(person, query));
}
export function maybe_incr_recipient_count(
message: MessageWithBooleans & {sent_by_me: boolean},
): void {
if (message.type !== "private") {
return;
}
assert(
typeof message.display_recipient !== "string",
"Private messages should have list of recipients",
);
if (!message.sent_by_me) {
return;
}
// Track the number of direct messages we've sent to this person
// to improve autocomplete
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const recip of message.display_recipient) {
if (recip.unknown_local_echo_user) {
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
continue;
}
const user_id = recip.id;
2020-08-20 21:33:00 +02:00
incr_recipient_count(user_id);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
2020-08-20 21:33:00 +02:00
}
export function set_full_name(person_obj: User, new_full_name: string): void {
2017-01-21 00:02:28 +01:00
if (people_by_name_dict.has(person_obj.full_name)) {
people_by_name_dict.delete(person_obj.full_name);
2017-01-21 00:02:28 +01:00
}
// Remove previous and add new full name to the duplicate full name tracker.
2020-08-20 21:33:00 +02:00
track_duplicate_full_name(person_obj.full_name, person_obj.user_id, true);
track_duplicate_full_name(new_full_name, person_obj.user_id);
2017-01-21 00:02:28 +01:00
people_by_name_dict.set(new_full_name, person_obj);
person_obj.full_name = new_full_name;
2020-08-20 21:33:00 +02:00
}
2017-01-21 00:02:28 +01:00
export function set_custom_profile_field_data(
user_id: number,
field: {id: number} & ProfileData,
): void {
if (field.id === undefined) {
blueslip.error("Trying to set undefined field id");
return;
}
const person = get_by_user_id(user_id);
person.profile_data[field.id] = {
value: field.value,
rendered_value: field.rendered_value,
};
2020-08-20 21:33:00 +02:00
}
export function is_current_user(email?: string | null): boolean {
if (email === null || email === undefined || page_params.is_spectator) {
return false;
}
2020-08-20 21:33:00 +02:00
return email.toLowerCase() === my_current_email().toLowerCase();
}
export function initialize_current_user(user_id: number): void {
my_user_id = user_id;
2020-08-20 21:33:00 +02:00
}
export function my_full_name(): string {
const person = get_by_user_id(my_user_id);
return person.full_name;
2020-08-20 21:33:00 +02:00
}
export function my_current_email(): string {
const person = get_by_user_id(my_user_id);
return person.email;
2020-08-20 21:33:00 +02:00
}
export function my_current_user_id(): number {
return my_user_id;
2020-08-20 21:33:00 +02:00
}
export function my_custom_profile_data(field_id: number): ProfileData | null | undefined {
if (field_id === undefined) {
blueslip.error("Undefined field id");
return undefined;
}
2020-08-20 21:33:00 +02:00
return get_custom_profile_data(my_user_id, field_id);
}
export function get_custom_profile_data(user_id: number, field_id: number): ProfileData | null {
const person = get_by_user_id(user_id);
const profile_data = person.profile_data;
if (profile_data === undefined) {
return null;
}
return profile_data[field_id];
2020-08-20 21:33:00 +02:00
}
export function get_custom_fields_by_type(
user_id: number,
field_type: number,
): ProfileData[] | null {
const person = get_by_user_id(user_id);
const profile_data = person.profile_data;
if (profile_data === undefined) {
return null;
}
const filteredProfileData: ProfileData[] = [];
for (const field of page_params.custom_profile_fields) {
if (field.type === field_type) {
filteredProfileData.push(profile_data[field.id]);
}
}
return filteredProfileData;
}
export function is_my_user_id(user_id: number | string): boolean {
if (!user_id) {
return false;
}
if (typeof user_id !== "number") {
blueslip.error("user_id is a string in my_user_id", {user_id});
user_id = Number.parseInt(user_id, 10);
}
return user_id === my_user_id;
2020-08-20 21:33:00 +02:00
}
export function compare_by_name(a: User, b: User): number {
return util.strcmp(a.full_name, b.full_name);
}
export function sort_but_pin_current_user_on_top(users: User[]): void {
const my_user = get_by_user_id(my_user_id);
if (users.includes(my_user)) {
users.splice(users.indexOf(my_user), 1);
users.sort(compare_by_name);
users.unshift(my_user);
} else {
users.sort(compare_by_name);
}
}
export function initialize(my_user_id: number, params: PeopleParams): void {
for (const person of params.realm_users) {
2020-08-20 21:33:00 +02:00
add_active_user(person);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
for (const person of params.realm_non_active_users) {
non_active_user_dict.set(person.user_id, person);
2020-08-20 21:33:00 +02:00
_add_user(person);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
for (const person of params.cross_realm_bots) {
2020-08-20 21:33:00 +02:00
add_cross_realm_user(person);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
2020-08-20 21:33:00 +02:00
initialize_current_user(my_user_id);
}