Skip to content

Commit 2e0882d

Browse files
committed
add support for 'great invitations' as mod_invites
1 parent b1ab280 commit 2e0882d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+4450
-40
lines changed

include/mod_invites.hrl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-define(INVITE_TOKEN_EXPIRE_SECONDS_DEFAULT, 5*86400).
2+
-define(INVITE_TOKEN_LENGTH_DEFAULT, 24).
3+
4+
-define(NS_INVITE_INVITE, <<"urn:xmpp:invite#invite">>).
5+
-define(NS_INVITE_CREATE_ACCOUNT, <<"urn:xmpp:invite#create-account">>).
6+
7+
-record(invite_token, {token :: binary(),
8+
inviter :: {binary(), binary()},
9+
%% A non-empty value if `invitee` indicates the invite has been used.
10+
invitee = <<>> :: binary(),
11+
created_at = calendar:now_to_datetime(erlang:timestamp()) :: calendar:datetime(),
12+
expires = calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(erlang:timestamp())) + ?INVITE_TOKEN_EXPIRE_SECONDS_DEFAULT) :: calendar:datetime(),
13+
type = roster_only :: roster_only | account_only | account_subscription,
14+
%% If type is 'roster_only' then we indicate a token has been used to create
15+
%% an account (if allowed) by setting `account_name` to the name of the user
16+
%% (which should match `invitee`).
17+
account_name = <<>> :: binary()
18+
}).

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ defmodule Ejabberd.MixProject do
108108
[{:cache_tab, "~> 1.0"},
109109
{:dialyxir, "~> 1.2", only: [:test], runtime: false},
110110
{:eimp, "~> 1.0"},
111+
{:erlydtl, git: "https://github.com/erlydtl/erlydtl", tag: "0.15.0", override: true},
111112
{:ex_doc, "~> 0.31", only: [:edoc], runtime: false},
112113
{:fast_tls, "~> 1.1.24"},
113114
{:fast_xml, "~> 1.1.56"},
@@ -119,7 +120,7 @@ defmodule Ejabberd.MixProject do
119120
{:p1_utils, "~> 1.0"},
120121
{:pkix, "~> 1.0"},
121122
{:stringprep, ">= 1.0.26"},
122-
{:xmpp, git: "https://github.com/processone/xmpp", ref: "7285aa7802bfa90bcefafdad3a342fbb93ce7eea", override: true},
123+
{:xmpp, git: "https://github.com/processone/xmpp", tag: "1.11.4", override: true},
123124
{:yconf, ">= 1.0.22"}]
124125
++ cond_deps()
125126
end

