#!/usr/bin/env python3.11 from datetime import datetime,timezone from urllib.parse import quote_plus from urllib.request import urlretrieve, urlopen, Request, HTTPError from threading import Thread from os import makedirs,path from functools import cache import json,sys,webbrowser,importlib,tomllib def update(): print('checking for updates...') with open(script_path()+'astrolobot.py','r') as file: current_script=file.read() urlretrieve( 'https://github.com/sugoidogo/astrolobot/releases/latest/download/astrolobot.py', script_path()+'astrolobot.py' ) with open(script_path()+'astrolobot.py','r') as file: updated_script=file.read() if current_script==updated_script: print('up to date') else: print('update downloaded, restart script to apply') def dependency_setup(): if not path.exists(script_path()+'pip'): print('installing dependencies') from pip._internal.cli.main import main as pip pip(['install','-qq','pyswisseph','twitch.py','--target',script_path()+'pip']) sys.path.append(script_path()+'pip') if not path.exists(script_path()+'ephe/seas_18.se1'): print('downloading database') makedirs(script_path()+'ephe') urlretrieve('https://github.com/aloistr/swisseph/raw/refs/heads/master/ephe/seas_18.se1',script_path()+'ephe/seas_18.se1') import swisseph as swe swe.set_ephe_path(script_path()+'ephe/') swe.set_sid_mode(swe.SIDM_LAHIRI) print('dependencies ready') def today(): now=datetime.utcnow() return datetime(now.year,now.month,now.day) # Astrology functions def load_settings(): try: with open(script_path()+'config.toml','r') as file: return tomllib.loads(file.read()) except FileNotFoundError: return { 'positions':'!positions', 'transits':'!transits', 'major_aspects':'!aspects major', 'minor_aspects':'!aspects minor', } def get_zodiac(angle): zodiac = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"] return zodiac[int(angle//30)] def is_retrograde(speed): return speed<0 @cache def get_positions_raw(date=today()): import swisseph as swe julian_day = swe.julday(date.year, date.month, date.day) planets = { "The Sun": swe.SUN, "The Moon": swe.MOON, "Mercury": swe.MERCURY, "Venus": swe.VENUS, "Mars": swe.MARS, "Jupiter": swe.JUPITER, "Saturn": swe.SATURN, "Uranus": swe.URANUS, "Neptune": swe.NEPTUNE, "Pluto": swe.PLUTO, "Chiron": swe.CHIRON, "North Node":swe.TRUE_NODE } positions={} for planet_name, planet_id in planets.items(): position, _ = swe.calc_ut(julian_day, planet_id, swe.FLG_SPEED) positions[planet_name]={ 'angle':position[0], 'speed':position[3], } if planet_name=='North Node': positions['South Node']={ 'angle':(position[0]+180)%360, 'speed':position[3], } return positions @cache def get_positions(date=today()): positions={} for name, position_raw in get_positions_raw(date).items(): positions[name]={ 'zodiac':get_zodiac(position_raw['angle']), 'retrograde':is_retrograde(position_raw['speed']), } return positions def get_list_formatted(in_list,isare=True): match len(in_list): case 0: output='Nothing' if isare: output+=' is ' case 1: output=in_list[0] if isare: output+=' is ' case 2: output=' and '.join(in_list) if isare: output+=' are ' case _: output=', '.join(in_list[0:-1])+', and '+in_list[-1] if isare: output+=' are ' return output @cache def get_positions_formatted(date=today()): retrograde_planets=[] position_strings=[] for name, position in get_positions(date).items(): if(position['retrograde']): retrograde_planets.append(name) position_strings.append(name+' is in '+position['zodiac']) positions_formatted=get_list_formatted(retrograde_planets)+'in Retrograde\n' positions_formatted+=', '.join(position_strings[0:2])+'\n' positions_formatted+=', '.join(position_strings[2:5])+'\n' positions_formatted+=', '.join(position_strings[5:8])+'\n' positions_formatted+=', '.join(position_strings[8:11])+'\n' positions_formatted+=', '.join(position_strings[11:13])+'\n' return positions_formatted @cache def get_transits(date=today(), maxdays=28): positions_now=get_positions(date) transits={} days=1 while len(transits)<12 and days None: print('astrolobot ready') while True: tokens=client.http.get_token(client.user.id) await asyncio.sleep(tokens['expire_in']*0.9) tokens=refresh_tokens(tokens['refresh_token']) await client.authorize(tokens['access_token'],tokens['refresh_token']) @client.event async def on_chat_message(data: eventsub.chat.MessageEvent): config=load_settings() if data['message']['text']==config['positions']: for line in get_positions_formatted().splitlines(False): await client.channel.chat.send_message(line,data['message_id']) return if data['message']['text']==config['transits']: for line in get_transits_formatted().splitlines(False): await client.channel.chat.send_message(line,data['message_id']) return if data['message']['text']==config['major_aspects']: for line in get_aspects_formatted().splitlines(False): await client.channel.chat.send_message(line,data['message_id']) return if data['message']['text']==config['minor_aspects']: for line in get_aspects_formatted(minor=True).splitlines(False): await client.channel.chat.send_message(line,data['message_id']) return client.run(*tokens.values()) # OBS functions obs_settings=None def script_main(): dependency_setup() main() def obs_update(*args): Thread(target=update,daemon=True).start() def obs_load_settings(): import obspython global obs_settings settings=json.loads(obspython.obs_data_get_json_with_defaults(obs_settings)) return settings def obs_load_tokens(): import obspython access_token=obspython.obs_data_get_string(obs_settings,'access_token') refresh_token=obspython.obs_data_get_string(obs_settings,'refresh_token') if not (access_token or refresh_token): print('astrolobot: login required',file=sys.stderr) return {} else: return { 'access_token':access_token, 'refresh_token':refresh_token, } def obs_save_tokens(access_token,refresh_token): import obspython obspython.obs_data_set_string(obs_settings,'access_token',access_token) obspython.obs_data_set_string(obs_settings,'refresh_token',refresh_token) print('login tokens saved to OBS') # https://docs.obsproject.com/scripting#script-function-exports def script_description(): return 'adds astrology commands to your twitch channel, providing the current and upcoming star signs and retrograde status of the planets' def script_properties(): import obspython properties=obspython.obs_properties_create() obspython.obs_properties_add_button(properties,'login','Login to Twitch',login) obspython.obs_properties_add_button(properties,'update','Check for Updates',obs_update) obspython.obs_properties_add_text(properties,'commands','commands',obspython.OBS_TEXT_INFO) obspython.obs_properties_add_text(properties,'positions','positions',obspython.OBS_TEXT_DEFAULT) obspython.obs_properties_add_text(properties,'transits','transits',obspython.OBS_TEXT_DEFAULT) obspython.obs_properties_add_text(properties,'major_aspects','major aspects',obspython.OBS_TEXT_DEFAULT) obspython.obs_properties_add_text(properties,'minor_aspects','minor aspects',obspython.OBS_TEXT_DEFAULT) return properties def script_defaults(settings): import obspython obspython.obs_data_set_default_string(settings,'commands','you can change the commands below, they save automatically') obspython.obs_data_set_default_string(settings,'positions','!positions') obspython.obs_data_set_default_string(settings,'transits','!transits') obspython.obs_data_set_default_string(settings,'major_aspects','!aspects major') obspython.obs_data_set_default_string(settings,'minor_aspects','!aspects minor') def script_load(settings): global obs_settings, save_tokens, load_tokens, load_settings obs_settings=settings save_tokens=obs_save_tokens load_tokens=obs_load_tokens load_settings=obs_load_settings def isatty(): return False sys.stdout.isatty=isatty Thread(target=script_main,daemon=True).start() def script_unload(): try: import asyncio global client asyncio.run(client.close()) except NameError or ImportError: pass def script_update(settings): global obs_settings obs_settings=settings # test functions if __name__ == '__main__': def script_path(): return path.dirname(__file__)+'/' dependency_setup() print(get_positions_formatted()) print(get_transits_formatted()) #main() print(get_aspects_formatted()) print(get_aspects_formatted(minor=True))