Compare commits
10 commits
14c28762bf
...
a9468c6970
Author | SHA1 | Date | |
---|---|---|---|
a9468c6970 | |||
2ce05d4f36 | |||
1852467535 | |||
9d3e07aa42 | |||
2e35b8cfc6 | |||
e50b31fc3f | |||
d0e802d77c | |||
54b15027a8 | |||
15ad3dfb94 | |||
c3cf86f6e9 |
49 changed files with 1344 additions and 492 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
|||
.env.local
|
||||
.env
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$/holycow_backend" />
|
||||
<envs />
|
||||
<envs>
|
||||
<env name="RUST_LOG" value="trace" />
|
||||
</envs>
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg height="100%" id="emblematic-background" viewBox="0 0 2000 2000" width="100%">
|
||||
<defs>
|
||||
<linearGradient id="gradientBg" x1="0.25" x2="0.75" y1="1.0" y2="0.0">
|
||||
<stop offset="0%" stop-color="#3d0c65"/>
|
||||
<stop offset="100%" stop-color="#221168"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gradientBg)" height="2000" width="2000"/>
|
||||
</svg>
|
||||
<svg height="63%" id="emblematic-icon" preserveAspectRatio="xMidYMid meet" viewBox="0 0 640 512" width="63%" x="94.72" xmlns="http://www.w3.org/2000/svg" y="94.72">
|
||||
<!--! Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. -->
|
||||
<path d="M96 224v32V416c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V327.8c9.9 6.6 20.6 12 32 16.1V368c0 8.8 7.2 16 16 16s16-7.2 16-16V351.1c5.3 .6 10.6 .9 16 .9s10.7-.3 16-.9V368c0 8.8 7.2 16 16 16s16-7.2 16-16V343.8c11.4-4 22.1-9.4 32-16.1V416c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V256l32 32v49.5c0 9.5 2.8 18.7 8.1 26.6L530 427c8.8 13.1 23.5 21 39.3 21c22.5 0 41.9-15.9 46.3-38l20.3-101.6c2.6-13-.3-26.5-8-37.3l-3.9-5.5V184c0-13.3-10.7-24-24-24s-24 10.7-24 24v14.4l-52.9-74.1C496 86.5 452.4 64 405.9 64H272 256 192 144C77.7 64 24 117.7 24 184v54C9.4 249.8 0 267.8 0 288v17.6c0 8 6.4 14.4 14.4 14.4C46.2 320 72 294.2 72 262.4V256 224 184c0-24.3 12.1-45.8 30.5-58.9C98.3 135.9 96 147.7 96 160v64zM560 336a16 16 0 1 1 32 0 16 16 0 1 1 -32 0zM166.6 166.6c-4.2-4.2-6.6-10-6.6-16c0-12.5 10.1-22.6 22.6-22.6H361.4c12.5 0 22.6 10.1 22.6 22.6c0 6-2.4 11.8-6.6 16l-23.4 23.4C332.2 211.8 302.7 224 272 224s-60.2-12.2-81.9-33.9l-23.4-23.4z" fill="#FFEE6F"/>
|
||||
</svg>
|
||||
<svg height="100%" id="emblematic-background" viewBox="0 0 2000 2000" width="100%">
|
||||
<defs>
|
||||
<linearGradient id="gradientBg" x1="0.25" x2="0.75" y1="1.0" y2="0.0">
|
||||
<stop offset="0%" stop-color="#3d0c65"/>
|
||||
<stop offset="100%" stop-color="#221168"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gradientBg)" height="2000" width="2000"/>
|
||||
</svg>
|
||||
<svg height="63%" id="emblematic-icon" preserveAspectRatio="xMidYMid meet" viewBox="0 0 640 512" width="63%" x="94.72" xmlns="http://www.w3.org/2000/svg"
|
||||
y="94.72">
|
||||
<!--! Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. -->
|
||||
<path
|
||||
d="M96 224v32V416c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V327.8c9.9 6.6 20.6 12 32 16.1V368c0 8.8 7.2 16 16 16s16-7.2 16-16V351.1c5.3 .6 10.6 .9 16 .9s10.7-.3 16-.9V368c0 8.8 7.2 16 16 16s16-7.2 16-16V343.8c11.4-4 22.1-9.4 32-16.1V416c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V256l32 32v49.5c0 9.5 2.8 18.7 8.1 26.6L530 427c8.8 13.1 23.5 21 39.3 21c22.5 0 41.9-15.9 46.3-38l20.3-101.6c2.6-13-.3-26.5-8-37.3l-3.9-5.5V184c0-13.3-10.7-24-24-24s-24 10.7-24 24v14.4l-52.9-74.1C496 86.5 452.4 64 405.9 64H272 256 192 144C77.7 64 24 117.7 24 184v54C9.4 249.8 0 267.8 0 288v17.6c0 8 6.4 14.4 14.4 14.4C46.2 320 72 294.2 72 262.4V256 224 184c0-24.3 12.1-45.8 30.5-58.9C98.3 135.9 96 147.7 96 160v64zM560 336a16 16 0 1 1 32 0 16 16 0 1 1 -32 0zM166.6 166.6c-4.2-4.2-6.6-10-6.6-16c0-12.5 10.1-22.6 22.6-22.6H361.4c12.5 0 22.6 10.1 22.6 22.6c0 6-2.4 11.8-6.6 16l-23.4 23.4C332.2 211.8 302.7 224 272 224s-60.2-12.2-81.9-33.9l-23.4-23.4z"
|
||||
fill="#FFEE6F"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
5
Caddyfile
Normal file
5
Caddyfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
:30002 {
|
||||
reverse_proxy http://localhost:30000
|
||||
reverse_proxy /api/* http://localhost:30001
|
||||
reverse_proxy /telegram/webhook http://localhost:30001
|
||||
}
|
287
LICENSE.txt
Normal file
287
LICENSE.txt
Normal file
|
@ -0,0 +1,287 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
16
README.md
16
README.md
|
@ -4,10 +4,20 @@
|
|||
|
||||
# Holy Cow
|
||||
|
||||
Telegram Mini App for Magic matches tracking
|
||||
Telegram Mini App for match and skill level tracking among players in a group.
|
||||
|
||||
</div>
|
||||
|
||||
> [!Caution]
|
||||
> [!Warning]
|
||||
>
|
||||
> This bot deliberately does not validate data.
|
||||
> Due to time constraints, many variables in this service are hardcoded to be specific to the **Holy Cow** Telegram group.
|
||||
>
|
||||
> Let me know if you'd be interested in a similar thing for your play group!
|
||||
>
|
||||
> I'm evaluating if making a similar thing at scale could be worthwhile.
|
||||
|
||||
> [!Caution]
|
||||
>
|
||||
> This service **does not automatically validate data** by design.
|
||||
>
|
||||
> It is expected that match submissions will be manually checked later by the l
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
[package]
|
||||
name = "holycow_backend"
|
||||
version = "0.1.0"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
edition = "2021"
|
||||
description = "Backend for a Telegram Mini App for match and skill level tracking among players in a group"
|
||||
repository = "https://forge.steffo.eu/unimore/tirocinio-canali-steffo-acrate"
|
||||
license = "EUPL-1.2"
|
||||
keywords = ["mtg", "openskill", "wenglin", "telegram", "telegram-miniapp"]
|
||||
categories = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.93"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
file = "src/database/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl REGCLASS);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
|
@ -16,21 +14,23 @@
|
|||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl REGCLASS) RETURNS VOID AS
|
||||
$$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
EXECUTE FORMAT('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
IF (
|
||||
new IS DISTINCT FROM old AND
|
||||
new.updated_at IS NOT DISTINCT FROM old.updated_at
|
||||
) THEN
|
||||
new.updated_at := CURRENT_TIMESTAMP;
|
||||
END IF;
|
||||
RETURN new;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
CREATE TYPE wenglin_t AS (
|
||||
rating float8,
|
||||
CREATE TYPE wenglin_t AS
|
||||
(
|
||||
rating float8,
|
||||
uncertainty float8
|
||||
);
|
||||
|
||||
CREATE TABLE players (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY,
|
||||
CREATE TABLE players
|
||||
(
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY,
|
||||
|
||||
wenglin wenglin_t NOT NULL DEFAULT ROW(25.0, 25.0 / 3),
|
||||
wenglin wenglin_t NOT NULL DEFAULT ROW (25.0, 25.0 / 3),
|
||||
|
||||
telegram_id BIGINT,
|
||||
|
||||
|
@ -18,22 +20,23 @@ CREATE TYPE outcome_t AS ENUM (
|
|||
'AWins',
|
||||
'BWins',
|
||||
'Tie'
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE matches (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY,
|
||||
instant TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
name VARCHAR,
|
||||
CREATE TABLE matches
|
||||
(
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY,
|
||||
instant timestamptz NOT NULL DEFAULT NOW(),
|
||||
name VARCHAR,
|
||||
|
||||
player_a_id BIGINT NOT NULL,
|
||||
player_a_wenglin_before wenglin_t NOT NULL,
|
||||
player_a_wenglin_after wenglin_t NOT NULL,
|
||||
player_a_id BIGINT NOT NULL,
|
||||
player_a_wenglin_before wenglin_t NOT NULL,
|
||||
player_a_wenglin_after wenglin_t NOT NULL,
|
||||
|
||||
player_b_id BIGINT NOT NULL,
|
||||
player_b_wenglin_before wenglin_t NOT NULL,
|
||||
player_b_wenglin_after wenglin_t NOT NULL,
|
||||
player_b_id BIGINT NOT NULL,
|
||||
player_b_wenglin_before wenglin_t NOT NULL,
|
||||
player_b_wenglin_after wenglin_t NOT NULL,
|
||||
|
||||
outcome outcome_t NOT NULL,
|
||||
outcome outcome_t NOT NULL,
|
||||
|
||||
CONSTRAINT match_unique_name UNIQUE (name),
|
||||
CONSTRAINT not_same_player CHECK (player_a_id != player_b_id),
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
ALTER TABLE players DROP COLUMN IF EXISTS competitive;
|
||||
ALTER TABLE players
|
||||
DROP COLUMN IF EXISTS competitive;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
ALTER TABLE players ADD COLUMN competitive BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE players
|
||||
ADD COLUMN competitive BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
ALTER TABLE matches ALTER COLUMN player_a_id TYPE BIGINT;
|
||||
ALTER TABLE matches ALTER COLUMN player_b_id TYPE BIGINT;
|
||||
ALTER TABLE matches
|
||||
ALTER COLUMN player_a_id TYPE BIGINT;
|
||||
ALTER TABLE matches
|
||||
ALTER COLUMN player_b_id TYPE BIGINT;
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
ALTER TABLE matches ALTER COLUMN player_a_id TYPE INTEGER;
|
||||
ALTER TABLE matches ALTER COLUMN player_b_id TYPE INTEGER;
|
||||
ALTER TABLE matches
|
||||
ALTER COLUMN player_a_id TYPE INTEGER;
|
||||
ALTER TABLE matches
|
||||
ALTER COLUMN player_b_id TYPE INTEGER;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE players
|
||||
DROP COLUMN IF EXISTS username;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE players
|
||||
ADD COLUMN username bpchar UNIQUE NOT NULL;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE matches
|
||||
ADD CONSTRAINT match_unique_name UNIQUE (name);
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE matches
|
||||
DROP CONSTRAINT match_unique_name;
|
|
@ -1,6 +1,10 @@
|
|||
use url; // Fixes unused dependency inspection.
|
||||
|
||||
micronfig::config! {
|
||||
DATABASE_URL: String,
|
||||
BIND_ADDRESS: String > std::net::SocketAddr,
|
||||
BACKEND_BIND_ADDRESS: String > std::net::SocketAddr,
|
||||
TELEGRAM_API_KEY: String,
|
||||
TELEGRAM_WEBHOOK_URL: String > url::Url,
|
||||
TELEGRAM_NOTIFICATION_CHAT_ID: String > i64,
|
||||
TELEGRAM_NOTIFICATION_TOPIC_ID?: String > i32,
|
||||
}
|
||||
|
|
1
holycow_backend/src/database/migrations.rs
Normal file
1
holycow_backend/src/database/migrations.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();
|
3
holycow_backend/src/database/mod.rs
Normal file
3
holycow_backend/src/database/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod model;
|
||||
pub mod migrations;
|
||||
mod schema;
|
|
@ -1,15 +1,16 @@
|
|||
use std::io::Write;
|
||||
use crate::database::schema;
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::{AsExpression, BoolExpressionMethods, FromSqlRow, Identifiable, Insertable, OptionalExtension, PgConnection, QueryDsl, QueryResult, Queryable, QueryableByName, RunQueryDsl, Selectable, SelectableHelper};
|
||||
use diesel::backend::Backend;
|
||||
use diesel::deserialize::FromSql;
|
||||
use diesel::dsl::insert_into;
|
||||
use diesel::pg::Pg;
|
||||
use diesel::serialize::ToSql;
|
||||
use diesel::sql_types as sql;
|
||||
use diesel::serialize::Output as DieselOutput;
|
||||
use diesel::serialize::{IsNull, ToSql};
|
||||
use diesel::sql_types as sql;
|
||||
use diesel::ExpressionMethods;
|
||||
use diesel::{AsExpression, BoolExpressionMethods, FromSqlRow, Identifiable, Insertable, OptionalExtension, PgConnection, QueryDsl, QueryResult, Queryable, QueryableByName, RunQueryDsl, Selectable, SelectableHelper};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::schema;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, Clone, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||
#[diesel(sql_type = sql::BigInt)]
|
||||
|
@ -31,6 +32,7 @@ pub struct Player {
|
|||
pub wenglin: WengLinRating,
|
||||
pub telegram_id: Option<TelegramId>,
|
||||
pub competitive: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable, Serialize, Deserialize)]
|
||||
|
@ -38,8 +40,9 @@ pub struct Player {
|
|||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct PlayerI {
|
||||
pub wenglin: WengLinRating,
|
||||
pub telegram_id: TelegramId,
|
||||
pub telegram_id: Option<TelegramId>,
|
||||
pub competitive: bool,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, Serialize, Deserialize)]
|
||||
|
@ -70,7 +73,6 @@ pub struct Match {
|
|||
#[diesel(table_name = schema::matches)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MatchI {
|
||||
pub instant: DateTime<Utc>,
|
||||
pub name: Option<String>,
|
||||
pub player_a_id: i32,
|
||||
pub player_a_wenglin_before: WengLinRating,
|
||||
|
@ -91,8 +93,7 @@ impl FromSql<sql::BigInt, Pg> for TelegramId {
|
|||
|
||||
impl FromSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
||||
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||
let rating = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
|
||||
let uncertainty = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
|
||||
let (rating, uncertainty) = <(f64, f64) as FromSql<sql::Record<(sql::Double, sql::Double)>, Pg>>::from_sql(bytes)?;
|
||||
|
||||
let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty));
|
||||
Ok(Self(rating))
|
||||
|
@ -120,10 +121,10 @@ impl ToSql<sql::BigInt, Pg> for TelegramId {
|
|||
|
||||
impl ToSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
|
||||
fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
|
||||
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.rating, out)?;
|
||||
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.uncertainty, out)?;
|
||||
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
diesel::serialize::WriteTuple::<(sql::Double, sql::Double)>::write_tuple(
|
||||
&(self.0.rating, self.0.uncertainty),
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,12 +138,54 @@ impl ToSql<schema::sql_types::OutcomeT, Pg> for Outcome {
|
|||
}
|
||||
)?;
|
||||
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Outcome> for skillratings::Outcomes {
|
||||
fn from(value: Outcome) -> Self {
|
||||
match value {
|
||||
Outcome::AWins => Self::WIN,
|
||||
Outcome::BWins => Self::LOSS,
|
||||
Outcome::Tie => Self::DRAW,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WengLinRating {
|
||||
pub fn human_score(&self) -> i64 {
|
||||
let rating = self.0.rating;
|
||||
let uncertainty = self.0.uncertainty;
|
||||
log::debug!("Getting human score for: {rating:?}±{uncertainty:?}");
|
||||
let uncertain = self.0.rating - self.0.uncertainty;
|
||||
log::trace!("Minimum score is: {uncertain:?}");
|
||||
let multiplied = uncertain * 300.0 / 5.0;
|
||||
log::trace!("Multiplied score is: {multiplied:?}");
|
||||
let ceiled: f64 = multiplied.ceil();
|
||||
log::trace!("Ceiled score is: {ceiled:?}");
|
||||
let converted: i64 = ceiled as i64;
|
||||
log::debug!("Human score for {rating:?}±{uncertainty:?} is {converted:?}");
|
||||
converted
|
||||
}
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn total(conn: &mut PgConnection) -> QueryResult<i64> {
|
||||
log::debug!("Querying total amount of players...");
|
||||
schema::players::table
|
||||
.select(diesel::dsl::count_star())
|
||||
.get_result::<i64>(conn)
|
||||
}
|
||||
|
||||
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
||||
log::debug!("Querying all players...");
|
||||
schema::players::table
|
||||
.select(Self::as_select())
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn get_by_id(conn: &mut PgConnection, player_id: i32) -> QueryResult<Option<Self>> {
|
||||
log::debug!("Querying player with id: {player_id:?}");
|
||||
schema::players::table
|
||||
.select(Self::as_select())
|
||||
.filter(schema::players::id.eq(player_id))
|
||||
|
@ -151,6 +194,7 @@ impl Player {
|
|||
}
|
||||
|
||||
pub fn get_by_telegram_id(conn: &mut PgConnection, telegram_id: TelegramId) -> QueryResult<Option<Self>> {
|
||||
log::debug!("Querying player with telegram id: {telegram_id:?}");
|
||||
schema::players::table
|
||||
.select(Self::as_select())
|
||||
.filter(schema::players::telegram_id.eq(telegram_id))
|
||||
|
@ -165,6 +209,12 @@ impl Player {
|
|||
pub fn won_count(&self, conn: &mut PgConnection) -> QueryResult<i64> {
|
||||
Match::won_by_count(conn, self.id)
|
||||
}
|
||||
|
||||
pub fn update_wenglin(self, conn: &mut PgConnection, value: &WengLinRating) -> QueryResult<Self> {
|
||||
diesel::update(schema::players::table.find(self.id))
|
||||
.set(schema::players::wenglin.eq(value))
|
||||
.get_result(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Match {
|
||||
|
@ -174,6 +224,22 @@ impl Match {
|
|||
.get_result::<i64>(conn)
|
||||
}
|
||||
|
||||
pub fn all(conn: &mut PgConnection) -> QueryResult<Vec<Self>> {
|
||||
schema::matches::table
|
||||
.select(Self::as_select())
|
||||
.order_by(schema::matches::instant)
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn played_by(conn: &mut PgConnection, player_id: i32) -> QueryResult<Vec<Self>> {
|
||||
schema::matches::table
|
||||
.select(Self::as_select())
|
||||
.or_filter(schema::matches::player_a_id.eq(player_id))
|
||||
.or_filter(schema::matches::player_b_id.eq(player_id))
|
||||
.order_by(schema::matches::instant.desc())
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn played_by_count(conn: &mut PgConnection, player_id: i32) -> QueryResult<i64> {
|
||||
schema::matches::table
|
||||
.select(diesel::dsl::count_star())
|
||||
|
@ -192,3 +258,19 @@ impl Match {
|
|||
.get_result::<i64>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerI {
|
||||
pub fn insert(&self, conn: &mut PgConnection) -> QueryResult<Player> {
|
||||
insert_into(schema::players::table)
|
||||
.values(self)
|
||||
.get_result::<Player>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchI {
|
||||
pub fn insert(self, conn: &mut PgConnection) -> QueryResult<Match> {
|
||||
insert_into(schema::matches::table)
|
||||
.values(self)
|
||||
.get_result::<Match>(conn)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
pub mod sql_types {
|
||||
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
|
||||
#[diesel(postgres_type(name = "outcome_t"))]
|
||||
pub struct OutcomeT;
|
||||
|
||||
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
|
||||
#[diesel(postgres_type(name = "wenglin_t"))]
|
||||
pub struct WenglinT;
|
||||
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
|
||||
#[diesel(postgres_type(name = "outcome_t"))]
|
||||
pub struct OutcomeT;
|
||||
|
||||
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
|
||||
#[diesel(postgres_type(name = "wenglin_t"))]
|
||||
pub struct WenglinT;
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
|
@ -38,6 +38,7 @@ diesel::table! {
|
|||
wenglin -> WenglinT,
|
||||
telegram_id -> Nullable<Int8>,
|
||||
competitive -> Bool,
|
||||
username -> Bpchar,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +1,113 @@
|
|||
use std::convert::Infallible;
|
||||
use std::process::exit;
|
||||
use anyhow::Context;
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use axum::Extension;
|
||||
use diesel::{Connection, PgConnection};
|
||||
use diesel_migrations::MigrationHarness;
|
||||
use serde::Serialize;
|
||||
use teloxide::{dptree};
|
||||
use teloxide::dispatching::{DefaultKey, MessageFilterExt, UpdateFilterExt};
|
||||
use std::convert::Infallible;
|
||||
use std::process::exit;
|
||||
use teloxide::dispatching::DefaultKey;
|
||||
use teloxide::error_handlers::LoggingErrorHandler;
|
||||
use teloxide::types::{Message, WebAppData};
|
||||
use teloxide::update_listeners::webhooks::Options;
|
||||
use crate::types::TelegramId;
|
||||
|
||||
mod config;
|
||||
mod schema;
|
||||
mod types;
|
||||
|
||||
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();
|
||||
mod database;
|
||||
mod routes;
|
||||
mod telegram;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<Infallible> {
|
||||
pretty_env_logger::init();
|
||||
log::trace!("Logging initialized!");
|
||||
|
||||
log::trace!("Determining database URL...");
|
||||
let db = config::DATABASE_URL();
|
||||
log::trace!("Database URL is: {db:?}");
|
||||
|
||||
log::trace!("Determining bind address...");
|
||||
let bind_address = config::BIND_ADDRESS();
|
||||
log::trace!("Bind address is: {bind_address:?}");
|
||||
|
||||
log::trace!("Connecting to: {db:?}");
|
||||
let mut db = match PgConnection::establish(db) {
|
||||
Err(e) => {
|
||||
log::error!("Failed to connect to the PostgreSQL database: {e:#?}");
|
||||
exit(2);
|
||||
}
|
||||
Ok(db) => db,
|
||||
};
|
||||
|
||||
log::trace!("Running migrations...");
|
||||
if let Err(e) = db.run_pending_migrations(MIGRATIONS) {
|
||||
log::error!("Failed to perform migration: {e:#?}");
|
||||
exit(2);
|
||||
};
|
||||
|
||||
log::trace!("Creating Telegram bot...");
|
||||
let bot = teloxide::Bot::new(config::TELEGRAM_API_KEY());
|
||||
|
||||
log::trace!("Setting up webhooks...");
|
||||
let (telegram_listener, _telegram_stop, telegram_router) = teloxide::update_listeners::webhooks::axum_to_router(
|
||||
bot.clone(),
|
||||
Options {
|
||||
address: config::BIND_ADDRESS().clone(),
|
||||
url: config::TELEGRAM_WEBHOOK_URL().clone(),
|
||||
path: "/".to_string(),
|
||||
certificate: None,
|
||||
max_connections: None,
|
||||
drop_pending_updates: false,
|
||||
secret_token: None,
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
log::trace!("Logging initialized!");
|
||||
|
||||
log::trace!("Determining database URL...");
|
||||
let db = config::DATABASE_URL();
|
||||
log::trace!("Database URL is: {db:?}");
|
||||
|
||||
log::trace!("Determining bind address...");
|
||||
let bind_address = config::BACKEND_BIND_ADDRESS();
|
||||
log::trace!("Bind address is: {bind_address:?}");
|
||||
|
||||
log::trace!("Connecting to: {db:?}");
|
||||
let mut db = match PgConnection::establish(db) {
|
||||
Err(e) => {
|
||||
log::error!("Failed to connect to the PostgreSQL database: {e:#?}");
|
||||
exit(2);
|
||||
}
|
||||
Ok(db) => db,
|
||||
};
|
||||
|
||||
log::trace!("Running migrations...");
|
||||
if let Err(e) = db.run_pending_migrations(database::migrations::MIGRATIONS) {
|
||||
log::error!("Failed to perform migration: {e:#?}");
|
||||
exit(2);
|
||||
};
|
||||
|
||||
log::trace!("Creating Telegram bot...");
|
||||
let bot = teloxide::Bot::new(config::TELEGRAM_API_KEY());
|
||||
|
||||
log::trace!("Setting up webhooks...");
|
||||
let (telegram_listener, _telegram_stop, telegram_router) = teloxide::update_listeners::webhooks::axum_to_router(
|
||||
bot.clone(),
|
||||
Options {
|
||||
address: config::BACKEND_BIND_ADDRESS().clone(),
|
||||
url: config::TELEGRAM_WEBHOOK_URL().clone(),
|
||||
path: "/".to_string(),
|
||||
certificate: None,
|
||||
max_connections: None,
|
||||
drop_pending_updates: false,
|
||||
secret_token: None,
|
||||
},
|
||||
).await?;
|
||||
|
||||
log::trace!("Creating Axum router...");
|
||||
let app = axum::Router::new()
|
||||
.route("/players/holycow/:player_id/results", axum::routing::get(results_by_id_handler))
|
||||
.route("/players/telegram/:telegram_id/results", axum::routing::get(results_by_telegram_id_handler))
|
||||
.nest("/telegram/webhook", telegram_router)
|
||||
;
|
||||
|
||||
log::trace!("Setting up Telegram dispatcher...");
|
||||
let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::<teloxide::Bot, anyhow::Error, DefaultKey>::builder(
|
||||
bot.clone(),
|
||||
Message::filter_web_app_data()
|
||||
.endpoint(telegram_web_app_handler)
|
||||
|
||||
log::trace!("Creating Axum router...");
|
||||
let app = axum::Router::new()
|
||||
.route("/api/results/",
|
||||
axum::routing::get(routes::results::get_all),
|
||||
)
|
||||
.route("/api/results/holycow/:player_id",
|
||||
axum::routing::get(routes::results::get_by_id),
|
||||
)
|
||||
.route("/api/results/telegram/:telegram_id",
|
||||
axum::routing::get(routes::results::get_by_telegram_id),
|
||||
)
|
||||
.route("/api/matches/",
|
||||
axum::routing::get(routes::matches::get_all),
|
||||
)
|
||||
.route("/api/matches/",
|
||||
axum::routing::post(routes::matches::post_match),
|
||||
)
|
||||
.route("/api/matches/holycow/:player_id",
|
||||
axum::routing::post(routes::matches::get_played_by_id),
|
||||
)
|
||||
.nest("/telegram/webhook",
|
||||
telegram_router,
|
||||
)
|
||||
.layer(Extension(bot.clone()))
|
||||
;
|
||||
|
||||
log::trace!("Setting up Telegram dispatcher...");
|
||||
let mut telegram_dispatcher = teloxide::dispatching::Dispatcher::<teloxide::Bot, anyhow::Error, DefaultKey>::builder(
|
||||
bot.clone(),
|
||||
teloxide::dptree::entry(),
|
||||
)
|
||||
.default_handler(|u| async move {
|
||||
log::trace!("Unhandled update: {u:#?}")
|
||||
})
|
||||
.build();
|
||||
|
||||
log::trace!("Creating Telegram dispatcher future...");
|
||||
let telegram_future = telegram_dispatcher.dispatch_with_listener(telegram_listener, LoggingErrorHandler::new());
|
||||
|
||||
log::trace!("Creating Tokio listener...");
|
||||
let tokio_listener = tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.context("failed to bind listener to address")?;
|
||||
|
||||
log::trace!("Creating Axum server future...");
|
||||
let axum_future = axum::serve(tokio_listener, app);
|
||||
|
||||
log::info!("Running Axum server future and Telegram dispatcher future!");
|
||||
let _ = tokio::join!(axum_future, telegram_future);
|
||||
|
||||
log::error!("Server exited!");
|
||||
exit(1)
|
||||
.default_handler(|u| async move {
|
||||
log::trace!("Unhandled update: {u:?}")
|
||||
})
|
||||
.build();
|
||||
|
||||
log::trace!("Creating Telegram dispatcher future...");
|
||||
let telegram_future = telegram_dispatcher.dispatch_with_listener(telegram_listener, LoggingErrorHandler::new());
|
||||
|
||||
log::trace!("Creating Tokio listener...");
|
||||
let tokio_listener = tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.context("failed to bind listener to address")?;
|
||||
|
||||
log::trace!("Creating Axum server future...");
|
||||
let axum_future = axum::serve(tokio_listener, app);
|
||||
|
||||
log::info!("Running Axum server future and Telegram dispatcher future!");
|
||||
let _ = tokio::join!(axum_future, telegram_future);
|
||||
|
||||
log::error!("Server exited!");
|
||||
exit(1)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
struct ResultsResponse {
|
||||
played: i64,
|
||||
won: i64,
|
||||
rating: f64,
|
||||
uncertainty: f64,
|
||||
}
|
||||
|
||||
fn results(
|
||||
conn: &mut PgConnection,
|
||||
player: types::Player,
|
||||
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||
let played = player.played_count(conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let won = player.won_count(conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let rating = match player.competitive {
|
||||
false => 0.0,
|
||||
true => player.wenglin.0.rating,
|
||||
};
|
||||
|
||||
let uncertainty = match player.competitive {
|
||||
false => 0.0,
|
||||
true => player.wenglin.0.uncertainty,
|
||||
};
|
||||
|
||||
Ok(Json(ResultsResponse {
|
||||
played, won, rating, uncertainty
|
||||
}))
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn results_by_id_handler(
|
||||
Path(player_id): Path<i32>,
|
||||
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player = types::Player::get_by_id(&mut conn, player_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
results(&mut conn, player)
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn results_by_telegram_id_handler(
|
||||
Path(telegram_id): Path<TelegramId>,
|
||||
) -> Result<Json<ResultsResponse>, StatusCode> {
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player = types::Player::get_by_telegram_id(&mut conn, telegram_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
results(&mut conn, player)
|
||||
}
|
||||
|
||||
async fn telegram_web_app_handler(
|
||||
web_app_data: WebAppData,
|
||||
) -> anyhow::Result<()> {
|
||||
log::trace!("{web_app_data:#?}");
|
||||
Ok(())
|
||||
}
|
185
holycow_backend/src/routes/matches.rs
Normal file
185
holycow_backend/src/routes/matches.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
use crate::config;
|
||||
use crate::database::model::{Match, MatchI, Outcome, Player, WengLinRating};
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{Extension, Json};
|
||||
use diesel::{Connection, PgConnection};
|
||||
use serde::Deserialize;
|
||||
use skillratings::weng_lin::WengLinConfig;
|
||||
use teloxide::requests::Requester;
|
||||
use teloxide::types::{ChatId, MessageId, ParseMode, ThreadId};
|
||||
use teloxide::Bot;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MatchII {
|
||||
name: Option<String>,
|
||||
player_a: i32,
|
||||
player_b: i32,
|
||||
outcome: Outcome,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_all()
|
||||
-> Result<Json<Vec<Match>>, StatusCode>
|
||||
{
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let matches = Match::all(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(matches))
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_played_by_id(
|
||||
Path(player_id): Path<i32>,
|
||||
)
|
||||
-> Result<Json<Vec<Match>>, StatusCode>
|
||||
{
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let matches = Match::played_by(&mut conn, player_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(matches))
|
||||
}
|
||||
|
||||
fn player_to_text(player: &Player, before: &WengLinRating, after: &WengLinRating) -> String {
|
||||
let name = &player.username;
|
||||
let competitive = &player.competitive;
|
||||
let telegram_id = &player.telegram_id.clone().map(|t| t.0);
|
||||
|
||||
match competitive {
|
||||
false => {
|
||||
match telegram_id {
|
||||
None =>
|
||||
format!(r#"<b>{name}</b>"#),
|
||||
Some(telegram_id) =>
|
||||
format!(r#"<b><a href="tg://user?id={telegram_id}">{name}</a></b>"#),
|
||||
}
|
||||
}
|
||||
true => {
|
||||
let before = before.human_score();
|
||||
let after = after.human_score();
|
||||
let change = after - before;
|
||||
|
||||
match telegram_id {
|
||||
None =>
|
||||
format!(r#"<b>{name}</b> ({change:+} ★)"#),
|
||||
Some(telegram_id) =>
|
||||
format!(r#"<b><a href="tg://user?id={telegram_id}">{name}</a></b> ({change:+} ★)"#),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_to_text(r#match: &Match, player_a: &Player, player_b: &Player) -> String {
|
||||
let player_a = player_to_text(player_a, &r#match.player_a_wenglin_before, &r#match.player_a_wenglin_after);
|
||||
let player_b = player_to_text(player_b, &r#match.player_b_wenglin_before, &r#match.player_b_wenglin_after);
|
||||
|
||||
match r#match.outcome {
|
||||
Outcome::AWins => match &r#match.name {
|
||||
Some(name) => format!("🔵 {player_a} ha trionfato su {player_b} in <b>{name}</b>!"),
|
||||
None => format!("🔵 {player_a} ha trionfato su {player_b}!"),
|
||||
},
|
||||
Outcome::BWins => match &r#match.name {
|
||||
Some(name) => format!("🟠 {player_a} è stato sconfitto da {player_b} in <b>{name}</b>!"),
|
||||
None => format!("🟠 {player_a} è stato sconfitto da {player_b}!"),
|
||||
},
|
||||
Outcome::Tie => match &r#match.name {
|
||||
Some(name) => format!("⚪️ {player_a} e {player_b} hanno pareggiato in <b>{name}</b>!"),
|
||||
None => format!("⚪️ {player_a} e {player_b} hanno pareggiato!"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post_match(
|
||||
Extension(bot): Extension<Bot>,
|
||||
Json(matchii): Json<MatchII>,
|
||||
)
|
||||
-> Result<Json<Match>, StatusCode>
|
||||
{
|
||||
log::debug!("New MatchII just dropped: {matchii:#?}");
|
||||
let name = matchii.name;
|
||||
let outcome = matchii.outcome;
|
||||
|
||||
log::trace!("Establishing database connection...");
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
log::trace!("Finding player A's info...");
|
||||
let player_a = Player::get_by_id(&mut conn, matchii.player_a)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or_else(|| StatusCode::NOT_FOUND)?;
|
||||
let player_a_id = player_a.id;
|
||||
let player_a_wenglin_before = player_a.wenglin.clone();
|
||||
|
||||
log::trace!("Finding player B's info...");
|
||||
let player_b = Player::get_by_id(&mut conn, matchii.player_b)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or_else(|| StatusCode::NOT_FOUND)?;
|
||||
let player_b_id = player_b.id;
|
||||
let player_b_wenglin_before = player_b.wenglin.clone();
|
||||
|
||||
log::trace!("Calculating rating changes...");
|
||||
let (player_a_wenglin_after, player_b_wenglin_after) = skillratings::weng_lin::weng_lin(
|
||||
&player_a_wenglin_before.0,
|
||||
&player_b_wenglin_before.0,
|
||||
&outcome.into(),
|
||||
&WengLinConfig::default(),
|
||||
);
|
||||
let player_a_wenglin_after = WengLinRating(player_a_wenglin_after);
|
||||
log::trace!("A's new rating is: {player_a_wenglin_after:?}");
|
||||
let player_b_wenglin_after = WengLinRating(player_b_wenglin_after);
|
||||
log::trace!("B's new rating is: {player_b_wenglin_after:?}");
|
||||
|
||||
log::trace!("Starting database transaction...");
|
||||
let (r#match, player_a, player_b) = conn.transaction(|tx| {
|
||||
log::trace!("Updating A's rating...");
|
||||
let player_a = player_a.update_wenglin(tx, &player_a_wenglin_after)
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
log::trace!("Updating B's rating...");
|
||||
let player_b = player_b.update_wenglin(tx, &player_b_wenglin_after)
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
log::trace!("Inserting match...");
|
||||
let matchi = MatchI {
|
||||
name,
|
||||
player_a_id,
|
||||
player_a_wenglin_before,
|
||||
player_a_wenglin_after,
|
||||
player_b_id,
|
||||
player_b_wenglin_before,
|
||||
player_b_wenglin_after,
|
||||
outcome,
|
||||
};
|
||||
let r#match = matchi.insert(tx)
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
Ok::<(Match, Player, Player), anyhow::Error>((r#match, player_a, player_b))
|
||||
})
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
log::trace!("Preparing send message future...");
|
||||
|
||||
let chat = config::TELEGRAM_NOTIFICATION_CHAT_ID();
|
||||
let chat = ChatId(*chat);
|
||||
let mut send_message_future = bot.send_message(chat, match_to_text(&r#match, &player_a, &player_b));
|
||||
|
||||
send_message_future.parse_mode = Some(ParseMode::Html);
|
||||
|
||||
let topic = config::TELEGRAM_NOTIFICATION_TOPIC_ID();
|
||||
if let Some(topic) = topic {
|
||||
let topic = MessageId(*topic);
|
||||
let topic = ThreadId(topic);
|
||||
send_message_future.message_thread_id = Some(topic);
|
||||
}
|
||||
|
||||
log::trace!("Sending message...");
|
||||
let _message = send_message_future.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(r#match))
|
||||
}
|
2
holycow_backend/src/routes/mod.rs
Normal file
2
holycow_backend/src/routes/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod results;
|
||||
pub mod matches;
|
117
holycow_backend/src/routes/results.rs
Normal file
117
holycow_backend/src/routes/results.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use crate::config;
|
||||
use crate::database::model;
|
||||
use crate::database::model::TelegramId;
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use diesel::{Connection, PgConnection};
|
||||
use model::Player;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlayerO {
|
||||
id: i32,
|
||||
telegram_id: Option<TelegramId>,
|
||||
username: String,
|
||||
human_score: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<Player> for PlayerO {
|
||||
fn from(value: Player) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
telegram_id: value.telegram_id,
|
||||
username: value.username,
|
||||
human_score: match value.competitive {
|
||||
true => Some(value.wenglin.human_score()),
|
||||
false => None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_all()
|
||||
-> Result<Json<Vec<PlayerO>>, StatusCode>
|
||||
{
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let players = Player::all(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(players.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
||||
// this is an awful hack but idc i'm out of time
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlayerOO {
|
||||
id: i32,
|
||||
telegram_id: Option<TelegramId>,
|
||||
username: String,
|
||||
human_score: Option<i64>,
|
||||
played: i64,
|
||||
wins: i64,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_by_id(
|
||||
Path(player_id): Path<i32>,
|
||||
)
|
||||
-> Result<Json<PlayerOO>, StatusCode>
|
||||
{
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player = Player::get_by_id(&mut conn, player_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
let played = player.played_count(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let wins = player.won_count(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player_o = PlayerO::from(player);
|
||||
|
||||
Ok(Json(PlayerOO {
|
||||
id: player_o.id,
|
||||
telegram_id: player_o.telegram_id,
|
||||
username: player_o.username,
|
||||
human_score: player_o.human_score,
|
||||
played,
|
||||
wins,
|
||||
}))
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_by_telegram_id(
|
||||
Path(telegram_id): Path<TelegramId>,
|
||||
)
|
||||
-> Result<Json<PlayerOO>, StatusCode>
|
||||
{
|
||||
let mut conn = PgConnection::establish(config::DATABASE_URL())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player = Player::get_by_telegram_id(&mut conn, telegram_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
let played = player.played_count(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let wins = player.won_count(&mut conn)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let player_o = PlayerO::from(player);
|
||||
|
||||
Ok(Json(PlayerOO {
|
||||
id: player_o.id,
|
||||
telegram_id: player_o.telegram_id,
|
||||
username: player_o.username,
|
||||
human_score: player_o.human_score,
|
||||
played,
|
||||
wins,
|
||||
}))
|
||||
}
|
0
holycow_backend/src/telegram/mod.rs
Normal file
0
holycow_backend/src/telegram/mod.rs
Normal file
|
@ -1,6 +0,0 @@
|
|||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "export"
|
||||
};
|
||||
|
||||
export default nextConfig;
|
6
holycow_frontend/next.config.ts
Normal file
6
holycow_frontend/next.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {NextConfig} from "next"
|
||||
|
||||
|
||||
const nextConfig: NextConfig = {}
|
||||
|
||||
export default nextConfig
|
|
@ -3,11 +3,12 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev --turbopack --port 30000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"license": "EUPL-1.2",
|
||||
"dependencies": {
|
||||
"@awesome.me/kit-2b1ce28b9d": "^1.0.4",
|
||||
"@steffo/bluelib": "^9.1.0",
|
||||
|
|
14
holycow_frontend/src/app/[telegramId]/profile/page.tsx
Normal file
14
holycow_frontend/src/app/[telegramId]/profile/page.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {ProfileBox} from "@/components/ProfileBox"
|
||||
import {PlayerOO} from "@/holycow"
|
||||
|
||||
|
||||
export default async function Page({params: {telegramId}}) {
|
||||
const playerResponse = await fetch(`${process.env.BASE_URL}/api/results/telegram/${telegramId}`)
|
||||
const player: PlayerOO = await playerResponse.json()
|
||||
|
||||
return (
|
||||
<ProfileBox
|
||||
player={player}
|
||||
/>
|
||||
)
|
||||
}
|
16
holycow_frontend/src/app/[telegramId]/report/page.tsx
Normal file
16
holycow_frontend/src/app/[telegramId]/report/page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ReportBoxInteractive} from "@/components/ReportBoxInteractive"
|
||||
import {PlayerO} from "@/holycow"
|
||||
|
||||
|
||||
export default async function Page({params: {telegramId}}) {
|
||||
const playersResponse = await fetch(`${process.env.BASE_URL}/api/results/`)
|
||||
const players: PlayerO[] = await playersResponse.json()
|
||||
const playerA: PlayerO = players.find(p => p.telegram_id == telegramId)
|
||||
|
||||
return (
|
||||
<ReportBoxInteractive
|
||||
players={players}
|
||||
playerA={playerA}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -6,7 +6,19 @@ nextjs-portal {
|
|||
position: absolute;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.separator-lightest {
|
||||
border-color: hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / .05);
|
||||
}
|
||||
|
||||
:where(body) :where(.form-flex) > :where(.form-flex-choice, label) > :where(*) {
|
||||
/* TODO: Fix in bluelib */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fof {
|
||||
text-align: center;
|
||||
}
|
|
@ -1,34 +1,40 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { TelegramContext, useTelegramLoader } from "@/components/useTelegram";
|
||||
import {TelegramContext, useTelegramLoader} from "@/components/useTelegram"
|
||||
import Script from "next/script"
|
||||
|
||||
import "@steffo/bluelib/dist/base.root.css";
|
||||
import "@steffo/bluelib/dist/classic.root.css";
|
||||
import "@steffo/bluelib/dist/glass.root.css";
|
||||
import "@steffo/bluelib/dist/layouts-center.root.css";
|
||||
import "@steffo/bluelib/dist/layouts-flex.root.css";
|
||||
import "@steffo/bluelib/dist/colors-purplestar.root.css";
|
||||
import "@steffo/bluelib/dist/fonts-fira-ghpages.root.css";
|
||||
import "./layout.css";
|
||||
import "@steffo/bluelib/dist/base.root.css"
|
||||
import "@steffo/bluelib/dist/classic.root.css"
|
||||
import "@steffo/bluelib/dist/glass.root.css"
|
||||
import "@steffo/bluelib/dist/layouts-center.root.css"
|
||||
import "@steffo/bluelib/dist/layouts-flex.root.css"
|
||||
import "@steffo/bluelib/dist/colors-purplestar.root.css"
|
||||
import "@steffo/bluelib/dist/fonts-fira-ghpages.root.css"
|
||||
import "./layout.css"
|
||||
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
export default function RootLayout({children}) {
|
||||
const {telegram, onReady} = useTelegramLoader()
|
||||
|
||||
return (
|
||||
<html lang="it">
|
||||
<head>
|
||||
<Script
|
||||
<head>
|
||||
<Script
|
||||
src={"https://telegram.org/js/telegram-web-app.js?56"}
|
||||
onLoad={onReady}
|
||||
onReady={onReady}
|
||||
/>
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>
|
||||
Stagione 1
|
||||
<span>
|
||||
Stagione 1:
|
||||
</span>
|
||||
<br/>
|
||||
<small>
|
||||
Standard Brawl
|
||||
</small>
|
||||
</h2>
|
||||
</header>
|
||||
<TelegramContext.Provider value={telegram}>
|
||||
|
@ -38,12 +44,12 @@ export default function RootLayout({ children }) {
|
|||
<p>
|
||||
© Stefano Pigozzi
|
||||
-
|
||||
A quanto pare non posso mettere link esterni qui
|
||||
Star Shard
|
||||
-
|
||||
Garasauto
|
||||
che cursata le miniapp di telegram
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
15
holycow_frontend/src/app/none/page.tsx
Normal file
15
holycow_frontend/src/app/none/page.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
export default async function Page() {
|
||||
return (
|
||||
<div className={"panel box fof"}>
|
||||
<h3>
|
||||
👍
|
||||
</h3>
|
||||
<p>
|
||||
hai scoperto la pagina di tutti i tempi
|
||||
</p>
|
||||
<p>
|
||||
congrats!!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,89 +1,37 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import {LoadingBox} from "@/components/LoadingBox"
|
||||
import {useTelegram} from "@/components/useTelegram"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {useEffect} from "react"
|
||||
|
||||
import { StatPanel } from "@/components/StatPanel";
|
||||
import { useTelegram } from "@/components/useTelegram";
|
||||
import {useEffect, useMemo} from "react"
|
||||
import classNames from "classnames";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter()
|
||||
const telegram = useTelegram()
|
||||
const telegramData = telegram?.WebApp?.initDataUnsafe
|
||||
const userId = telegramData?.user?.id
|
||||
const userName = telegramData?.user?.first_name ?? "???"
|
||||
const data = telegram?.WebApp?.initDataUnsafe
|
||||
const startParam = data?.start_param
|
||||
|
||||
const resultsData = undefined
|
||||
const resultsError = undefined
|
||||
useEffect(
|
||||
() => {
|
||||
switch(startParam) {
|
||||
case "profile":
|
||||
router.replace(`/${data.user.id}/profile`)
|
||||
return
|
||||
case "report":
|
||||
router.replace(`/${data.user.id}/report`)
|
||||
return
|
||||
default:
|
||||
router.replace(`/none`)
|
||||
return
|
||||
}
|
||||
},
|
||||
[startParam],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if(telegramData.start_param === "report") {
|
||||
// TODO
|
||||
}
|
||||
}, [telegramData])
|
||||
|
||||
const contents = useMemo(() => {
|
||||
if(resultsError) {
|
||||
return resultsError.toString()
|
||||
}
|
||||
|
||||
const played = resultsData?.["played"] ?? 0
|
||||
const wins = resultsData?.["wins"] ?? 0
|
||||
const rating = resultsData?.["rating"] ?? 0
|
||||
|
||||
return (
|
||||
<div className={"chapter-1"}>
|
||||
<div className={"panel box"}>
|
||||
<h3>
|
||||
{userName}
|
||||
</h3>
|
||||
<div className={"chapter-3"}>
|
||||
<StatPanel
|
||||
name={"Giocate"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": played === 0
|
||||
})}
|
||||
value={played}
|
||||
>
|
||||
{played}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Vinte"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": wins === 0
|
||||
})}
|
||||
value={wins}
|
||||
>
|
||||
{wins}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Rating"}
|
||||
value={(
|
||||
<data
|
||||
className={classNames({
|
||||
"fade": rating === 0
|
||||
})}
|
||||
value={rating}
|
||||
>
|
||||
{rating === 0 ? "-" : rating}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [userName, resultsData, resultsError])
|
||||
|
||||
return <>
|
||||
<main>
|
||||
{contents}
|
||||
</main>
|
||||
</>
|
||||
return (
|
||||
<LoadingBox>
|
||||
Connessione a Telegram in corso...
|
||||
</LoadingBox>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useTelegram } from "@/components/useTelegram";
|
||||
import classNames from "classnames";
|
||||
import {FormEvent, useCallback, useMemo, useState} from "react"
|
||||
|
||||
export default function Page() {
|
||||
const telegram = useTelegram()
|
||||
const telegramData = telegram?.WebApp?.initDataUnsafe
|
||||
const userId = telegramData?.user?.id
|
||||
const userName = telegramData?.user?.first_name ?? "???"
|
||||
|
||||
const [opponent, setOpponent] = useState("")
|
||||
const [result, setResult] = useState(null)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(e: FormEvent) => {
|
||||
telegram?.WebApp?.sendData?.(`${result} ${opponent}`)
|
||||
},
|
||||
[telegram, result, opponent]
|
||||
)
|
||||
|
||||
const contents = useMemo(() => {
|
||||
return (
|
||||
<div className={"chapter-1"}>
|
||||
<form
|
||||
className={classNames({
|
||||
"panel": true,
|
||||
"box": true,
|
||||
"form-flex": true,
|
||||
"red": result === "L",
|
||||
"yellow": result === "T",
|
||||
"green": result === "W",
|
||||
})}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<h3>
|
||||
Registra risultato
|
||||
</h3>
|
||||
<label>
|
||||
<span>Tu</span>
|
||||
<div>
|
||||
{userName}
|
||||
</div>
|
||||
<span></span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Avversario</span>
|
||||
<select onChange={(e) => setOpponent(e.target.value)} value={opponent}>
|
||||
<option value={""}></option>
|
||||
<option value={"34053709"}>@AleCose</option>
|
||||
<option value={"843330513"}>@Alleander</option>
|
||||
<option value={"200821462"}>@catstolker</option>
|
||||
<option value={"454281712"}>@CookieSin</option>
|
||||
<option value={"524944901"}>@druidsfluid</option>
|
||||
<option value={"48371848"}>@Francesco_Cuoghi</option>
|
||||
<option value={"148374774"}>@GioOmbra</option>
|
||||
<option value={"19611986"}>@GoodBalu</option>
|
||||
<option value={"131057096"}>@Malbyx</option>
|
||||
<option value={"488463576"}>@Mallllco</option>
|
||||
<option value={"33523022"}>@MaxBubblegum</option>
|
||||
<option value={"139079908"}>@SnowyCoder</option>
|
||||
<option value={"165792255"}>@Spaggia</option>
|
||||
<option value={"25167391"}>@Steffo</option>
|
||||
<option value={"890339572"}>@xZefyr</option>
|
||||
<option value={"19097832"}>@zezelda</option>
|
||||
</select>
|
||||
<span></span>
|
||||
</label>
|
||||
<div className={"form-flex-choice"}>
|
||||
<span>Risultato</span>
|
||||
<div>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"W"} onChange={(e) => setResult(e.target.value)} checked={result === "W"}/> Vittoria
|
||||
</label>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"T"} onChange={(e) => setResult(e.target.value)} checked={result === "T"}/> Pareggio
|
||||
</label>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"L"} onChange={(e) => setResult(e.target.value)} checked={result === "L"}/> Sconfitta
|
||||
</label>
|
||||
</div>
|
||||
<span></span>
|
||||
</div>
|
||||
<input
|
||||
type={"submit"}
|
||||
value={"Invia"}
|
||||
className={classNames({
|
||||
// TODO: "fade": result === null || opponent === ""
|
||||
"fade": true,
|
||||
})}
|
||||
disabled={
|
||||
// TODO: result === null || opponent === ""
|
||||
true
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}, [onSubmit, userName, opponent, result])
|
||||
|
||||
return <>
|
||||
<main>
|
||||
{contents}
|
||||
</main>
|
||||
</>
|
||||
}
|
13
holycow_frontend/src/components/LoadingBox.module.css
Normal file
13
holycow_frontend/src/components/LoadingBox.module.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
@keyframes loading {
|
||||
0% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1.00;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: loading 0.5s infinite alternate ease-in-out;
|
||||
}
|
22
holycow_frontend/src/components/LoadingBox.tsx
Normal file
22
holycow_frontend/src/components/LoadingBox.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import classNames from "classnames"
|
||||
import {ReactNode} from "react"
|
||||
import style from "./LoadingBox.module.css"
|
||||
|
||||
|
||||
export type LoadingBoxProps = {
|
||||
children?: ReactNode,
|
||||
}
|
||||
|
||||
export function LoadingBox({children = "Loading..."}: LoadingBoxProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"panel": true,
|
||||
"box": true,
|
||||
[style.loading]: true,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
59
holycow_frontend/src/components/ProfileBox.tsx
Normal file
59
holycow_frontend/src/components/ProfileBox.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {StatPanel} from "@/components/StatPanel"
|
||||
import {PlayerOO} from "@/holycow"
|
||||
import classNames from "classnames"
|
||||
|
||||
|
||||
export type ProfileBoxProps = {
|
||||
player: PlayerOO
|
||||
}
|
||||
|
||||
|
||||
export function ProfileBox({player}: ProfileBoxProps) {
|
||||
return (
|
||||
<div className={"chapter-1"}>
|
||||
<div className={"panel box"}>
|
||||
<h3>
|
||||
{player.username}
|
||||
</h3>
|
||||
<div className={"chapter-4"}>
|
||||
<StatPanel
|
||||
name={"Giocate"}
|
||||
className={classNames({
|
||||
"fade": player.played === 0,
|
||||
})}
|
||||
value={(
|
||||
<data value={player.played}>
|
||||
{player.played}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
<StatPanel
|
||||
name={"Vinte"}
|
||||
className={classNames({
|
||||
"fade": player.wins === 0,
|
||||
"green": player.wins !== 0,
|
||||
})}
|
||||
value={(
|
||||
<data value={player.wins}>
|
||||
{player.wins}
|
||||
</data>
|
||||
)}
|
||||
/>
|
||||
{player.human_score !== null &&
|
||||
<StatPanel
|
||||
name={"★"}
|
||||
className={"yellow"}
|
||||
value={(
|
||||
<span>
|
||||
<data value={player.human_score}>
|
||||
{player.human_score}
|
||||
</data>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
97
holycow_frontend/src/components/ReportBox.tsx
Normal file
97
holycow_frontend/src/components/ReportBox.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {Outcome, PlayerO} from "@/holycow"
|
||||
import classNames from "classnames"
|
||||
import {FormEvent} from "react"
|
||||
|
||||
|
||||
export type ReportBoxProps = {
|
||||
players: PlayerO[],
|
||||
|
||||
playerA: PlayerO,
|
||||
playerB?: PlayerO,
|
||||
setPlayerB: (player?: PlayerO) => void,
|
||||
|
||||
outcome: Outcome,
|
||||
setOutcome: (outcome: Outcome) => void,
|
||||
|
||||
name: string,
|
||||
setName: (name: string) => void,
|
||||
|
||||
onSubmit: (e: FormEvent) => void,
|
||||
}
|
||||
|
||||
export function ReportBox({players, playerA, playerB, setPlayerB, outcome, setOutcome, name, setName, onSubmit}: ReportBoxProps) {
|
||||
return (
|
||||
<form
|
||||
className={classNames({
|
||||
"panel": true,
|
||||
"box": true,
|
||||
"form-flex": true,
|
||||
"green": outcome === Outcome.AWins,
|
||||
"red": outcome === Outcome.BWins,
|
||||
"yellow": outcome === Outcome.Tie,
|
||||
})}
|
||||
onSubmit={e => {
|
||||
e.preventDefault()
|
||||
onSubmit(e)
|
||||
}}
|
||||
>
|
||||
<h3>
|
||||
Registra risultato
|
||||
</h3>
|
||||
<hr className="separator-lightest"/>
|
||||
<label>
|
||||
<span>Titolo</span>
|
||||
<input type={"text"} onChange={e => setName(e.target.value)} value={name} placeholder={"Scontro al Garasauto"}/>
|
||||
<small>opzion.</small>
|
||||
</label>
|
||||
<hr className="separator-lightest"/>
|
||||
<label>
|
||||
<span>Tu</span>
|
||||
<select disabled={true}>
|
||||
<option>{playerA.username}</option>
|
||||
</select>
|
||||
<span>{playerA.human_score && `${playerA.human_score} ★`}</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Avv.</span>
|
||||
<select onChange={e => setPlayerB(players.find(p => p.id == Number.parseInt(e.target.value)))} value={playerB?.id}>
|
||||
<option value={undefined}></option>
|
||||
{players
|
||||
.filter(player => player.id !== playerA.id)
|
||||
.map(player => (
|
||||
<option key={player.id} value={player.id}>{player.username}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
<span>{playerB && playerB.human_score && `${playerB.human_score} ★`}</span>
|
||||
</label>
|
||||
<hr className="separator-lightest"/>
|
||||
<div className={"form-flex-choice"}>
|
||||
<span>Ris.</span>
|
||||
<div>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"AWins"} checked={outcome === Outcome.AWins} onChange={e => e.target.checked && setOutcome(Outcome.AWins)}/> Vittoria
|
||||
</label>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"Tie"} checked={outcome === Outcome.Tie} onChange={e => e.target.checked && setOutcome(Outcome.Tie)}/> Pareggio
|
||||
</label>
|
||||
<label>
|
||||
<input type={"radio"} name={"result"} value={"BWins"} checked={outcome === Outcome.BWins} onChange={e => e.target.checked && setOutcome(Outcome.BWins)}/> Sconfitta
|
||||
</label>
|
||||
</div>
|
||||
<span></span>
|
||||
</div>
|
||||
<hr className="separator-lightest"/>
|
||||
<input
|
||||
type={"submit"}
|
||||
value={"Invia"}
|
||||
className={classNames({
|
||||
"fade": outcome === undefined || playerB === undefined,
|
||||
})}
|
||||
disabled={
|
||||
outcome === undefined || playerB === undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
60
holycow_frontend/src/components/ReportBoxInteractive.tsx
Normal file
60
holycow_frontend/src/components/ReportBoxInteractive.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
"use client"
|
||||
|
||||
import {ReportBox} from "@/components/ReportBox"
|
||||
import {useTelegram} from "@/components/useTelegram"
|
||||
import {Outcome, PlayerO} from "@/holycow"
|
||||
import {useCallback, useState} from "react"
|
||||
|
||||
|
||||
export type ReportBoxInteractiveProps = {
|
||||
players: PlayerO[],
|
||||
playerA: PlayerO,
|
||||
}
|
||||
|
||||
export function ReportBoxInteractive({players, playerA}: ReportBoxInteractiveProps) {
|
||||
const [playerB, setPlayerB] = useState<PlayerO | undefined>(undefined)
|
||||
const [outcome, setOutcome] = useState<Outcome | undefined>(undefined)
|
||||
const [name, setName] = useState<string>("")
|
||||
const [running, setRunning] = useState<boolean>(false)
|
||||
const telegram = useTelegram()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
() => {
|
||||
if(!telegram) {
|
||||
return
|
||||
}
|
||||
setRunning(true)
|
||||
const body = JSON.stringify({
|
||||
name: name === "" ? null : name,
|
||||
player_a: playerA.id,
|
||||
player_b: playerB.id,
|
||||
outcome: outcome.toString(),
|
||||
})
|
||||
fetch("/api/matches/", {
|
||||
method: "POST",
|
||||
body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).finally(() => {
|
||||
telegram?.WebApp?.close?.()
|
||||
})
|
||||
},
|
||||
[telegram, name, playerA, playerB, outcome],
|
||||
)
|
||||
|
||||
return (
|
||||
<ReportBox
|
||||
players={players}
|
||||
playerA={playerA}
|
||||
playerB={playerB}
|
||||
setPlayerB={setPlayerB}
|
||||
outcome={outcome}
|
||||
setOutcome={setOutcome}
|
||||
name={name}
|
||||
setName={setName}
|
||||
onSubmit={running ? () => {
|
||||
} : onSubmit}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import classNames from "classnames";
|
||||
import classNames from "classnames"
|
||||
import style from "./StatPanel.module.css"
|
||||
|
||||
export function StatPanel({name, value, display = value}) {
|
||||
|
||||
export function StatPanel({className, name, value, display = value}) {
|
||||
return (
|
||||
<div className={classNames("panel", style.panel)}>
|
||||
<div className={classNames("panel", style.panel, className)}>
|
||||
<h4 className={style.name}>
|
||||
{name}
|
||||
</h4>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
import {createContext, useCallback, useContext, useState} from "react"
|
||||
|
||||
|
||||
export function useTelegramLoader() {
|
||||
|
@ -12,7 +12,7 @@ export function useTelegramLoader() {
|
|||
return {telegram, onReady}
|
||||
}
|
||||
|
||||
export const TelegramContext = createContext(undefined);
|
||||
export const TelegramContext = createContext(undefined)
|
||||
|
||||
export function useTelegram(): Telegram | undefined {
|
||||
return useContext(TelegramContext)
|
||||
|
|
22
holycow_frontend/src/holycow.ts
Normal file
22
holycow_frontend/src/holycow.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export enum Outcome {
|
||||
AWins = "AWins",
|
||||
BWins = "BWins",
|
||||
Tie = "Tie",
|
||||
}
|
||||
|
||||
|
||||
export type PlayerO = {
|
||||
id: number,
|
||||
telegram_id: number,
|
||||
username: string,
|
||||
human_score: null | number,
|
||||
}
|
||||
|
||||
export type PlayerOO = {
|
||||
id: number,
|
||||
telegram_id: number,
|
||||
username: string,
|
||||
human_score: null | number,
|
||||
played: number,
|
||||
wins: number,
|
||||
}
|
|
@ -2,17 +2,21 @@ interface Telegram {
|
|||
WebApp?: TelegramWebApp,
|
||||
}
|
||||
|
||||
|
||||
interface TelegramWebApp {
|
||||
initDataUnsafe?: TelegramWebAppInitData
|
||||
sendData?: (data: string) => void,
|
||||
close?: () => void,
|
||||
}
|
||||
|
||||
|
||||
interface TelegramWebAppInitData {
|
||||
user: TelegramWebAppUser,
|
||||
receiver: TelegramWebAppUser,
|
||||
start_param?: string,
|
||||
}
|
||||
|
||||
|
||||
interface TelegramWebAppUser {
|
||||
id: number,
|
||||
first_name: string,
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"noImplicitAny": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"noImplicitAny": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue