|
@@ -14,18 +14,51 @@ |
|
|
# You should have received a copy of the GNU Affero General Public License |
|
|
# You should have received a copy of the GNU Affero General Public License |
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
import shutil, platform, os |
|
|
|
|
|
path_prefix = os.path.dirname(os.path.abspath(__file__)) |
|
|
|
|
|
source = "" |
|
|
|
|
|
dest = "" |
|
|
|
|
|
if platform.system() == "Windows": |
|
|
|
|
|
source = "lib/wcwp_rust/target/release/whatcanweplay.dll" |
|
|
|
|
|
dest = "lib/bin/whatcanweplay.pyd" |
|
|
|
|
|
else: |
|
|
|
|
|
source = "lib/wcwp_rust/target/release/libwhatcanweplay.so" |
|
|
|
|
|
dest = "lib/bin/whatcanweplay.so" |
|
|
|
|
|
source = os.path.join(path_prefix, source) |
|
|
|
|
|
dest = os.path.join(path_prefix, dest) |
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(dest): |
|
|
|
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True) |
|
|
|
|
|
shutil.copy2(source, dest) |
|
|
|
|
|
else: |
|
|
|
|
|
source_time = os.path.getmtime(source) |
|
|
|
|
|
dest_time = os.path.getmtime(dest) |
|
|
|
|
|
try: |
|
|
|
|
|
if dest_time < source_time: |
|
|
|
|
|
print("Updating WhatCanWePlay rust library with newer library file...") |
|
|
|
|
|
shutil.copy2(source, dest) |
|
|
|
|
|
except Exception: |
|
|
|
|
|
print("Failed to update WhatCanWePlay rust library. It will be re-attempted when next launched.") |
|
|
|
|
|
|
|
|
|
|
|
from .lib.bin import whatcanweplay as wcwp |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
print("Failed to load WhatCanWePlay rust library. Please go to \"lib/wcwp_rust\" and run \"cargo build --release\"") |
|
|
|
|
|
print(e) |
|
|
|
|
|
raise e |
|
|
|
|
|
print("WhatCanWePlay rust lib loaded (" + str(wcwp.__file__) + ")") |
|
|
|
|
|
|
|
|
from flask import Flask, request, jsonify, Response, render_template, redirect, session, url_for, make_response |
|
|
from flask import Flask, request, jsonify, Response, render_template, redirect, session, url_for, make_response |
|
|
import requests |
|
|
import requests |
|
|
from urllib import parse |
|
|
from urllib import parse |
|
|
from werkzeug.exceptions import BadRequest |
|
|
from werkzeug.exceptions import BadRequest |
|
|
import json |
|
|
import json |
|
|
from .steam_utils import get_steam_user_info, get_steam_user_friend_list, get_owned_steam_games |
|
|
|
|
|
from .igdb_utils import get_steam_game_info |
|
|
|
|
|
from requests import HTTPError |
|
|
from requests import HTTPError |
|
|
import secrets |
|
|
import secrets |
|
|
from datetime import timezone, datetime, timedelta |
|
|
from datetime import timezone, datetime, timedelta |
|
|
from itsdangerous import URLSafeSerializer |
|
|
from itsdangerous import URLSafeSerializer |
|
|
from os import path |
|
|
from os import path |
|
|
|
|
|
import traceback |
|
|
|
|
|
|
|
|
# Load config |
|
|
# Load config |
|
|
def create_app(): |
|
|
def create_app(): |
|
@@ -33,25 +66,30 @@ def create_app(): |
|
|
config = json.load(open(path.join(root_path, "config.json"), "r")) |
|
|
config = json.load(open(path.join(root_path, "config.json"), "r")) |
|
|
steam_key = config["steam-key"] |
|
|
steam_key = config["steam-key"] |
|
|
igdb_key = config["igdb-client-id"] |
|
|
igdb_key = config["igdb-client-id"] |
|
|
|
|
|
igdb_secret = config["igdb-secret"] |
|
|
debug = config.get("debug", config.get("DEBUG", False)) |
|
|
debug = config.get("debug", config.get("DEBUG", False)) |
|
|
enable_api_tests = config.get("enable-api-tests", debug) |
|
|
enable_api_tests = config.get("enable-api-tests", debug) |
|
|
cookie_max_age_dict = config.get("cookie-max-age", {}) |
|
|
cookie_max_age_dict = config.get("cookie-max-age", {}) |
|
|
info_max_age_dict = config.get("info-max-age", {}) |
|
|
|
|
|
|
|
|
cache_max_age_dict = config.get("igdb-cache-max-age", config.get("igdb-cache-info-age", {})) |
|
|
source_url = config.get("source-url", "") |
|
|
source_url = config.get("source-url", "") |
|
|
contact_email = config["contact-email"] |
|
|
contact_email = config["contact-email"] |
|
|
privacy_email = config.get("privacy-email", contact_email) |
|
|
privacy_email = config.get("privacy-email", contact_email) |
|
|
connect_timeout = config.get("connect-timeout", 0.0) |
|
|
connect_timeout = config.get("connect-timeout", 0.0) |
|
|
|
|
|
commit_hash_filename = config.get("commit-hash-file", ".wcwp-commit-hash") |
|
|
donate_url = config.get("donate-url", "") |
|
|
donate_url = config.get("donate-url", "") |
|
|
if connect_timeout <= 0.0: |
|
|
if connect_timeout <= 0.0: |
|
|
connect_timeout = None |
|
|
connect_timeout = None |
|
|
read_timeout = config.get("read-timeout", 0.0) |
|
|
read_timeout = config.get("read-timeout", 0.0) |
|
|
if read_timeout <= 0.0: |
|
|
if read_timeout <= 0.0: |
|
|
read_timeout = None |
|
|
read_timeout = None |
|
|
|
|
|
cache_file = config.get("igdb-cache-file") |
|
|
|
|
|
if not os.path.isabs(cache_file): |
|
|
|
|
|
cache_file = os.path.join(root_path, cache_file) |
|
|
|
|
|
|
|
|
# Create uWSGI callable |
|
|
# Create uWSGI callable |
|
|
app = Flask(__name__) |
|
|
app = Flask(__name__) |
|
|
app.debug = debug |
|
|
app.debug = debug |
|
|
app.secret_key = config.get("secret-key", secrets.token_hex()) # If not set, cookies will be invalidated every time the app is reloaded |
|
|
|
|
|
|
|
|
app.secret_key = config["secret-key"] |
|
|
|
|
|
|
|
|
# Hide requests to /steam_login to prevent linking Steam ID to IP in logs |
|
|
# Hide requests to /steam_login to prevent linking Steam ID to IP in logs |
|
|
from werkzeug import serving |
|
|
from werkzeug import serving |
|
@@ -68,20 +106,61 @@ def create_app(): |
|
|
cookie_max_age = timedelta(**cookie_max_age_dict).total_seconds() |
|
|
cookie_max_age = timedelta(**cookie_max_age_dict).total_seconds() |
|
|
if cookie_max_age == 0: |
|
|
if cookie_max_age == 0: |
|
|
cookie_max_age = None |
|
|
cookie_max_age = None |
|
|
info_max_age = timedelta(**info_max_age_dict).total_seconds() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Setup cache info max age |
|
|
|
|
|
cache_max_age = 0.0 |
|
|
|
|
|
if cache_file: |
|
|
|
|
|
cache_max_age = timedelta(**cache_max_age_dict).total_seconds() |
|
|
|
|
|
|
|
|
|
|
|
print("cookies set to expire after %f seconds" % cookie_max_age) |
|
|
|
|
|
print("cache set to expire after %f seconds" % cache_max_age) |
|
|
|
|
|
|
|
|
|
|
|
def fetch_and_store_commit_hash(): |
|
|
|
|
|
f = open(commit_hash_filename, "w") |
|
|
|
|
|
import subprocess |
|
|
|
|
|
args = ['--git-dir=' + os.path.join(os.path.abspath(os.path.dirname(__file__)), ".git"), 'rev-parse', '--short', 'HEAD'] |
|
|
|
|
|
try: |
|
|
|
|
|
try: |
|
|
|
|
|
commit_hash = subprocess.check_output(['git'] + args).decode("utf-8").strip() |
|
|
|
|
|
f.write(commit_hash) |
|
|
|
|
|
f.close() |
|
|
|
|
|
return commit_hash |
|
|
|
|
|
except: |
|
|
|
|
|
commit_hash = subprocess.check_output(['/usr/bin/git'] + args).decode("utf-8").strip() |
|
|
|
|
|
f.write(commit_hash) |
|
|
|
|
|
f.close() |
|
|
|
|
|
return commit_hash |
|
|
|
|
|
except: |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
|
|
|
@app.before_first_request |
|
|
|
|
|
def before_first_request(): |
|
|
|
|
|
fetch_and_store_commit_hash() |
|
|
|
|
|
|
|
|
print("cookies set to expire after {} seconds".format(cookie_max_age)) |
|
|
|
|
|
print("steam info set to refresh after {} seconds".format(info_max_age)) |
|
|
|
|
|
|
|
|
def get_commit_hash(): |
|
|
|
|
|
try: |
|
|
|
|
|
f = open(commit_hash_filename, "r") |
|
|
|
|
|
hash = f.read() |
|
|
|
|
|
return hash |
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
def basic_info_dict(): |
|
|
def basic_info_dict(): |
|
|
email_rev = contact_email.split("@") |
|
|
email_rev = contact_email.split("@") |
|
|
return { |
|
|
|
|
|
|
|
|
basic_info = { |
|
|
"contact_email_user_reversed": email_rev[0][::-1], |
|
|
"contact_email_user_reversed": email_rev[0][::-1], |
|
|
"contact_email_domain_reversed": email_rev[1][::-1], |
|
|
"contact_email_domain_reversed": email_rev[1][::-1], |
|
|
"source_url": source_url, |
|
|
"source_url": source_url, |
|
|
"donate_url": donate_url |
|
|
"donate_url": donate_url |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
commit = get_commit_hash() |
|
|
|
|
|
if commit: |
|
|
|
|
|
basic_info["commit"] = commit |
|
|
|
|
|
|
|
|
|
|
|
return basic_info |
|
|
|
|
|
|
|
|
# Tries to fetch the Steam info cookie, returns an errcode and a dict |
|
|
# Tries to fetch the Steam info cookie, returns an errcode and a dict |
|
|
# |
|
|
# |
|
|
# Errcodes: |
|
|
# Errcodes: |
|
@@ -113,18 +192,20 @@ def create_app(): |
|
|
|
|
|
|
|
|
def refresh_steam_cookie(steamid: int, response): |
|
|
def refresh_steam_cookie(steamid: int, response): |
|
|
if steamid <= 0: |
|
|
if steamid <= 0: |
|
|
response.set_cookie("steam_info", "", secure=True) |
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
|
|
|
info = get_steam_user_info(steam_key, [steamid], connect_timeout, read_timeout) |
|
|
|
|
|
|
|
|
|
|
|
if info["errcode"] != 0 or not info["users"].get(steamid, {}).get("exists", False): |
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True) |
|
|
|
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True, httponly=True) |
|
|
return {} |
|
|
return {} |
|
|
|
|
|
|
|
|
info = info["users"][steamid] |
|
|
|
|
|
if info_max_age: |
|
|
|
|
|
info["expires"] = (datetime.now(timezone.utc) + timedelta(seconds=info_max_age)).timestamp() |
|
|
|
|
|
|
|
|
info = {} |
|
|
|
|
|
try: |
|
|
|
|
|
info = wcwp.steam.get_steam_users_info(steam_key, [steamid])[0] |
|
|
|
|
|
except IndexError: |
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True, httponly=True) |
|
|
|
|
|
return {} |
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True, httponly=True) |
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
ser = URLSafeSerializer(app.secret_key) |
|
|
ser = URLSafeSerializer(app.secret_key) |
|
|
response.set_cookie( |
|
|
response.set_cookie( |
|
|
"steam_info", |
|
|
"steam_info", |
|
@@ -143,7 +224,7 @@ def create_app(): |
|
|
if errcode == 3: |
|
|
if errcode == 3: |
|
|
steam_info = refresh_steam_cookie(steam_info.get("steam_id", -1), response) |
|
|
steam_info = refresh_steam_cookie(steam_info.get("steam_id", -1), response) |
|
|
elif errcode != 0: |
|
|
elif errcode != 0: |
|
|
response.set_cookie("steam_info", "", secure=True) |
|
|
|
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True, httponly=True) |
|
|
steam_info = {} |
|
|
steam_info = {} |
|
|
|
|
|
|
|
|
response.data = render_template("home.html", steam_info=steam_info, **basic_info_dict()) |
|
|
response.data = render_template("home.html", steam_info=steam_info, **basic_info_dict()) |
|
@@ -157,7 +238,7 @@ def create_app(): |
|
|
def steam_login(): |
|
|
def steam_login(): |
|
|
if request.method == "POST": |
|
|
if request.method == "POST": |
|
|
steam_openid_url = 'https://steamcommunity.com/openid/login' |
|
|
steam_openid_url = 'https://steamcommunity.com/openid/login' |
|
|
return_url = url_for("steam_login", _external=True) |
|
|
|
|
|
|
|
|
return_url = request.base_url |
|
|
params = { |
|
|
params = { |
|
|
'openid.ns': "http://specs.openid.net/auth/2.0", |
|
|
'openid.ns': "http://specs.openid.net/auth/2.0", |
|
|
'openid.identity': "http://specs.openid.net/auth/2.0/identifier_select", |
|
|
'openid.identity': "http://specs.openid.net/auth/2.0/identifier_select", |
|
@@ -183,7 +264,7 @@ def create_app(): |
|
|
def steam_logout(): |
|
|
def steam_logout(): |
|
|
response = redirect(url_for("index")) |
|
|
response = redirect(url_for("index")) |
|
|
response.headers["X-Robots-Tag"] = "none" |
|
|
response.headers["X-Robots-Tag"] = "none" |
|
|
response.set_cookie("steam_info", "", secure=True) |
|
|
|
|
|
|
|
|
response.set_cookie("steam_info", "", secure=True, httponly=True) |
|
|
return response |
|
|
return response |
|
|
|
|
|
|
|
|
def validate_steam_identity(params): |
|
|
def validate_steam_identity(params): |
|
@@ -214,46 +295,196 @@ def create_app(): |
|
|
) |
|
|
) |
|
|
errcode, steam_info = fetch_steam_cookie(request) |
|
|
errcode, steam_info = fetch_steam_cookie(request) |
|
|
if "steam_id" not in steam_info.keys(): |
|
|
if "steam_id" not in steam_info.keys(): |
|
|
return ("Not signed in to Steam", 403) |
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
"Not signed in to Steam. Please refresh the page.", |
|
|
|
|
|
403 |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
friends_info = get_steam_user_friend_list( |
|
|
|
|
|
steam_key, |
|
|
|
|
|
steam_info["steam_id"], |
|
|
|
|
|
connect_timeout, |
|
|
|
|
|
read_timeout |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
friends_info = wcwp.steam.get_friend_list( |
|
|
|
|
|
steam_key, |
|
|
|
|
|
steam_info["steam_id"] |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
for user in friends_info: |
|
|
|
|
|
if "steam_id" in user.keys(): |
|
|
|
|
|
user["steam_id"] = str(user["steam_id"]) |
|
|
|
|
|
user["exists"] = True |
|
|
|
|
|
|
|
|
|
|
|
return jsonify(friends_info) |
|
|
|
|
|
except wcwp.steam.BadWebkeyException: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return ( |
|
|
|
|
|
"Site has bad Steam API key. Please contact us about this error at " + contact_email, |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
except wcwp.steam.ServerErrorException: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return ( |
|
|
|
|
|
"Steam had an internal server error. Please try again later.", |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
except wcwp.steam.BadResponseException: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return ( |
|
|
|
|
|
"Steam returned an unparseable response. Please try again later.", |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
except wcwp.steam.FriendListPrivateException: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return ( |
|
|
|
|
|
"WhatCanWePlay cannot retrieve your friend list. Please change your friend list visibility to public and refresh the page.", |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
if debug: |
|
|
|
|
|
return ( |
|
|
|
|
|
traceback.format_exc(), |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
else: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return ( |
|
|
|
|
|
"An unknown error has occurred. Please try again later.", |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
errcode = friends_info.pop("errcode") |
|
|
|
|
|
|
|
|
|
|
|
if errcode == 1: |
|
|
|
|
|
return ("Site has bad Steam API key. Please contact us about this error at " + contact_email, 500) |
|
|
|
|
|
elif errcode == 2: |
|
|
|
|
|
return ("Steam took too long to respond. Please try again later.", 500) |
|
|
|
|
|
elif errcode == 3: |
|
|
|
|
|
return ("Steam took too long to transmit info. Please try again later.", 500) |
|
|
|
|
|
elif errcode == 4: |
|
|
|
|
|
return ("Your Friend List is not publicly accessible, and cannot be retrieved by WhatCanWePlay. Please set your Friend list visibility to Public and refresh the page.", 500) |
|
|
|
|
|
elif errcode == -1: |
|
|
|
|
|
return ("An unknown error occurred. Please try again later.", 500) |
|
|
|
|
|
|
|
|
def refresh_igdb_token(): |
|
|
|
|
|
try: |
|
|
|
|
|
token_path = path.join(root_path, "bearer-token.json") |
|
|
|
|
|
token = wcwp.igdb.fetch_twitch_token(igdb_key, igdb_secret) |
|
|
|
|
|
token["expiry"] = datetime.now(timezone.utc).timestamp() + token.get("expires_in", 0) |
|
|
|
|
|
token.pop("expires_in") |
|
|
|
|
|
json.dump(token, open(token_path, "w")) |
|
|
|
|
|
return token.get("access_token", "") |
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
|
|
|
def get_igdb_token(): |
|
|
|
|
|
try: |
|
|
|
|
|
token_path = path.join(root_path, "bearer-token.json") |
|
|
|
|
|
if path.exists(token_path): |
|
|
|
|
|
token_file = open(token_path) |
|
|
|
|
|
token = json.load(token_file) |
|
|
|
|
|
token_file.close() |
|
|
|
|
|
if datetime.now(timezone.utc).timestamp() >= token.get("expiry", 0): |
|
|
|
|
|
return refresh_igdb_token() |
|
|
|
|
|
else: |
|
|
|
|
|
return token.get("access_token", "") |
|
|
|
|
|
else: |
|
|
|
|
|
return refresh_igdb_token() |
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return "" |
|
|
|
|
|
|
|
|
|
|
|
cache_init_query = """ |
|
|
|
|
|
CREATE TABLE IF NOT EXISTS game ( |
|
|
|
|
|
steam_id INTEGER PRIMARY KEY, |
|
|
|
|
|
igdb_id INTEGER, |
|
|
|
|
|
name STRING, |
|
|
|
|
|
supported_players INTEGER DEFAULT(0), |
|
|
|
|
|
cover_id STRING, |
|
|
|
|
|
has_multiplayer BOOLEAN, |
|
|
|
|
|
expiry REAL DEFAULT(0.0) |
|
|
|
|
|
); |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
CACHE_VERSION = 1 |
|
|
|
|
|
|
|
|
|
|
|
def initialize_cache(): |
|
|
|
|
|
import sqlite3 |
|
|
|
|
|
cache = sqlite3.connect(cache_file) |
|
|
|
|
|
cache.execute(cache_init_query) |
|
|
|
|
|
#cache.execute("PRAGMA user_version = ?;", [CACHE_VERSION]) # Doesn't work? |
|
|
|
|
|
cache.execute("PRAGMA user_version = %d" % CACHE_VERSION) |
|
|
|
|
|
return cache |
|
|
|
|
|
|
|
|
|
|
|
def cache_is_correct_version(cache): |
|
|
|
|
|
return cache.execute("PRAGMA user_version;").fetchone()[0] == CACHE_VERSION |
|
|
|
|
|
|
|
|
|
|
|
def update_cached_games(game_info): |
|
|
|
|
|
if not cache_file: |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
friends_info = get_steam_user_info(steam_key, friends_info["friends"], connect_timeout, read_timeout) |
|
|
|
|
|
|
|
|
|
|
|
errcode = friends_info.pop("errcode") |
|
|
|
|
|
if errcode == 1: |
|
|
|
|
|
return ("Site has bad Steam API key. Please contact us about this error at " + contact_email, 500) |
|
|
|
|
|
elif errcode == 2: |
|
|
|
|
|
return ("Steam took too long to respond. Please try again later.", 500) |
|
|
|
|
|
elif errcode == 3: |
|
|
|
|
|
return ("Steam took too long to transmit info. Please try again later.", 500) |
|
|
|
|
|
elif errcode == -1: |
|
|
|
|
|
return ("An unknown error occurred. Please try again later.", 500) |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
import sqlite3 |
|
|
|
|
|
cache = None |
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(cache_file): |
|
|
|
|
|
cache = sqlite3.connect(cache_file) |
|
|
|
|
|
# Check if cache is correct version |
|
|
|
|
|
if not cache_is_correct_version(cache): |
|
|
|
|
|
# Cache is the wrong version, rebuild |
|
|
|
|
|
print("Cache file is the wrong version! Rebuilding... ") |
|
|
|
|
|
cache.close() |
|
|
|
|
|
os.remove(cache_file) |
|
|
|
|
|
cache = initialize_cache() |
|
|
|
|
|
else: |
|
|
|
|
|
cache = initialize_cache() |
|
|
|
|
|
|
|
|
|
|
|
insert_info = [ |
|
|
|
|
|
[ |
|
|
|
|
|
game.get("steam_id"), |
|
|
|
|
|
game.get("igdb_id"), |
|
|
|
|
|
game.get("name"), |
|
|
|
|
|
game.get("supported_players"), |
|
|
|
|
|
game.get("cover_id"), |
|
|
|
|
|
game.get("has_multiplayer"), |
|
|
|
|
|
datetime.now(timezone.utc).timestamp() + cache_max_age |
|
|
|
|
|
] for game in game_info |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
cache.executemany( |
|
|
|
|
|
"INSERT OR REPLACE INTO game VALUES (?,?,?,?,?,?,?);", |
|
|
|
|
|
insert_info |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
cache.commit() |
|
|
|
|
|
cache.close() |
|
|
|
|
|
except Exception: |
|
|
|
|
|
print("FAILED TO UPDATE CACHE DB") |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
# returns [info of cached games], (set of uncached ids) |
|
|
|
|
|
def get_cached_games(steam_ids): |
|
|
|
|
|
if not cache_file: |
|
|
|
|
|
return [], set(steam_ids) |
|
|
|
|
|
|
|
|
|
|
|
game_info = [] |
|
|
|
|
|
uncached = set(steam_ids) |
|
|
|
|
|
|
|
|
for user in friends_info["users"].values(): |
|
|
|
|
|
if "steam_id" in user.keys(): |
|
|
|
|
|
user["steam_id"] = str(user["steam_id"]) |
|
|
|
|
|
user["exists"] = True |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
import sqlite3 |
|
|
|
|
|
cache = sqlite3.connect(cache_file) |
|
|
|
|
|
cache.row_factory = sqlite3.Row |
|
|
|
|
|
|
|
|
return jsonify(friends_info["users"]) |
|
|
|
|
|
|
|
|
if not cache_is_correct_version(cache): |
|
|
|
|
|
return [], set(steam_ids) |
|
|
|
|
|
|
|
|
|
|
|
query_str = "SELECT * FROM game WHERE steam_id IN (%s)" % ("?" + (",?" * (len(steam_ids) - 1))) # Construct a query with arbitrary parameter length |
|
|
|
|
|
|
|
|
|
|
|
cursor = cache.execute( |
|
|
|
|
|
query_str, |
|
|
|
|
|
steam_ids |
|
|
|
|
|
) |
|
|
|
|
|
for row in cursor.fetchall(): |
|
|
|
|
|
game = dict(row) |
|
|
|
|
|
if datetime.now(timezone.utc).timestamp() < game.pop("expiry"): |
|
|
|
|
|
# Info hasn't expired |
|
|
|
|
|
game_info.append(game) |
|
|
|
|
|
uncached.remove(game["steam_id"]) |
|
|
|
|
|
|
|
|
|
|
|
# Expired info gets updated during update_cached_games() |
|
|
|
|
|
except Exception: |
|
|
|
|
|
print("EXCEPTION THROWN WHILE QUERYING GAME CACHE!") |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
return [], set(steam_ids) |
|
|
|
|
|
|
|
|
|
|
|
return game_info, uncached |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Errcodes |
|
|
# Errcodes |
|
|
# -1: An error occurred with a message. Additional fields: "message" |
|
|
# -1: An error occurred with a message. Additional fields: "message" |
|
@@ -316,90 +547,86 @@ def create_app(): |
|
|
json.dumps({"message": "Games intersection is capped at 10 users.", "errcode": -1}), |
|
|
json.dumps({"message": "Games intersection is capped at 10 users.", "errcode": -1}), |
|
|
200 |
|
|
200 |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
free_games = bool(body.get("include_free_games", False)) |
|
|
|
|
|
|
|
|
|
|
|
all_own = None |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
token = get_igdb_token() |
|
|
|
|
|
game_ids = wcwp.steam.intersect_owned_game_ids(steam_key, list(steamids)) |
|
|
|
|
|
|
|
|
for steamid in steamids: |
|
|
|
|
|
user_owned_games = get_owned_steam_games(steam_key, steamid, free_games, connect_timeout, read_timeout) |
|
|
|
|
|
errcode = user_owned_games["errcode"] |
|
|
|
|
|
|
|
|
fetched_game_count = 0 |
|
|
|
|
|
cached_game_count = 0 |
|
|
|
|
|
game_info = [] |
|
|
|
|
|
|
|
|
if errcode == 1: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": "Site has bad Steam API key. Please contact us about this error at {}.".format(contact_email), "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
elif errcode == 2: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": "Steam took too long to respond. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
elif errcode == 3: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": "Steam took too long to transmit info. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
elif errcode == 4: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"user": str(steamid), "errcode": 1}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
elif errcode == -1: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": "An unknown error occurred. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
games = user_owned_games.get("games") |
|
|
|
|
|
if len(games) == 0: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"user": str(steamid), "errcode": 2}), |
|
|
|
|
|
200 |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if not all_own: |
|
|
|
|
|
all_own = set(games) |
|
|
|
|
|
else: |
|
|
|
|
|
all_own = all_own & set(games) |
|
|
|
|
|
|
|
|
|
|
|
if len(all_own) == 0: |
|
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
# Step two: Fetch the game info |
|
|
|
|
|
game_info = get_steam_game_info(igdb_key, all_own, connect_timeout, read_timeout) |
|
|
|
|
|
|
|
|
if game_ids: |
|
|
|
|
|
game_info, uncached_ids = get_cached_games(game_ids) |
|
|
|
|
|
|
|
|
errcode = game_info["errcode"] |
|
|
|
|
|
|
|
|
cached_game_count = len(game_info) |
|
|
|
|
|
|
|
|
if errcode == 1: |
|
|
|
|
|
|
|
|
if uncached_ids: |
|
|
|
|
|
fetched_info, not_found = wcwp.igdb.get_steam_game_info(igdb_key, token, list(uncached_ids)) |
|
|
|
|
|
|
|
|
|
|
|
cache_info_update = fetched_info |
|
|
|
|
|
if not_found: |
|
|
|
|
|
for uncached_id in [id for id in not_found]: |
|
|
|
|
|
cache_info_update.append({"steam_id": uncached_id}) # Cache empty data to prevent further IGDB fetch attempts |
|
|
|
|
|
update_cached_games(cache_info_update) # TODO: Spin up separate process for caching? |
|
|
|
|
|
|
|
|
|
|
|
game_info += fetched_info |
|
|
|
|
|
fetched_game_count = len(fetched_info) |
|
|
|
|
|
|
|
|
|
|
|
print("Intersection resulted in %d games (%d from cache, %d from IGDB)" % (len(game_info), cached_game_count, fetched_game_count)) |
|
|
|
|
|
|
|
|
|
|
|
return jsonify({ |
|
|
|
|
|
"message": "Intersected successfully", |
|
|
|
|
|
"games": game_info, |
|
|
|
|
|
"errcode": 0 |
|
|
|
|
|
}) |
|
|
|
|
|
except wcwp.steam.BadWebkeyException: |
|
|
|
|
|
traceback.print_exc() |
|
|
return ( |
|
|
return ( |
|
|
json.dumps({"message": "Site has bad IGDB API key. Please contact us about this error at {}.".format(contact_email), "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
|
|
|
json.dumps({"message": "Site has bad Steam API key. Please contact us about this error at " + contact_email, "errcode": -1}), |
|
|
|
|
|
500 |
|
|
) |
|
|
) |
|
|
elif errcode == 2: |
|
|
|
|
|
|
|
|
except wcwp.steam.ServerErrorException: |
|
|
|
|
|
traceback.print_exc() |
|
|
return ( |
|
|
return ( |
|
|
json.dumps({"message": "IGDB took too long to respond. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
|
|
|
json.dumps({"message": "Steam had an internal server error. Please try again later.", "errcode": -1}), |
|
|
|
|
|
500 |
|
|
) |
|
|
) |
|
|
elif errcode == 3: |
|
|
|
|
|
|
|
|
except wcwp.steam.BadResponseException: |
|
|
|
|
|
traceback.print_exc() |
|
|
return ( |
|
|
return ( |
|
|
json.dumps({"message": "IGDB took too long to transmit info. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
|
|
|
json.dumps({"message": "Steam returned an unparseable response. Please try again later.", "errcode": -1}), |
|
|
|
|
|
500 |
|
|
) |
|
|
) |
|
|
elif errcode == 4: |
|
|
|
|
|
|
|
|
except wcwp.steam.GamesListPrivateException as e: |
|
|
|
|
|
if debug: |
|
|
|
|
|
print(e) |
|
|
|
|
|
else: |
|
|
|
|
|
print("Intersection interrupted due to private games list") |
|
|
return ( |
|
|
return ( |
|
|
json.dumps({"message": "WhatCanWePlay failed to acquire an IGDB token. Please contact us about this error at {}.".format(contact_email), "errcode": -1}) |
|
|
|
|
|
|
|
|
json.dumps({"errcode": 1, "user": str(e.args[1])}), |
|
|
|
|
|
500 |
|
|
) |
|
|
) |
|
|
elif errcode == -1: |
|
|
|
|
|
|
|
|
except wcwp.steam.GamesListEmptyException as e: |
|
|
|
|
|
if debug: |
|
|
|
|
|
print(e) |
|
|
|
|
|
else: |
|
|
|
|
|
print("Intersection interrupted due to private games list") |
|
|
return ( |
|
|
return ( |
|
|
json.dumps({"message": "An unknown error occurred. Please try again later.", "errcode": -1}), |
|
|
|
|
|
200 |
|
|
|
|
|
|
|
|
json.dumps({"errcode": 2, "user": str(e.args[1])}), |
|
|
|
|
|
500 |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
return jsonify({ |
|
|
|
|
|
"message": "Intersected successfully", |
|
|
|
|
|
"games": list(game_info.get("games", {}).values()), |
|
|
|
|
|
"errcode": 0 |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
except Exception: |
|
|
|
|
|
traceback.print_exc() |
|
|
|
|
|
if debug: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": traceback.format_exc(), "errcode": -1}), |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
else: |
|
|
|
|
|
return ( |
|
|
|
|
|
json.dumps({"message": "An unknown error has occurred. Please try again later.", "errcode": -1}), |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
return app |
|
|
return app |