├── LICENSE
├── README.md
├── admin
├── actions.py
├── main.py
├── static
│ ├── form.js
│ └── replauth.js
└── templates
│ ├── index.html
│ └── table.html
├── api
├── .replit
├── README.md
├── api
│ ├── __init__.py
│ ├── models.py
│ └── routes
│ │ ├── __init__.py
│ │ ├── cards.py
│ │ └── index.py
└── runner.py
├── design
├── styles
│ ├── anims.css
│ ├── card.css
│ ├── helpers.css
│ └── rarities.css
└── tcg.css
└── discord
├── bot
├── __init__.py
├── comms.py
├── events.py
└── utility.py
├── main.py
└── web
└── __init__.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 RTCG Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [RTCG](https://rtcg.repl.co/)
2 |
3 | RTCG, or Replit Trading Card Game, is a community-led open source project made to create a Pokemon-like card index for the Replit community and its members. This repository contains the source for the API, Admin Dashboard, and CSS Library for the RTCG project. You can find the main site [here](https://rtcg.repl.co/).
4 |
5 | ### Purpose
6 |
7 | Despite it's name, this project will not contain any game-like interactions for the card index. The purpose of this project is to build a comprehensive API for interacting *with* the card index itself. Other operations, like managing user authentication and inventory, will be handled elsewhere. While there are no plans to create a game, you're more than welcome to use the API to make one of your own!
8 |
9 | ### Contributing and Help
10 |
11 | RTCG is currently closed to contributions, feel free to help out once we open up! If you like the project or want to find help, report a bug, or give us feedback, feel free to join our [Discord Server](https://replcardga.me/invite)!
12 |
13 | ### License
14 |
15 | All code contained in this repository is under an [**MIT License**](https://github.com/frissyn/rtcg/blob/master/LICENSE).
16 |
--------------------------------------------------------------------------------
/admin/actions.py:
--------------------------------------------------------------------------------
1 | import os
2 | import flask
3 | import requests
4 | import urllib.parse
5 |
6 |
7 | base = "https://api.rtcg.repl.co"
8 |
9 | def all_cards():
10 | return requests.get(f"{base}/cards").json()
11 |
12 | def add(req, temp: str):
13 | r = requests.post(
14 | base + "/card/add",
15 | headers={"X-API-TOKEN": os.getenv("TOKEN")},
16 | data={
17 | "name": req.form.get("name", ""),
18 | "color": req.form.get("color", ""),
19 | "image": req.form.get("image", ""),
20 | "title": req.form.get("title", ""),
21 | "rarity": req.form.get("rarity", ""),
22 | "description": req.form.get("description", ""),
23 | "shiny": req.form.get("shiny") == "True",
24 | }
25 | )
26 |
27 | if r.status_code <= 399: msg = "Card added successfully."
28 | else: msg = f"An error occured: {r.content}"
29 |
30 | return flask.redirect(f"{temp}?msg={urllib.parse.quote(msg, safe='~()*!.')}")
31 |
32 | def delete(req, temp: str):
33 | r = requests.delete(
34 | base + "/card/" + req.form.get("id"),
35 | headers={"X-API-TOKEN": os.getenv("TOKEN")}
36 | )
37 |
38 | if r.status_code <= 399: msg = "Card deleted successfully."
39 | else: msg = f"An error occured: {r.content}"
40 |
41 | return flask.redirect(f"{temp}?msg={urllib.parse.quote(msg, safe='~()*!.')}")
42 |
43 | def update(req, temp: str):
44 | r = requests.put(
45 | base + "/card/" + req.form.get("id"),
46 | headers={"X-API-TOKEN": os.getenv("TOKEN")},
47 | data={
48 | "name": req.form.get("name", ""),
49 | "color": req.form.get("color", ""),
50 | "image": req.form.get("image", ""),
51 | "title": req.form.get("title", ""),
52 | "rarity": req.form.get("rarity", ""),
53 | "description": req.form.get("description", ""),
54 | "shiny": req.form.get("shiny") == "True",
55 | }
56 | )
57 |
58 | if r.status_code <= 399: msg = "Card updated successfully."
59 | else: msg = f"An error occured: {r.content}"
60 |
61 | return flask.redirect(f"{temp}?msg={urllib.parse.quote(msg, safe='~()*!.')}")
62 |
--------------------------------------------------------------------------------
/admin/main.py:
--------------------------------------------------------------------------------
1 | import flask
2 | import actions
3 |
4 | app = flask.Flask(__name__)
5 | admins = ["frissyn", "Dart", "CoolCoderSJ"]
6 |
7 |
8 | @app.route("/", methods=["GET", "POST"])
9 | def index():
10 | req = flask.request
11 | name = req.headers.get("X-Replit-User-Name")
12 |
13 | print(name)
14 |
15 | if name in admins:
16 | if req.method == "GET":
17 | msg = req.args.get("msg")
18 | return flask.render_template("index.html", admin=True, msg=msg)
19 | elif req.method == "POST":
20 | print(req.form)
21 | return getattr(actions, req.form.get("action"))(req, "/")
22 | else:
23 | return flask.redirect("/")
24 | else:
25 | return flask.render_template("index.html")
26 |
27 |
28 | @app.route("/tableview", methods=["GET", "POST"])
29 | def table():
30 | req = flask.request
31 | name = req.headers.get("X-Replit-User-Name")
32 |
33 | print(name)
34 |
35 | if name in admins:
36 | if req.method == "GET":
37 | msg = req.args.get("msg")
38 | c = sorted(actions.all_cards(), key=lambda i: i["id"])
39 | return flask.render_template("table.html", admin=True, c=c, msg=msg)
40 | elif req.method == "POST":
41 | print(req.form)
42 | return getattr(actions, req.form.get("action"))(req, "/tableview")
43 | else:
44 | print(f"lol xD -> {{ {name} }}")
45 | return flask.redirect("/tableview")
46 | else:
47 | return flask.render_template("table.html")
48 |
49 |
50 | app.run(host="0.0.0.0", port=8080)
51 |
--------------------------------------------------------------------------------
/admin/static/form.js:
--------------------------------------------------------------------------------
1 | function send(i, t) {
2 | let form = document.getElementById(i);
3 | addDataToForm(form, {action: t})
4 | form.submit();
5 | }
6 |
7 | function addDataToForm(form, data) {
8 | if(typeof form === 'string') {
9 | if(form[0] === '#') form = form.slice(1);
10 | form = document.getElementById(form);
11 | }
12 |
13 | var keys = Object.keys(data);
14 | var name;
15 | var value;
16 | var input;
17 |
18 | for (var i = 0; i < keys.length; i++) {
19 | name = keys[i];
20 | Array.prototype.forEach.call(form.elements, function (inpt) {
21 | if (inpt.name === name) {
22 | inpt.parentNode.removeChild(inpt);
23 | }
24 | });
25 |
26 | value = data[name];
27 | input = document.createElement('input');
28 | input.setAttribute('name', name);
29 | input.setAttribute('value', value);
30 | input.setAttribute('type', 'hidden');
31 |
32 | form.appendChild(input);
33 | }
34 |
35 | return form;
36 | }
--------------------------------------------------------------------------------
/admin/static/replauth.js:
--------------------------------------------------------------------------------
1 | ;
2 | (function() {
3 | var selem = document.currentScript;
4 |
5 | var button = document.createElement('button');
6 | button.className = "button is-primary replit-auth-button";
7 | button.textContent = 'Login with Replit';
8 |
9 | if (location.protocol !== 'https:') {
10 | var err = document.createElement('div');
11 | err.className = "replit-auth-error";
12 | err.textContent = 'Replit auth requires https!';
13 | selem.parentNode.insertBefore(err, selem);
14 | }
15 |
16 | button.onclick = function() {
17 | window.addEventListener('message', authComplete);
18 |
19 | var h = 500;
20 | var w = 350;
21 | var left = (screen.width / 2) - (w / 2);
22 | var top = (screen.height / 2) - (h / 2);
23 |
24 | var authWindow = window.open(
25 | 'https://repl.it/auth_with_repl_site?domain=' + location.host,
26 | '_blank',
27 | 'modal =yes, toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
28 |
29 | function authComplete(e) {
30 | if (e.data !== 'auth_complete') {
31 | return;
32 | }
33 |
34 | window.removeEventListener('message', authComplete);
35 |
36 | authWindow.close();
37 | if (selem.attributes.authed.value) {
38 | eval(selem.attributes.authed.value);
39 | } else {
40 | location.reload();
41 | }
42 | }
43 | }
44 |
45 | selem.parentNode.insertBefore(button, selem);
46 | })();
--------------------------------------------------------------------------------
/admin/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RTCG Admin
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% if not admin %}
12 |
13 | {% else %}
14 | Admin Dashboard
15 | {% if msg %}
{% endif %}
16 |
17 | Delete Card by ID:
18 |
23 |
24 |
Add a Card
25 |
62 |
63 |
Update Card by ID:
64 |
106 | {% endif %}
107 |
108 |
--------------------------------------------------------------------------------
/admin/templates/table.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RTCG Admin
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {% if not admin %}
13 |
14 | {% else %}
15 | Admin Dashboard
16 | {% if msg %}
{% endif %}
17 |
18 |
19 |
20 | ID |
21 | Name |
22 | Color |
23 | Title |
24 | Description |
25 | Image |
26 | Rarity |
27 | Shiny |
28 | Actions |
29 |
30 |
31 | {% for card in c %}
32 |
33 |
61 |
62 | {% endfor %}
63 |
64 |
65 | {% endif %}
66 |
67 |
--------------------------------------------------------------------------------
/api/.replit:
--------------------------------------------------------------------------------
1 | run = "export FLASK_APP=api/__init__.py; python runner.py"
--------------------------------------------------------------------------------
/api/README.md:
--------------------------------------------------------------------------------
1 | # RTCG API
2 |
3 | The RTCG API allows you interact with the card index and card data. Anyone can get card data, but updating or adding cards is a whitelisted feature only. We currently are not accepting applications for whitelisting. Below you can find the docs for each route and its return data.
4 |
5 | ## Requests
6 |
7 | The base URL for the API is `api.rtcg.repl.co`. Each listed route can be added to the end of that URL. All requests require the `Content-Type: application/json` header to be set. If you have an API token, add a header to your requests like so: `X-API-TOKEN: token-name`
8 |
9 | ## Routes
10 |
11 | + **`/cards`**
12 | + `GET`: Returns list of all cards in the index.
13 | + **`/card/{id}`**
14 | + `GET`: Return an individual card by ID. `404` if card does exist.
15 | + `PUT`: Update an existing card in the index. Returns updated card on success, `404` if card does not exist.
16 | + `DELETE`: Deletes an existing card in the index. Returns `204` empty response on success, `404` if card does not exist.
17 | + **`/card/add`**
18 | + `POST`: Create and a new card to the index. Returns `201` on success, `400-403` or `500` on fail.
19 |
20 | ## Objects
21 |
22 | The standard card object looks like this:
23 |
24 | ```json
25 | {
26 | "color":"#808080",
27 | "description":"A ReplTalk moderator and anime fanatic.",
28 | "id":2,
29 | "image":"https://storage.googleapis.com/replit/images/1612721588723_7d2dd8f20ea4fe3f5a7f770825f40eca.jpeg",
30 | "name":"frissyn",
31 | "rarity":"Very Rare",
32 | "shiny":true,
33 | "title":"God of Anime"
34 | }
35 | ```
36 |
--------------------------------------------------------------------------------
/api/api/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import flask
3 | import secrets
4 | import psycopg2
5 |
6 | from flask_migrate import Migrate
7 | from flask_sqlalchemy import SQLAlchemy
8 |
9 | from flask_limiter import Limiter
10 | from flask_limiter.util import get_remote_address
11 |
12 | app = flask.Flask(__name__)
13 | app.config.from_mapping({
14 | "DEBUG": False,
15 | "TESTING": False,
16 | "FLASK_DEBUG": 0,
17 | "CACHE_TYPE": "simple",
18 | "CACHE_DEFAULT_TIMEOUT": 300,
19 | "SECRET_KEY": secrets.token_hex(16),
20 | "SQLALCHEMY_DATABASE_URI": os.environ["DATABASE_URL"],
21 | "SQLALCHEMY_TRACK_MODIFICATIONS": False
22 | })
23 | conn = psycopg2.connect(os.environ["DATABASE_URL"], sslmode='require')
24 | limiter = Limiter(
25 | app, default_limits=["75 per minute"],
26 | key_func=get_remote_address
27 | )
28 |
29 | tokens = [os.getenv("TOKEN")]
30 | db = SQLAlchemy(app)
31 | migrator = Migrate(app, db)
32 |
33 | from .routes import *
--------------------------------------------------------------------------------
/api/api/models.py:
--------------------------------------------------------------------------------
1 | from api import db
2 |
3 | from sqlalchemy.inspection import inspect
4 |
5 |
6 | class Card(db.Model):
7 | id = db.Column(db.Integer, primary_key=True)
8 | name = db.Column(db.String(64), nullable=False)
9 | image = db.Column(db.String, nullable=False)
10 | rarity = db.Column(db.String(32), nullable=False)
11 | title = db.Column(db.String(64), nullable=False)
12 | description = db.Column(db.String, nullable=False)
13 | shiny = db.Column(db.Boolean, nullable=False, default=True, server_default="True")
14 | color = db.Column(db.String(7), nullable=False, default="#808080", server_default="#808080")
15 |
16 | def serialize(self):
17 | return {c: getattr(self, c) for c in inspect(self).attrs.keys()}
18 |
19 | def update(self, d : dict):
20 | for key, val in d.items():
21 | if hasattr(self, key) and val != None and val != "":
22 | setattr(self, key, val)
23 | else:
24 | pass
25 |
26 | return True
27 |
28 | def __repr__(self):
29 | return f""
--------------------------------------------------------------------------------
/api/api/routes/__init__.py:
--------------------------------------------------------------------------------
1 | import glob
2 |
3 | from os.path import join
4 | from os.path import isfile
5 | from os.path import dirname
6 | from os.path import basename
7 |
8 |
9 | __all__ = []
10 | modules = glob.glob(join(dirname(__file__), "*.py"))
11 |
12 | for m in modules:
13 | flag1 = isfile(m)
14 | flag2 = m.endswith("__init__.py")
15 |
16 | if flag1 and not flag2:
17 | __all__.append(basename(m)[:-3])
--------------------------------------------------------------------------------
/api/api/routes/cards.py:
--------------------------------------------------------------------------------
1 | import flask
2 |
3 | from api import db
4 | from api import app
5 | from api import tokens
6 | from api import limiter
7 |
8 | from ..models import Card
9 |
10 |
11 | @app.route("/cards")
12 | @limiter.limit("75 per minute")
13 | def cards_route():
14 | cards = Card.query.all()
15 |
16 | return flask.jsonify([c.serialize() for c in cards])
17 |
18 |
19 | @app.route("/card/", methods=["GET", "DELETE", "PUT"])
20 | @limiter.limit("75 per minute")
21 | def card_route(iden: int):
22 | req = flask.request
23 | card = Card.query.get_or_404(iden)
24 |
25 | if req.method == "GET":
26 | return flask.jsonify(card.serialize())
27 | elif req.method == "PUT":
28 | if req.headers.get("X-API-TOKEN") in tokens:
29 | card.update({
30 | "name": req.form.get("name"),
31 | "image": req.form.get("image"),
32 | "title": req.form.get("title"),
33 | "rarity": req.form.get("rarity"),
34 | "color": req.form.get("color"),
35 | "shiny": req.form.get("shiny") == "True",
36 | "description": req.form.get("description"),
37 | })
38 |
39 | db.session.commit()
40 |
41 | return flask.jsonify(card.serialize())
42 | else:
43 | return flask.abort(404)
44 | elif req.method == "DELETE":
45 | if req.headers.get("X-API-TOKEN") in tokens:
46 | db.session.delete(card)
47 | db.session.commit()
48 |
49 | return "", 204
50 | else:
51 | return flask.abort(403)
52 |
53 |
54 | @app.route("/card/add", methods=["POST"])
55 | @limiter.limit("10 per minute")
56 | def card_add_route():
57 | req = flask.request
58 |
59 | if req.headers.get("X-API-TOKEN") in tokens:
60 | card = Card()
61 | card.update({
62 | "name": req.form.get("name"),
63 | "image": req.form.get("image"),
64 | "title": req.form.get("title"),
65 | "rarity": req.form.get("rarity"),
66 | "color": req.form.get("color"),
67 | "shiny": req.form.get("shiny") == "True",
68 | "description": req.form.get("description")
69 | })
70 |
71 | db.session.add(card)
72 | db.session.commit()
73 |
74 | return "", 201
75 | else:
76 | return flask.abort(403)
77 |
--------------------------------------------------------------------------------
/api/api/routes/index.py:
--------------------------------------------------------------------------------
1 | # import flask
2 |
3 | from api import app
4 |
5 | @app.route("/")
6 | def index_route():
7 | return "", 200
8 |
9 |
10 | @app.route("/ping")
11 | def ping_route():
12 | return "Alive!", 200
--------------------------------------------------------------------------------
/api/runner.py:
--------------------------------------------------------------------------------
1 | from api import db
2 | from api import app
3 |
4 | try:
5 | db.create_all(bind="__all__")
6 | except Exception as e:
7 | print(f"DB Bind Error: {e}")
8 |
9 | app.run(host="0.0.0.0", port=8080, debug=False)
--------------------------------------------------------------------------------
/design/styles/anims.css:
--------------------------------------------------------------------------------
1 | .card.glean {
2 | animation: cardGlean 15s ease infinite;
3 | }
4 |
5 | @keyframes cardGlean {
6 | 0%, 100% {
7 | transform: none;
8 | }
9 | 25% {
10 | transform: rotateZ(-3deg) rotateX(-7deg) rotateY(6deg);
11 | }
12 | 66% {
13 | transform: rotateZ(3deg) rotateX(7deg) rotateY(-6deg);
14 | }
15 | }
16 |
17 | @keyframes cardSparkle {
18 | 0%, 5% {
19 | opacity: 0.1;
20 | }
21 | 20% {
22 | opacity: 1;
23 | }
24 | 100% {
25 | opacity: 0.1;
26 | }
27 | }
28 |
29 | @keyframes holoGradient {
30 | 0%, 100% {
31 | opacity: 0;
32 | background-position: 0% 0%;
33 | }
34 | 8% {
35 | opacity: 0;
36 | }
37 | 10% {
38 | background-position: 0% 0%;
39 | }
40 | 19% {
41 | background-position: 100% 100%;
42 | opacity: 0.5;
43 | }
44 | 35% {
45 | background-position: 100% 100%;
46 | }
47 | 55% {
48 | background-position: 0% 0%;
49 | opacity: 0.3;
50 | }
51 | 75% {
52 | opacity: 0;
53 | }
54 | }
--------------------------------------------------------------------------------
/design/styles/card.css:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 320px;
3 | height: 446px;
4 | margin-top: 5px;
5 | margin-left: 5px;
6 | margin-right: 5px;
7 | margin-bottom: 5px;
8 | border: 5px solid;
9 | background-size: cover;
10 | background-repeat: no-repeat;
11 | background-position: center;
12 | background-color: darkgray;
13 | border-radius: 5% / 3.5%;
14 | box-shadow:
15 | -3px -3px 3px 0 rgba(#26e6f7, 0.3),
16 | 3px 3px 3px 0 rgba(#f759e4, 0.3),
17 | 0 0 6px 2px rgba(#ffe759, 0.3),
18 | 0 35px 25px -15px rgba(0, 0, 0, 0.3);
19 | position: relative;
20 | overflow: hidden;
21 | display: inline-block;
22 | vertical-align: middle;
23 | }
24 |
25 | .card.small {
26 | width: 120px !important;
27 | height: 167px !important;
28 | }
29 |
30 | .card.medium {
31 | width: 180px !important;
32 | height: 251px !important;
33 | }
34 |
35 | .card.large {
36 | width: 240px !important;
37 | height: 335px !important;
38 | }
39 |
40 | .card.shiny::before,
41 | .card.shiny::after {
42 | content: "";
43 | position: absolute;
44 | left: 0;
45 | right: 0;
46 | bottom: 0;
47 | top: 0;
48 | background-image: linear-gradient(
49 | 115deg,
50 | transparent 0%,
51 | rgb(0, 231, 255) 30%,
52 | rgb(255, 0, 231) 70%,
53 | transparent 100%
54 | );
55 | background-position: 0% 0%;
56 | background-repeat: no-repeat;
57 | background-size: 300% 300%;
58 | mix-blend-mode: color-dodge;
59 | opacity: 0.2;
60 | z-index: 1;
61 | animation: holoGradient 15s ease infinite;
62 | }
63 |
64 |
65 | .card.shiny::after {
66 | background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/13471/sparkles.gif");
67 | background-position: center;
68 | background-size: 180%;
69 | mix-blend-mode: color-dodge;
70 | opacity: 1;
71 | z-index: 2;
72 | animation: cardSparkle 15s ease infinite;
73 | }
74 |
75 | .card > img.card-header {
76 | top: 5%;
77 | left: 5%;
78 | width: 90%;
79 | height: 50%;
80 | position: relative;
81 | object-fit: cover;
82 | border-top-left-radius: 12px;
83 | border-top-right-radius: 12px;
84 | }
85 |
86 | .card-content {
87 | top: 40%;
88 | font-size: 90%;
89 | text-align: center;
90 | }
--------------------------------------------------------------------------------
/design/styles/helpers.css:
--------------------------------------------------------------------------------
1 | body {
2 | max-width: 850px;
3 | margin: 20px auto;
4 | padding: 0 10px;
5 | color: #363636;
6 | background: #ffffff;
7 | text-rendering: optimizeLegibility;
8 | }
--------------------------------------------------------------------------------
/design/styles/rarities.css:
--------------------------------------------------------------------------------
1 | .card.rarity-common {
2 | border-color: gray !important;
3 | }
4 |
5 | .card.rarity-uncommon {
6 | border-color: lightblue !important;
7 | }
8 |
9 | .card.rarity-rare {
10 | border-color: pink !important;
11 | }
12 |
13 | .card.rarity-veryrare {
14 | border-color: #ff7676 !important;
15 | }
16 |
17 | .card.rarity-legendary {
18 | border-color: gold !important;
19 | }
20 |
21 | .card.rarity-unobtainable {
22 | border-image: linear-gradient(
23 | to bottom right,
24 | #b827fc 0%,
25 | #2c90fc 25%,
26 | #b8fd33 50%,
27 | #fec837 75%,
28 | #fd1892 100%
29 | );
30 | border-image-slice: 1;
31 | }
--------------------------------------------------------------------------------
/design/tcg.css:
--------------------------------------------------------------------------------
1 | .card.glean{animation:cardGlean 15s ease infinite}@keyframes cardGlean{0%,100%{transform:none}25%{transform:rotateZ(-3deg) rotateX(-7deg) rotateY(6deg)}66%{transform:rotateZ(3deg) rotateX(7deg) rotateY(-6deg)}}@keyframes cardSparkle{0%,5%{opacity:.1}20%{opacity:1}100%{opacity:.1}}@keyframes holoGradient{0%,100%{opacity:0;background-position:0 0}8%{opacity:0}10%{background-position:0 0}19%{background-position:100% 100%;opacity:.5}35%{background-position:100% 100%}55%{background-position:0 0;opacity:.3}75%{opacity:0}}.card{width:320px;height:446px;margin-top:5px;margin-left:5px;margin-right:5px;margin-bottom:5px;border:5px solid;background-size:cover;background-repeat:no-repeat;background-position:center;background-color:#a9a9a9;border-radius:5%/3.5%;box-shadow:-3px -3px 3px 0 rgba(#26e6f7,.3),3px 3px 3px 0 rgba(#f759e4,.3),0 0 6px 2px rgba(#ffe759,.3),0 35px 25px -15px rgba(0,0,0,.3);position:relative;overflow:hidden;display:inline-block;vertical-align:middle}.card.small{width:120px!important;height:167px!important}.card.medium{width:180px!important;height:251px!important}.card.large{width:240px!important;height:335px!important}.card.shiny::after,.card.shiny::before{content:"";position:absolute;left:0;right:0;bottom:0;top:0;background-image:linear-gradient(115deg,transparent 0,#00e7ff 30%,#ff00e7 70%,transparent 100%);background-position:0 0;background-repeat:no-repeat;background-size:300% 300%;mix-blend-mode:color-dodge;opacity:.2;z-index:1;animation:holoGradient 15s ease infinite}.card.shiny::after{background-image:url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/13471/sparkles.gif);background-position:center;background-size:180%;mix-blend-mode:color-dodge;opacity:1;z-index:2;animation:cardSparkle 15s ease infinite}.card>img.card-header{top:5%;left:5%;width:90%;height:50%;position:relative;object-fit:cover;border-top-left-radius:12px;border-top-right-radius:12px}.card-content{top:40%;font-size:90%;text-align:center}.card.rarity-common{border-color:gray!important}.card.rarity-uncommon{border-color:#add8e6!important}.card.rarity-rare{border-color:pink!important}.card.rarity-veryrare{border-color:#ff7676!important}.card.rarity-legendary{border-color:gold!important}.card.rarity-unobtainable{border-image:linear-gradient(to bottom right,#b827fc 0,#2c90fc 25%,#b8fd33 50%,#fec837 75%,#fd1892 100%);border-image-slice:1}
--------------------------------------------------------------------------------
/discord/bot/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import discord
3 |
4 | from discord.ext import commands
5 |
6 | COLOR = 0x9370DB
7 | TOKEN = os.environ["BOT_TOKEN"]
8 | INTENTS = discord.Intents.default()
9 | INTENTS.members = True
10 | INTENTS.presences = True
11 |
12 | robot = commands.Bot(command_prefix="r! ", intents=INTENTS)
13 |
14 | from .comms import *
15 | from .events import *
--------------------------------------------------------------------------------
/discord/bot/comms.py:
--------------------------------------------------------------------------------
1 | import discord
2 | import requests
3 |
4 | from bot import robot
5 | from bot import COLOR
6 |
7 | from datetime import datetime
8 |
9 | watcher_role = 845360539672248320
10 |
11 |
12 | @robot.command(name="ping")
13 | async def ping(ctx):
14 | await ctx.channel.trigger_typing()
15 | l = round(robot.latency * 1000, 2)
16 | em = discord.Embed(title="Pong!", color=COLOR)
17 | now = datetime.now().strftime("[%B %d] %I:%M %p")
18 |
19 | em.add_field(name="**Timestamp:**", value=f"{now}", inline=True)
20 | em.add_field(name="**Latency:**", value=f"{l} ms", inline=True)
21 |
22 | await ctx.send(embed=em)
23 |
24 |
25 | @robot.command(name="watch")
26 | async def toggle_watch(ctx):
27 | await ctx.channel.trigger_typing()
28 | roles = [r.id for r in ctx.author.roles]
29 |
30 | if watcher_role in roles:
31 | em = discord.Embed(
32 | title="Toggled Watcher Role!",
33 | description="You will no longer be notified of Trade Offers.",
34 | color=COLOR
35 | )
36 |
37 | await ctx.author.remove_roles(discord.Object(845360539672248320))
38 | else:
39 | em = discord.Embed(
40 | title="Toggled Watcher Role!",
41 | description="You will now be notified of Trade Offers.",
42 | color=COLOR
43 | )
44 |
45 | await ctx.author.add_roles(discord.Object(845360539672248320))
46 |
47 | await ctx.send(embed=em)
48 |
49 |
50 | @robot.command(name="offers")
51 | async def get_offers(ctx):
52 | await ctx.channel.trigger_typing()
53 | em = discord.Embed(
54 | title="Recent Trade Offers!",
55 | description="There are no listed trade offers right now.",
56 | color=COLOR
57 | )
58 |
59 | await ctx.send(embed=em)
60 |
61 |
62 | @robot.command(name="offer")
63 | async def send_offer(ctx, i: int, c: int):
64 | await ctx.channel.trigger_typing()
65 | r = requests.get(f"https://api.rtcg.repl.co/card/{i}")
66 |
67 | if r.status_code == 200:
68 | card = r.json()
69 | em = discord.Embed(
70 | title="Posted Trade Offer!",
71 | description=f"{ctx.author.name} is offering {c} of '**{card['name']}**'!",
72 | color=COLOR
73 | )
74 |
75 | em.set_thumbnail(url=card["image"])
76 |
77 | for k, v in card.items():
78 | if k not in ["image", "id"]:
79 | em.add_field(name=k.upper(), value=str(v).title(), inline=True)
80 | else:
81 | em = discord.Embed(
82 | title="Invalid Trade Offer!",
83 | description="Card you listed does not exist.",
84 | color=COLOR
85 | )
86 |
87 | ping = ":tada:"
88 |
89 | if card["rarity"] in ["Very Rare", "Legendary", "Unobtainable"]:
90 | ping += "<@&845360539672248320>"
91 |
92 | await ctx.send(ping, embed=em)
93 |
94 |
95 | @robot.command(name="card")
96 | async def card_info(ctx, i: int):
97 | await ctx.channel.trigger_typing()
98 | r = requests.get(f"https://api.rtcg.repl.co/card/{i}")
99 |
100 | if r.status_code == 200:
101 | card = r.json()
102 | em = discord.Embed(title="Card Info!", color=COLOR)
103 |
104 | em.set_thumbnail(url=card["image"])
105 |
106 | for k, v in card.items():
107 | if k == "name":
108 | em.add_field(name=k.upper(), value=str(v).title() + f" [{str(card['id']).zfill(4)}]", inline=True)
109 | elif k not in ["id", "image"] and k != "" and v != "":
110 | em.add_field(name=k.upper(), value=str(v).title(), inline=True)
111 | else:
112 | em = discord.Embed(
113 | title="Error Occured",
114 | description="Card you listed does not exist.",
115 | color=COLOR
116 | )
117 |
118 | await ctx.send(embed=em)
119 |
120 |
121 | @robot.command(name="want")
122 | async def send_want(ctx, i: int, c: int):
123 | await ctx.channel.trigger_typing()
124 | r = requests.get(f"https://api.rtcg.repl.co/card/{i}")
125 |
126 | if r.status_code == 200:
127 | card = r.json()
128 | em = discord.Embed(
129 | title="Posted Trade Listing!",
130 | description=f"{ctx.author.name} wants {c} of '**{card['name']}**'!",
131 | color=COLOR
132 | )
133 |
134 | em.set_thumbnail(url=card["image"])
135 |
136 | for k, v in card.items():
137 | if k not in ["image", "id"]:
138 | em.add_field(name=k.upper(), value=str(v).title(), inline=True)
139 | else:
140 | em = discord.Embed(
141 | title="Invalid Trade Offer!",
142 | description="Card you listed does not exist.",
143 | color=COLOR
144 | )
145 |
146 | ping = ":tada: "
147 |
148 | if card["rarity"] in ["Very Rare", "Legendary", "Unobtainable"]:
149 | ping += "<@&845360539672248320>"
150 |
151 | await ctx.send(ping, embed=em)
152 |
153 |
154 |
155 | @robot.command(name="search")
156 | async def card_search(ctx, param: str, query: str):
157 | await ctx.channel.trigger_typing()
158 |
159 | card = None
160 | r = requests.get(f"https://api.rtcg.repl.co/cards")
161 | em = discord.Embed(
162 | title="Error Occured",
163 | description="Couldn't find the card you're looking for!",
164 | color=COLOR
165 | )
166 |
167 | if r.status_code == 200:
168 | cards = r.json()
169 | em = discord.Embed(title="Card Search!", color=COLOR)
170 |
171 | for card in cards:
172 | if str(card[param.lower()]).lower() == query.lower():
173 | card = card
174 | else:
175 | card = None
176 |
177 | if card != None:
178 | em = discord.Embed(title="Card Search!", color=COLOR)
179 | em.set_thumbnail(url=card["image"])
180 | for k, v in card.items():
181 | if k == "name":
182 | em.add_field(name=k.upper(), value=str(v).title() + f" [{str(card['id']).zfill(4)}]", inline=True)
183 | elif k not in ["id", "image"] and k != "" and v != "":
184 | em.add_field(name=k.upper(), value=str(v).title(), inline=True)
185 |
186 | await ctx.send(embed=em)
187 |
--------------------------------------------------------------------------------
/discord/bot/events.py:
--------------------------------------------------------------------------------
1 | import discord
2 |
3 | from bot import robot
4 |
5 |
6 | @robot.event
7 | async def on_ready():
8 | robot.remove_command("help")
9 |
10 | game = discord.Game("Replit Trading Card Game")
11 | await robot.change_presence(status=discord.Status.online, activity=game)
12 |
13 | print(f"{robot.user.name} has connected to Discord")
14 |
--------------------------------------------------------------------------------
/discord/bot/utility.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frissyn/rtcg/28a11e98a24e359389fd58a31314e6cb1969015e/discord/bot/utility.py
--------------------------------------------------------------------------------
/discord/main.py:
--------------------------------------------------------------------------------
1 | from bot import TOKEN
2 | from bot import robot
3 |
4 | from web import server
5 |
6 | server.start()
7 | robot.run(TOKEN)
--------------------------------------------------------------------------------
/discord/web/__init__.py:
--------------------------------------------------------------------------------
1 | import flask
2 | import threading
3 |
4 | app = flask.Flask(__name__)
5 |
6 | @app.route("/")
7 | def index(): return ""
8 |
9 | @app.route("/ping")
10 | def ping(): return "pong"
11 |
12 | server = threading.Thread(
13 | target=app.run,
14 | kwargs={
15 | "host": "0.0.0.0",
16 | "port": 8080,
17 | "debug": False,
18 | "threaded": True
19 | }
20 | )
21 |
--------------------------------------------------------------------------------