def tv_channel(): redis = get_redis() channel = redis.get("tvchannel").decode("utf-8") id_ = redis.get("tvid").decode("utf-8") or 0 if channel is None: channel = restore_default_tv_channel() return jsonify(channel=channel, id=id_)
def run(self, context: CommandContext): if len(context.args) != 1: context.api.chat_post_ephemeral({ 'channel': context.event.channel, 'user': context.event.user, 'text': f'Expected a single argument (f/c)' }) return units, = context.args units = units.lower() unit_map = { 'fahrenheit': 'us', 'f': 'us', 'celsius': 'si', 'c': 'si' } if units not in unit_map: context.api.chat_post_ephemeral( { 'channel': context.event.channel, 'user': context.event.user, 'text': f'Units must either be `f` or `c`. Got {units}' } ) return redis = get_redis() redis.hset('user_units', context.event.user, unit_map[units]) context.api.chat_post_ephemeral( { 'channel': context.event.channel, 'user': context.event.user, 'text': f'Set default units to {units}' } )
def run(self, context: CommandContext): if len(context.args) != 1: context.api.chat_post_ephemeral( { "channel": context.event.channel, "user": context.event.user, "text": "Expected a single argument (f/c)", } ) return (units,) = context.args units = units.lower() unit_map = {"fahrenheit": "us", "f": "us", "celsius": "si", "c": "si"} if units not in unit_map: context.api.chat_post_ephemeral( { "channel": context.event.channel, "user": context.event.user, "text": f"Units must either be `f` or `c`. Got {units}", } ) return redis = get_redis() redis.hset("user_units", context.event.user, unit_map[units]) context.api.chat_post_ephemeral( { "channel": context.event.channel, "user": context.event.user, "text": f"Set default units to {units}", } )
def get_message_args(self, context: CommandContext): team_id = context.team_id user_id = context.event.user ts = time.time() redis = get_redis() last_poke_time_key = f'poke:last:{team_id}' user_count_key = f'poke:user:{team_id}' last_poke_user_key = f'poke:lastuser:{team_id}' last_poke_time = redis.get(last_poke_time_key) redis.set(last_poke_time_key, ts) last_poked_user_id = redis.get(last_poke_user_key) if last_poked_user_id: last_poked_user_id = last_poked_user_id.decode('utf-8') redis.set(last_poke_user_key, user_id) total_pokes = redis.hincrby(user_count_key, user_id) if last_poke_time is None: return { 'icon_emoji': f'{Emoji.SHOOKCAT}', 'text': (f'You have poked meowbot 1 time!\n\n' 'You\'re the first to poke meowbot!') } s = '' if total_pokes == 1 else 's' last_poke = arrow.get(float(last_poke_time)).humanize() last_user = quote_user_id(last_poked_user_id) return { 'icon_emoji': f'{Emoji.SHOOKCAT}', 'text': f'You have poked meowbot {total_pokes} time{s}!\n\n' f'Meowbot was last poked {last_poke} by {last_user}' }
def get_message_args(self, context: CommandContext): team_id = context.team_id user_id = context.event.user ts = time.time() redis = get_redis() last_poke_time_key = f"poke:last:{team_id}" user_count_key = f"poke:user:{team_id}" last_poke_user_key = f"poke:lastuser:{team_id}" last_poke_time = redis.get(last_poke_time_key) redis.set(last_poke_time_key, ts) last_poked_user_id = redis.get(last_poke_user_key) if last_poked_user_id: last_poked_user_id = last_poked_user_id.decode("utf-8") redis.set(last_poke_user_key, user_id) total_pokes = redis.hincrby(user_count_key, user_id) if last_poke_time is None: return { "icon_emoji": f"{Emoji.SHOOKCAT}", "text": ("You have poked meowbot 1 time!\n\n" "You're the first to poke meowbot!"), } s = "" if total_pokes == 1 else "s" last_poke = arrow.get(float(last_poke_time)).humanize() last_user = quote_user_id(last_poked_user_id) return { "icon_emoji": f"{Emoji.SHOOKCAT}", "text": f"You have poked meowbot {total_pokes} time{s}!\n\n" f"Meowbot was last poked {last_poke} by {last_user}", }
def get_message_args(self, context: CommandContext): redis = get_redis() redis.set('killtv', '1') restore_default_tv_channel() return { 'text': ('Meowbot TV has been disabled. ' 'Contact Meowbot admin to reenable') }
def run(self, context: CommandContext): location = ' '.join(context.args) redis = get_redis() redis.hset('user_location', context.event.user, location) context.api.chat_post_ephemeral({ 'channel': context.event.channel, 'user': context.event.user, 'text': f'Set default location to {location}' })
def get_message_args(self, context: CommandContext): redis = get_redis() if redis.exists("killtv"): admin_user_id = get_admin_user_id() return { "text": ( "Meowbot TV has been disabled. " f"Contact {quote_user_id(admin_user_id)} to reenable" ) } channels = get_channels() available_channels = ", ".join(sorted(channels.keys())) if len(context.args) == 1: (channel,) = context.args if channel not in channels: return { "text": f"{channel} is not a valid channel.\n\n" f"Available channels: {available_channels}", "thread_ts": context.event.ts, } redis.incr("tvid") redis.set("tvchannel", channels[channel]["url"]) return { "text": f'Changing channel to {channels[channel]["name"]}!', } elif len(context.args) == 2: channel_type, value = context.args if channel_type == "url": url = value[1:-1] redis.incr("tvid") redis.set("tvchannel", url) return { "text": f"Changing channel to {url}!", } elif channel_type == "twitch": url = f"https://player.twitch.tv/?channel={value}" redis.incr("tvid") redis.set("tvchannel", url) return { "text": f"Changing channel to {Emoji.TWITCH} {value}!", } elif channel_type == "youtube": url = ( f"https://www.youtube.com/embed/{value}?" f"autoplay=1&loop=1&playlist={value}" ) redis.incr("tvid") redis.set("tvchannel", url) return { "text": f"Changing channel to {Emoji.YOUTUBE} {value}!", } return { "text": "Must provide a channel.\n\n" f"Available channels: {available_channels}", "thread_ts": context.event.ts, }
def get_message_args(self, context: CommandContext): if len(context.args) == 0: return {"text": "must specify at least one argument"} redis = get_redis() func_name, *func_args = context.args func = getattr(redis, func_name, None) if func is None: return {"text": f"invalid func_name: {func_name}"} arg_names = ", ".join(func_args) ret = func(*func_args) return {"text": f"Ran `{func_name}({arg_names})`\nReturned `{ret}`"}
def run(self, context: CommandContext): location = " ".join(context.args) redis = get_redis() redis.hset("user_location", context.event.user, location) context.api.chat_post_ephemeral( { "channel": context.event.channel, "user": context.event.user, "text": f"Set default location to {location}", } )
def get_message_args(self, context: CommandContext): redis = get_redis() redis.set("killtv", "1") restore_default_tv_channel() admin_user_id = get_admin_user_id() return { "text": ( "Meowbot TV has been disabled. " f"Contact {quote_user_id(admin_user_id)} to reenable" ) }
def get_message_args(self, context: CommandContext): redis = get_redis() if len(context.args) >= 1: query = " ".join(context.args) else: if redis.hexists(USER_LOCATION, context.event.user): query = redis.hget(USER_LOCATION, context.event.user).decode("utf-8") else: query = get_default_zip_code() if redis.hexists(USER_UNITS, context.event.user): units = redis.hget(USER_UNITS, context.event.user).decode("utf-8") else: units = DEFAULT_UNITS return self._weather_arguments(query, units=units)
def get_message_args(self, context: CommandContext): redis = get_redis() if redis.exists('killtv'): return { 'text': ('Meowbot TV has been disabled. ' 'Contact Meowbot admin to reenable') } channels = get_channels() available_channels = ', '.join(sorted(channels.keys())) if len(context.args) == 1: channel, = context.args if channel not in channels: return { 'text': f'{channel} is not a valid channel.\n\n' f'Available channels: {available_channels}', 'thread_ts': context.event.ts } redis.set('tvchannel', channels[channel]['url']) return { 'text': f'Changing channel to {channels[channel]["name"]}!', } elif len(context.args) == 2: channel_type, value = context.args if channel_type == 'url': url = value[1:-1] redis.set('tvchannel', url) return { 'text': f'Changing channel to {url}!', } elif channel_type == 'twitch': url = f'https://player.twitch.tv/?channel={value}' redis.set('tvchannel', url) return { 'text': f'Changing channel to {Emoji.TWITCH} {value}!', } elif channel_type == 'youtube': url = (f'https://www.youtube.com/embed/{value}?' f'autoplay=1&loop=1&playlist={value}') redis.set('tvchannel', url) return { 'text': f'Changing channel to {Emoji.YOUTUBE} {value}!', } return { 'text': 'Must provide a channel.\n\n' f'Available channels: {available_channels}', 'thread_ts': context['event']['ts'] }
def get_message_args(self, context: CommandContext): redis = get_redis() key = "concertcal" ical_data = redis.get(key) if ical_data is None: ical_data = requests.get( "https://ybgfestival.org/events/?ical=1&tribe_display=list" ).content redis.set(key, ical_data) # Expire at midnight PST redis.expireat( key, (arrow.utcnow().to("US/Pacific") + timedelta(days=1)).replace( hour=0, minute=0).timestamp, ) cal = ics.Calendar(ical_data.decode("utf-8")) events = cal.timeline.start_after(arrow.utcnow() - timedelta(hours=3)) colors = ["#7aff33", "#33a2ff"] return { "text": "Upcoming concerts at <https://ybgfestival.org/|" "Yerba Buena Gardens Festival>:", "attachments": [{ "title": event.name, "title_link": event.url, "text": event.description, "color": color, "fields": [ { "title": "When", "value": "{} - {}".format( event.begin.strftime("%a %b %d, %I:%M"), event.end.strftime("%I:%M %p"), ), "short": False, }, ], } for event, color in zip(events, colors)], }
def get_message_args(self, context: CommandContext): redis = get_redis() key = 'concertcal' ical_data = redis.get(key) if ical_data is None: ical_data = requests.get( 'https://ybgfestival.org/events/?ical=1&tribe_display=list' ).content redis.set(key, ical_data) # Expire at midnight PST redis.expireat( key, (arrow.utcnow().to('US/Pacific') + timedelta(days=1)).replace( hour=0, minute=0).timestamp ) cal = ics.Calendar(ical_data.decode('utf-8')) events = cal.timeline.start_after(arrow.utcnow() - timedelta(hours=3)) colors = ['#7aff33', '#33a2ff'] return { 'text': 'Upcoming concerts at <https://ybgfestival.org/|' 'Yerba Buena Gardens Festival>:', 'attachments': [ { 'title': event.name, 'title_link': event.url, 'text': event.description, 'color': color, 'fields': [ { 'title': 'When', 'value': '{} - {}'.format( event.begin.strftime('%a %b %d, %I:%M'), event.end.strftime('%I:%M %p') ), 'short': False }, ] } for event, color in zip(events, colors) ] }
def get_message_args(self, context: CommandContext): redis = get_redis() redis.delete("killtv") return { "text": "Meowbot TV has been enabled", }
def tv_channel(): redis = get_redis() channel = redis.get('tvchannel') if channel is None: channel = restore_default_tv_channel() return channel
def _weather_arguments(self, query, units): key = f"weather:{units}:{query}" redis = get_redis() data = redis.get(key) location = get_location(query) if location is None: return {"text": f"Location `{query}` not found"} icon_map = { "clear-day": Emoji.SUNNY, "clear-night": Emoji.CRESCENT_MOON, "rain": Emoji.RAIN_CLOUD, "snow": Emoji.SNOW_CLOUD, "sleet": Emoji.RAIN_CLOUD, "wind": Emoji.WIND_BLOWING_FACE, "fog": Emoji.FOG, "cloudy": Emoji.CLOUD, "partly-cloudy-day": Emoji.PARTLY_SUNNY, "partly-cloudy-night": Emoji.PARTLY_SUNNY, } icon_default = Emoji.EARTH_AFRICA if data is None: api_key = get_darksky_api_key() lat = location["lat"] lon = location["lon"] data = requests.get( f"https://api.darksky.net/forecast/{api_key}/{lat},{lon}", params={ "exclude": "minutely,alerts,flags", "lang": "en", "units": units, }, ).content redis.set(key, data, ex=5 * 60) result = json.loads(data.decode("utf-8")) temp_symbol = "℉" if units == "us" else "℃" other_symbol = "℃" if units == "us" else "℉" other_unit = "si" if units == "us" else "us" return { "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": f'*Forecast for {location["display_name"]}*', }, }, { "type": "actions", "elements": [ { "type": "button", "action_id": f"weather:{units}", "text": { "type": "plain_text", "text": (f"{Emoji.ARROWS_COUNTERCLOCKWISE} Refresh"), "emoji": True, }, "value": str(query), }, { "type": "button", "action_id": f"weather:{other_unit}", "text": { "type": "plain_text", "text": f"to {other_symbol}", }, "value": str(query), }, ], }, {"type": "divider"}, { "type": "section", "text": { "type": "mrkdwn", "text": "*Current Weather*\n{summary}\n" "{icon} {current_temperature}{temp_unit}\n".format( summary=result["hourly"]["summary"], icon=icon_map.get( result["currently"]["icon"], icon_default ), current_temperature=int(result["currently"]["temperature"]), temp_unit=temp_symbol, ), }, }, {"type": "divider"}, { "type": "section", "text": {"type": "mrkdwn", "text": "*This Week*"}, "fields": [ { "type": "mrkdwn", "text": ( "*{day}*\n{icon} {high}{temp_unit} / " "{low}{temp_unit}".format( day=arrow.get(day["time"]).format("ddd"), icon=icon_map.get(day["icon"], icon_default), high=int(day["temperatureHigh"]), low=int(day["temperatureLow"]), temp_unit=temp_symbol, ) ), } for day in result["daily"]["data"] ], }, { "type": "context", "elements": [ { "type": "mrkdwn", "text": "<https://darksky.net/poweredby/|" "Powered by Dark Sky>", } ], }, ] }
def _weather_arguments(self, query, units): key = f'weather:{units}:{query}' redis = get_redis() data = redis.get(key) location = get_location(query) if location is None: return {'text': f'Location `{query}` not found'} icon_map = { 'clear-day': Emoji.SUNNY, 'clear-night': Emoji.CRESCENT_MOON, 'rain': Emoji.RAIN_CLOUD, 'snow': Emoji.SNOW_CLOUD, 'sleet': Emoji.RAIN_CLOUD, 'wind': Emoji.WIND_BLOWING_FACE, 'fog': Emoji.FOG, 'cloudy': Emoji.CLOUD, 'partly-cloudy-day': Emoji.PARTLY_SUNNY, 'partly-cloudy-night': Emoji.PARTLY_SUNNY, } icon_default = Emoji.EARTH_AFRICA if data is None: api_key = get_darksky_api_key() lat = location['lat'] lon = location['lon'] data = requests.get( f'https://api.darksky.net/forecast/{api_key}/{lat},{lon}', params={ 'exclude': 'minutely,alerts,flags', 'lang': 'en', 'units': units, } ).content redis.set(key, data, ex=5*60) result = json.loads(data.decode('utf-8')) temp_symbol = '℉' if units == 'us' else '℃' other_symbol = '℃' if units == 'us' else '℉' other_unit = 'si' if units == 'us' else 'us' return { 'blocks': [ { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f'*Forecast for {location["display_name"]}*' } }, { 'type': 'actions', 'elements': [ { 'type': 'button', 'action_id': f'weather:{units}', 'text': { 'type': 'plain_text', 'text': ( f'{Emoji.ARROWS_COUNTERCLOCKWISE} Refresh' ), 'emoji': True }, 'value': str(query) }, { 'type': 'button', 'action_id': f'weather:{other_unit}', 'text': { 'type': 'plain_text', 'text': f'to {other_symbol}' }, 'value': str(query) } ] }, { 'type': 'divider' }, { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': '*Current Weather*\n{summary}\n' '{icon} {current_temperature}{temp_unit}\n'.format( summary=result['hourly']['summary'], icon=icon_map.get( result['currently']['icon'], icon_default), current_temperature=int( result['currently']['temperature']), temp_unit=temp_symbol, ), } }, { 'type': 'divider' }, { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': '*This Week*' }, 'fields': [ { 'type': 'mrkdwn', 'text': ( '*{day}*\n{icon} {high}{temp_unit} / ' '{low}{temp_unit}'.format( day=arrow.get(day['time']).format('ddd'), icon=icon_map.get( day['icon'], icon_default), high=int(day['temperatureHigh']), low=int(day['temperatureLow']), temp_unit=temp_symbol, ) ), } for day in result['daily']['data'] ], }, { 'type': 'context', 'elements': [ { 'type': 'mrkdwn', 'text': '<https://darksky.net/poweredby/|' 'Powered by Dark Sky>' } ] } ] }
def get_message_args(self, context: CommandContext): if len(context.args) == 1: (zip_code,) = context.args if not zip_code.isnumeric(): return {"text": f"Zip code must be a number. Got `{zip_code}`"} elif len(context.args) > 1: return {"text": "Usage: `airquality [zipcode]`"} else: zip_code = get_default_zip_code() redis = get_redis() key = f"aqi:{zip_code}" data = redis.get(key) if data is None: airnow_api_key = get_airnow_api_key() observation_url = "http://www.airnowapi.org/aq/observation/zipCode/current/" data = requests.get( observation_url, params={ "API_KEY": airnow_api_key, "distance": 25, "zipCode": zip_code, "format": "application/json", }, ).content redis.set(key, data, ex=10 * 60) observations = json.loads(data.decode("utf-8")) # https://docs.airnowapi.org/aq101 category_color_map = { 1: "#00e400", # Good - Green 2: "#ffff00", # Moderate - Yellow 3: "#ff7e00", # USG - Orange 4: "#ff0000", # Unhealthy - Red 5: "#99004c", # Very Unhealthy - Purple 6: "#7e0023", # Hazardous - Maroon 7: "#000000", # Unavailable - Black } parameter_map = { "PM2.5": "fine particulate matter", "PM10": "particulate matter", "O3": "ozone", } if len(observations) == 0: return {"text": f"No data available for `{zip_code}`"} return { "text": "Air Quality for {}, {}:".format( observations[0]["ReportingArea"], observations[0]["StateCode"] ), "attachments": [ { "title": "{} ({})".format( observation["ParameterName"], parameter_map[observation["ParameterName"]], ), "fallback": "{}\n{} - {}".format( observation["ParameterName"], observation["AQI"], observation["Category"]["Name"], ), "color": category_color_map[observation["Category"]["Number"]], "text": "{} - {}".format( observation["AQI"], observation["Category"]["Name"] ), "footer": "Reported at {}{}:00 {}".format( observation["DateObserved"], observation["HourObserved"], observation["LocalTimeZone"], ), } for observation in observations ], }
def __init__(self): redis = get_redis() self._access_token = _get_strava_access_token(redis)
def get_message_args(self, context: CommandContext): if len(context.args) == 1: zip_code, = context.args if not zip_code.isnumeric(): return {'text': f'Zip code must be a number. Got `{zip_code}`'} elif len(context.args) > 1: return {'text': 'Usage: `airquality [zipcode]`'} else: zip_code = get_default_zip_code() redis = get_redis() key = f'aqi:{zip_code}' data = redis.get(key) if data is None: airnow_api_key = get_airnow_api_key() observation_url = ( 'http://www.airnowapi.org/aq/observation/zipCode/current/') data = requests.get(observation_url, params={ 'API_KEY': airnow_api_key, 'distance': 25, 'zipCode': zip_code, 'format': 'application/json' }).content redis.set(key, data, ex=10 * 60) observations = json.loads(data.decode('utf-8')) # https://docs.airnowapi.org/aq101 category_color_map = { 1: '#00e400', # Good - Green 2: '#ffff00', # Moderate - Yellow 3: '#ff7e00', # USG - Orange 4: '#ff0000', # Unhealthy - Red 5: '#99004c', # Very Unhealthy - Purple 6: '#7e0023', # Hazardous - Maroon 7: '#000000', # Unavailable - Black } parameter_map = { 'PM2.5': 'fine particulate matter', 'PM10': 'particulate matter', 'O3': 'ozone' } if len(observations) == 0: return {'text': f'No data available for `{zip_code}`'} return { "text": "Air Quality for {}, {}:".format(observations[0]['ReportingArea'], observations[0]['StateCode']), "attachments": [{ "title": "{} ({})".format( observation['ParameterName'], parameter_map[observation['ParameterName']], ), "fallback": "{}\n{} - {}".format(observation['ParameterName'], observation['AQI'], observation['Category']['Name']), "color": category_color_map[observation['Category']['Number']], "text": "{} - {}".format(observation['AQI'], observation['Category']['Name']), "footer": "Reported at {}{}:00 {}".format(observation['DateObserved'], observation['HourObserved'], observation['LocalTimeZone']) } for observation in observations] }
def get_message_args(self, context: CommandContext): redis = get_redis() if len(context.args) >= 1: query = " ".join(context.args) else: if redis.hexists(USER_LOCATION, context.event.user): query = redis.hget(USER_LOCATION, context.event.user).decode("utf-8") else: query = get_default_zip_code() location = get_location(query) if location is None: return {"text": f"Location `{query}` not found"} location_coords = (float(location["lat"]), float(location["lon"])) key = "purpleair" data = redis.get(key) if data is None: data = requests.get(PURPLEAIR_DATA_URL).content redis.set(key, data, ex=5 * 60) results = json.loads(data.decode("utf-8"))["results"] filtered_points = [ point for point in results if ( point.get("DEVICE_LOCATIONTYPE") == "outside" and "Lat" in point and "Lon" in point and point["AGE"] <= 30 ) ] child_points = { point["ParentID"]: point for point in results if "ParentID" in point } point_distances = ( (point, distance((point["Lat"], point["Lon"]), location_coords)) for point in filtered_points ) point_distances = sorted( filter(lambda p_d: p_d[1].miles <= 25.0, point_distances), key=lambda p_d: p_d[1], ) closest_points = point_distances[:3] if len(closest_points) == 0: return { "text": "No PurpleAir sensor within 25 miles of " f"{location['display_name']}" } def build_attachment(point, distance): pm25_10_min_avg = json.loads(point["Stats"])["v1"] flagged = "Flag" in point child_point = child_points.get(point["ID"]) if child_point: pm25_10_min_avg = ( pm25_10_min_avg + json.loads(child_point["Stats"])["v1"] ) / 2 flagged |= "Flag" in child_point for band in BANDS: lower_pm, upper_pm = band["pm25"] lower_aqi, upper_aqi = band["aqi"] color = band["color"] name = band["name"] if pm25_10_min_avg <= upper_pm: # https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI ten_min_aqi = (upper_aqi - lower_aqi) / (upper_pm - lower_pm) * ( pm25_10_min_avg - lower_pm ) + lower_aqi break return { "fallback": f"{ten_min_aqi} - {name}", "color": color, "text": f"{ten_min_aqi:.0f} - {name}", "footer": "{} | {:.1f} mi away | reported {:.0f} minutes ago".format( point["Label"], distance.miles, (time.time() - point["LastSeen"]) / 60, ), } map_url = ( "https://www.purpleair.com/map?opt=1/i/mAQI/a10/cC0#13.0/" f"{location_coords[0]}/{location_coords[1]}" ) return { "text": f"PurpleAir sensors near <{map_url}|{location['display_name']}>:", "icon_emoji": ":purpleair:", "attachments": [ build_attachment(point, distance) for point, distance in closest_points ], }
def get_message_args(self, context: CommandContext): get_redis().incr("tvid") return {"text": "Refreshed tv!"}