diff --git a/PS2bot/ps2bot.py b/PS2bot/ps2bot.py new file mode 100644 index 0000000..ce12040 --- /dev/null +++ b/PS2bot/ps2bot.py @@ -0,0 +1,478 @@ +import json +import praw +import oauthPS2Bot +import os +import re +import requests +import shlex +import sqlite3 +import sys +import time +import traceback +from datetime import datetime,timedelta +import warnings +warnings.filterwarnings("ignore") + + +############################################################################## +## CONFIG +URL_CENSUS_CHAR_PC = 'http://census.daybreakgames.com/s:vAPP/get/ps2:v2/character/?name.first=%s&c:case=false&c:resolve=stat_history,faction,world,outfit_member_extended' +URL_CENSUS_CHAR_PS4_US = 'http://census.daybreakgames.com/s:vAPP/get/ps2ps4us:v2/character/?name.first=%s&c:case=false&c:resolve=stat_history,faction,world,outfit_member_extended' +URL_CENSUS_CHAR_PS4_EU = 'http://census.daybreakgames.com/s:vAPP/get/ps2ps4eu:v2/character/?name.first=%s&c:case=false&c:resolve=stat_history,faction,world,outfit_member_extended' + +URL_CENSUS_CHAR_STAT_PC = 'http://census.daybreakgames.com/s:vAPP/get/ps2:v2/characters_stat?character_id=%s&c:limit=5000' +URL_CENSUS_CHAR_STAT_PS4_US = 'http://census.daybreakgames.com/s:vAPP/get/ps2ps4us:v2/characters_stat?character_id=%s&c:limit=5000' +URL_CENSUS_CHAR_STAT_PS4_EU = 'http://census.daybreakgames.com/s:vAPP/get/ps2ps4eu:v2/characters_stat?character_id=%s&c:limit=5000' + +URL_SERVER_STATUS = 'https://census.daybreakgames.com/json/status?game=ps2' + +URL_DASANFALL = "[[dasanfall]](http://stats.dasanfall.com/ps2/player/%s)" +URL_FISU = "[[fisu]](http://ps2.fisu.pw/player/?name=%s)" +URL_FISU_PS4_US = '[[fisu]](http://ps4us.ps2.fisu.pw/player/?name=%s)' +URL_FISU_PS4_EU = '[[fisu]](http://ps4eu.ps2.fisu.pw/player/?name=%s)' +URL_PSU = "[[psu]](http://www.planetside-universe.com/character-%s.php)" +URL_PLAYERS = "[[players]](https://www.planetside2.com/players/#!/%s)" +URL_KILLBOARD = "[[killboard]](https://www.planetside2.com/players/#!/%s/killboard)" + +USERNAME = "ps2bot" + +SERVERS = { + '1': 'Connery (US West)', + '17': 'Emerald (US East)', + '10': 'Miller (EU)', + '13': 'Cobalt (EU)', + '25': 'Briggs (AU)', + '19': 'Jaeger', + '1000': 'Genudine', + '1001': 'Palos', + '1002': 'Crux', + '2000': 'Ceres', + '2001': 'Lithcorp' +} + +REPLY_TEXT_TEMPLATE = ''' +**Some stats about {char_name_truecase} ({game_version}).** + +------ + +- Character created: {char_creation} +- Last login: {char_login} +- Time played: {char_playtime} ({char_logins} login{login_plural}) +- Battle rank: {char_rank} +- Faction: {char_faction_en} +- Server: {char_server} +- Outfit: {char_outfit} +- Score: {char_score} | Captured: {char_captures} | Defended: {char_defended} +- Medals: {char_medals} | Ribbons: {char_ribbons} | Certs: {char_certs} +- Kills: {char_kills} | Assists: {char_assists} | Deaths: {char_deaths} | KDR: {char_kdr} +- Links: {third_party_websites} + + +''' + +REPLY_TEXT_FOOTER = ''' + +------ + +^^This ^^post ^^was ^^made ^^by ^^a ^^bot. +^^Have ^^feedback ^^or ^^a ^^suggestion? +[^^\[pm ^^the ^^creator\]] +(https://np.reddit.com/message/compose/?to=microwavable_spoon&subject=PS2Bot%20Feedback) +^^| [^^\[see ^^my ^^code\]](https://github.com/plasticantifork/PS2Bot) +''' + +#### #### +# For FUNCTION_MAP, see the bottom of the file # +#### #### +COMMAND_IDENTIFIERS = ['/u/' + USERNAME, 'u/' + USERNAME] +COMMAND_IDENTIFIERS = [c.lower() for c in COMMAND_IDENTIFIERS] +MULTIPLE_COMMAND_JOINER = '\n \n_____\n_____\n \n' + +## END OF CONFIG +############################################################################## + + +sql = sqlite3.connect((os.path.join(sys.path[0],'ps2bot-sql.db'))) +cur = sql.cursor() + +cur.execute('CREATE TABLE IF NOT EXISTS oldmentions(id TEXT)') +cur.execute('CREATE INDEX IF NOT EXISTS mentionindex on oldmentions(id)') +sql.commit() + +print('logging in') +r = oauthPS2Bot.login() +#import bot +#r = bot.oG() + +def now_stamp(): + psttime = datetime.utcnow() - timedelta(hours=7) + time_stamp = psttime.strftime("%m-%d-%y %I:%M:%S %p PST ::") + return time_stamp + + +############################################################################### +## FUNCTIONMAP FUNCTIONS +## The arguments for these functions are provided by functionmap_line(). +## Since we're working with user-provided input, we need to be ready to accept +## 0 - inf arguments. Thus, each function has default values for each parameter +## and a *trash bin where we can dump anything extra. This allows us to return +## when given insufficient input, and accept unlimited trash if we need to. +## Your function may take advantage of *trash if you want. + +def generate_report_pc(charname=None, *trash): + if charname is None: + return + third_parties = [ + {'url': URL_DASANFALL, 'identifier': 'char_id'}, + {'url': URL_FISU, 'identifier': 'char_name'}, + {'url': URL_PSU, 'identifier': 'char_id'}, + {'url': URL_PLAYERS, 'identifier': 'char_id'}, + {'url': URL_KILLBOARD, 'identifier': 'char_id'} + ] + return generate_report(charname, URL_CENSUS_CHAR_PC, URL_CENSUS_CHAR_STAT_PC, third_parties, 'PC') + +def generate_report_ps4_us(charname=None, *trash): + if charname is None: + return + third_parties = [ + {'url': URL_FISU_PS4_US, 'identifier': 'char_name'} + ] + return generate_report(charname, URL_CENSUS_CHAR_PS4_US, URL_CENSUS_CHAR_STAT_PS4_US, third_parties, 'PS4 US') + +def generate_report_ps4_eu(charname=None, *trash): + if charname is None: + return + third_parties = [ + {'url': URL_FISU_PS4_EU, 'identifier': 'char_name'} + ] + return generate_report(charname, URL_CENSUS_CHAR_PS4_EU, URL_CENSUS_CHAR_STAT_PS4_EU, third_parties, 'PS4 EU') + +def report_server_status(*trash): + status_updown = {'low': 'UP','medium': 'UP','high': 'UP','down': 'DOWN'} + status_pop = {'down': ''} + server_regions = {'Palos': 'Palos (US)','Genudine': 'Genudine (US)','Crux': 'Crux (US)'} + jcontent = json.loads(requests.get(URL_SERVER_STATUS).text) + results = [] + + def status_reader(jinfo, header): + table = [] + entries = [] + table.append(header) + table.append('\nserver | status | population') + table.append(':- | :- | :-') + + for server, status in jinfo.items(): + # These servers had their players migrated to other servers + # https://forums.daybreakgames.com/ps2/index.php?threads/ps4-game-update-2-8-12.231243/ + if any(nonexist in server for nonexist in ['Dahaka', 'Xelas', 'Rashnu', 'Searhus']): + continue + server = server_regions.get(server, server) + pop = status['status'] + updown = status_updown[pop] + pop = status_pop.get(pop, pop) + entries.append('%s | %s | %s' % (server, updown, pop)) + + entries.sort(key=lambda x: ('(US' in x, '(EU' in x, '(AU' in x, x), reverse=True) + table += entries + table.append('\n\n') + return table + + results += status_reader(jcontent['ps2']['Live'], '**PC**') + results += status_reader(jcontent['ps2']['Live PS4'], '**PS4**') + results = '\n'.join(results) + return results + +## END OF FUNCTIONMAP FUNCTIONS +############################################################################### + + +def generate_report(charname, url_census, url_statistics, third_parties, game_version): + try: + census_char = requests.get(url_census % charname) + census_char = census_char.text + census_char = json.loads(census_char) + except (IndexError, KeyError, requests.exceptions.HTTPError): + return None + + if census_char['returned'] != 1: + # no player with this name was found + return + + census_char = census_char['character_list'][0] + char_name_truecase = census_char['name']['first'] + char_id = census_char['character_id'] + try: + census_stat = requests.get(url_statistics % char_id) + census_stat = census_stat.text + census_stat = json.loads(census_stat) + if census_stat['returned'] == 0: + # When a player has an account, but never logged in / played, + # his stats page is empty and broken, so just return + return + except (IndexError, KeyError, requests.exceptions.HTTPError): + return + + time_format = "%a, %b %d, %Y (%m/%d/%y), %I:%M:%S %p PST" + char_creation = time.strftime(time_format, time.localtime(float(census_char['times']['creation']))) + char_login = time.strftime(time_format, time.localtime(float(census_char['times']['last_login']))) + char_login_count = int(float(census_char['times']['login_count'])) + char_hours, char_minutes = divmod(int(census_char['times']['minutes_played']), 60) + + char_playtime = "{:,} hour{s}".format(char_hours, s='' if char_hours == 1 else 's') + char_playtime += " {:,} minute{s}".format(char_minutes, s='' if char_minutes == 1 else 's') + + try: + char_score = int(census_char['stats']['stat_history'][8]['all_time']) + char_capture = int(census_char['stats']['stat_history'][3]['all_time']) + char_defend = int(census_char['stats']['stat_history'][4]['all_time']) + char_medal = int(census_char['stats']['stat_history'][6]['all_time']) + char_ribbon = int(census_char['stats']['stat_history'][7]['all_time']) + char_certs = int(census_char['stats']['stat_history'][1]['all_time']) + except (IndexError, KeyError, ValueError): + char_score = 0 + char_capture = 0 + char_defend = 0 + char_medal = 0 + char_ribbon = 0 + char_certs = 0 + + char_rank = '%s' % census_char['battle_rank']['value'] + char_rank_next = census_char['battle_rank']['percent_to_next'] + if char_rank_next != "0": + char_rank += " (%s%% to next)" % char_rank_next + + char_faction = census_char['faction'] + try: + char_outfit = census_char['outfit_member'] + if char_outfit['member_count'] != "1": + members = '{:,}'.format(int(char_outfit['member_count'])) + char_outfit = '[%s] %s (%s members)' % (char_outfit['alias'], char_outfit['name'], members) + else: + char_outfit = '[%s] %s (1 member)' % (char_outfit['alias'], char_outfit['name']) + except KeyError: + char_outfit = "None" + + try: + char_kills = int(census_char['stats']['stat_history'][5]['all_time']) + char_deaths = int(census_char['stats']['stat_history'][2]['all_time']) + if char_deaths != 0: + char_kdr = round(char_kills/char_deaths,3) + else: + char_kdr = char_kills + except (KeyError, ZeroDivisionError): + char_kills = 0 + char_deaths = 0 + char_kdr = 0 + + char_stat = census_stat['characters_stat_list'] + #print(char_stat) + char_assists = 0 + try: + for stat in char_stat: + if stat['stat_name'] == 'assist_count': + char_assists = int(stat['value_forever']) + break + except (IndexError, KeyError, ValueError): + char_assists = 0 + + third_parties_filled = [] + for website in third_parties: + url = website['url'] + if website['identifier'] == 'char_id': + url = url % char_id + elif website['identifier'] == 'char_name': + url = url % char_name_truecase + third_parties_filled.append(url) + third_parties_filled = ' '.join(third_parties_filled) + + reply_text = REPLY_TEXT_TEMPLATE.format( + char_name_truecase = char_name_truecase, + game_version = game_version, + char_creation = char_creation, + char_login = char_login, + char_playtime = char_playtime, + char_logins = '{:,}'.format(char_login_count), + login_plural = 's' if char_login_count != 1 else '', + char_rank = char_rank, + char_faction_en = char_faction['name']['en'], + char_server = SERVERS[census_char['world_id']], + char_outfit = char_outfit, + char_score = '{:,}'.format(char_score), + char_captures ='{:,}'.format(char_capture), + char_defended = '{:,}'.format(char_defend), + char_medals = '{:,}'.format(char_medal), + char_ribbons = '{:,}'.format(char_ribbon), + char_certs = '{:,}'.format(char_certs), + char_kills = '{:,}'.format(char_kills), + char_assists = '{:,}'.format(char_assists), + char_deaths = '{:,}'.format(char_deaths), + char_kdr = '{:,}'.format(char_kdr), + third_party_websites = third_parties_filled + ) + return reply_text + +def handle_username_mention(mention, *trash): + #print('handling username mention', mention.id) + mention.mark_as_read() + + try: + pauthor = mention.author.name + except AttributeError: + # Don't respond to deleted accounts + return + + if pauthor.lower() == USERNAME.lower(): + # Don't respond to yourself + return + + cur.execute('SELECT * FROM oldmentions WHERE ID=?', [mention.id]) + if cur.fetchone(): + # Item is already in database + return + + cur.execute('INSERT INTO oldmentions VALUES(?)', [mention.id]) + sql.commit() + + reply_text = functionmap_comment(mention.body) + + if reply_text in [[], None]: + return + + reply_text = MULTIPLE_COMMAND_JOINER.join(reply_text) + reply_text += REPLY_TEXT_FOOTER + #print('Generated reply text:', reply_text[:10]) + + print('%s Replying to %s by %s' % (now_stamp(), mention.id, pauthor)) + try: + mention.reply(reply_text) + except praw.errors.PRAWException: + return + +def functionmap_line(text): + #print('User said:', text) + elements = shlex.split(text) + #print('Broken into:', elements) + results = [] + for element_index, element in enumerate(elements): + if element.lower() not in COMMAND_IDENTIFIERS: + continue + + arguments = elements[element_index:] + assert arguments.pop(0).lower() in COMMAND_IDENTIFIERS + + # If the user has multiple command calls on one line + # (Which is stupid but they might do it anyway) + # Let's only process one at a time please. + for argument_index, argument in enumerate(arguments): + if argument.lower() in COMMAND_IDENTIFIERS: + arguments = arguments[:argument_index] + break + + #print('Found command:', arguments) + if len(arguments) == 0: + #print('Did nothing') + continue + + command = arguments[0].lower() + actual_function = command in FUNCTION_MAP + function = FUNCTION_MAP.get(command, DEFAULT_FUNCTION) + #print('Using function:', function.__name__) + + if actual_function: + # Currently, the first argument is the name of the command + # If we found an actual function, we can remove that + # (because add() doesn't need "add" as the first arg) + # If we're using the default, let's keep that first arg + # because it might be important. + arguments = arguments[1:] + result = function(*arguments) + #print('Output: %s' % result) + results.append(result) + return results + +def functionmap_comment(comment): + lines = comment.split('\n') + results = [] + for line in lines: + result = functionmap_line(line) + if result is None: + continue + result = list(filter(None, result)) + if result is []: + continue + results += result + + # If the user inputs the same command multiple times + # lets delete the duplicates + # We flip the list before and after so that dupes are removed + # from the back instead of front (because list.remove takes the + # first match) + results.reverse() + for item in results[:]: + if results.count(item) > 1: + results.remove(item) + results.reverse() + + return results + +def ps2bot(): + print('checking unreads') + unreads = list(r.get_unread(limit=None)) + mention_identifier = 'u/' + USERNAME.lower() + for message in unreads: + if mention_identifier in message.body.lower(): + handle_username_mention(message) + else: + message.mark_as_read() + + + +# This must be defined down here because it can't come before +# the function definitions (or else NameError) +DEFAULT_FUNCTION = generate_report_pc +FUNCTION_MAP = { + '!player': generate_report_pc, + '!p': generate_report_pc, + + '!playerps4us': generate_report_ps4_us, + '!ps4us': generate_report_ps4_us, + '!p4us': generate_report_ps4_us, + + '!playerps4eu': generate_report_ps4_eu, + '!ps4eu': generate_report_ps4_eu, + '!p4eu': generate_report_ps4_eu, + + '!status': report_server_status, + '!s': report_server_status +} +# lowercase it baby +FUNCTION_MAP = {c.lower():FUNCTION_MAP[c] for c in FUNCTION_MAP} + + + +try: + ps2bot() +except requests.exceptions.HTTPError: + print(now_stamp(), 'A site/service is down. Probably Reddit.') +except Exception: + traceback.print_exc() + +#SAMPLES = [ +#'u/ps2bot higby', +#'/u/ps2bot higby', +#'/u/PS2BOT higby', +#'/u/ps2bot !player higby', +#'/u/ps2bot !PLAYER higby', +#'/u/ps2bot higby /u/ps2bot !player higby', +#'/u/ps2bot', +#'/u/ps2bot !s !s !s !s', +#'/u/ps2bot !p4us bloodwolf\n/u/ps2bot !p bloodwolf\n/u/ps2bot !s', +#] +#for sample in SAMPLES: +# result = functionmap_comment(sample) +# #print(result) +# if result in [[], None]: +# continue +# message = MULTIPLE_COMMAND_JOINER.join(result) +# message += REPLY_TEXT_FOOTER +# print(message)