def pic(args: t.Optional[str] = None): gargling_id = get_jwt_identity() log.info(gargling_id) arg_list = args.split(",") if args is not None else [] with app.pool.get_connection() as conn: pic_url, *_ = pictures.get_pic(conn, app.dbx, arg_list=arg_list) return jsonify({"url": pic_url})
def add_faces_in_pic(self, cursor: LoggingCursor, pic: Path, dbx_path: str): sql_command = "SELECT pic_id FROM dbx_pictures WHERE path = %(path)s" data = {"path": dbx_path} cursor.execute(sql_command, data) try: pic_id = cursor.fetchone()["pic_id"] except KeyError: log.info(f"pic not in db: {dbx_path}") return sql_command = "SELECT * FROM dbx_pictures_faces WHERE pic_id = %(pic_id)s" data = {"pic_id": pic_id} cursor.execute(sql_command, data) result = cursor.fetchone() if result is not None: log.info(f"{dbx_path} pic faces already in db with id {pic_id}") return tags = DropPics.get_tags(pic) if tags is None: return faces = set(tags).intersection(self.firstname_to_db_id) for face in faces: db_id = self.firstname_to_db_id[face] sql_command = ("INSERT INTO dbx_pictures_faces (db_id, pic_id) " "VALUES (%(db_id)s, %(pic_id)s);") data = {"db_id": db_id, "pic_id": pic_id} cursor.execute(sql_command, data)
def execute(command_str: str, args: t.List, conn: connection, dbx: dropbox.Dropbox) -> t.Dict: log.info(f"command: {command_str}") log.info(f"args: {args}") switch: t.Dict[str, t.Callable] = { "ping": cmd_ping, "new_channel": cmd_welcome, "gargbot": cmd_server_explanation, "hvem": partial(cmd_hvem, args, conn=conn), "pic": partial(cmd_pic, args, conn=conn, dbx=dbx), "forum": partial(cmd_forum, args, conn=conn), "msn": partial(cmd_msn, args, conn=conn), } try: command_func = switch[command_str] except KeyError: command_func = partial(cmd_not_found, command_str) try: return command_func() except psycopg2.OperationalError: raise except (SSLError, dropbox.exceptions.ApiError): # Dropbox sometimes gives SSLerrors, (or ApiError if file not there) try again: try: log.error("SSLerror/ApiError, retrying", exc_info=True) return command_func() except Exception as exc: log.error("Error in command execution", exc_info=True) return cmd_panic(exc) except Exception as exc: log.error("Error in command execution", exc_info=True) return cmd_panic(exc)
def add_pics_in_folder(self, folder: Path, topic: str, dbx_folder: str) -> None: if not self.conn: raise Exception cursor = self.conn.cursor() for pic in folder.iterdir(): if not pic.suffix.lower() in {".jpg", ".jpeg"}: continue dbx_path = dbx_folder + "/" + pic.name.lower() sql_command = "SELECT pic_id FROM dbx_pictures WHERE path = %(path)s" data = {"path": dbx_path} cursor.execute(sql_command, data) if cursor.fetchone() is not None: log.info(f"{dbx_path} pic already in db") continue date_obj = DropPics.get_date_taken(pic) timestr = date_obj.strftime("%Y-%m-%d %H:%M:%S") sql_command = """INSERT INTO dbx_pictures (path, topic, taken) VALUES (%(path)s, %(topic)s, %(taken)s);""" data = {"path": dbx_path, "topic": topic, "taken": timestr} cursor.execute(sql_command, data) self.add_faces_in_pic(cursor, pic, dbx_path) self.conn.commit()
def send_response(slack_client: SlackClient, response: Dict, channel: str): log.info(dt.datetime.now()) log.info(f"response: {response}") slack_client.api_call("chat.postMessage", channel=channel, as_user=True, **response)
def send_congrats(conn: connection, slack_client: SlackClient): dbx = pictures.connect_dbx() recipients = todays_recipients(conn) log.info(f"Recipients today {recipients}") for recipient in recipients: greet = formulate_congrat(recipient, conn, dbx) task.send_response(slack_client, greet, channel=config.main_channel)
def main(): # no test coverage try: log.info("Starting gargbot_3000") parser = argparse.ArgumentParser() parser.add_argument("--mode", "-m") parser.add_argument("--debug", "-d", action="store_true") parser.add_argument("--bind", "-b", default="0.0.0.0") parser.add_argument("--workers", "-w", default=3) parser.add_argument("--port", "-p", default=":5000") args = parser.parse_args() if args.mode == "server": options = { "bind": "%s%s" % (args.bind, args.port), "workers": args.workers } server.main(options=options, debug=args.debug) elif args.mode == "scheduler": scheduler.main() elif args.mode == "migrate": database.migrate() else: raise Exception(f"Incorrect mode, {args.mode}") except Exception as exc: log.exception(exc) raise
def delete_ephemeral(response_url): delete_original = { "response_type": "ephemeral", "replace_original": True, "text": "Sharing is caring!", } r = requests.post(response_url, json=delete_original) log.info(r.text)
def backup(): # no test coverage log.info("Backing up database") cmd = f"pg_dump --no-owner --dbname={config.db_uri}" result = subprocess.check_output(cmd, shell=True) dbx = Dropbox(config.dropbox_token) date = pendulum.now().date() filename = f"{config.db_name}_{date.year}_{date.month}_{date.day}.sql" path = config.dbx_db_backup_folder / filename dbx.files_upload(f=result, path=path.as_posix(), autorename=True)
def send_response( slack_client, response: dict, channel: str, thread_ts: t.Optional[str] = None, ): # no test coverage log.info("Sending to slack: ", response) slack_client.chat_postMessage(channel=channel, thread_ts=thread_ts, **response)
def main(self, cursor): for fname in os.listdir(os.path.join(config.home, "data", "logs")): if not fname.lower().endswith(".xml"): continue log.info(fname) for message_data in MSN.parse_log(fname): MSN.add_entry(cursor, *message_data) self.conn.commit()
def authorize(service_name: str): gargling_id = get_jwt_identity() if gargling_id is None: # no test coverage raise Exception("JWT token issued to None") log.info(f"gargling_id: {gargling_id}") service = init_service(service_name) url = service.authorization_url() response = jsonify(is_registered=False, auth_url=url) log.info(response) return response
def migrate() -> None: conn = connect() queries = aiosql.from_path("sql/migrations.sql", "psycopg2") queries.migrations(conn) conn.commit() remaining_diffs = get_migrations() remaining_diffs.set_safety(False) remaining_diffs.add_all_changes() if remaining_diffs.statements: log.info(remaining_diffs.sql)
def steps(self, date: pendulum.Date) -> t.Optional[int]: start_dt = pendulum.datetime(date.year, date.month, date.day).in_timezone(config.tz) start_ms = start_dt.timestamp() * 1000 end_ms = start_dt.add(days=1).timestamp() * 1000 data = self._steps_api_call(start_ms, end_ms) log.info(data) try: return data["bucket"][0]["dataset"][0]["point"][0]["value"][0][ "intVal"] except IndexError: return 0
def weight(client: FitbitApi) -> t.Optional[dict]: data = client.get_bodyweight(period="7d") if len(data["weight"]) == 0: log.info("No weight data") return None entries = data["weight"] for entry in entries: entry["datetime"] = pendulum.parse(f"{entry['date']}T{entry['time']}") entries.sort(key=itemgetter("datetime"), reverse=True) most_recent = entries[0] log.info(f"weight data: {most_recent}") return most_recent
def send_congrats() -> None: # no test coverage dbx = pictures.connect_dbx() conn = database.connect() recipients = todays_recipients(conn) if not recipients: return log.info(f"Recipients today {recipients}") slack_client = slack.WebClient(config.slack_bot_user_token) for recipient in recipients: greet = formulate_congrat(recipient, conn, dbx) commands.send_response(slack_client, greet, channel=config.main_channel)
def get_pic( conn: connection, dbx: Dropbox, arg_list: t.Optional[t.List[str]]) -> t.Tuple[str, dt.datetime, str]: description = "" if not arg_list: url, taken_at = get_random_pic(conn, dbx) return url, taken_at, description args = {arg.lower() for arg in arg_list} parsed = queries.parse_args(conn, args=list(args)) valid_args, invalid_args = sortout_args(args, **parsed) if invalid_args: all_args = queries.get_possible_args(conn) description = get_description_for_invalid_args(invalid_args, **all_args) if not valid_args: description += "Her er et tilfeldig bilde i stedet." url, taken_at = get_random_pic(conn, dbx) return url, taken_at, description data = queries.pic_for_topic_year_garglings(conn, **parsed) if data is not None: valid_args_fmt = ", ".join(f"`{arg}`" for arg in valid_args) description += f"Her er et bilde med {valid_args_fmt}." path = data["path"] taken_at = data["taken_at"] url = get_url_for_dbx_path(dbx, path) return url, taken_at, description # No pics found for arg-combination. Reduce args until pic found valid_args_fmt = ", ".join(f"`{arg}`" for arg in valid_args) description += f"Fant ikke bilde med {valid_args_fmt}. " for arg_combination in reduce_arg_combinations(valid_args): log.info(f"arg_combination: {arg_combination}") parsed = queries.parse_args(conn, args=list(arg_combination)) data = queries.pic_for_topic_year_garglings(conn, **parsed) if data is None: continue arg_combination_fmt = ", ".join(f"`{arg}`" for arg in arg_combination) description += f"Her er et bilde med {arg_combination_fmt} i stedet." taken_at = data["taken_at"] url = get_url_for_dbx_path(dbx, path=data["path"]) return url, taken_at, description # No pics found for any args url, taken_at = get_random_pic(conn, dbx) return url, taken_at, description
def send_response( slack_client: SlackClient, response: t.Dict, channel: str, thread_ts: t.Optional[str] = None, ): log.info(dt.datetime.now()) log.info(f"response: {response}") slack_client.api_call( "chat.postMessage", channel=channel, as_user=True, thread_ts=thread_ts, **response, )
def migrate() -> None: # no test coverage conn = connect() queries = aiosql.from_path("sql/migrations.sql", "psycopg2") queries.migrations(conn) conn.commit() previous_level = logging.root.manager.disable # type: ignore logging.disable(logging.CRITICAL) remaining_diffs = get_migrations() remaining_diffs.set_safety(False) remaining_diffs.add_all_changes() logging.disable(previous_level) if remaining_diffs.statements: log.info(remaining_diffs.sql) else: log.info("Migrations up to date")
def interactive(): log.info("incoming interactive request:") data = json.loads(request.form["payload"]) log.info(data) if not data.get("token") == config.slack_verification_token: return Response(status=403) action = data["actions"][0]["name"] if action in {"share", "shuffle", "cancel"}: result = handle_share_interaction(action, data) elif action in {"pic", "quote", "msn"}: trigger_id = data["trigger_id"] result = handle_command(command_str=action, args=[], trigger_id=trigger_id) return json_response(result)
def handle_message(event_data): # no test coverage log.info("Receiving Slack event") log.info(event_data) AT_BOT = f"<@{config.bot_id}>" message = event_data["event"] channel = message["channel"] text = message.get("text").replace(AT_BOT, "").strip() if message.get("subtype") is not None: return try: command_str, *args = text.replace("@", "").lower().split() except ValueError: command_str = "" args = [] result = handle_command(command_str, args, buttons=False) commands.send_response(app.slack_client, result, channel)
def delete_sync_reminders(conn: connection, slack_client) -> None: reminder_users = queries.get_sync_reminder_users(conn) log.info(reminder_users) for user in reminder_users: if user["last_sync_reminder_ts"] is None: # no test coverage continue try: slack_client.chat_delete( channel=user["slack_id"], ts=user["last_sync_reminder_ts"] ) except Exception: # no test coverage log.error( f"Error deleting sync reminder for user id: {user['id']}", exc_info=True, ) queries.update_reminder_ts(conn, ts=None, id=user["id"]) conn.commit()
def weight(client: WithingsApi) -> t.Optional[dict]: return None data = client.measure_get_meas( startdate=pendulum.today().subtract(weeks=1), enddate=pendulum.today(), meastype=MeasureType.WEIGHT, ).measuregrps if not data: log.info("No weight data") return None data = sorted(data, key=attrgetter("date"), reverse=True) most_recent = data[0] measure = most_recent.measures[0] return { "weight": float(measure.value * pow(10, measure.unit)), "datetime": most_recent.date, }
def handle_redirect(service_name: str): gargling_id = get_jwt_identity() if gargling_id is None: raise Exception("JWT token issued to None") log.info(f"gargling_id: {gargling_id}") service = Service[service_name] service_user_id, token = service.value.handle_redirect(request) with current_app.pool.get_connection() as conn: service.value.persist_token(token, conn) queries.match_ids( conn, gargling_id=gargling_id, service_user_id=service_user_id, service=service.name, ) conn.commit() return Response(status=200)
def main() -> None: log.info("GargBot 3000 greeter starter") try: while True: event = Event.next() log.info(f"Next greeting check at: {event.until.end}, " f"sleeping for {event.until.in_words()}") time.sleep(event.until.total_seconds()) try: slack_client = SlackClient(config.slack_bot_user_token) conn = database.connect() event.func(conn, slack_client) except Exception: log.error("Error in command execution", exc_info=True) finally: conn.close() except KeyboardInterrupt: sys.exit()
def street_view_for_location(lat, lon) -> t.Optional[bytes]: # no test coverage def encode_url(domain, endpoint, params): params = params.copy() url_to_sign = endpoint + urllib.parse.urlencode(params) secret = config.google_api_secret decoded_key = base64.urlsafe_b64decode(secret) signature = hmac.new(decoded_key, url_to_sign.encode(), hashlib.sha1) encoded_signature = base64.urlsafe_b64encode(signature.digest()) params["signature"] = encoded_signature.decode() encoded_url = domain + endpoint + urllib.parse.urlencode(params) return encoded_url domain = "https://maps.googleapis.com" metadata_endpoint = "/maps/api/streetview/metadata?" img_endpoint = "/maps/api/streetview?" params = { "size": "600x400", "location": f"{lat}, {lon}", "fov": 120, "heading": 251.74, "pitch": 0, "key": config.google_api_key, } metadata_url = encode_url(domain, metadata_endpoint, params) try: response = requests.get(metadata_url) metadata = response.json() if metadata["status"] != "OK": log.info(f"Metadata indicates no streetview image: {metadata}") return None except Exception: log.error("Error downloading streetview image metadata", exc_info=True) return None photo_url = encode_url(domain, img_endpoint, params) try: response = requests.get(photo_url) data = response.content except Exception: log.error("Error downloading streetview image", exc_info=True) return None return data
def handle_congrats(slack_client: SlackClient, drop_pics): db_connection = database_manager.connect_to_database() birthdays = congrats.get_birthdays(db_connection) db_connection.close() for birthday in itertools.cycle(birthdays): log.info(f"Next birthday: {birthday.nick}, at {birthday.next_bday}") try: time.sleep(birthday.seconds_to_bday()) except OverflowError: log.info( f"Too long sleep length for OS. Restart before next birthday, at {birthday.next_bday}" ) break db_connection = database_manager.connect_to_database() response = congrats.get_greeting(birthday, db_connection, drop_pics) send_response(slack_client, response=response, channel=config.main_channel) db_connection.close()
def authorize(service_name: str): gargling_id = get_jwt_identity() if gargling_id is None: raise Exception("JWT token issued to None") log.info(f"gargling_id: {gargling_id}") with current_app.pool.get_connection() as conn: data = queries.is_registered( conn, gargling_id=gargling_id, service=service_name ) if data is None: log.info("not registered") url = Service[service_name].value.authorize_user() log.info(url) response = jsonify(auth_url=url) else: report_enabled = data["enable_report"] log.info(f"registered, report enabled: {report_enabled}") response = jsonify(report_enabled=report_enabled) log.info(response) return response
def _steps_api_call(self, date: pendulum.Date) -> dict: # no test coverage kwargs = { "resource": "activities/steps", "base_date": date, "period": "1d" } exc = None data = None for _ in range(10): try: data = self.client.time_series(**kwargs) break except fitbit.exceptions.HTTPServerError as e: log.info("Error fetching fitbit data. Retrying") exc = e continue if data is None: assert exc is not None raise exc return data
def main(): slack_client, drop_pics, quotes_db, db_connection = setup() log.info("GargBot 3000 task operational!") try: while True: time.sleep(1) text, channel, user = wait_for_slack_output(slack_client) try: command_str, *args = text.split() except ValueError: command_str = "" args = [] command_str = command_str.lower() command_func = partial( commands.execute, command_str=command_str, args=args, db_connection=db_connection, drop_pics=drop_pics, quotes_db=quotes_db, ) try: response = command_func() except psycopg2.OperationalError as op_exc: db_connection = database_manager.connect_to_database() try: return command_func() except Exception as exc: # OperationalError not caused by connection issue. log.error("Error in command execution", exc_info=True) return commands.cmd_panic(exc) send_response(slack_client, response, channel) except KeyboardInterrupt: sys.exit() finally: database_manager.close_database_connection(db_connection)