mix.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"epam": {:hex, :epam, "1.0.14", "aa0b85d27f4ef3a756ae995179df952a0721237e83c6b79d644347b75016681a", [:rebar3], [], "hexpm", "2f3449e72885a72a6c2a843f561add0fc2f70d7a21f61456930a547473d4d989"},
88
"eredis": {:hex, :eredis, "1.7.1", "39e31aa02adcd651c657f39aafd4d31a9b2f63c6c700dc9cece98d4bc3c897ab", [:mix, :rebar3], [], "hexpm", "7c2b54c566fed55feef3341ca79b0100a6348fd3f162184b7ed5118d258c3cc1"},
99
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
10+
"erlydtl": {:git, "https://github.com/erlydtl/erlydtl", "aae414692b6052e96d890e03bbeeeca0f4dc01c2", [tag: "0.15.0"]},
1011
"esip": {:hex, :esip, "1.0.59", "eb202f8c62928193588091dfedbc545fe3274c34ecd209961f86dcb6c9ebce88", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.21", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "0bdf2e3c349dc0b144f173150329e675c6a51ac473d7a0b2e362245faad3fbe6"},
1112
"ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"},
1213
"exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
@@ -34,6 +35,6 @@
3435
"stringprep": {:hex, :stringprep, "1.0.33", "22f42866b4f6f3c238ea2b9cb6241791184ddedbab55e94a025511f46325f3ca", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "96f8b30bc50887f605b33b46bca1d248c19a879319b8c482790e3b4da5da98c0"},
3536
"stun": {:hex, :stun, "1.2.21", "735855314ad22cb7816b88597d2f5ca22e24aa5e4d6010a0ef3affb33ceed6a5", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "3d7fe8efb9d05b240a6aa9a6bf8b8b7bff2d802895d170443c588987dc1e12d9"},
3637
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
37-
"xmpp": {:git, "https://github.com/processone/xmpp", "7285aa7802bfa90bcefafdad3a342fbb93ce7eea", [ref: "7285aa7802bfa90bcefafdad3a342fbb93ce7eea"]},
38+
"xmpp": {:git, "https://github.com/processone/xmpp", "f96c9adde9841bdeb184740857bddd60d3f51ab7", [tag: "1.11.4"]},
3839
"yconf": {:hex, :yconf, "1.0.22", "52a435f9b60ab1e13950dfe3f7131ecdd8b3d1ca72c44bf66fc74b4571027124", [:rebar3], [{:fast_yaml, "1.0.39", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "aca83457ceabe70756484b5c87ba7b1955f511d499168687eaeaa7c300e857f1"},
3940
}

priv/mod_invites/apps.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<div class="container">
2+
<div class="row">
3+
{% for item in apps %}
4+
<div class="card m-3 client-card {% for platform in item.platforms %}app-platform-{{ platform|lower }} {% endfor %} flex-wrap col-sm-12 col-md-8 col-lg-5">
5+
<div class="row no-gutters h-100">
6+
<div class="col-md-4">
7+
<img src="{{ static }}/{{ item.image }}" class="p-2 img-fluid" alt="{{ item.imagetext }}">
8+
</div>
9+
<div class="col-md-8">
10+
<div class="card-body d-flex flex-column h-100">
11+
<h5 class="card-title text-nowrap mb-1">{{ item.name }}</h5>
12+
<div>
13+
{% for platform in item.platforms %}<span class="badge badge-info client-platform-badge client-platform-badge-{{ platform|lower }} mr-1 mb-3">{{ platform }}</span>{% endfor %}
14+
</div>
15+
<p class="card-text">{{ item.text }}</p>
16+
<a href="{{ item.proceed_url }}" class="btn btn-primary mt-md-auto">{% if item.select_text %}{{ item.select_text }}{% else %}{% trans "Select" %}{% endif %}</a>
17+
</div>
18+
</div>
19+
</div>
20+
</div>
21+
{% endfor %}
22+
</div>
23+
</div>
24+
<div id="show-all-clients-button-container" class="d-none alert alert-info">
25+
{% trans "Showing apps for <span class='platform-name'>your current platform</span> only. You may also <a href='#' id='show-all-clients-button'>view all apps.</a>" %}
26+
</div>

priv/mod_invites/apps.json

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
[
2+
{
3+
"download": {
4+
"buttons": [
5+
{
6+
"image": "{{ static }}/logos/google_ps.png",
7+
"url": "https://play.google.com/store/apps/details?id=eu.siacs.conversations",
8+
"magic_link_format": "https://play.google.com/store/apps/details?id=eu.siacs.conversations&referrer={{ uri }}"
9+
},
10+
{
11+
"image": "{{ static }}/logos/fdroid.png",
12+
"url": "https://f-droid.org/en/packages/eu.siacs.conversations/",
13+
"magic_link_format": "https://f-droid.org/packages/eu.siacs.conversations/"
14+
}
15+
]
16+
},
17+
"image": "logos/conversations.svg",
18+
"link": "https://play.google.com/store/apps/details?id=eu.siacs.conversations",
19+
"magic_link_format": "https://play.google.com/store/apps/details?id=eu.siacs.conversations&referrer={{ uri }}",
20+
"name": "Conversations",
21+
"platforms": [
22+
"Android"
23+
],
24+
"supports_preauth_uri": true,
25+
"text": "{% trans "Conversations is a Jabber/XMPP client for Android 6.0+ smartphones that has been optimized to provide a unique mobile experience." %}"
26+
},
27+
{
28+
"download": {
29+
"buttons": [
30+
{
31+
"image": "{{ static }}/logos/apple_as.svg",
32+
"target": "_blank",
33+
"url": "https://apps.apple.com/app/id317711500"
34+
}
35+
]
36+
},
37+
"image": "logos/monal-tmp.svg",
38+
"link": "https://monal-im.org/",
39+
"name": "Monal",
40+
"platforms": [
41+
"iOS", "iPadOS"
42+
],
43+
"supports_preauth_uri": true,
44+
"text": "{% trans "A modern open-source chat client for iPhone and iPad. It is easy to use and has a clean user interface." %}"
45+
},
46+
{
47+
"download": {
48+
"buttons": [
49+
{
50+
"image": "{{ static }}/logos/apple_as.svg",
51+
"target": "_blank",
52+
"url": "https://apps.apple.com/app/id1637078500"
53+
}
54+
]
55+
},
56+
"image": "logos/monal-tmp.svg",
57+
"link": "https://monal-im.org/",
58+
"name": "Monal (macOS)",
59+
"platforms": [
60+
"macOS"
61+
],
62+
"supports_preauth_uri": true,
63+
"text": "{% trans "A modern open-source chat client for Mac. It is easy to use and has a clean user interface." %}"
64+
},
65+
{
66+
"download": {
67+
"buttons": [
68+
{
69+
"image": "{{ static }}/logos/apple_as.svg",
70+
"target": "_blank",
71+
"url": "https://apps.apple.com/us/app/siskin-im/id1153516838"
72+
}
73+
]
74+
},
75+
"image": "logos/siskin-im.svg",
76+
"link": "https://apps.apple.com/us/app/siskin-im/id1153516838",
77+
"name": "Siskin IM",
78+
"platforms": [
79+
"iOS", "iPadOS"
80+
],
81+
"supports_preauth_uri": true,
82+
"text": "{% trans "A lightweight and powerful XMPP client for iPhone and iPad. It provides an easy way to talk and share moments with your friends." %}"
83+
},
84+
{
85+
"download": {
86+
"buttons": [
87+
{
88+
"target": "_blank",
89+
"text": "{% trans "Download from Mac App Store" %}",
90+
"url": "https://apps.apple.com/us/app/beagle-im/id1445349494"
91+
}
92+
]
93+
},
94+
"image": "logos/beagle-im.svg",
95+
"link": "https://apps.apple.com/us/app/beagle-im/id1445349494",
96+
"name": "Beagle IM",
97+
"platforms": [
98+
"macOS"
99+
],
100+
"setup": {
101+
"text": "{% trans "Launch Beagle IM, and select 'Yes' to add a new account. Click the '+' button under the empty account list and then enter your credentials." %}"
102+
},
103+
"text": "{% trans "Beagle IM by Tigase, Inc. is a lightweight and powerful XMPP client for macOS." %}"
104+
},
105+
{
106+
"download": {
107+
"buttons": [
108+
{
109+
"target": "_blank",
110+
"text": "{% trans "Download Dino for Linux" %}",
111+
"url": "https://dino.im/#download"
112+
}
113+
],
114+
"text": "{% trans "Click the button to open the Dino website where you can download and install it on your PC." %}"
115+
},
116+
"image": "logos/dino.svg",
117+
"link": "https://dino.im/",
118+
"name": "Dino",
119+
"platforms": [
120+
"Linux"
121+
],
122+
"text": "{% trans "A modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind." %}"
123+
},
124+
{
125+
"download": {
126+
"buttons": [
127+
{
128+
"target": "_blank",
129+
"text": "{% trans "Download Gajim" %}",
130+
"url": "https://gajim.org/download/"
131+
}
132+
]
133+
},
134+
"image": "logos/gajim.svg",
135+
"link": "https://gajim.org/",
136+
"name": "Gajim",
137+
"platforms": [
138+
"Windows",
139+
"Linux"
140+
],
141+
"text": "{% trans "A fully-featured desktop chat client for Windows and Linux." %}"
142+
},
143+
{
144+
"download": {
145+
"buttons": [
146+
{
147+
"target": "_blank",
148+
"text": "{% trans "Download Renga for Haiku" %}",
149+
"url": "https://depot.haiku-os.org/#!/pkg/renga?bcguid=bc233-PQIA"
150+
}
151+
]
152+
},
153+
"image": "logos/renga.svg",
154+
"link": "https://pulkomandy.tk/projects/renga",
155+
"name": "Renga",
156+
"platforms": [
157+
"Haiku"
158+
],
159+
"text": "{% trans "XMPP client for Haiku" %}"
160+
}
161+
]

priv/mod_invites/base.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{% extends "base_min.html" %}
2+
3+
{% block rel_alternate %}
4+
<link rel="alternate" href="{{ uri }}">
5+
{% endblock %}
6+
7+
{% block qr_button %}
8+
<div id="qr-button-container" class="float-right w-25 border border-info p-3 d-none">
9+
{% trans "<strong>Tip:</strong> You can open this invite on your mobile device by scanning a barcode with your camera." %}
10+
<button id="qr-modal-show" class="mt-2 d-block btn btn-info" title="{% trans "Send this invite to your device" %}"
11+
data-toggle="modal" data-target="#qr-modal">
12+
<img src="{{ static }}/qr-logo.png" alt="{% trans "QR code icon" %}" class="align-middle h-50 mt-1" style="display:inline" >
13+
{% trans "Scan with mobile device" %}
14+
</button>
15+
</div>
16+
{% endblock %}
17+
18+
{% block qr_code %}
19+
<div class="modal" tabindex="-1" role="dialog" id="qr-modal">
20+
<div class="modal-dialog" role="document">
21+
<div class="modal-content">
22+
<div class="modal-header">
23+
<h5 class="modal-title">{% trans "Scan invite code" %}</h5>
24+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
25+
<span aria-hidden="true">&times;</span>
26+
</button>
27+
</div>
28+
<div class="modal-body">
29+
<p>{% trans "You can transfer this invite to your mobile device by scanning a code with your camera." %}</p>
30+
<div id="qr-info-url" class="tab-pane show active">
31+
<p>{% trans "Use a <em>QR code</em> scanner on your mobile device to scan the code below:" %}</p>
32+
<div id="qr-invite-page" style="width: 256px;" class="bg-light mx-auto"></div>
33+
</div>
34+
</div>
35+
<div class="modal-footer">
36+
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans "Close" %}</button>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
{% endblock %}
42+
43+
{% block extra_scripts %}
44+
<script src="{{ static }}/qrcode.min.js"></script>
45+
<script src="{{ static }}/platform.min.js"></script>
46+
<script src="{{ static }}/invite.js"></script>
47+
{% endblock %}

