def get_guild_user_list(guild_id, bgg=None): """Fetch the member list for a BGG Guild""" if bgg is None: bgg = BoardGameGeek() guild = bgg.guild(guild_id) return guild.members
def fill_game_table(game_ids): bgg = BoardGameGeek() con = mdb.connect(user= '******', passwd='', db='userlist',unix_socket="/tmp/mysql.sock") #user, password, #database, #unix socket cur = con.cursor() tmpfile = open('gameIDs_done.txt','w') for idx,id in enumerate(game_ids): if idx%10==0: print "On file: ",idx g = bgg.game(game_id=id) try: cur.execute('''INSERT into games_new (id,name,expansion,year,playtime,bggrank,image,thumbnail,min_players,max_players,min_age,rating_average,rating_average_weight,rating_bayes_average,rating_median,rating_num_weights,rating_stddev,users_commented,users_owned,users_rated,users_trading,users_wanting,users_wishing,mechanics,categories,description) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)''', (g.id,g.name.encode('latin-1','ignore'),g.expansion,g.year,g.playing_time,g.boardgame_rank,g.image,g.thumbnail,g.min_players,g.max_players,g.min_age,g.rating_average,g.rating_average_weight,g.rating_bayes_average,g.rating_median,g.rating_num_weights,g.rating_stddev,g.users_commented,g.users_owned,g.users_rated,g.users_trading,g.users_wanting,g.users_wishing,', '.join(g.mechanics),', '.join(g.categories),g.description.encode('latin-1','ignore'))) except mdb.IntegrityError: print "Integrity Error, duplicate game id found: ",g.id con.commit() tmpfile.write(str(id)+'\n') tmpfile.close() con.close()
def test_sqlite_caching(): # test that we can use the SQLite cache # generate a temporary file fd, name = tempfile.mkstemp(suffix=".cache") # close the file and unlink it, we only need the temporary name os.close(fd) os.unlink(name) assert not os.path.isfile(name) with pytest.raises(BoardGameGeekError): # invalid value for the ttl parameter BoardGameGeek(cache="sqlite://{}?ttl=blabla&fast_save=0".format(name)) with pytest.raises(BoardGameGeekError): BoardGameGeek(cache="invalid://cache") bgg = BoardGameGeek(cache="sqlite://{}?ttl=1000".format(name)) user = bgg.user(TEST_VALID_USER) assert user is not None assert user.name == TEST_VALID_USER assert os.path.isfile(name) # clean up.. os.unlink(name)
def get(self, request): bgg = BoardGameGeek(retries=10, retry_delay=2) game = bgg.game("Dominion") game_rank = game.id ctx = {'game': game_rank} return render(request, 'board/bgg.html', ctx)
def test_no_caching(): # test that we can disable caching bgg = BoardGameGeek(cache=None) user = bgg.user(TEST_VALID_USER) assert user is not None assert user.name == TEST_VALID_USER
def bgg_game(request, game_id): bgg = BoardGameGeek() # we return error if game already in DB try: game = Game.objects.get(id_bgg=game_id) except Game.DoesNotExist: # the game is not in the DB. All fine # fetch game data from BGG try: bgg_game = bgg.game(game_id=game_id) except BoardGameGeekError: pass # We try to guess the game type - for suggestion to the user if "Children's Game" in bgg_game.categories: type_genre = 'Enfants' elif 'Abstract Strategy' in bgg_game.categories: type_genre = 'Stratégie' elif 'Area Control / Area Influence' in bgg_game.mechanics or 'Tile Placement' in bgg_game.mechanics: type_genre = 'Placement' elif 'Worker Placement' in bgg_game.mechanics: type_genre = 'Gestion' elif 'Racing' in bgg_game.mechanics: type_genre = 'Parcours' elif 'Auction/Bidding' in bgg_game.mechanics: type_genre = 'Enchères' elif 'Co-operative Play' in bgg_game.mechanics: type_genre = 'Coopératif' elif 'Party Game' in bgg_game.categories: type_genre = 'Ambiance' else: type_genre = None data = { 'id_bgg': bgg_game.id, 'name': bgg_game.name, 'type_genre': type_genre, 'min_player': bgg_game.min_players, 'max_player': bgg_game.max_players, 'min_age': bgg_game.min_age, 'duration': bgg_game.playing_time, 'description': bgg_game.description, 'thumbnail': bgg_game.thumbnail, 'image': bgg_game.image } return JsonResponse(data) else: # Game is already in DB - return 400 return JsonResponse( {"__errors__": [ { 'field': 'ID BGG', 'errors': ['Ce jeu existe deja. Nom du jeu: %s (ID BGG: %s)' % (game.name, game_id)] } ]}, status=400)
def save(self, *args, **kwargs): bgg = BoardGameGeek() game = bgg.game(game_id=self.bgg_id) self.name = "{0} ({1})".format(game.name, game.year) if game.image: img_response = requests.get(game.image) filename = game.image.split('/')[-1] self.image.save(filename, ContentFile(img_response.content), save=False) self.data['url'] = "https://boardgamegeek.com/boardgame/{0}".format(game.id) super().save(*args, **kwargs)
def get_game_info(game_id, bgg=None): """Fetch the BGG info for game having game_id""" if bgg is None: bgg = BoardGameGeek() try: game = bgg.game(game_id=game_id) except BoardGameGeekAPIRetryError: game = None return game
def render_to_response(self, context, **response_kwargs): bgg = BoardGameGeek() results = bgg.search( self.request.GET.get('query'), search_type = ['boardgame'] ) result_dict = [{'id': 'bgg_{0}'.format(r.id), 'name': "{0} ({1})".format(r.name, r.year)} for r in results] filtered_results = list(dict((v['id'], v) for v in result_dict).values()) return JsonResponse( {"results": filtered_results} )
def bgg_game(request, game_id): bgg = BoardGameGeek() # we return error if game already in DB try: game = Game.objects.get(id_bgg=game_id) except Game.DoesNotExist: # the game is not in the DB. All fine # fetch game data from BGG try: bgg_game = bgg.game(game_id=game_id) except BoardGameGeekError: pass # We try to guess the game type - for suggestion to the user if "Children's Game" in bgg_game.categories: type_genre = 'Enfants' elif 'Abstract Strategy' in bgg_game.categories: type_genre = 'Stratégie' elif 'Area Control / Area Influence' in bgg_game.mechanics or 'Tile Placement' in bgg_game.mechanics: type_genre = 'Placement' elif 'Worker Placement' in bgg_game.mechanics: type_genre = 'Gestion' elif 'Racing' in bgg_game.mechanics: type_genre = 'Parcours' elif 'Auction/Bidding' in bgg_game.mechanics: type_genre = 'Enchères' elif 'Co-operative Play' in bgg_game.mechanics: type_genre = 'Coopératif' elif 'Party Game' in bgg_game.categories: type_genre = 'Ambiance' else: type_genre = None data = { 'id_bgg': bgg_game.id, 'name': bgg_game.name, 'type_genre': type_genre, 'min_player': bgg_game.min_players, 'max_player': bgg_game.max_players, 'min_age': bgg_game.min_age, 'duration': bgg_game.playing_time, 'description': bgg_game.description, 'thumbnail': bgg_game.thumbnail, 'image': bgg_game.image } return JsonResponse(data) else: # Game is already in DB - return 400 return JsonResponse({"__errors__": [{'field': 'ID BGG', 'errors': ['Ce jeu existe deja. Nom du jeu: %s (ID BGG: %s)' % (game.name, game_id)]}]}, status=400)
def get_user_ratings(username, bgg=None): """Returns a dict: gameid -> rating""" if bgg is None: bgg = BoardGameGeek() collection = bgg.collection(username) user_ratings = dict() for item in collection: if item.rating: user_ratings[item.id] = item.rating return user_ratings
def test_rate_limiting_for_requests(): # create two threads, give each a list of games to fetch, disable cache and time the amount needed to # fetch the data. requests should be serialized, even if made from two different threads test_set_1 = [5, # acquire 31260, # agricola 72125] # "eclipse" test_set_2 = [18602, #caylus 28720, # brass 53953] # thunderstone] def _worker_thread(games): bgg = BoardGameGeek(cache=None, requests_per_minute=20) for g in games: bgg.game(game_id=g) t1 = threading.Thread(target=_worker_thread, args=(test_set_1, )) t2 = threading.Thread(target=_worker_thread, args=(test_set_2, )) start_time = time.time() t1.start() t2.start() t1.join(timeout=10000) t2.join(timeout=10000) end_time = time.time() # 20 requests per minute => a request every 3 seconds x 6 games => should take around 18 seconds assert 15 < end_time - start_time < 21 # +/- a few seconds... # second test, use caching and confirm it's working when combined with the rate limiting algorithm # do cached requests for the test set, then do them again. should take only half of the time bgg = BoardGameGeek(requests_per_minute=20) start_time = time.time() for g in test_set_1: bgg.game(game_id=g) end_time = time.time() assert 7 < end_time - start_time < 11 # 3 games should take ~9 seconds # repeat requests, should be served from cache for g in test_set_1: bgg.game(game_id=g) assert 0 < time.time() - end_time < 2
def __init__(self, db): self.db = db self.bgg = BoardGameGeek() self.publishers = { "999 Games": "&include%5Bpublisherid%5D=267", "Asmodee": "&include%5Bpublisherid%5D=157", "Avalon Hill": "&include%5Bpublisherid%5D=5", "Bergsala Enigma": "&include%5Bpublisherid%5D=6784", "Cool mini or not": "&include%5Bpublisherid%5D=34793", "Czech Games Edition": "&include%5Bpublisherid%5D=7345", "Days of Wonder": "&include%5Bpublisherid%5D=1027", "Don & Co": "&include%5Bpublisherid%5D=137", "Fantasy Flight Games": "&include%5Bpublisherid%5D=17", "Fun Forge": "&include%5Bpublisherid%5D=8832", "Games Workshop": "&include%5Bpublisherid%5D=26", "Guillotine Games": "&include%5Bpublisherid%5D=21020", "Hurrican": "&include%5Bpublisherid%5D=6015", "Iello": "&include%5Bpublisherid%5D=8923", "Intrafin Games": "&include%5Bpublisherid%5D=5380", "Libellud": "&include%5Bpublisherid%5D=9051", "Ludonaute": "&include%5Bpublisherid%5D=11688", "Monolith Games": "&include%5Bpublisherid%5D=27147", "Osprey Games": "&include%5Bpublisherid%5D=29313", "Plaid Hat Games": "&include%5Bpublisherid%5D=10754", "Queen Games": "&include%5Bpublisherid%5D=47", "Repos Production": "&include%5Bpublisherid%5D=4384", "Space Cowboys": "&include%5Bpublisherid%5D=25842", "Steve Jackson Games": "&include%5Bpublisherid%5D=19", "Story Factory": "&include%5Bpublisherid%5D=17940", "Studio McVey": "&include%5Bpublisherid%5D=21608", "The Game Master": "&include%5Bpublisherid%5D=2862", "White Goblin Games": "&include%5Bpublisherid%5D=4932", "Z-Man Games": "&include%5Bpublisherid%5D=538" }
def getNotFoundGames(games): '''take a list of games and return those that are not found.''' bgg = BGG() not_found = [] for game in games: try: log.info(u'Looking for {} on BGG.'.format(game)) found = bgg.game(game) except boardgamegeek.exceptions.BoardGameGeekError as e: log.critical(u'Error talking to BGG about {}'.format(game)) continue if not found: log.warning(u'Unable to find {} on BGG'.format(game)) not_found.append(game) return not_found
def __init__(self, UID, botdb): self._botdb = botdb self._botname = UID self._header = ( u'^*[{}](/r/r2d8)* ^*issues* ^*a* ^*series* ^*of* ^*sophisticated* ' u'^*bleeps* ^*and* ^*whistles...*\n\n'.format(self._botname)) dbpath = pjoin(getcwd(), u'{}-bgg.db'.format(self._botname)) self._bgg = BGG(cache=u'sqlite://{}?ttl=86400'.format(dbpath))
def get(self, request, game_id): form = Game.objects.get(pk=game_id) title_en = form.title_en try: bgg = BoardGameGeek(retries=10, retry_delay=2) game = bgg.game(title_en) game_rank = game.ranks game_rank_dict = game_rank[0] rank = game_rank_dict['value'] ctx = {'form': form, 'game_rank': rank} return render(request, 'board/game.html', ctx) except AttributeError: rank = "n/a" ctx = {'form': form, 'game_rank': rank} return render(request, 'board/game.html', ctx)
def bgg_games(ids=None, user=None, filename=None, number=None, progress=False, **kwargs): """Return a list of BoardGameGeek games; sourced by ID, or user, or file Args: ids: list games IDs (integers) used by BGG user: string BGG user name; if supplied, then : * IDs will be ignored filename: string name of file containing game data saved in JSON format number: integer max no. of valid games to retrieve; default is 10 progress: boolean show which games are being retrieved """ if not number: number = 10 else: number = int(number) tmpdir = tempfile.mkdtemp() predictable_filename = 'bgggames.cache' dbname = os.path.join(tmpdir, predictable_filename) bgg = BoardGameGeek(cache="sqlite://{}?ttl=1000".format(dbname), disable_ssl=True) games = [] if user: ids = [] #bgg_user = bgg.user(user) try: collection = bgg.collection(user) if collection: for game in collection: ids.append(game.id) else: print 'Unable to retrieve collection for %s - do they exist?' \ % user except BoardGameGeekAPIRetryError, err: print err
def __init__(self, game_id, short=500): """ Args: short: int number of characters to use for short description """ self._game = None self.short = int(short) or 500 self.bgg = BoardGameGeek(disable_ssl=True) if isinstance(game_id, int): self._game = self.bgg.game(game_id=game_id) elif isinstance(game_id, ""): self._game = self.bgg.game(name=game_id) else: print self.set_properties()
def test_rate_limiting_for_requests(): # create two threads, give each a list of games to fetch, disable cache and time the amount needed to # fetch the data. requests should be serialized, even if made from two different threads test_set_1 = [ 5, # acquire 31260, # agricola 72125 ] # "eclipse" test_set_2 = [ 18602, #caylus 28720, # brass 53953 ] # thunderstone] def _worker_thread(games): bgg = BoardGameGeek(cache=None, requests_per_minute=20) for g in games: bgg.game(game_id=g) t1 = threading.Thread(target=_worker_thread, args=(test_set_1, )) t2 = threading.Thread(target=_worker_thread, args=(test_set_2, )) start_time = time.time() t1.start() t2.start() t1.join(timeout=10000) t2.join(timeout=10000) end_time = time.time() # 20 requests per minute => a request every 3 seconds x 6 games => should take around 18 seconds assert 15 < end_time - start_time < 21 # +/- a few seconds... # second test, use caching and confirm it's working when combined with the rate limiting algorithm # do cached requests for the test set, then do them again. should take only half of the time bgg = BoardGameGeek(requests_per_minute=20) start_time = time.time() for g in test_set_1: bgg.game(game_id=g) end_time = time.time() assert 7 < end_time - start_time < 11 # 3 games should take ~9 seconds # repeat requests, should be served from cache for g in test_set_1: bgg.game(game_id=g) assert 0 < time.time() - end_time < 2
def bgg(): return BoardGameGeek(cache=None, retries=0, retry_delay=0) # disable retrying for testing
import csv import time from boardgamegeek import BoardGameGeek bgg = BoardGameGeek() # first import the game id list f = open("bgg_game_ids.txt") idlist = [] for line in f: idlist.append(int(line.split()[0])) f.close() # data file datafile = "bgg_games.csv" # max and min game id # (if you don't want to scrape the whole dataset in one go) min_game_id = 1 max_game_id = 100 # header line (variable names) header = ( 'snapshot_date', \ 'id', \ 'name', \ 'year', \ 'artists', \ 'categories', \ 'designers', \ 'expansion', \ 'expands', \
username = "******" # your name oldest_game = datetime(2015,12,31,0,0,0) # year, month, day of the oldest game you want to import (only valid for first time running the script) # connect to spreadsheet json_key = json.load(open(json_file)) scope = ['https://spreadsheets.google.com/feeds'] credentials = SignedJwtAssertionCredentials( json_key['client_email'], json_key['private_key'], scope) gc = gspread.authorize(credentials) sh = gc.open(worksheet) sheet1 = sh.worksheet("Sheet1") # connect to BGG bgg = BoardGameGeek() userplays = bgg.plays(bgg_username).plays # most recent game in spreadsheet gamedates = sheet1.col_values(1) oldest_game.strftime('%m/%d/%Y') if gamedates[len(gamedates)-1] == 'date': latestgame = oldest_game else: latestgame = gamedates[len(gamedates)-1] latestgame = datetime.strptime(latestgame,'%m/%d/%Y') lastrow = len(gamedates) headers = sheet1.row_values(1)
""" This script creates a list of all games belonging to my collection on boardgamegeek.com, and saves that list to a JSON file. It uses the BoardGameGeek library to get the collection and game information. """ from boardgamegeek import BoardGameGeek import json import urllib import re bgg = BoardGameGeek() collection = bgg.collection("escrimeuse") # First we get the IDs of all the games in the collection ids = [] for item in collection: ids.append(item.id) # Now we get the information for each game games = [] for i in ids: games.append(bgg.game(None,i)) # We're only interested in a subset of properties, so we create new objects wanted_keys = ["name", "year", "mechanics", "thumbnail", "categories", "max_players", "min_players", "playing_time"] new_games = [ { "id" : game.id, "name" : game.name, "year" : game.year, "mechanic" : game.mechanics, "thumbnail" : game.thumbnail, "category" : game.categories, "max_players" : game.max_players, "min_players" : game.min_players,
import csv import time from boardgamegeek import BoardGameGeek import json import diskcache as dc import progressbar cache = dc.Cache('tmp') name = { 'Ludimus': 'Ludimus', } bgg = BoardGameGeek(retries=10) def create_line(pair): id, owners = pair if cache.get(id, default=False): return json.loads(cache[id]) try: game = bgg.game(game_id=id) except: time.sleep(1) try: game = bgg.game(game_id=id) except: return None if game.expansion:
import json import requests import datetime from boardgamegeek import BoardGameGeek from collections import defaultdict # DATA PATH DIRPATH = "/data/bodogeimu" # Hipchat Option URL="api.hipchat.com" AUTH_TOKEN="" ROOM="" # BoardGameGeek Object bgg = BoardGameGeek() # ======================================== # Hipchat Notify # ======================================== def notify_hipchat(rank, rank_diff) : # Message Option CARD_URL="https://www.boardgamegeek.com/boardgame/191894/imagine" ID="ranking" TITLE="BoardGameGeek Imagine Ranking" ICON_URL="http://cf.geekdo-images.com/images/pic3061260.jpg" COLOR="yellow" DESCRIPTION_VALUE="2016/08/24 <b>"+str(rank)+"位</b> (前日比"+rank_diff+") です" # Create CONTENT
def _worker_thread(games): bgg = BoardGameGeek(cache=None, requests_per_minute=20) for g in games: bgg.game(game_id=g)
from boardgamegeek import BoardGameGeek from boardgamegeek import BoardGameGeekAPIError import pandas as pd bgg = BoardGameGeek() gameframe = pd.DataFrame({ "Name": [], "Year": [], "ID": [], "BGGRank": [], "Mechanics": [], "Playtime": [], "Min_Age": [], "Median_Rating": [], "Bays_Rating": [] }) for i in range(1, 12): try: g = bgg.game(game_id=i) gamedat = pd.DataFrame({ "Name": [g.name], "ID": [g.id], "Year": [g.year], "BGGRank": [g.boardgame_rank], "Mechanics": [g.mechanics], "Playtime": [g.playing_time], "Min_Age": [g.min_age], "Median_Rating": [g.rating_median], "Bays_Rating": [g.rating_bayes_average] }) gameframe = pd.concat([gameframe, gamedat])
class CommentHandler(object): def __init__(self, UID, botdb): self._botdb = botdb self._botname = UID self._header = ( '^*[{}](/r/r2d8)* ^*issues* ^*a* ^*series* ^*of* ^*sophisticated* ' '^*bleeps* ^*and* ^*whistles...*\n\n'.format(self._botname)) self._footer = '' dbpath = pjoin(getcwd(), '{}-bgg.db'.format(self._botname)) self._bgg = BGG(cache='sqlite://{}?ttl=86400'.format(dbpath)) def _bggQueryGame(self, name): '''Try "name", then if not found try a few other small things in an effort to find it.''' name = name.lower().strip( ) # GTL extra space at ends shouldn't be matching anyway, fix this. if not name: return None if len(name) > 128: log.warn('Got too long game name: {}'.format(name)) return None # Search IDs when name format is '#1234' if re.search('^#(\d+)$', name): game = self._bgg.game(name=None, game_id=name[1:]) if game: log.debug('found game {} via searching by ID'.format(name)) return game game = self._bgg.game(name) if game: return game # embedded url? If so, extract. log.debug('Looking for embedded URL') m = re.search('\[([^]]*)\]', name) if m: name = m.group(1) game = self._bgg.game(name) if game: return game # note: unembedded from here down # remove 'the's log.debug('removing "the"s') tmpname = re.sub('^the ', '', name) tmpname = re.sub('\sthe\s', ' ', tmpname) if tmpname != name: game = self._bgg.game(tmpname) if game: return game # add a "the" at start. log.debug('adding "the" at start') game = self._bgg.game('The ' + name) if game: return game # various substistutions. subs = [('[?!.:,]*', '', 'removing punctuation'), ('\sand\s', ' & ', 'and --> &'), ('\s&\s', ' and ', '& --> and')] for search, sub, logmess in subs: log.debug(logmess) tmpname = re.sub(search, sub, name) if tmpname != name: game = self._bgg.game(tmpname) if game: return game # well OK - let's pull out the heavy guns and use the search API. # this will give us a bunch of things to sort through, but hopefully # find something. return self._bggSearchGame(name) def _bggSearchGame(self, name): '''Use the much wider search API to find the game.''' items = self._bgg.search( name, search_type=BoardGameGeekNetworkAPI.SEARCH_BOARD_GAME, exact=True) if items and len(items) == 1: log.debug('Found exact match using search().') return self._bgg.game(items[0].name) # exact match not found, trying sloppy match items = self._bgg.search( name, search_type=BoardGameGeekNetworkAPI.SEARCH_BOARD_GAME) if items and not len(items): log.debug('Found no matches at all using search().') return None if items and len(items) == 1: log.debug('Found one match usinh search().') return self._bgg.game(items[0].name) if not items: return None # assume most owned is what people want. Is this good? Dunno. most_owned = None for i in items: game = self._bgg.game(None, game_id=i.id) # GTL the lib throws an uncaught exception if BGG assessed too quickly. # GTL - this needs to be fixed in the library. sleep(1) if getattr(game, 'expansion', False): log.debug('ignoring expansion') continue else: if not most_owned: most_owned = game else: most_owned = game if getattr( game, 'owned', 0) > most_owned.owned else most_owned if most_owned: return most_owned return None NO_SORT_KEYWORD = 'nosort' DEFAULT_SORT = 'sort_name' SORT_FUNCTIONS = { 'sort_name': lambda g: g.name, 'sort_year': lambda g: g.year, 'sort_rank': lambda g: g.rank # TODO } def _findGames(self, items, sort='name'): # convert aliases to real names. It may be better to do this after we don't find the # game. Oh, well. # I think this might be better behavior, since it makes it easy to # replace findable-but-unlikely results with the more popular result # that was probably intended. -TDHS for i in range(len(items)): real_name = self._botdb.get_name_from_alias(items[i]) if real_name: items[i] = real_name # filter out dups. items = list(set(items)) items = [unquote(b) for b in items] games = [] not_found = [] seen = set() for game_name in items: log.info('asking BGG for info on {}'.format(game_name)) try: # game = self._bgg.game(game_name) game = self._bggQueryGame(game_name) if game: if game.id not in seen: games.append(game) # don't add dups. This can happen when the same game is calledby two valid # names in a post. seen.add(game.id) else: not_found.append(game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.error('Error getting info from BGG on {}: {}'.format( game_name, e)) continue # sort by game name because why not? if sort and sort in self.SORT_FUNCTIONS: fn = self.SORT_FUNCTIONS.get(sort) games = sorted(games, key=fn) return [games, not_found] def _getBoldedEntries(self, comment): body = comment.body # bolded = re.findall(u'\*\*([^\*]+)\*\*', body) # Now I've got two problems. bolded = re.findall( '\*\*(#?[\w][\w\.\s:\-?$,!\'–&()\[\]]*[\w\.:\-?$,!\'–&()\[\]])\*\*', body, flags=re.UNICODE) if not bolded: log.warn( 'Got getinfo command, but nothing is bolded. Ignoring comment.' ) log.debug('comment was: {}'.format(body)) return # we now have all the games. if comment.subreddit.display_name.lower() == 'boardgamescirclejerk': cjgames = ['Gloomhaven', 'Patchwork', 'Scythe'] bolded = [choice(cjgames), 'Keyforge', 'Keyforge', 'Keyforge'] return bolded def _getInfoResponseBody(self, comment, gameNames, mode, columns=None, sort=None): assert mode assert gameNames [games, not_found] = self._findGames(gameNames, sort) # disallow long mode for if mode == 'long' and len(games) > 6: mode = 'short' if comment.subreddit.display_name.lower() == 'boardgamescirclejerk': not_found = None if not_found: log.debug('not found: {}'.format(', '.join(not_found))) if games: log.debug('Found games {}'.format(','.join( ['{} ({})'.format(g.name, g.year) for g in games]))) else: log.warn('Found no games in comment {}'.format(comment.id)) log.warning('Using mode {} and columns {}'.format(mode, columns)) if mode == 'short': infos = self._getShortInfos(games) elif mode == 'long': infos = self._getLongInfos(games) elif mode == 'tabular': assert columns infos = self._getInfoTable(games, columns) else: infos = self._getStdInfos(games) # append not found string if we didn't find a bolded string. if not_found: not_found = [ '[{}](http://boardgamegeek.com/geeksearch.php?action=search' '&objecttype=boardgame&q={}&B1=Go)'.format(n, quote(n)) for n in not_found ] infos.append( '\n\nBolded items not found at BGG (click to search): {}\n\n'. format(', '.join(not_found))) response = None if len(infos): response = self._header + '\n'.join([i for i in infos ]) + self._footer # TODO: why the copied list? return response def _getPlayers(self, game): if not game.min_players: return None if game.min_players == game.max_players: players = '{} p'.format(game.min_players) else: players = '{}-{} p'.format(game.min_players, game.max_players) return players DISPLAY_MODES = ['short', 'standard', 'long', 'tabular'] DEFAULT_DISPLAY_MODE = 'standard' def getInfo(self, comment: praw.models.Comment, subcommands: list, config: dict, replyTo=None): '''Reply to comment with game information. If replyTo is given reply to original else reply to given comment.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return mode = None if len(subcommands) > 0 and subcommands[0].lower( ) in self.DISPLAY_MODES: mode = subcommands[0].lower() else: mode = self.DEFAULT_DISPLAY_MODE columns = subcommands[1:] if mode == 'tabular' else None sort = self.DEFAULT_SORT if self.NO_SORT_KEYWORD in subcommands: sort = None else: for sort_type in self.SORT_FUNCTIONS.keys(): if sort_type in subcommands: sort = sort_type break footer = '\n' + config['footer'] if 'footer' in config else '' bolded = self._getBoldedEntries(comment) response = None if bolded: response = self._getInfoResponseBody(comment, bolded, mode, columns, sort) if response: if replyTo: replyTo.reply(response + footer) else: comment.reply(response + footer) log.info('Replied to info request for comment {}'.format( comment.id)) else: log.warn('Did not find anything to reply to in comment {}'.format( comment.id)) def _getShortInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = (' * [**{}**](http://boardgamegeek.com/boardgame/{}) ' ' ({}) by {}. '.format( game.name, game.id, game.year, ', '.join(getattr(game, 'designers', 'Unknown')))) if players: info += '{}; '.format(players) if game.playing_time and int(game.playing_time) != 0: info += '{} mins '.format(game.playing_time) infos.append(info) return infos def _getStdInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = ('[**{}**](http://boardgamegeek.com/boardgame/{}) ' ' ({}) by {}. {}; '.format( game.name, game.id, game.year, ', '.join(getattr(game, 'designers', 'Unknown')), players)) if game.playing_time and int(game.playing_time) != 0: info += '{} minutes; '.format(game.playing_time) if game.image: info += '[BGG Image]({}) '.format(game.image) info += '\n\n' data = ', '.join(getattr(game, 'mechanics', '')) if data: info += ' * Mechanics: {}\n'.format(data) people = 'people' if game.users_rated > 1 else 'person' info += ' * Average rating is {}; rated by {} {}. Weight: {}\n'.format( game.rating_average, game.users_rated, people, game.rating_average_weight) data = ', '.join([ '{}: {}'.format(r['friendlyname'], r['value']) for r in game.ranks ]) info += ' * {}\n\n'.format(data) log.debug('adding info: {}'.format(info)) infos.append(info) return infos ALLOWED_COLUMNS = { 'year': 'Year Published', # 'designers': 'Designers', # 'artists': 'Artists', 'rank': 'BGG Rank', 'rating': 'Average Rating', 'score': 'Geek Score (Weighted Rating)', 'rating_median': 'Median Rating', 'rating_stddev': 'Rating Standard Deviation', 'raters': 'Rating Count', 'owners': 'BGG Owner Count', # 'playercount': 'Player Count (min-max)' 'id': 'BGG ID' } def _getGameColumn(self, game: boardgamegeek.games.BoardGame, column): if column == 'year': return str(game.year) elif column == 'rank': return str(game.boardgame_rank) elif column == 'rating': return str(game.rating_average) elif column == 'score': return str(game.rating_bayes_average) elif column == 'rating_median': return str(game.rating_median) elif column == 'rating_stddev': return str(game.rating_stddev) elif column == 'raters': return str(game.users_rated) elif column == 'owners': return str(game.users_owned) elif column == 'id': return str(game.id) return 'unknown `{}`'.format(column) def _getInfoTable(self, games, columns): rows = list() # build header header = 'Game Name' alignment = ':--' unknownColumns = list() for column in columns: if column not in self.ALLOWED_COLUMNS: log.info('Unknown tabular column {}, skipping'.format(column)) unknownColumns.append(column) continue header = header + '|' + self.ALLOWED_COLUMNS[column] alignment = alignment + '|:--' rows.append(header) rows.append(alignment) columns = [c for c in columns if c not in unknownColumns] # build rows for game in games: row = '[**{}**](http://boardgamegeek.com/boardgame/{})'.format( game.name, game.id) for column in columns: row = row + '|' + self._getGameColumn(game, column) log.info('adding info: {}'.format(row)) rows.append(row) return rows def _getLongInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = ( 'Details for [**{}**](http://boardgamegeek.com/boardgame/{}) ' ' ({}) by {}. '.format( game.name, game.id, game.year, ', '.join(getattr(game, 'designers', 'Unknown')))) if players: info += '{}; '.format(players) if game.playing_time and int(game.playing_time) != 0: info += '{} minutes; '.format(game.playing_time) if game.image: info += '[BGG Image]({}) '.format(game.image) info += '\n\n' data = ', '.join(getattr(game, 'mechanics', '')) if data: info += ' * Mechanics: {}\n'.format(data) people = 'people' if game.users_rated > 1 else 'person' info += ' * Average rating is {}; rated by {} {}\n'.format( game.rating_average, game.users_rated, people) info += ' * Average Weight: {}; Number of Weights {}\n'.format( game.rating_average_weight, game.rating_num_weights) data = ', '.join([ '{}: {}'.format(r['friendlyname'], r['value']) for r in game.ranks ]) info += ' * {}\n\n'.format(data) info += 'Description:\n\n{}\n\n'.format(game.description) if len(games) > 1: info += '------' log.debug('adding info: {}'.format(info)) infos.append(info) return infos def repairComment(self, comment: praw.models.Comment, subcommands: list, config: dict): '''Look for maps from missed game names to actual game names. If found repair orginal comment.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return # # The repair is done by replacing the new games names with the old (wrong) # games names in the original /u/r2d8 response, then recreating the entire # post by regenerating it with the new (fixed) bolded game names. The just replacing # the orginal response with the new one. # log.debug('Got repair response, id {}'.format(comment.id)) if comment.is_root: # error here - this comment should be in response to a u/r2d8 comment. log.info('Got a repair comment as root, ignoring.') return parent = comment.parent() if parent.author.name != self._botname: log.info( 'Parent of repair comment is not authored by the bot, ignoring.' ) return # Look for patterns of **something**=**somethingelse**. This line creates a dict # of something: somethingelse for each one pattern found. repairs = { match[0]: match[1] for match in re.findall('\*\*([^\*]+)\*\*=\*\*([^\*]+)\*\*', comment.body) } pbody = parent.body for wrongName, repairedName in repairs.items(): # check to see if it's actually a game. log.info('Repairing {} --> {}'.format(wrongName, repairedName)) alias = self._botdb.get_name_from_alias(repairedName) tmp_name = alias if alias else repairedName tmp_game = self._bggQueryGame( tmp_name) # with caching it's ok to check twice if tmp_game: # In the parent body we want to replace [NAME](http://... with **NAME**(http:// pbody = pbody.replace('[' + wrongName + ']', '**' + tmp_name + '**') else: log.info( '{} seems to not be a game name according to BGG, ignoring.' .format(tmp_name)) # Now re-bold the not found strings so they are re-searched or re-added to the not found list. for nf in re.findall( '\[([\w|\s]+)]\(http://boardgamegeek.com/geeksearch.php', pbody): pbody += ' **{}**'.format(nf) # now re-insert the original command to retain the mode. grandparent = parent.parent() modes = list() if not grandparent: log.error('Cannot find original GP post. Assuming normal mode.') else: modes = re.findall('[getparent|get]info\s(\w+)', grandparent.body) targetmode = modes[0] if modes else self.DEFAULT_DISPLAY_MODE parent = parent.edit(pbody) bolded = self._getBoldedEntries(comment) new_reply = self._getInfoResponseBody(parent, bolded, targetmode) # should check for Editiable class somehow here. GTL log.debug('Replacing bot comment {} with: {}'.format( parent.id, new_reply)) parent.edit(new_reply) def xyzzy(self, comment: praw.models.Comment, subcommands: list, config: dict): comment.reply('Nothing happens.') def getParentInfo(self, comment: praw.models.Comment, subcommands: list, config: dict): '''Allows others to call the bot to getInfo for parent posts.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return log.debug('Got getParentInfo comment in id {}'.format(comment.id)) if comment.is_root: # error here - this comment should be in response to a u/r2d8 comment. log.info('Got a repair comment as root, ignoring.') return parent = comment.parent() self.getInfo(parent, subcommands=subcommands, config=config, replyTo=comment) def alias(self, comment: praw.models.Comment, subcommands: list, config: dict): '''add an alias to the database.''' if not self._botdb.is_admin(comment.author.name): log.info('got alias command from non admin {}, ignoring.'.format( comment.author.name)) return response = 'executing alias command.\n\n' # TODO: use bold fn for match in re.findall('\*\*([^\*]+)\*\*=\*\*([^\*]+)\*\*', comment.body): mess = 'Adding alias to database: "{}" = "{}"'.format( match[0], match[1]) log.info(mess) response += mess + '\n\n' self._botdb.add_alias(match[0], match[1]) comment.reply(response) def getaliases(self, comment: praw.models.Comment, subcommands: list, config: dict): if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return aliases = self._botdb.aliases() response = 'Current aliases:\n\n' for name, alias in sorted(aliases, key=lambda g: g[1]): response += ' * {} = {}\n'.format(alias, name) log.info('Responding to getalaises request with {} aliases'.format( len(aliases))) comment.reply(response) def expandURLs(self, comment: praw.models.Comment, subcommands: list, config: dict): if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return replyTo = None mode = None if len(subcommands) > 0 and subcommands[0].lower( ) in self.DISPLAY_MODES: mode = subcommands[0].lower() else: mode = self.DEFAULT_DISPLAY_MODE footer = '\n' + config['footer'] if 'footer' in config else '' body = comment.body urls = [ ('#' + id) for id in re.findall('boardgamegeek.com/(?:boardgame|thing)/(\d+)', body, flags=re.UNICODE) ] response = self._getInfoResponseBody(comment, urls, mode) log.error('footer {} ({})'.format(footer, type(footer))) if response: if replyTo: replyTo.reply(response + footer) else: comment.reply(response + footer) log.info('Replied to info request for comment {}'.format( comment.id)) else: log.warn('Did not find anything to reply to in comment {}'.format( comment.id)) def removalRequest(self, comment: praw.models.Comment, subcommands: list, config: dict): # if self._botdb.ignore_user(comment.author.name): # log.info("Ignoring comment by {}".format(comment.author.name)) # return # for now removals are limited to admins if not self._botdb.is_admin(comment.author.name): log.info('got remove command from non admin {}, ignoring.'.format( comment.author.name)) return if comment.is_root: log.error( 'removal requested on top-level comment {}, ignoring'.format( comment.id)) return botmessage: praw.models.Comment = comment.parent() try: # delete the post botmessage.delete() # attempt to unmark the parent as read if not botmessage.is_root: self._botdb.remove_comment(botmessage.parent) except boardgamegeek.exceptions.BoardGameGeekError as e: log.error('Error deleting comment {} by {}'.format( original.id, original.author.name)) return
from slackbot.bot import respond_to,listen_to from boardgamegeek import BoardGameGeek import re, json bgg = BoardGameGeek() def jointwo(string1, string2, joinstr): return joinstr.join([x for x in (string2, string2) if x]) @respond_to('user (.*)') @listen_to('\[\[user (.*)\]\]') def user_info(message, username): print('Getting information on user ' + username) message.reply('Getting user information on *{}*'.format(username)) user = bgg.user(username) if user == None: message.reply('No information found for user *{}*'.format(username)) return attachments = [{ 'fallback' : 'User information on ' + username, 'author_name': jointwo(user.firstname, user.lastname, ' ') + ' (' + user.name + ')', 'author_link': 'https://boardgamegeek.com/user/' + user.name, 'author_icon': 'https:' + user.avatarlink, 'color': 'good', 'thumb_url': 'https:' + user.avatarlink, 'fields' : [ {
def getGotWPostText(game_name, next_game_name): '''Take the name of a game, and return the GotW text to post to Reddit''' bgg = BGG() try: game = bgg.game(game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.critical(u'Error getting info from BGG on {}: {}'.format( game_name, e)) return None if not game: log.critical(u'Unable to find {} on BGG'.format(game_name)) return None title = u'Game of the Week: {}'.format(game.name) text = u'[//]: # (GOTWS)\n' text += (u'This week\'s game is [**{}**](http:{})\n\n'.format( game.name, game.image)) text += u' * **BGG Link**: [{}](http://www.boardgamegeek.com/boardgame/{})\n'.format( game.name, game.id) designers = getattr(game, u'designers', [u'Unknown']) plural = u's' if len(designers) > 1 else u'' text += u' * **Designer{}**: {}\n'.format(plural, ', '.join(designers)) publishers = getattr(game, u'publishers', [u'Unknown']) plural = u's' if len(publishers) > 1 else u'' text += u' * **Publisher{}**: {}\n'.format(plural, ', '.join(publishers)) text += u' * **Year Released**: {}\n'.format(game.year) mechanics = getattr(game, u'mechanics', [u'Unknown']) plural = u's' if len(mechanics) > 1 else u'' text += u' * **Mechanic{}**: {}\n'.format(plural, ', '.join(mechanics)) if game.min_players == game.max_players: players = '{}'.format(game.min_players) else: players = '{} - {}'.format(game.min_players, game.max_players) text += u' * **Number of Players**: {}\n'.format(players) text += u' * **Playing Time**: {} minutes\n'.format(game.playing_time) expansions = getattr(game, 'expansions', None) if expansions: text += u' * **Expansions**: {}\n'.format(', '.join( [e.name for e in expansions])) text += u' * **Ratings**:\n' people = u'people' if game.users_rated > 1 else u'person' text += u' * Average rating is {} (rated by {} {})\n'.format( game.rating_average, game.users_rated, people) ranks = u', '.join([ u'{}: {}'.format(r[u'friendlyname'], r[u'value']) for r in game.ranks ]) text += u' * {}\n'.format(ranks) text += u'\n\n' text += u'**Description from Boardgamegeek**:\n\n{}\n\n'.format( game.description) text += u'[//]: # (GOTWE)\n\n' text += '---------------------\n\n' if not next_game_name: text += u'There is no Game of the Week scheduled for next week.' else: try: game = bgg.game(next_game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.critical(u'Error getting info from BGG on {}: {}'.format( next_game_name, e)) if not game: text += u'Next Week: {}'.format(next_game_name) else: text += (u'Next Week: [**{}**](http://www.boardgamegeek' u'.com/boardgame/{})\n\n'.format(game.name, game.id)) text += (u' * The GOTW archive and schedule can be found ' u'[here](http://www.reddit.com/r/boardgames/wiki/' u'game_of_the_week).\n\n * Vote for future Games of the Week ' u'[here](/2l5xum).\n') return title, text
class CommentHandler(object): def __init__(self, UID, botdb): self._botdb = botdb self._botname = UID self._header = ( u'^*[{}](/r/r2d8)* ^*issues* ^*a* ^*series* ^*of* ^*sophisticated* ' u'^*bleeps* ^*and* ^*whistles...*\n\n'.format(self._botname)) dbpath = pjoin(getcwd(), u'{}-bgg.db'.format(self._botname)) self._bgg = BGG(cache=u'sqlite://{}?ttl=86400'.format(dbpath)) def _bggQueryGame(self, name): '''Try "name", then if not found try a few other small things in an effort to find it.''' name = name.lower().strip( ) # GTL extra space at ends shouldn't be matching anyway, fix this. if not name: return None if len(name) > 128: log.warn('Got too long game name: {}'.format(name)) return None game = self._bgg.game(name) if game: return game # Well OK, how about game ID? if not re.search(u'([^\d]+)', name): # all digits is probably an ID game = self._bgg.game(name=None, game_id=name) if game: log.debug('found game {} via searching by ID'.format(name)) return game # embedded url? If so, extract. log.debug('Looking for embedded URL') m = re.search('\[([^]]*)\]', name) if m: name = m.group(1) game = self._bgg.game(name) if game: return game # note: unembedded from here down # remove 'the's log.debug('removing "the"s') tmpname = re.sub('^the ', '', name) tmpname = re.sub('\sthe\s', ' ', tmpname) if tmpname != name: game = self._bgg.game(tmpname) if game: return game # add a "the" at start. log.debug('adding "the" at start') game = self._bgg.game('The ' + name) if game: return game # various substistutions. subs = [('[?!.:,]*', '', 'removing punctuation'), ('\sand\s', ' & ', 'and --> &'), ('\s&\s', ' and ', '& --> and')] for search, sub, logmess in subs: log.debug(logmess) tmpname = re.sub(search, sub, name) if tmpname != name: game = self._bgg.game(tmpname) if game: return game # well OK - let's pull out the heavy guns and use the search API. # this will give us a bunch of things to sort through, but hopefully # find something. return self._bggSearchGame(name) def _bggSearchGame(self, name): '''Use the much wider search API to find the game.''' items = self._bgg.search( name, search_type=BoardGameGeekNetworkAPI.SEARCH_BOARD_GAME, exact=True) if items and len(items) == 1: log.debug('Found exact match using search().') return self._bgg.game(items[0].name) # exact match not found, trying sloppy match items = self._bgg.search( name, search_type=BoardGameGeekNetworkAPI.SEARCH_BOARD_GAME) if items and not len(items): log.debug('Found no matches at all using search().') return None if items and len(items) == 1: log.debug('Found one match usinh search().') return self._bgg.game(items[0].name) if not items: return None # assume most owned is what people want. Is this good? Dunno. most_owned = None for i in items: game = self._bgg.game(None, game_id=i.id) # GTL the lib throws an uncaught exception if BGG assessed too quickly. # GTL - this needs to be fixed in the library. sleep(1) if getattr(game, 'expansion', False): log.debug('ignoring expansion') continue else: if not most_owned: most_owned = game else: most_owned = game if getattr( game, 'owned', 0) > most_owned.owned else most_owned if most_owned: return most_owned return None def _getInfoResponseBody(self, comment, mode=None): body = comment.body # bolded = re.findall(u'\*\*([^\*]+)\*\*', body) # Now I've got two problems. bolded = re.findall( u'\*\*([\w][\w\.\s:\-?$,!\'–&()\[\]]*[\w\.:\-?$,!\'–&()\[\]])\*\*', body, flags=re.UNICODE) if not bolded: log.warn( u'Got getinfo command, but nothing is bolded. Ignoring comment.' ) log.debug(u'comment was: {}'.format(body)) return # convert aliases to real names. It may be better to do this after we don't find the # game. Oh, well. for i in xrange(len(bolded)): real_name = self._botdb.get_name_from_alias(bolded[i]) if real_name: bolded[i] = real_name # filter out dups. bolded = list(set(bolded)) bolded = [unquote(b) for b in bolded] games = [] not_found = [] if comment.subreddit.display_name.lower() == u'boardgamescirclejerk': cjgames = [[u'Dead of Winter: A Crossroads Game'], [u'Scythe']] bolded = choice(cjgames) bolded = ['Scythe', 'Scythe', 'Scythe'] seen = set() for game_name in bolded: log.info(u'asking BGG for info on {}'.format(game_name)) try: # game = self._bgg.game(game_name) game = self._bggQueryGame(game_name) if game: if game.name not in seen: games.append(game) # don't add dups. This can happen when the same game is calledby two valid # names in a post. seen.add(game.name) else: not_found.append(game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.error(u'Error getting info from BGG on {}: {}'.format( game_name, e)) continue # sort by game name because why not? games = sorted(games, key=lambda g: g.name) # we now have all the games. mode = u'short' if len(games) > 6 else mode # not_found = list(set(bolded) - set([game.name for game in games])) if comment.subreddit.display_name.lower() == u'boardgamescirclejerk': not_found = None if not_found: log.debug(u'not found: {}'.format(u', '.join(not_found))) if games: log.debug(u'Found games {}'.format(u','.join( [u'{} ({})'.format(g.name, g.year) for g in games]))) else: log.warn(u'Found no games in comment {}'.format(comment.id)) # get the information for each game in a nice tidy list of strings. # get the mode if given. Can be short or long or normal. Default is normal. if not mode: m = re.search(u'getinfo\s(\w+)', body, flags=re.IGNORECASE) if m: mode = m.group(1).lower() if m.group(1).lower() in [ u'short', u'long' ] else mode if mode == u'short': infos = self._getShortInfos(games) elif mode == u'long': infos = self._getLongInfos(games) else: infos = self._getStdInfos(games) # append not found string if we didn't find a bolded string. if not_found: not_found = [ u'[{}](http://boardgamegeek.com/geeksearch.php?action=search' '&objecttype=boardgame&q={}&B1=Go)'.format(n, quote(n)) for n in not_found ] infos.append( u'\n\nBolded items not found at BGG (click to search): {}\n\n'. format(u', '.join(not_found))) response = None if len(infos): response = self._header + u'\n'.join([i for i in infos]) return response def _getPlayers(self, game): if not game.min_players: return None if game.min_players == game.max_players: players = '{} p'.format(game.min_players) else: players = '{}-{} p'.format(game.min_players, game.max_players) return players def getInfo(self, comment, replyTo=None, mode=None): '''Reply to comment with game information. If replyTo isot given reply to original else reply to given comment.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return response = self._getInfoResponseBody(comment, mode) if response: if replyTo: replyTo.reply(response) else: comment.reply(response) log.info(u'Replied to info request for comment {}'.format( comment.id)) else: log.warn(u'Did not find anything to reply to in comment'.format( comment.id)) def _getShortInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = (u' * [**{}**](http://boardgamegeek.com/boardgame/{}) ' u' ({}) by {}. '.format( game.name, game.id, game.year, u', '.join(getattr(game, u'designers', u'Unknown')))) if players: info += '{}; '.format(players) if game.playing_time and int(game.playing_time) != 0: info += '{} mins '.format(game.playing_time) infos.append(info) return infos def _getStdInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = (u'[**{}**](http://boardgamegeek.com/boardgame/{}) ' u' ({}) by {}. {}; '.format( game.name, game.id, game.year, u', '.join(getattr(game, u'designers', u'Unknown')), players)) if game.playing_time and int(game.playing_time) != 0: info += '{} minutes; '.format(game.playing_time) if game.image: info += '[img]({}) '.format(game.image) info += '\n\n' data = u', '.join(getattr(game, u'mechanics', u'')) if data: info += u' * Mechanics: {}\n'.format(data) people = u'people' if game.users_rated > 1 else u'person' info += u' * Average rating is {}; rated by {} {}. Weight: {}\n'.format( game.rating_average, game.users_rated, people, game.rating_average_weight) data = u', '.join([ u'{}: {}'.format(r[u'friendlyname'], r[u'value']) for r in game.ranks ]) info += u' * {}\n\n'.format(data) log.debug(u'adding info: {}'.format(info)) infos.append(info) return infos def _getLongInfos(self, games): infos = list() for game in games: players = self._getPlayers(game) info = ( u'Details for [**{}**](http://boardgamegeek.com/boardgame/{}) ' u' ({}) by {}. '.format( game.name, game.id, game.year, u', '.join(getattr(game, u'designers', u'Unknown')))) if players: info += '{}; '.format(players) if game.playing_time and int(game.playing_time) != 0: info += '{} minutes; '.format(game.playing_time) if game.image: info += '[img]({}) '.format(game.image) info += '\n\n' data = u', '.join(getattr(game, u'mechanics', u'')) if data: info += u' * Mechanics: {}\n'.format(data) people = u'people' if game.users_rated > 1 else u'person' info += u' * Average rating is {}; rated by {} {}\n'.format( game.rating_average, game.users_rated, people) info += u' * Average Weight: {}; Number of Weights {}\n'.format( game.rating_average_weight, game.rating_num_weights) data = u', '.join([ u'{}: {}'.format(r[u'friendlyname'], r[u'value']) for r in game.ranks ]) info += u' * {}\n\n'.format(data) info += u'Description:\n\n{}\n\n'.format(game.description) if len(games) > 1: info += u'------' log.debug(u'adding info: {}'.format(info)) infos.append(info) return infos def repairComment(self, comment): '''Look for maps from missed game names to actual game names. If found repair orginal comment.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return # # The repair is done by replacing the new games names with the old (wrong) # games names in the original /u/r2d8 response, then recreating the entire # post by regenerating it with the new (fixed) bolded game names. The just replacing # the orginal response with the new one. # log.debug(u'Got repair response, id {}'.format(comment.id)) if comment.is_root: # error here - this comment should be in response to a u/r2d8 comment. log.info(u'Got a repair comment as root, ignoring.') return parent = comment.reddit_session.get_info(thing_id=comment.parent_id) if parent.author.name != self._botname: log.info( u'Parent of repair comment is not authored by the bot, ignoring.' ) return # Look for patterns of **something**=**somethingelse**. This line creates a dict # of something: somethingelse for each one pattern found. repairs = { match[0]: match[1] for match in re.findall(u'\*\*([^\*]+)\*\*=\*\*([^\*]+)\*\*', comment.body) } pbody = parent.body for wrongName, repairedName in repairs.iteritems(): # check to see if it's actually a game. log.info(u'Repairing {} --> {}'.format(wrongName, repairedName)) alias = self._botdb.get_name_from_alias(repairedName) tmp_name = alias if alias else repairedName tmp_game = self._bggQueryGame( tmp_name) # with caching it's ok to check twice if tmp_game: # In the parent body we want to replace [NAME](http://... with **NAME**(http:// pbody = pbody.replace(u'[' + wrongName + u']', u'**' + tmp_name + u'**') else: log.info( u'{} seems to not be a game name according to BGG, ignoring.' .format(tmp_name)) # Now re-bold the not found strings so they are re-searched or re-added to the not found list. for nf in re.findall( u'\[([\w|\s]+)]\(http://boardgamegeek.com/geeksearch.php', pbody): pbody += u' **{}**'.format(nf) # now re-insert the original command to retain the mode. grandparent = parent.reddit_session.get_info(thing_id=parent.parent_id) modes = list() if not grandparent: log.error(u'Cannot find original GP post. Assuming normal mode.') else: modes = re.findall(u'[getparent|get]info\s(\w+)', grandparent.body) if modes: log.debug(u'Recreating {} mode from the GP.'.format(modes[0])) pbody += u' /u/{} getinfo {}'.format(self._botname, modes[0]) else: pbody += u' /u/{} getinfo'.format(self._botname) parent = parent.edit(pbody) new_reply = self._getInfoResponseBody(parent) # should check for Editiable class somehow here. GTL log.debug(u'Replacing bot comment {} with: {}'.format( parent.id, new_reply)) parent.edit(new_reply) def xyzzy(self, comment): comment.reply(u'Nothing happens.') def getParentInfo(self, comment): '''Allows others to call the bot to getInfo for parent posts.''' if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return log.debug(u'Got getParentInfo comment in id {}'.format(comment.id)) if comment.is_root: # error here - this comment should be in response to a u/r2d8 comment. log.info(u'Got a repair comment as root, ignoring.') return m = re.search(u'getparentinfo\s(\w+)', comment.body, re.IGNORECASE) mode = None if m: mode = u'short' if m.group(1).lower() == u'short' else u'long' parent = comment.reddit_session.get_info(thing_id=comment.parent_id) self.getInfo(parent, comment, mode) def alias(self, comment): '''add an alias to the database.''' if not self._botdb.is_admin(comment.author.name): log.info(u'got alias command from non admin {}, ignoring.'.format( comment.author.name)) return response = u'executing alias command.\n\n' for match in re.findall(u'\*\*([^\*]+)\*\*=\*\*([^\*]+)\*\*', comment.body): mess = u'Adding alias to database: "{}" = "{}"'.format( match[0], match[1]) log.info(mess) response += mess + u'\n\n' self._botdb.add_alias(match[0], match[1]) comment.reply(response) def getaliases(self, comment): if self._botdb.ignore_user(comment.author.name): log.info("Ignoring comment by {}".format(comment.author.name)) return aliases = self._botdb.aliases() response = u'Current aliases:\n\n' for name, alias in sorted(aliases, key=lambda g: g[1]): response += u' * {} = {}\n'.format(alias, name) log.info(u'Responding to getalaises request with {} aliases'.format( len(aliases))) comment.reply(response)
def boardgamegeek(self) -> BoardGameGeek: return BoardGameGeek()
def getGotWPostText(game_name, next_game_name, vote_thread_url): '''Take the name of a game, and return the GotW text to post to Reddit''' bgg = BGG() try: game = bgg.game(game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.critical(u'Error getting info from BGG on {}: {}'.format( game_name, e)) return None if not game: log.critical(u'Unable to find {} on BGG'.format(game_name)) return None title = u'Game of the Week: {}'.format(game.name) text = u'[//]: # (GOTWS)\n' text += (u'This week\'s game is [**{}**]({})\n\n'.format(game.name, game.image)) text += u' * **BGG Link**: [{}](http://www.boardgamegeek.com/boardgame/{})\n'.format( game.name, game.id) designers = getattr(game, u'designers', [u'Unknown']) plural = u's' if len(designers) > 1 else u'' text += u' * **Designer{}**: {}\n'.format(plural, ', '.join(designers)) publishers = getattr(game, u'publishers', [u'Unknown']) plural = u's' if len(publishers) > 1 else u'' text += u' * **Publisher{}**: {}\n'.format(plural, ', '.join(publishers)) text += u' * **Year Released**: {}\n'.format(game.year) mechanics = getattr(game, u'mechanics', [u'Unknown']) plural = u's' if len(mechanics) > 1 else u'' text += u' * **Mechanic{}**: {}\n'.format(plural, ', '.join(mechanics)) categories = getattr(game, u'categories', [u'unknown']) plural = u'ies' if len(categories) > 1 else u'y' text += u' * **Categor{}**: {}\n'.format(plural, ', '.join(categories)) if game.min_players == game.max_players: players = '{}'.format(game.min_players) else: players = '{} - {}'.format(game.min_players, game.max_players) text += u' * **Number of Players**: {}\n'.format(players) text += u' * **Playing Time**: {} minutes\n'.format(game.playing_time) expansions = getattr(game, 'expansions', None) if expansions: text += u' * **Expansions**: {}\n'.format(', '.join([e.name for e in expansions])) text += u' * **Ratings**:\n' people = u'people' if game.users_rated > 1 else u'person' text += u' * Average rating is {} (rated by {} {})\n'.format( game.rating_average, game.users_rated, people) ranks = u', '.join([u'{}: {}'.format( r[u'friendlyname'], r[u'value']) for r in game.ranks]) text += u' * {}\n'.format(ranks) text += u'\n\n' text += u'**Description from Boardgamegeek**:\n\n{}\n\n'.format(game.description) text += u'[//]: # (GOTWE)\n\n' text += '---------------------\n\n' if not next_game_name: text += u'There is no Game of the Week scheduled for next week.' else: try: game = bgg.game(next_game_name) except boardgamegeek.exceptions.BoardGameGeekError as e: log.critical(u'Error getting info from BGG on {}: {}'.format( next_game_name, e)) if not game: text += u'Next Week: {}'.format(next_game_name) else: text += (u'Next Week: [**{}**](http://www.boardgamegeek' u'.com/boardgame/{})\n\n'.format(game.name, game.id)) text += (u' * The GOTW archive and schedule can be found ' u'[here](http://www.reddit.com/r/boardgames/wiki/' u'game_of_the_week).\n\n * Vote for future Games of the Week ' u'[here](/{}).\n'.format(vote_thread_url)) return title, text
Add logger for boardgamegeek.api Fix bug that causes certain game names to crash the program """ #change these to check other bgg user and send to different email bggUser = "******" userEmail = "*****@*****.**" r = praw.Reddit('BGG Wishlist Mention Tracker by /u/shbones v 1.1') sent_already = [] while True: #Repopulate boardgamegeek.com wishlist every run wishlist = [] bgg = BoardGameGeek() collection = bgg.collection(bggUser) try: games = collection.items print games for each_game in games: if each_game.wishlist == True: wishlist.append(each_game.name.lower()) #Check for mentions in Title or Text in Top 10 current posts on reddit.com/r/boardgames subreddit = r.get_subreddit('boardgames') for submission in subreddit.get_hot(limit=10): post_text = submission.selftext.lower() post_title = submission.title.lower() has_game = any(string in (post_text + post_title) for string in wishlist)
def game_to_db(game, cur, conn): insert_board(game, cur) insert_designer(game, cur) insert_artist(game, cur) insert_family(game, cur) insert_genre(game, cur) insert_mechanic(game, cur) insert_publisher(game, cur) def print_game(game): print("Inserted " + game.name) bgg = BoardGameGeek() output = open('output.txt', 'w') game_ids = [line.rstrip('\n').split(";")[0] for line in open('data.csv', 'r')] games = [] for game_id in game_ids: try: game_id = int(game_id) except: continue if (game_id > 2290): try: game = bgg.game(game_id=game_id) print_game(game) # print_to_file(game, output) game_to_db(game, cur, conn)
def trending_games(): bgg = BoardGameGeek() hot_items_dict = dict() for item in bgg.hot_items("boardgame"): hot_items_dict[item.id] = item.name return render_template('page.html', hot_items_dict=hot_items_dict)
def game_to_db(game,cur,conn): insert_board(game, cur) insert_designer(game, cur) insert_artist(game, cur) insert_family(game, cur) insert_genre(game, cur) insert_mechanic(game, cur) insert_publisher(game, cur) def print_game(game): print ("Inserted " + game.name ) bgg = BoardGameGeek() output = open('output.txt', 'w') game_ids = [line.rstrip('\n').split(";")[0] for line in open('data.csv', 'r')] games = [] for game_id in game_ids: try: game_id = int(game_id) except: continue if (game_id > 2290): try: game = bgg.game(game_id = game_id) print_game(game) # print_to_file(game, output) game_to_db(game,cur,conn)
from functs import add_scores, read_cell from boardgamegeek import BoardGameGeek from datetime import datetime from connect import connect bgg = BoardGameGeek() al = bgg.plays('mad4hatter') alplays = al.plays entry, client, key = connect() latestgame = read_cell(client,key) print latestgame for games in alplays: if games.date > latestgame: temp = games.players troytest = ['yes' for players in temp if players.name == 'Troy'] if troytest == ['yes']: entry, client, key = connect() entry.set_value('date',games.date.strftime('%m/%d/%y')) entry.set_value('gamename',games.game_name) entry.set_value('duration', str(games.duration))
class BGGGame(object): """Wrapper around the `game` object from the boardgamegeek API""" def __init__(self, game_id, short=500): """ Args: short: int number of characters to use for short description """ self._game = None self.short = int(short) or 500 self.bgg = BoardGameGeek(disable_ssl=True) if isinstance(game_id, int): self._game = self.bgg.game(game_id=game_id) elif isinstance(game_id, ""): self._game = self.bgg.game(name=game_id) else: print self.set_properties() def get_description_custom(self): """Create a custom, abbreviated description for a game.""" if self._game: desc = self._game.description[0:self.short] _cut = int( (len(desc) - len(desc.replace(',', '').replace('.', '').replace(':', ''))) / 2 + self.short) desc = self._game.description[0:_cut] return desc[0:-3] + '...' def HTML_description(self, text): """Changes the BGG [] notation to <> and adds line breaks""" #print "bgg.self._game.description\n", self._game.description return self._game.description.replace('[', '<').replace(']', '>').\ replace('\n', '<br/>') def set_properties(self): """Create both raw (_ prefix) and string formatted versions of props""" if self._game: self._alternative_names = self._game.alternative_names self.alternative_names = ', '.join(self._game.alternative_names) self._artists = self._game.artists self.artists = ', '.join(self._game.artists) self._average = self._game.average self.average = '%.3f' % self._game.average self._averageweight = self._game.averageweight self.averageweight = '%.2f' % self._game.averageweight self.percentageweight = '%s' % math.ceil(self._game.averageweight * 20.0) self._bayesaverage = self._game.bayesaverage self.bayesaverage = '%.3f' % self._game.bayesaverage self._categories = self._game.categories self.categories = ', '.join(self._game.categories) self._description = self._game.description self.description = '%s' % self._game.description self.description_html = '%s' % \ self.HTML_description(self._game.description) try: self.description_short = '%s' % self._game.description_short self.description_short_html = '%s' % \ self.HTML_description(self._game.description_short) except AttributeError: self.description_short = '' self.description_short_html = '' self._designers = self._game.designers self.designers = ', '.join(self._game.designers) self._expands = self._game.expands self.expands = ', '.join([exp.name for exp in self._game.expands]) self._expansion = self._game.expansion if self._game.expansion is True: self.expansion = 'Yes' else: self.expansion = 'No' self._expansions = self._game.expansions self.expansions = ', '.join( [exp.name for exp in self._game.expansions]) self._families = self._game.families self.families = ', '.join(self._game.families) self._id = self._game.id self.id = '%s' % self._game.id self._image = self._game.image self.image = '%s' % self._game.image self._implementations = self._game.implementations self.implementations = ', '.join(self._game.implementations) self._maxplayers = self._game.maxplayers self.maxplayers = '%s' % self._game.maxplayers self._mechanics = self._game.mechanics self.mechanics = ', '.join(self._game.mechanics) self._median = self._game.median self.median = '%.3f' % self._game.median self._minage = self._game.minage self.minage = '%s' % self._game.minage self._minplayers = self._game.minplayers self.minplayers = '%s' % self._game.minplayers self._name = self._game.name self.name = '%s' % self._game.name self._numcomments = self._game.numcomments self.numcomments = '%s' % self._game.numcomments self._numweights = self._game.numweights self.numweights = '%s' % self._game.numweights self._owned = self._game.owned self.owned = '%s' % self._game.owned self._playingtime = self._game.playingtime self.playingtime = '%s' % self._game.playingtime self._publishers = self._game.publishers self.publishers = ', '.join(self._game.publishers) self._ranks = self._game.ranks self.ranks = '%s' % self._game.ranks self._stddev = self._game.stddev self.stddev = '%.3f' % self._game.stddev self._thumbnail = self._game.thumbnail self.thumbnail = '%s' % self._game.thumbnail self._trading = self._game.trading self.trading = '%s' % self._game.trading self._usersrated = self._game.usersrated self.usersrated = '%s' % self._game.usersrated self._wanting = self._game.wanting self.wanting = '%s' % self._game.wanting self._wishing = self._game.wishing self.wishing = '%s' % self._game.wishing self._yearpublished = self._game.yearpublished self.yearpublished = '%s' % self._game.yearpublished # custom fields self.description_custom = self.get_description_custom() self._description_custom = self.description_custom if self._game.minplayers == self._game.maxplayers: self.players = '%s' % self._game.maxplayers else: self.players = '%s-%s' % (self._game.minplayers, self._game.maxplayers) self._players = (self._game.minplayers, self._game.maxplayers) self.age = '%s+' % self._game.minage self._age = self._game.minage