Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
pypb/
Binary file not shown.
@@ -0,0 +1,5 @@
# Local state - never commit these
state.json
state.json.tmp
venv/
__pycache__/
@@ -0,0 +1,163 @@
# Account registration monitor
Posts a Discord message whenever a new account is registered in Servatrice
(`cockatrice_users`). Each message includes the username, real name (if set),
email, and registration time.
It runs as a periodic read-only query against the production database. It does
not modify the database and does not touch the running Servatrice process.
## How it decides what is "new"
Accounts get an auto-increment `id`, so "new since last time" is just
`id > last_seen_id`. The monitor stores that single high-water-mark id in its
state file. Each run it posts every account above the mark, oldest first, then
advances the mark to the highest id it posted.
Because the mark only moves forward and an id is posted exactly once, there are
no duplicate messages and nothing is missed, even if the monitor is down for a
while. The state file holds a single number, so it never grows.
The first run (when no state file exists yet) records the current maximum id as
the baseline and posts nothing. This prevents the entire existing user base from
being dumped into the channel. Only accounts registered after that baseline are
posted.
If a post to Discord fails, the monitor stops there without advancing the mark
past it, so that account and everything after it are retried on the next run.
## Privacy note
Messages contain personal data (real name and email). Discord stores message
content on their servers, so post only to a private channel that the right
people can see, and treat the webhook URL as a secret. It lives in the config
ini alongside the database password, so keep that file readable only by the user
that runs the monitor.
## Setup
The monitor reads its database credentials and the webhook from a
servatrice-style ini file passed with `--config` (or the `CONFIG_FILE` env var).
You can point it at your existing `servatrice.ini`, or keep a small separate ini
just for the monitor.
### 1. Create a read-only database user
Run as a DB admin. Adjust the host (`'%'` allows any host; restrict it to the
machine running the monitor if you can) and the table prefix if yours is not the
default `cockatrice`.
```sql
CREATE USER 'account_monitor'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT (id, name, realname, email, registrationDate)
ON servatrice.cockatrice_users TO 'account_monitor'@'%';
FLUSH PRIVILEGES;
```
Using a read-only user is recommended over pointing `--config` at the real
`servatrice.ini`, because Servatrice's own DB account usually has write access
the monitor does not need.
### 2. Create the Discord webhook and add it to the config
In Discord: open the target channel, then Edit Channel -> Integrations ->
Webhooks -> New Webhook. Name it, pick the channel, and copy the webhook URL.
Add a `[discord]` section with the URL to the ini you will pass to `--config`.
If you want the read-only user above, set the `[database]` section to use it. A
small dedicated `monitor.ini` looks like this:
```ini
[database]
hostname=127.0.0.1
database=servatrice
user=account_monitor
password=a-strong-password
prefix=cockatrice
[discord]
new_user_activation_webhook=https://discord.com/api/webhooks/XXXX/YYYY
```
If you would rather use one file, add the `[discord]` section to the real
`servatrice.ini` instead. Servatrice ignores sections it does not use. Note that
Servatrice (a Qt app) rewrites ini values it touches in quoted, backslash-escaped
form, for example `"https\://..."`. The monitor strips that encoding from the
webhook automatically, so either the plain or the escaped form works.
### 3. Install
```bash
cd servatrice/scripts/account_monitor
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
```
### 4. Verify before scheduling
```bash
# Confirm the webhook works (sends one test message to the channel)
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --test-webhook
# Confirm DB access and see what it would do, without posting or writing state
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --dry-run --verbose
```
The first real run seeds the baseline and posts nothing:
```bash
./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini
```
After that, test it end to end by registering a throwaway account and confirming
a message appears on the next run.
## Run it every 2 minutes with cron
Edit the crontab of the user that owns the script directory (`crontab -e`) and
add one line. This runs the monitor every 2 minutes, using the venv's Python and
your config ini, and appends output to a log:
```cron
*/2 * * * * cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1
```
Adjust the three paths to your install: the script directory after `cd`, and the
`--config` and log paths. The `*/2` field is what makes it run every 2 minutes;
change it to `*/5` for every 5, and so on.
By default the high-water-mark is stored in `state.json` next to the script, so
the directory must be writable by the cron user. To put it elsewhere, set
`STATE_FILE`:
```cron
*/2 * * * * STATE_FILE=/var/lib/account_monitor/state.json cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1
```
The interval only controls how often it checks; it is not a lookback window, so
a longer interval never causes missed accounts. The query is cheap: an indexed
range scan on the primary key for `id > last_seen`.
## Options
- `--config PATH` / `-c PATH` — read DB settings from `[database]` and the webhook from `[discord] new_user_activation_webhook` of a servatrice-style ini (falls back to the `CONFIG_FILE` env var).
- `--dry-run` — query and log what would be posted; no Discord posts, no state write.
- `--test-webhook` — send one test message to the webhook and exit (does not need DB credentials).
- `--verbose` — debug logging.
## Configuration reference
Settings come from the `--config` ini, with environment variables available as
overrides if you need them (env takes precedence over the ini).
| Setting | ini (`--config`) | Environment override |
| --- | --- | --- |
| DB host | `[database] hostname` | `DB_HOST` |
| DB port | `[database] port` (optional) | `DB_PORT` |
| DB name | `[database] database` | `DB_NAME` |
| DB user | `[database] user` | `DB_USER` |
| DB password | `[database] password` | `DB_PASSWORD` |
| Table prefix | `[database] prefix` | `DB_TABLE_PREFIX` |
| Webhook URL | `[discord] new_user_activation_webhook` | `DISCORD_WEBHOOK_URL` |
| DB TLS | — | `DB_SSL` / `DB_SSL_CA` |
| State file path | — | `STATE_FILE` (default: `state.json` next to the script) |
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""Post a Discord message when a new Servatrice account is registered.
Accounts get an auto-increment `id`, so "what is new since last time" is simply
`id > last_seen_id`. The monitor stores that single high-water-mark id in a
small state file. Each run it posts every account above the mark (oldest
first), then advances the mark. This means no duplicate posts, nothing missed
across downtime, and a state file that never grows (it holds one number).
On the very first run (no state file yet) it records the current maximum id as
the baseline and posts nothing, so existing users are not dumped into the
channel. From then on only newly-registered accounts are posted.
Intended to be run on a schedule (cron). Pass a servatrice-style ini with
--config (or CONFIG_FILE) for the database credentials and webhook; see
README.md.
"""
import argparse
import configparser
import json
import logging
import os
import re
import sys
import time
import urllib.error
import urllib.request
import pymysql
log = logging.getLogger("account_monitor")
# Columns we use. `id` drives the high-water-mark; `name` is the login/username,
# `realname` is the optional display name, `registrationDate` is when the account
# row was created.
NEW_ACCOUNTS_QUERY = (
"SELECT id, name, realname, email, registrationDate "
"FROM `{prefix}_users` WHERE id > %s ORDER BY id ASC"
)
EMBED_COLOR = 0x5865F2 # discord blurple
DISCORD_MAX_EMBED_FIELD = 1024
POST_DELAY_SECONDS = 1.0 # gap between webhook posts to stay under rate limits
MAX_RATELIMIT_RETRIES = 5
def _clean_ini_value(value):
"""Undo Qt QSettings ini encoding of a value.
Qt apps (including Servatrice) write ini values that contain special
characters wrapped in double quotes and backslash-escaped, e.g. a webhook
URL stored as "https\\://...". configparser returns that text literally, so
strip the wrapping quotes and remove the backslash escapes. This is applied
to the webhook URL only, where it is safe (URLs contain no quotes or
backslashes); DB values are left untouched so passwords are never altered.
"""
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
return re.sub(r"\\(.)", r"\1", value)
def load_config_file(path):
"""Read DB and Discord settings from a servatrice-style ini.
Pulls the [database] section (hostname/database/user/password/prefix/port)
and the Discord webhook from [discord] new_user_activation_webhook. Returns
a dict using this module's internal config keys; only keys actually present
(and non-empty) in the file are returned, so missing values fall back to
defaults or the environment.
"""
# interpolation=None so a '%' in a password is not treated as a token.
parser = configparser.ConfigParser(interpolation=None)
if not parser.read(path):
log.error("Config file not found or unreadable: %s", path)
sys.exit(2)
result = {}
if parser.has_section("database"):
db = parser["database"]
db_mapping = {
"hostname": "db_host",
"database": "db_name",
"user": "db_user",
"password": "db_password",
"prefix": "db_prefix",
"port": "db_port", # not in stock servatrice.ini, but honored if present
}
result.update({cfg_key: db[ini_key] for ini_key, cfg_key in db_mapping.items() if db.get(ini_key)})
if parser.has_section("discord") and parser["discord"].get("new_user_activation_webhook"):
result["webhook_url"] = _clean_ini_value(parser["discord"]["new_user_activation_webhook"])
return result
def get_config(config_path=None, require_db=True):
"""Build configuration from defaults, an optional ini file, then env vars.
Precedence, highest first: environment variables, the ini file, built-in
defaults. Database credentials and the Discord webhook may come from either
the ini file or the environment; the state file is environment-only.
"""
cfg = {
"db_host": "localhost",
"db_port": 3306,
"db_name": "servatrice",
"db_user": None,
"db_password": None,
"db_prefix": "cockatrice",
"db_ssl": False,
"db_ssl_ca": None,
"webhook_url": None,
"state_file": os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"),
}
if config_path:
cfg.update(load_config_file(config_path))
env_map = {
"DB_HOST": "db_host",
"DB_PORT": "db_port",
"DB_NAME": "db_name",
"DB_USER": "db_user",
"DB_PASSWORD": "db_password",
"DB_TABLE_PREFIX": "db_prefix",
"DB_SSL_CA": "db_ssl_ca",
"DISCORD_WEBHOOK_URL": "webhook_url",
"STATE_FILE": "state_file",
}
for env_key, cfg_key in env_map.items():
if os.environ.get(env_key):
cfg[cfg_key] = os.environ[env_key]
if os.environ.get("DB_SSL"):
cfg["db_ssl"] = os.environ["DB_SSL"].lower() in ("1", "true", "yes")
cfg["db_port"] = int(cfg["db_port"])
required = {"webhook_url": "DISCORD_WEBHOOK_URL or [discord] new_user_activation_webhook"}
if require_db:
required["db_user"] = "DB_USER or [database] user"
required["db_password"] = "DB_PASSWORD or [database] password"
missing = [label for key, label in required.items() if not cfg[key]]
if missing:
log.error("Missing required configuration: %s", "; ".join(missing))
sys.exit(2)
if cfg["webhook_url"] and not cfg["webhook_url"].lower().startswith(("http://", "https://")):
log.error("Webhook URL does not look like an http(s) URL: %r", cfg["webhook_url"])
sys.exit(2)
return cfg
def connect(cfg):
"""Open a read-only connection to the Servatrice database."""
ssl = None
if cfg["db_ssl"]:
ssl = {"ca": cfg["db_ssl_ca"]} if cfg["db_ssl_ca"] else {}
return pymysql.connect(
host=cfg["db_host"],
port=cfg["db_port"],
user=cfg["db_user"],
password=cfg["db_password"],
database=cfg["db_name"],
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
connect_timeout=15,
read_timeout=30,
ssl=ssl,
)
def fetch_max_id(conn, prefix):
"""Return the highest account id currently in the table, or 0 if empty."""
with conn.cursor() as cur:
cur.execute("SELECT MAX(id) AS max_id FROM `{prefix}_users`".format(prefix=prefix))
row = cur.fetchone()
return int(row["max_id"]) if row and row["max_id"] is not None else 0
def fetch_new_accounts(conn, prefix, last_id):
"""Return detail rows for accounts with id > last_id, oldest first."""
with conn.cursor() as cur:
cur.execute(NEW_ACCOUNTS_QUERY.format(prefix=prefix), (last_id,))
return cur.fetchall()
def load_state(path):
"""Load the high-water-mark id. Returns (last_id, is_first_run)."""
if not os.path.exists(path):
return 0, True
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
return int(data.get("last_id", 0)), False
def save_state(path, last_id):
"""Atomically persist the high-water-mark id."""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump({"version": 2, "last_id": int(last_id)}, fh)
os.replace(tmp, path)
def build_embed(row):
"""Build a Discord embed dict for one newly-registered account."""
fields = [
{"name": "Username", "value": str(row["name"]) or "(none)", "inline": False},
]
realname = (row.get("realname") or "").strip()
if realname:
fields.append({"name": "Real name", "value": realname[:DISCORD_MAX_EMBED_FIELD], "inline": False})
fields.append({"name": "Email", "value": str(row.get("email") or "(none)"), "inline": False})
reg = row.get("registrationDate")
fields.append({"name": "Reg time", "value": str(reg) if reg is not None else "(unknown)", "inline": False})
return {
"title": "New account registered",
"color": EMBED_COLOR,
"fields": fields,
}
def post_embed(webhook_url, embed):
"""POST a single embed to the Discord webhook, honoring 429 rate limits."""
payload = json.dumps({"embeds": [embed]}).encode("utf-8")
for attempt in range(MAX_RATELIMIT_RETRIES):
req = urllib.request.Request(
webhook_url,
data=payload,
headers={"Content-Type": "application/json", "User-Agent": "servatrice-account-monitor/1.0"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status in (200, 204):
return True
log.warning("Unexpected Discord status %s", resp.status)
return False
except urllib.error.HTTPError as err:
if err.code == 429:
retry_after = _retry_after_seconds(err)
log.warning("Rate limited by Discord; sleeping %.2fs", retry_after)
time.sleep(retry_after)
continue
log.error("Discord webhook HTTP %s: %s", err.code, err.read().decode("utf-8", "replace")[:500])
return False
except urllib.error.URLError as err:
log.error("Discord webhook connection error: %s", err)
return False
log.error("Gave up posting after %d rate-limit retries", MAX_RATELIMIT_RETRIES)
return False
def _retry_after_seconds(err):
"""Extract the retry delay (seconds) from a Discord 429 response."""
header = err.headers.get("Retry-After")
if header:
try:
return float(header)
except ValueError:
pass
try:
body = json.loads(err.read().decode("utf-8", "replace"))
return float(body.get("retry_after", 1.0))
except (ValueError, json.JSONDecodeError):
return 1.0
def main():
parser = argparse.ArgumentParser(description="Post new Servatrice account registrations to Discord.")
parser.add_argument(
"--config", "-c",
help="Path to a servatrice-style ini; reads DB settings from its [database] "
"section (hostname/database/user/password/prefix) and the webhook from "
"[discord] new_user_activation_webhook. Defaults to the CONFIG_FILE env "
"var if set.",
)
parser.add_argument("--dry-run", action="store_true", help="Log what would be posted; do not post or write state.")
parser.add_argument("--test-webhook", action="store_true", help="Send a single test message to the webhook and exit.")
parser.add_argument("--verbose", action="store_true", help="Enable debug logging.")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
config_path = args.config or os.environ.get("CONFIG_FILE")
if args.test_webhook:
cfg = get_config(config_path, require_db=False)
ok = post_embed(
cfg["webhook_url"],
{"title": "Account monitor test", "color": EMBED_COLOR,
"description": "If you can see this, the webhook is configured correctly."},
)
sys.exit(0 if ok else 1)
cfg = get_config(config_path)
try:
conn = connect(cfg)
except pymysql.MySQLError as err:
log.error("Database connection failed: %s", err)
sys.exit(1)
try:
last_id, first_run = load_state(cfg["state_file"])
if first_run:
baseline = fetch_max_id(conn, cfg["db_prefix"])
log.info("First run: seeding high-water-mark at id=%d; posting nothing.", baseline)
if not args.dry_run:
save_state(cfg["state_file"], baseline)
return
rows = fetch_new_accounts(conn, cfg["db_prefix"], last_id)
if not rows:
log.info("No new accounts since id=%d.", last_id)
return
log.info("Found %d new account(s) since id=%d.", len(rows), last_id)
# Post oldest first. Advance the mark only past accounts we successfully
# posted; on the first failure, stop so nothing after it is posted out of
# order or skipped. The failed account (and the rest) retry next run.
for row in rows:
if args.dry_run:
log.info("[dry-run] would post: id=%s name=%s email=%s reg=%s",
row["id"], row["name"], row.get("email"), row.get("registrationDate"))
continue
if not post_embed(cfg["webhook_url"], build_embed(row)):
log.error("Failed to post account id=%s; stopping. Will retry from here next run.", row["id"])
break
last_id = row["id"]
log.info("Posted account id=%s (%s)", row["id"], row["name"])
time.sleep(POST_DELAY_SECONDS)
if not args.dry_run:
save_state(cfg["state_file"], last_id)
finally:
conn.close()
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
PyMySQL==1.2.0
@@ -0,0 +1,46 @@
#!/bin/bash
set -u
set -e
SLEEPTIME=5
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
LOGAPPENDDATE=`date +%m%d%Y`
EXPIRATION=`date +%m%d%Y -d "-3 days"`
DBNAME="servatrice"
APPNAME="servatrice"
ROOTFOLDER="./backups" #set this to the root path you want backups to be stored in
BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME"
TABLES=(
"cockatrice_users"
"cockatrice_decklist_files"
"cockatrice_replays"
"cockatrice_buddylist"
"cockatrice_ignorelist"
"cockatrice_bans"
"cockatrice_sessions"
"cockatrice_decklist_folders"
"cockatrice_replays_access"
"cockatrice_games"
"cockatrice_games_players"
"cockatrice_uptime"
"cockatrice_schema_version"
"cockatrice_servermessages"
"cockatrice_servers"
"cockatrice_rooms"
"cockatrice_rooms_gametypes"
)
PROCESSNAME="mysqldump"
if [ "$(pgrep $PROCESSNAME)" == "" ];
then
[ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/
for TABLENAME in "${TABLES[@]}"
do
BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE"
echo "Backing up DB Table [$TABLENAME]"
ionice -c3 nice -n19 mysqldump --defaults-file=$SQLCONFFILE $DBNAME $TABLENAME > $BACKUPFILE
sleep $SLEEPTIME
done
rm -rf "$ROOTFOLDER/$EXPIRATION/"
else
echo "Backup in progress, aborting"
fi
@@ -0,0 +1,52 @@
#!/bin/bash
set -u
set -e
SLEEPTIME=5
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
LOGAPPENDDATE=`date +%m%d%Y`
EXPIRATION=`date +%m%d%Y -d "-3 days"`
DBNAME="servatrice"
APPNAME="servatrice"
ROOTFOLDER="./backups" #set this to the root path that contains the backup files
BACKUPDIR="$ROOTFOLDER/$LOGAPPENDDATE/db/$APPNAME"
TABLES=(
"cockatrice_users"
"cockatrice_decklist_files"
"cockatrice_replays"
"cockatrice_buddylist"
"cockatrice_ignorelist"
"cockatrice_bans"
"cockatrice_sessions"
"cockatrice_decklist_folders"
"cockatrice_replays_access"
"cockatrice_games"
"cockatrice_games_players"
"cockatrice_uptime"
"cockatrice_schema_version"
"cockatrice_servermessages"
"cockatrice_servers"
"cockatrice_rooms"
"cockatrice_rooms_gametypes"
)
PROCESSNAME="mysqldump"
if [ "$(pgrep $PROCESSNAME)" == "" ];
then
[ ! -d $BACKUPDIR ] && mkdir -p $BACKUPDIR/
for TABLENAME in "${TABLES[@]}"
do
BACKUPFILE="$BACKUPDIR/$APPNAME.$TABLENAME.sql.$LOGAPPENDDATE"
if [ -f "$BACKUPFILE" ]
then
echo "Restoring up DB Table [$TABLENAME]"
ionice -c3 nice -n19 mysql --defaults-file=$SQLCONFFILE $DBNAME < $BACKUPFILE
sleep $SLEEPTIME
else
echo "Missing backup file [$$TABLENAME]"
sleep $SLEEPTIME
fi
done
rm -rf "$ROOTFOLDER/$EXPIRATION/"
else
echo "Restore in progress, aborting"
fi
@@ -0,0 +1,8 @@
#!/bin/bash
#USE THIS SCRIPT TO IDENTIFY THE SIZE OF YOUR TABLES IN THE DATABASE
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -e 'SELECT table_name AS "Tables", round(((data_length + index_length) / 1024 / 1024), 2) "Size in MB" FROM information_schema.TABLES WHERE table_schema = "'$DBNAME'" ORDER BY (data_length + index_length) DESC;'
@@ -0,0 +1,36 @@
#!/bin/bash
# THIS SCRIPT EXPECTS TO BE EXECUTED FROM THE GITHUB SOURCE FOLDER PATH STRUCTURE
# OTHERWISE, UPDATE THE 'COUNTRYCODEIMAGEPATH' TO POINT TO THE FOLDER CONTAINING THE COUNTRY CODE IMAGES
# USE THIS SCRIPT TO COMPARE EXISTING USER ACCOUNTS TO VALID COUNTRY CODES AND CLEAR INVALID COUNTRY CODE DATA
MODE="report" #set this to correct to fix invalid country codes, otherwise it only reports
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
COUNTRYCODEIMAGEPATH='../../../cockatrice/resources/countries'
VALIDCOUNT=0
INVALIDCOUNT=0
for i in `mysql --defaults-file=$SQLCONFFILE -h localhost -e "select distinct(country) from ""$DBNAME"".""$TABLEPREFIX""_users;"`
do
if [ "$i" != "country" ]; then
if [ -f "$COUNTRYCODEIMAGEPATH/$i.svg" ]; then
((VALIDCOUNT++))
else
((INVALIDCOUNT++))
if [ "$MODE" == "correct" ]; then
echo "$i COUNTRY CODE INVALID, ATTEMPTING TO CORRECT"
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = '' where country = '$i';"
fi
fi
fi
done
if [ "$MODE" == "correct" ]; then
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = lower(country);"
fi
echo "INVALID: $INVALIDCOUNT"
echo "VALID: $VALIDCOUNT"
@@ -0,0 +1,9 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB ON A REGULAR BASIS. NUMBER OF DAYS IS THE AMOUNT OF DAYS TO KEEP INACTIVE ACCOUNTS (EX: 1 DAY REMOVES ALL INACTIVE ACCOUNTS OLDER THAN A SINGLE DAY).
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names with in the database
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=5 #set this to the number of days to search for
mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_users where active = 0 AND registrationDate < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY);"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
#SCHEDULE WITH CRONTAB AND ADJUST THE INTERVALS FOR THE NUMBER OF DAYS OF LOGS TO KEEP IN THE DATABASE
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=10 #set this to the number of days desired
mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from ""$DBNAME"".""$TABLEPREFIX""_log where log_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)'
@@ -0,0 +1,8 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set privlevel = 'NONE' where privelevel != 'NONE" AND privlevelEndDate < NOW()"
@@ -0,0 +1,7 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB DAILY
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e 'delete from servatrice.cockatrice_games where time_finished < DATE_SUB(now(), INTERVAL 8 DAY)'
@@ -0,0 +1,9 @@
#!/bin/bash
# SCHEDULE WITH CRONTAB TO RUN ON A REGULAR BASIS
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
NUMBEROFDAYS=10 #set this to the number of days desired
mysql --defaults-file=$SQLCONFFILE -h localhost -e "delete from ""$DBNAME"".""$TABLEPREFIX""_sessions where start_time < DATE_SUB(now(), INTERVAL ""$NUMBEROFDAYS"" DAY)"
@@ -0,0 +1,8 @@
#!/bin/bash
# SCRIPT TO ADD THE FIRST ADMIN USER NAMED SERVATRICE WITH THE PASSWORD OF PASSWORD
DBNAME="servatrice" #set this to the database name used
TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _)
SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file
mysql --defaults-file=$SQLCONFFILE -h localhost -e "insert into ""$DBNAME"".""$TABLEPREFIX""_users ((admin,name,password_sha512,active,realname,email,country,avatar_bmp,registrationDate,clientID,adminnotes,privlevelStartDate,privlevelEndDate) values (1,'servatrice','jbB4kSWDmjaVzMNdU13n73SpdBCJTCJ/JYm5ZBZvfxlzbISbXir+e/aSvMz86KzOoaBfidxO0s6GVd8t00qC0TNPl+udHfECaF7MsA==',1,'servatrice','servatrice@localhost','us','null.bmp','1970-01-01 10:00:00','','','1970-01-01 10:00:00','9999-01-01 10:00:00');
+3
View File
@@ -0,0 +1,3 @@
[client]
user={db_username}
password={db_password}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python
import socket, sys, struct, time
from pypb.server_message_pb2 import ServerMessage
from pypb.session_commands_pb2 import Command_Register as Reg
from pypb.commands_pb2 import CommandContainer as Cmd
from pypb.event_server_identification_pb2 import Event_ServerIdentification as ServerId
from pypb.response_pb2 import Response
HOST = "localhost"
PORT = 4748
CMD_ID = 1
def build_reg():
global CMD_ID
cmd = Cmd()
sc = cmd.session_command.add()
reg = sc.Extensions[Reg.ext]
reg.user_name = "testUser"
reg.email = "test@example.com"
reg.password = "password"
cmd.cmd_id = CMD_ID
CMD_ID += 1
return cmd
def send(msg):
packed = struct.pack('>I', len(msg))
sock.sendall(packed)
sock.sendall(msg)
def print_resp(resp):
print "<<<"
print repr(resp)
m = ServerMessage()
m.ParseFromString(bytes(resp))
print m
def recv(sock):
print "< header"
header = sock.recv(4)
msg_size = struct.unpack('>I', header)[0]
print "< ", msg_size
raw_msg = sock.recv(msg_size)
print_resp(raw_msg)
if __name__ == "__main__":
address = (HOST, PORT)
sock = socket.socket()
print "Connecting to server ", address
sock.connect(address)
# hack for old xml clients - server expects this and discards first message
print ">>> xml hack"
xmlClientHack = Cmd().SerializeToString()
send(xmlClientHack)
print sock.recv(60)
recv(sock)
print ">>> register"
r = build_reg()
print r
msg = r.SerializeToString()
send(msg)
recv(sock)
print "Done"