priv/mod_invites/base_min.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>{% block title %}{% blocktrans %}Invite to {{ site_name }}{% endblocktrans %}{% endblock %}</title>
7+
{% block rel_alternate %}{% endblock %}
8+
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
9+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
10+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
11+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
12+
<link rel="manifest" href="/site.webmanifest">
13+
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
14+
<meta name="msapplication-TileColor" content="#fbd308">
15+
<meta name="theme-color" content="#fbd308">
16+
</head>
17+
<body>
18+
<div id="background" class="fixed-top overflow-hidden"></div>
19+
<div id="form" class="{% block form_class %}container col-md-10 col-md-offset-1 col-sm-8 col-sm-offset-2 col-lg-10 col-lg-offset-1 mt-2 mt-md-5{% endblock %}">
20+
<div class="card rounded-lg shadow">
21+
<h1 class="card-header rounded-lg rounded-lg">
22+
{%block h1 %}{% blocktrans %}Invite to {{ site_name }}{% endblocktrans %}{% endblock %}<br/>
23+
</h1>
24+
<div class="card-body">
25+
{% block qr_button %}{% endblock %}
26+
{% block content %}{% endblock %}
27+
</div>
28+
</div>
29+
</div>
30+
{% block qr_code %}{% endblock %}
31+
{% block extra_scripts %}{% endblock %}
32+
<script src="/share/jquery/jquery.min.js"></script>
33+
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
34+
</body>
35+
</html>

priv/mod_invites/client.html

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{% extends "base.html" %}
2+
3+
{% block h1 %}
4+
{% blocktrans with app_name=app.name %}Join {{ site_name }} with {{ app_name }}{% endblocktrans %}
5+
{% endblock %}
6+
7+
{% block content %}
8+
<p>{% if invite.inviter|user %}
9+
{% blocktrans with inviter=invite.inviter|user %}You have been invited to chat with <strong>{{ inviter }}</strong> on {{ site_name }}, part of the XMPP secure and decentralized messaging network.{% endblocktrans %}
10+
{% else %}
11+
{% blocktrans %}You have been invited to chat on {{ site_name }}, part of the XMPP secure and decentralized messaging network.{% endblocktrans %}
12+
{% endif %}
13+
</p>
14+
15+
<p>{% blocktrans with app_name=app.name %}You can start chatting right away with {{ app_name }}. Let's get started!{% endblocktrans %}</p>
16+
17+
<div class="card m-3 client-card {% for item in app.platforms %}app-platform-{{ item|lower }} {% endfor %} flex-wrap col-sm-12 col-md-8 col-lg-5">
18+
<div class="row no-gutters h-100">
19+
<div class="col-md-4">
20+
<img src="{{ static }}/{{ app.image }}" class="p-2 img-fluid" alt="{{ app.imagetext }}">
21+
</div>
22+
<div class="col-md-8 h-100">
23+
<div class="card-body d-flex flex-column h-100">
24+
<h5 class="card-title text-nowrap mb-1">{{ app.name }}</h5>
25+
<div>
26+
{% for item in app.platforms %}<span class="badge badge-info client-platform-badge client-platform-badge-{{ item|lower }} mr-1 mb-3">{{ item }}</span> {% endfor %}
27+
</div>
28+
<p class="card-text">{{ app.text }}</p>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
34+
<h3 style="clear:both">{% blocktrans with app_name=app.name %}Step 1: Install {{ app_name }}{% endblocktrans %}</h3>
35+
36+
<p>{% if app.download.text %}{{ app.download.text }}{% else %}{% blocktrans with app_name=app.name %}Download and install {{ app_name }} below:{% endblocktrans %}{% endif %}</p>
37+
38+
<div class="ml-5">
39+
{% for button in app.download.buttons %}
40+
{% if button.image %}
41+
<a href="{% if button.magic_link %}{{ button.magic_link }}{% else %}{{ button.url }}{% endif %}" {% if button.target %}target="{{ button.target }}"{% endif %} rel="noopener">
42+
<img src="{{ button.image }}" {% if button.alttext %}alt="{{ button.alttext }}"{% endif %} style="max-width: 160px;">
43+
</a>
44+
{% endif %}
45+
{% if button.text %}
46+
<a href="{{ button.url }}" {% if button.target %}target="{{ button.target }}"{% endif %} class="btn btn-primary" rel="noopener">
47+
{{ button.text }}
48+
</a>
49+
{% endif %}
50+
{% endfor %}
51+
</div>
52+
53+
<p class="mt-3">{% blocktrans with app_name=app.name %}After successfully installing {{ app_name }}, come back to this page and <strong>continue with Step 2</strong>.{% endblocktrans %}</p>
54+
55+
<h3>{% trans "Step 2: Activate your account" %}</h3>
56+
57+
<p>{% trans "Installed ok? Great! <strong>Click or tap the button below</strong> to accept your invite and continue with your account setup:" %}</p>
58+
59+
<div>
60+
<a href="{{ uri }}" id="uri-cta" class="btn btn-primary ml-5 mt-1 mb-3">{% blocktrans with app_name=app.name %}Accept invite using {{ app_name }}{% endblocktrans %}</a><br/>
61+
</div>
62+
63+
<p>{% blocktrans with app_name=app.name %}After clicking the button you will be taken to {{ app_name }} to finish setting up your new {{ site_name }} account.{% endblocktrans %}</p>
64+
{% endblock %}
65+
66+
{% block extra_scripts %}
67+
<script src="{{ static }}/qrcode.min.js"></script>
68+
<script src="{{ static }}/platform.min.js"></script>
69+
<script src="{{ static }}/invite.js"></script>
70+
{% endblock %}

0 commit comments

Comments
 (0)