vrchatapi-async 1.20.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. vrchatapi/__init__.py +742 -0
  2. vrchatapi/api/__init__.py +23 -0
  3. vrchatapi/api/authentication_api.py +6253 -0
  4. vrchatapi/api/avatars_api.py +4234 -0
  5. vrchatapi/api/calendar_api.py +4090 -0
  6. vrchatapi/api/economy_api.py +11547 -0
  7. vrchatapi/api/favorites_api.py +2368 -0
  8. vrchatapi/api/files_api.py +6109 -0
  9. vrchatapi/api/friends_api.py +1708 -0
  10. vrchatapi/api/groups_api.py +15085 -0
  11. vrchatapi/api/instances_api.py +1754 -0
  12. vrchatapi/api/inventory_api.py +4333 -0
  13. vrchatapi/api/invite_api.py +3337 -0
  14. vrchatapi/api/jams_api.py +1446 -0
  15. vrchatapi/api/miscellaneous_api.py +2384 -0
  16. vrchatapi/api/notifications_api.py +3624 -0
  17. vrchatapi/api/playermoderation_api.py +1127 -0
  18. vrchatapi/api/prints_api.py +1478 -0
  19. vrchatapi/api/props_api.py +2269 -0
  20. vrchatapi/api/users_api.py +7894 -0
  21. vrchatapi/api/worlds_api.py +6153 -0
  22. vrchatapi/api_client.py +865 -0
  23. vrchatapi/api_response.py +21 -0
  24. vrchatapi/configuration.py +686 -0
  25. vrchatapi/exceptions.py +218 -0
  26. vrchatapi/models/__init__.py +345 -0
  27. vrchatapi/models/account_deletion_log.py +253 -0
  28. vrchatapi/models/add_favorite_request.py +246 -0
  29. vrchatapi/models/add_group_gallery_image_request.py +234 -0
  30. vrchatapi/models/admin_asset_bundle.py +337 -0
  31. vrchatapi/models/admin_unity_package.py +261 -0
  32. vrchatapi/models/age_verification_status.py +38 -0
  33. vrchatapi/models/agreement.py +280 -0
  34. vrchatapi/models/agreement_code.py +36 -0
  35. vrchatapi/models/agreement_request.py +256 -0
  36. vrchatapi/models/agreement_status.py +261 -0
  37. vrchatapi/models/api_config.py +1299 -0
  38. vrchatapi/models/api_config_access_logs_urls.py +258 -0
  39. vrchatapi/models/api_config_announcement.py +217 -0
  40. vrchatapi/models/api_config_audio_config.py +311 -0
  41. vrchatapi/models/api_config_avatar_perf_limiter.py +293 -0
  42. vrchatapi/models/api_config_constants.py +262 -0
  43. vrchatapi/models/api_config_constants_groups.py +306 -0
  44. vrchatapi/models/api_config_constants_instance.py +238 -0
  45. vrchatapi/models/api_config_constants_instancepopulationbrackets.py +262 -0
  46. vrchatapi/models/api_config_constants_instancepopulationbracketscrowded.py +216 -0
  47. vrchatapi/models/api_config_constants_instancepopulationbracketsfew.py +216 -0
  48. vrchatapi/models/api_config_constants_instancepopulationbracketsmany.py +216 -0
  49. vrchatapi/models/api_config_constants_language.py +234 -0
  50. vrchatapi/models/api_config_download_url_list.py +258 -0
  51. vrchatapi/models/api_config_events.py +314 -0
  52. vrchatapi/models/api_config_ios_version.py +216 -0
  53. vrchatapi/models/api_config_min_supported_client_build_number.py +348 -0
  54. vrchatapi/models/api_config_offline_analysis.py +216 -0
  55. vrchatapi/models/api_health.py +248 -0
  56. vrchatapi/models/avatar.py +463 -0
  57. vrchatapi/models/avatar_moderation.py +249 -0
  58. vrchatapi/models/avatar_moderation_created.py +248 -0
  59. vrchatapi/models/avatar_moderation_type.py +36 -0
  60. vrchatapi/models/avatar_performance.py +265 -0
  61. vrchatapi/models/avatar_published_listings_inner.py +271 -0
  62. vrchatapi/models/avatar_style.py +239 -0
  63. vrchatapi/models/avatar_styles.py +231 -0
  64. vrchatapi/models/avatar_unity_package_url_object.py +234 -0
  65. vrchatapi/models/badge.py +308 -0
  66. vrchatapi/models/balance.py +247 -0
  67. vrchatapi/models/ban_group_member_request.py +234 -0
  68. vrchatapi/models/boop_request.py +250 -0
  69. vrchatapi/models/calendar_day_of_week.py +42 -0
  70. vrchatapi/models/calendar_event.py +475 -0
  71. vrchatapi/models/calendar_event_access.py +37 -0
  72. vrchatapi/models/calendar_event_category.py +48 -0
  73. vrchatapi/models/calendar_event_discovery.py +247 -0
  74. vrchatapi/models/calendar_event_discovery_inclusion.py +38 -0
  75. vrchatapi/models/calendar_event_discovery_scope.py +38 -0
  76. vrchatapi/models/calendar_event_frequency.py +39 -0
  77. vrchatapi/models/calendar_event_occurrence_kind.py +38 -0
  78. vrchatapi/models/calendar_event_platform.py +38 -0
  79. vrchatapi/models/calendar_event_recurrence.py +261 -0
  80. vrchatapi/models/calendar_event_recurrence_end.py +246 -0
  81. vrchatapi/models/calendar_event_recurrence_end_type.py +37 -0
  82. vrchatapi/models/calendar_event_user_interest.py +251 -0
  83. vrchatapi/models/change_user_tags_request.py +212 -0
  84. vrchatapi/models/change_world_tags_request.py +212 -0
  85. vrchatapi/models/content_filter.py +40 -0
  86. vrchatapi/models/create_avatar_moderation_request.py +243 -0
  87. vrchatapi/models/create_avatar_request.py +324 -0
  88. vrchatapi/models/create_calendar_event_request.py +385 -0
  89. vrchatapi/models/create_file_request.py +251 -0
  90. vrchatapi/models/create_file_version_request.py +259 -0
  91. vrchatapi/models/create_group_announcement_request.py +253 -0
  92. vrchatapi/models/create_group_gallery_request.py +297 -0
  93. vrchatapi/models/create_group_invite_request.py +242 -0
  94. vrchatapi/models/create_group_post_request.py +267 -0
  95. vrchatapi/models/create_group_request.py +295 -0
  96. vrchatapi/models/create_group_role_request.py +255 -0
  97. vrchatapi/models/create_instance_request.py +384 -0
  98. vrchatapi/models/create_jam_submission_request.py +239 -0
  99. vrchatapi/models/create_listing_request.py +285 -0
  100. vrchatapi/models/create_product_request.py +270 -0
  101. vrchatapi/models/create_prop_request.py +308 -0
  102. vrchatapi/models/create_world_request.py +322 -0
  103. vrchatapi/models/current_user.py +909 -0
  104. vrchatapi/models/current_user_platform_history_inner.py +245 -0
  105. vrchatapi/models/current_user_presence.py +379 -0
  106. vrchatapi/models/decline_group_invite_request.py +211 -0
  107. vrchatapi/models/developer_type.py +39 -0
  108. vrchatapi/models/disable2_fa_result.py +211 -0
  109. vrchatapi/models/discord_details.py +216 -0
  110. vrchatapi/models/dynamic_content_row.py +276 -0
  111. vrchatapi/models/earnings_metrics.py +248 -0
  112. vrchatapi/models/earnings_metrics_totals.py +267 -0
  113. vrchatapi/models/economy_account.py +360 -0
  114. vrchatapi/models/economy_balances.py +221 -0
  115. vrchatapi/models/economy_payout.py +385 -0
  116. vrchatapi/models/economy_payout_eligibility.py +263 -0
  117. vrchatapi/models/economy_payout_list.py +219 -0
  118. vrchatapi/models/economy_payout_status.py +295 -0
  119. vrchatapi/models/equip_inventory_item_request.py +235 -0
  120. vrchatapi/models/error.py +215 -0
  121. vrchatapi/models/favorite.py +251 -0
  122. vrchatapi/models/favorite_group.py +278 -0
  123. vrchatapi/models/favorite_group_limits.py +249 -0
  124. vrchatapi/models/favorite_group_visibility.py +38 -0
  125. vrchatapi/models/favorite_limits.py +265 -0
  126. vrchatapi/models/favorite_type.py +39 -0
  127. vrchatapi/models/favorited_world.py +441 -0
  128. vrchatapi/models/feedback.py +331 -0
  129. vrchatapi/models/file.py +325 -0
  130. vrchatapi/models/file_analysis.py +281 -0
  131. vrchatapi/models/file_analysis_avatar_stats.py +575 -0
  132. vrchatapi/models/file_data.py +279 -0
  133. vrchatapi/models/file_status.py +39 -0
  134. vrchatapi/models/file_upload_url.py +212 -0
  135. vrchatapi/models/file_version.py +254 -0
  136. vrchatapi/models/file_version_upload_status.py +269 -0
  137. vrchatapi/models/finish_file_data_upload_request.py +248 -0
  138. vrchatapi/models/follow_calendar_event_request.py +234 -0
  139. vrchatapi/models/friend_status.py +250 -0
  140. vrchatapi/models/get_group_posts200_response.py +219 -0
  141. vrchatapi/models/get_user_group_instances200_response.py +248 -0
  142. vrchatapi/models/group.py +514 -0
  143. vrchatapi/models/group_access_type.py +38 -0
  144. vrchatapi/models/group_announcement.py +315 -0
  145. vrchatapi/models/group_audit_log_entry.py +287 -0
  146. vrchatapi/models/group_gallery.py +319 -0
  147. vrchatapi/models/group_gallery_file_order.py +211 -0
  148. vrchatapi/models/group_gallery_file_order_request.py +239 -0
  149. vrchatapi/models/group_gallery_image.py +301 -0
  150. vrchatapi/models/group_instance.py +257 -0
  151. vrchatapi/models/group_join_request_action.py +37 -0
  152. vrchatapi/models/group_join_state.py +39 -0
  153. vrchatapi/models/group_member.py +415 -0
  154. vrchatapi/models/group_member_limited_user.py +290 -0
  155. vrchatapi/models/group_member_status.py +41 -0
  156. vrchatapi/models/group_my_member.py +399 -0
  157. vrchatapi/models/group_permission.py +260 -0
  158. vrchatapi/models/group_permissions.py +63 -0
  159. vrchatapi/models/group_post.py +317 -0
  160. vrchatapi/models/group_post_visibility.py +37 -0
  161. vrchatapi/models/group_privacy.py +37 -0
  162. vrchatapi/models/group_role.py +309 -0
  163. vrchatapi/models/group_role_template.py +39 -0
  164. vrchatapi/models/group_role_template_values.py +254 -0
  165. vrchatapi/models/group_role_template_values_roles.py +253 -0
  166. vrchatapi/models/group_search_sort.py +37 -0
  167. vrchatapi/models/group_transferable.py +215 -0
  168. vrchatapi/models/group_transferable_requirements.py +266 -0
  169. vrchatapi/models/group_user_visibility.py +38 -0
  170. vrchatapi/models/image_animation_style.py +62 -0
  171. vrchatapi/models/image_loop_style.py +37 -0
  172. vrchatapi/models/image_mask.py +41 -0
  173. vrchatapi/models/image_purpose.py +46 -0
  174. vrchatapi/models/info_push.py +306 -0
  175. vrchatapi/models/info_push_data.py +288 -0
  176. vrchatapi/models/info_push_data_article.py +215 -0
  177. vrchatapi/models/info_push_data_article_content.py +251 -0
  178. vrchatapi/models/info_push_data_clickable.py +224 -0
  179. vrchatapi/models/instance.py +597 -0
  180. vrchatapi/models/instance_content_settings.py +236 -0
  181. vrchatapi/models/instance_platforms.py +222 -0
  182. vrchatapi/models/instance_region.py +40 -0
  183. vrchatapi/models/instance_short_name_response.py +248 -0
  184. vrchatapi/models/instance_type.py +40 -0
  185. vrchatapi/models/inventory.py +247 -0
  186. vrchatapi/models/inventory_consumption_results.py +255 -0
  187. vrchatapi/models/inventory_default_attributes_value.py +243 -0
  188. vrchatapi/models/inventory_default_attributes_value_validator.py +211 -0
  189. vrchatapi/models/inventory_drop.py +331 -0
  190. vrchatapi/models/inventory_equip_slot.py +39 -0
  191. vrchatapi/models/inventory_flag.py +43 -0
  192. vrchatapi/models/inventory_item.py +420 -0
  193. vrchatapi/models/inventory_item_type.py +42 -0
  194. vrchatapi/models/inventory_metadata.py +287 -0
  195. vrchatapi/models/inventory_notification_details.py +244 -0
  196. vrchatapi/models/inventory_spawn.py +216 -0
  197. vrchatapi/models/inventory_template.py +351 -0
  198. vrchatapi/models/inventory_user_attributes.py +250 -0
  199. vrchatapi/models/invite_message.py +276 -0
  200. vrchatapi/models/invite_message_type.py +39 -0
  201. vrchatapi/models/invite_request.py +243 -0
  202. vrchatapi/models/invite_response.py +235 -0
  203. vrchatapi/models/jam.py +302 -0
  204. vrchatapi/models/jam_state_change_dates.py +276 -0
  205. vrchatapi/models/jam_submission.py +275 -0
  206. vrchatapi/models/join_group_request.py +234 -0
  207. vrchatapi/models/license.py +261 -0
  208. vrchatapi/models/license_action.py +37 -0
  209. vrchatapi/models/license_group.py +235 -0
  210. vrchatapi/models/license_type.py +39 -0
  211. vrchatapi/models/limited_group.py +377 -0
  212. vrchatapi/models/limited_unity_package.py +251 -0
  213. vrchatapi/models/limited_user_friend.py +399 -0
  214. vrchatapi/models/limited_user_groups.py +386 -0
  215. vrchatapi/models/limited_user_instance.py +430 -0
  216. vrchatapi/models/limited_user_search.py +395 -0
  217. vrchatapi/models/limited_world.py +410 -0
  218. vrchatapi/models/mime_type.py +50 -0
  219. vrchatapi/models/model_print.py +299 -0
  220. vrchatapi/models/moderate_user_request.py +217 -0
  221. vrchatapi/models/moderation_report.py +296 -0
  222. vrchatapi/models/mutual_friend.py +302 -0
  223. vrchatapi/models/mutuals.py +217 -0
  224. vrchatapi/models/notification.py +283 -0
  225. vrchatapi/models/notification_detail_invite.py +250 -0
  226. vrchatapi/models/notification_detail_invite_response.py +242 -0
  227. vrchatapi/models/notification_detail_request_invite.py +239 -0
  228. vrchatapi/models/notification_detail_request_invite_response.py +242 -0
  229. vrchatapi/models/notification_detail_vote_to_kick.py +242 -0
  230. vrchatapi/models/notification_type.py +43 -0
  231. vrchatapi/models/notification_v2.py +466 -0
  232. vrchatapi/models/notification_v2_details_boop.py +255 -0
  233. vrchatapi/models/notification_v2_response.py +259 -0
  234. vrchatapi/models/notification_v2_type.py +58 -0
  235. vrchatapi/models/ok_status.py +211 -0
  236. vrchatapi/models/ok_status2.py +234 -0
  237. vrchatapi/models/order_option.py +37 -0
  238. vrchatapi/models/order_option_short.py +37 -0
  239. vrchatapi/models/paginated_calendar_event_list.py +255 -0
  240. vrchatapi/models/paginated_group_audit_log_entry_list.py +255 -0
  241. vrchatapi/models/paginated_moderation_report_list.py +255 -0
  242. vrchatapi/models/past_display_name.py +241 -0
  243. vrchatapi/models/pending2_fa_result.py +239 -0
  244. vrchatapi/models/performance_limiter_info.py +234 -0
  245. vrchatapi/models/performance_ratings.py +41 -0
  246. vrchatapi/models/permission.py +280 -0
  247. vrchatapi/models/permission_data.py +258 -0
  248. vrchatapi/models/platform_build_info.py +242 -0
  249. vrchatapi/models/player_moderation.py +276 -0
  250. vrchatapi/models/player_moderation_type.py +44 -0
  251. vrchatapi/models/print_files.py +239 -0
  252. vrchatapi/models/private_profile.py +267 -0
  253. vrchatapi/models/private_profile_activity.py +292 -0
  254. vrchatapi/models/product.py +389 -0
  255. vrchatapi/models/product_listing.py +538 -0
  256. vrchatapi/models/product_listing_type.py +39 -0
  257. vrchatapi/models/product_listing_variant.py +272 -0
  258. vrchatapi/models/product_purchase.py +571 -0
  259. vrchatapi/models/product_purchase_history.py +256 -0
  260. vrchatapi/models/product_purchase_location_type.py +46 -0
  261. vrchatapi/models/product_purchase_product.py +269 -0
  262. vrchatapi/models/product_purchase_purchase_context.py +250 -0
  263. vrchatapi/models/product_purchase_record.py +312 -0
  264. vrchatapi/models/product_type.py +39 -0
  265. vrchatapi/models/profile_represented_group.py +262 -0
  266. vrchatapi/models/prop.py +358 -0
  267. vrchatapi/models/prop_publish_status.py +234 -0
  268. vrchatapi/models/prop_unity_package.py +269 -0
  269. vrchatapi/models/public_profile.py +398 -0
  270. vrchatapi/models/purchase_context_data.py +251 -0
  271. vrchatapi/models/purchase_product_listing_request.py +281 -0
  272. vrchatapi/models/region.py +42 -0
  273. vrchatapi/models/register_user_account_request.py +278 -0
  274. vrchatapi/models/release_status.py +39 -0
  275. vrchatapi/models/report_category.py +226 -0
  276. vrchatapi/models/report_reason.py +216 -0
  277. vrchatapi/models/represented_group.py +348 -0
  278. vrchatapi/models/request_invite_request.py +235 -0
  279. vrchatapi/models/respond_group_join_request.py +217 -0
  280. vrchatapi/models/respond_notification_v2_request.py +242 -0
  281. vrchatapi/models/response.py +217 -0
  282. vrchatapi/models/reward_badge.py +308 -0
  283. vrchatapi/models/reward_redemption.py +220 -0
  284. vrchatapi/models/reward_redemption_data.py +224 -0
  285. vrchatapi/models/reward_redemption_request.py +211 -0
  286. vrchatapi/models/reward_redemption_result.py +250 -0
  287. vrchatapi/models/search_group_members200_response.py +224 -0
  288. vrchatapi/models/seller_eligibility.py +211 -0
  289. vrchatapi/models/sent_notification.py +278 -0
  290. vrchatapi/models/service_queue_stats.py +234 -0
  291. vrchatapi/models/service_status.py +281 -0
  292. vrchatapi/models/share_inventory_item_direct_request.py +239 -0
  293. vrchatapi/models/sort_option.py +53 -0
  294. vrchatapi/models/sort_option_product_purchase.py +36 -0
  295. vrchatapi/models/store.py +372 -0
  296. vrchatapi/models/store_context.py +249 -0
  297. vrchatapi/models/store_shelf.py +304 -0
  298. vrchatapi/models/store_type.py +38 -0
  299. vrchatapi/models/store_view.py +40 -0
  300. vrchatapi/models/submit_moderation_report_request.py +263 -0
  301. vrchatapi/models/submit_moderation_report_request_details.py +283 -0
  302. vrchatapi/models/subscription.py +301 -0
  303. vrchatapi/models/subscription_period.py +40 -0
  304. vrchatapi/models/success.py +215 -0
  305. vrchatapi/models/success_flag.py +211 -0
  306. vrchatapi/models/tilia_kyc.py +246 -0
  307. vrchatapi/models/tilia_status.py +259 -0
  308. vrchatapi/models/tilia_tos.py +211 -0
  309. vrchatapi/models/token_bundle.py +286 -0
  310. vrchatapi/models/transaction.py +322 -0
  311. vrchatapi/models/transaction_agreement.py +353 -0
  312. vrchatapi/models/transaction_status.py +39 -0
  313. vrchatapi/models/transaction_steam_info.py +271 -0
  314. vrchatapi/models/transaction_steam_wallet_info.py +227 -0
  315. vrchatapi/models/transfer_group_request.py +234 -0
  316. vrchatapi/models/tutorial_status.py +255 -0
  317. vrchatapi/models/two_factor_auth_code.py +211 -0
  318. vrchatapi/models/two_factor_email_code.py +211 -0
  319. vrchatapi/models/two_factor_recovery_codes.py +247 -0
  320. vrchatapi/models/two_factor_recovery_codes_otp_inner.py +216 -0
  321. vrchatapi/models/unity_package.py +360 -0
  322. vrchatapi/models/update_asset_review_notes_request.py +234 -0
  323. vrchatapi/models/update_avatar_request.py +293 -0
  324. vrchatapi/models/update_calendar_event_request.py +365 -0
  325. vrchatapi/models/update_favorite_group_request.py +246 -0
  326. vrchatapi/models/update_group_gallery_request.py +297 -0
  327. vrchatapi/models/update_group_member_request.py +256 -0
  328. vrchatapi/models/update_group_representation_request.py +234 -0
  329. vrchatapi/models/update_group_request.py +300 -0
  330. vrchatapi/models/update_group_role_request.py +255 -0
  331. vrchatapi/models/update_inventory_item_request.py +254 -0
  332. vrchatapi/models/update_invite_message_request.py +211 -0
  333. vrchatapi/models/update_listing_request.py +211 -0
  334. vrchatapi/models/update_product_request.py +261 -0
  335. vrchatapi/models/update_prop_request.py +303 -0
  336. vrchatapi/models/update_tilia_tos_request.py +211 -0
  337. vrchatapi/models/update_user_badge_request.py +216 -0
  338. vrchatapi/models/update_user_note_request.py +239 -0
  339. vrchatapi/models/update_user_request.py +358 -0
  340. vrchatapi/models/update_world_request.py +317 -0
  341. vrchatapi/models/user.py +549 -0
  342. vrchatapi/models/user_credits_eligible.py +216 -0
  343. vrchatapi/models/user_exists.py +242 -0
  344. vrchatapi/models/user_note.py +273 -0
  345. vrchatapi/models/user_note_target_user.py +277 -0
  346. vrchatapi/models/user_state.py +38 -0
  347. vrchatapi/models/user_status.py +40 -0
  348. vrchatapi/models/user_subscription.py +330 -0
  349. vrchatapi/models/user_subscription_eligible.py +274 -0
  350. vrchatapi/models/verify2_fa_email_code_result.py +211 -0
  351. vrchatapi/models/verify2_fa_result.py +216 -0
  352. vrchatapi/models/verify_auth_token_result.py +217 -0
  353. vrchatapi/models/world.py +459 -0
  354. vrchatapi/models/world_metadata.py +216 -0
  355. vrchatapi/models/world_publish_status.py +234 -0
  356. vrchatapi/py.typed +0 -0
  357. vrchatapi/rest.py +303 -0
  358. vrchatapi/websocket.py +652 -0
  359. vrchatapi_async-1.20.8.dist-info/METADATA +183 -0
  360. vrchatapi_async-1.20.8.dist-info/RECORD +363 -0
  361. vrchatapi_async-1.20.8.dist-info/WHEEL +5 -0
  362. vrchatapi_async-1.20.8.dist-info/licenses/LICENSE +21 -0
  363. vrchatapi_async-1.20.8.dist-info/top_level.txt +1 -0
vrchatapi/websocket.py ADDED
@@ -0,0 +1,652 @@
1
+ """VRChat Websocket (Pipeline) API client.
2
+
3
+ The VRChat Websocket API, also known as "the pipeline", pushes real-time
4
+ updates to the authenticated client (invites, friend requests, friend online
5
+ events, group updates, ...). The connection is receive-only: the client only
6
+ listens for messages.
7
+
8
+ The connection URL is ``wss://pipeline.vrchat.cloud/?authToken=<auth cookie>``
9
+ where ``authToken`` is the ``auth`` cookie obtained by logging in through the
10
+ REST API. A proper ``User-Agent`` header is also required.
11
+
12
+ Most messages are double-encoded: the outer envelope is JSON of the form
13
+ ``{"type": "...", "content": "..."}`` and the ``content`` field is itself a
14
+ stringified JSON object. This client automatically unpacks ``content`` into a
15
+ plain ``dict`` (see :class:`VRChatEvent`). The ``see-notification`` and
16
+ ``hide-notification`` events carry a plain notification-ID string instead, and
17
+ ``clear-notification`` has no content at all.
18
+
19
+ Example::
20
+
21
+ import asyncio
22
+
23
+ import vrchatapi
24
+ from vrchatapi.api import authentication_api
25
+ from vrchatapi.websocket import VRChatWebSocket
26
+
27
+
28
+ async def main():
29
+ configuration = vrchatapi.Configuration(username="user", password="pass")
30
+ async with vrchatapi.ApiClient(configuration) as api_client:
31
+ await authentication_api.AuthenticationApi(api_client).get_current_user()
32
+ ws = VRChatWebSocket.from_client(api_client)
33
+
34
+ @ws.on("friend-online")
35
+ async def on_friend_online(event):
36
+ print("friend online:", event.content["user"]["displayName"])
37
+
38
+ await ws.run()
39
+
40
+
41
+ if __name__ == "__main__":
42
+ asyncio.run(main())
43
+ """
44
+
45
+ import asyncio
46
+ import inspect
47
+ import json
48
+ import logging
49
+ import uuid
50
+ from typing import Any, Callable, Dict, FrozenSet, List, Optional
51
+ from urllib.parse import quote
52
+
53
+ import aiohttp
54
+
55
+ __all__ = [
56
+ "VRChatWebSocket",
57
+ "VRChatEvent",
58
+ "VRChatWebSocketError",
59
+ "get_auth_cookie",
60
+ "DEFAULT_ENDPOINT",
61
+ "DEFAULT_USER_AGENT",
62
+ "DEFAULT_HEARTBEAT_INTERVAL",
63
+ "WS_EVENT_TYPES",
64
+ ]
65
+
66
+ logger = logging.getLogger(__name__)
67
+
68
+ DEFAULT_ENDPOINT = "wss://pipeline.vrchat.cloud"
69
+ DEFAULT_USER_AGENT = "vrchatapi-py"
70
+ DEFAULT_HEARTBEAT_INTERVAL = 30.0
71
+
72
+ #: End of stream sentinel pushed onto the shared queue to stop ``async for``.
73
+ _END = object()
74
+
75
+ #: Events whose ``content`` is a plain notification-ID string, not JSON.
76
+ _STRING_CONTENT_EVENTS: FrozenSet[str] = frozenset(
77
+ {"see-notification", "hide-notification"}
78
+ )
79
+
80
+ #: Events that carry no ``content`` at all.
81
+ _NO_CONTENT_EVENTS: FrozenSet[str] = frozenset({"clear-notification"})
82
+
83
+ #: All event types documented at https://vrchat.community/websocket.
84
+ WS_EVENT_TYPES: FrozenSet[str] = frozenset(
85
+ {
86
+ # Notification events
87
+ "notification",
88
+ "response-notification",
89
+ "see-notification",
90
+ "hide-notification",
91
+ "clear-notification",
92
+ "notification-v2",
93
+ "notification-v2-update",
94
+ "notification-v2-delete",
95
+ # Friend events
96
+ "friend-add",
97
+ "friend-delete",
98
+ "friend-online",
99
+ "friend-active",
100
+ "friend-offline",
101
+ "friend-update",
102
+ "friend-location",
103
+ # User events
104
+ "user-update",
105
+ "user-location",
106
+ "user-badge-assigned",
107
+ "user-badge-unassigned",
108
+ "content-refresh",
109
+ "economy-update",
110
+ "modified-image-update",
111
+ "instance-queue-joined",
112
+ "instance-queue-ready",
113
+ # Group events
114
+ "group-joined",
115
+ "group-left",
116
+ "group-member-updated",
117
+ "group-role-updated",
118
+ }
119
+ )
120
+
121
+
122
+ class VRChatWebSocketError(Exception):
123
+ """Raised for pipeline-level errors (server ``{"err": ...}`` messages)."""
124
+
125
+ def __init__(self, message: str, *, raw: Optional[str] = None) -> None:
126
+ super().__init__(message)
127
+ self.message = message
128
+ self.raw = raw
129
+
130
+
131
+ class VRChatEvent:
132
+ """A single decoded message received from the pipeline.
133
+
134
+ Attributes:
135
+ type: The event type, e.g. ``"friend-online"``.
136
+ content: The decoded ``content`` field. Usually a ``dict`` (the
137
+ double-encoded JSON payload); a plain ``str`` for
138
+ ``see-notification`` / ``hide-notification``; ``None`` for
139
+ ``clear-notification``.
140
+ raw: The complete raw JSON message as received over the wire.
141
+ raw_content: The raw ``content`` field before decoding (``None`` when
142
+ the event carries no content).
143
+ """
144
+
145
+ __slots__ = ("type", "content", "raw", "raw_content")
146
+
147
+ def __init__(
148
+ self,
149
+ type: str,
150
+ content: Any = None,
151
+ raw: str = "",
152
+ raw_content: Optional[str] = None,
153
+ ) -> None:
154
+ self.type = type
155
+ self.content = content
156
+ self.raw = raw
157
+ self.raw_content = raw_content
158
+
159
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
160
+ return (
161
+ f"VRChatEvent(type={self.type!r}, content={self.content!r}, "
162
+ f"raw={self.raw!r}, raw_content={self.raw_content!r})"
163
+ )
164
+
165
+
166
+ def get_auth_cookie(api_client) -> Optional[str]:
167
+ """Return the ``auth`` cookie value stored on an ``ApiClient``.
168
+
169
+ The cookie is populated by the login flow (``GET /auth/user``). Returns
170
+ ``None`` when the client has not logged in yet.
171
+ """
172
+ rest_client = getattr(api_client, "rest_client", None)
173
+ cookie_jar = getattr(rest_client, "cookie_jar", None)
174
+ if cookie_jar is None:
175
+ return None
176
+ for cookie in cookie_jar:
177
+ if cookie.name == "auth":
178
+ return cookie.value
179
+ return None
180
+
181
+
182
+ class VRChatWebSocket:
183
+ """Async client for the VRChat Websocket (Pipeline) API.
184
+
185
+ Events can be consumed two ways, at the same time:
186
+
187
+ * callbacks registered with :meth:`on` / :meth:`on_event`
188
+ (``async def`` handlers that receive a :class:`VRChatEvent`), and
189
+ * by iterating::
190
+
191
+ async for event in ws:
192
+ ...
193
+
194
+ The iterator yields every event that the connection delivers and stops
195
+ when the client is closed.
196
+
197
+ Usage::
198
+
199
+ ws = VRChatWebSocket(auth_token="authcookie_...", user_agent="MyApp/1.0 me@example.com")
200
+
201
+ @ws.on("friend-online")
202
+ async def on_friend_online(event):
203
+ print("online:", event.content["user"]["displayName"])
204
+
205
+ await ws.run() # blocks, reconnects automatically
206
+ # or, without auto-reconnect:
207
+ # ws = VRChatWebSocket(..., auto_reconnect=False)
208
+ # await ws.run()
209
+
210
+ Args:
211
+ auth_token: The ``auth`` cookie value obtained by logging in.
212
+ user_agent: User-Agent header; the pipeline rejects connections
213
+ without a proper one.
214
+ endpoint: Websocket base URL.
215
+ auto_reconnect: When ``True`` (default), :meth:`run` reconnects with
216
+ exponential backoff after the connection drops or the server
217
+ reports an error. When ``False``, :meth:`run` returns once the
218
+ connection is closed.
219
+ reconnect_max_delay: Upper bound (seconds) for the exponential
220
+ reconnect backoff (1s, 2s, 4s, ... capped at this value).
221
+ heartbeat_interval: Interval (seconds) for the application-level
222
+ heartbeat message that keeps the connection alive; ``None``
223
+ disables it. The server is receive-only per the documentation,
224
+ but the official clients (and the website) send heartbeats.
225
+ session: Optional pre-existing ``aiohttp.ClientSession`` to reuse.
226
+ When omitted, the client creates and owns its own session.
227
+ """
228
+
229
+ def __init__(
230
+ self,
231
+ auth_token: str,
232
+ user_agent: str = DEFAULT_USER_AGENT,
233
+ endpoint: str = DEFAULT_ENDPOINT,
234
+ auto_reconnect: bool = True,
235
+ reconnect_max_delay: float = 60.0,
236
+ heartbeat_interval: Optional[float] = DEFAULT_HEARTBEAT_INTERVAL,
237
+ session: Optional[aiohttp.ClientSession] = None,
238
+ ) -> None:
239
+ if not auth_token:
240
+ raise ValueError("auth_token is required")
241
+ self._auth_token = auth_token
242
+ self._user_agent = user_agent
243
+ self._endpoint = endpoint
244
+ self._auto_reconnect = auto_reconnect
245
+ self._reconnect_max_delay = max(0.0, reconnect_max_delay)
246
+ self._heartbeat_interval = heartbeat_interval
247
+
248
+ self._session = session
249
+ self._owned_session = session is None
250
+
251
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
252
+ self._receiver_task: Optional[asyncio.Task] = None
253
+ self._heartbeat_task: Optional[asyncio.Task] = None
254
+ self._closed = False
255
+ self._queue: "asyncio.Queue[Any]" = asyncio.Queue()
256
+
257
+ #: event-type -> list of async handlers
258
+ self._handlers: Dict[str, List[Callable[..., Any]]] = {}
259
+ self._event_handlers: List[Callable[..., Any]] = []
260
+ self._error_handlers: List[Callable[..., Any]] = []
261
+ self._connect_handlers: List[Callable[..., Any]] = []
262
+ self._disconnect_handlers: List[Callable[..., Any]] = []
263
+ self._reconnect_handlers: List[Callable[..., Any]] = []
264
+
265
+ # ------------------------------------------------------------------
266
+ # Public API
267
+ # ------------------------------------------------------------------
268
+
269
+ @classmethod
270
+ def from_client(cls, api_client, **kwargs) -> "VRChatWebSocket":
271
+ """Build a client from a logged-in ``ApiClient``.
272
+
273
+ Reads the ``auth`` cookie from the client's cookie jar and inherits
274
+ its ``User-Agent``. ``kwargs`` are forwarded to the constructor.
275
+ """
276
+ auth_token = get_auth_cookie(api_client)
277
+ if auth_token is None:
278
+ raise ValueError(
279
+ "no 'auth' cookie found; log in with the API client first "
280
+ "(e.g. call get_current_user())"
281
+ )
282
+ kwargs.setdefault("user_agent", getattr(api_client, "user_agent", DEFAULT_USER_AGENT))
283
+ return cls(auth_token=auth_token, **kwargs)
284
+
285
+ @property
286
+ def is_connected(self) -> bool:
287
+ ws = self._ws
288
+ return ws is not None and not ws.closed
289
+
290
+ def on(
291
+ self, event_type: str, handler: Optional[Callable[..., Any]] = None
292
+ ) -> Callable[..., Any]:
293
+ """Register ``handler`` for ``event_type``.
294
+
295
+ Works both directly (``ws.on("x", handler)``) and as a decorator
296
+ (``@ws.on("x")``); in both cases the handler is returned.
297
+ """
298
+ if handler is None:
299
+
300
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
301
+ self._handlers.setdefault(event_type, []).append(fn)
302
+ return fn
303
+
304
+ return decorator
305
+ self._handlers.setdefault(event_type, []).append(handler)
306
+ return handler
307
+
308
+ def off(self, event_type: str, handler: Callable[..., Any]) -> None:
309
+ """Remove a previously registered event handler."""
310
+ handlers = self._handlers.get(event_type)
311
+ if handlers is not None and handler in handlers:
312
+ handlers.remove(handler)
313
+
314
+ def on_event(self, handler: Callable[..., Any]) -> Callable[..., Any]:
315
+ """Register a catch-all handler invoked for every event."""
316
+ self._event_handlers.append(handler)
317
+ return handler
318
+
319
+ def on_error(self, handler: Callable[..., Any]) -> Callable[..., Any]:
320
+ """Register a handler invoked with an ``Exception`` on pipeline errors."""
321
+ self._error_handlers.append(handler)
322
+ return handler
323
+
324
+ def on_connect(self, handler: Callable[..., Any]) -> Callable[..., Any]:
325
+ """Register a handler invoked (no arguments) after connecting."""
326
+ self._connect_handlers.append(handler)
327
+ return handler
328
+
329
+ def on_disconnect(self, handler: Callable[..., Any]) -> Callable[..., Any]:
330
+ """Register a handler invoked (no arguments) after disconnecting."""
331
+ self._disconnect_handlers.append(handler)
332
+ return handler
333
+
334
+ def on_reconnect(self, handler: Callable[..., Any]) -> Callable[..., Any]:
335
+ """Register a handler invoked with the backoff delay (seconds) before reconnecting."""
336
+ self._reconnect_handlers.append(handler)
337
+ return handler
338
+
339
+ async def connect(self) -> None:
340
+ """Establish a single connection and start the background receiver.
341
+
342
+ Intended for iterator-style consumption::
343
+
344
+ await ws.connect()
345
+ async for event in ws:
346
+ ...
347
+ await ws.close()
348
+
349
+ For automatic reconnection use :meth:`run` instead.
350
+ """
351
+ if self.is_connected:
352
+ return
353
+ await self._connect_once()
354
+ self._ensure_receiver()
355
+
356
+ async def run(self) -> None:
357
+ """Run the receive loop until :meth:`close` or (without auto-reconnect)
358
+ until the connection is closed.
359
+
360
+ Connects if needed, dispatches events to callbacks and the shared
361
+ queue, and reconnects with exponential backoff when
362
+ ``auto_reconnect`` is enabled.
363
+ """
364
+ attempt = 0
365
+ while not self._closed:
366
+ if not self.is_connected:
367
+ try:
368
+ await self._connect_once()
369
+ attempt = 0
370
+ except asyncio.CancelledError:
371
+ raise
372
+ except Exception as exc:
373
+ await self._fire(self._error_handlers, exc)
374
+ if not await self._wait_before_reconnect(attempt):
375
+ break
376
+ attempt += 1
377
+ continue
378
+
379
+ self._ensure_receiver()
380
+ try:
381
+ await self._receiver_task
382
+ except asyncio.CancelledError:
383
+ if not self._closed:
384
+ raise
385
+ finally:
386
+ await self._teardown()
387
+
388
+ if self._closed or not self._auto_reconnect:
389
+ break
390
+ if not await self._wait_before_reconnect(attempt):
391
+ break
392
+ attempt += 1
393
+
394
+ self._put_end()
395
+ # run() is done for good: release the session we own so connections
396
+ # do not linger. (close() already closed it in that path.)
397
+ if self._owned_session and self._session is not None:
398
+ try:
399
+ await self._session.close()
400
+ except Exception:
401
+ pass
402
+ self._session = None
403
+
404
+ async def close(self) -> None:
405
+ """Close the connection and stop the receiver; safe to call twice."""
406
+ if self._closed:
407
+ return
408
+ self._closed = True
409
+ self._put_end()
410
+
411
+ heartbeat_task = self._heartbeat_task
412
+ self._heartbeat_task = None
413
+ if (
414
+ heartbeat_task is not None
415
+ and heartbeat_task is not asyncio.current_task()
416
+ and not heartbeat_task.done()
417
+ ):
418
+ heartbeat_task.cancel()
419
+ try:
420
+ await heartbeat_task
421
+ except (asyncio.CancelledError, Exception):
422
+ pass
423
+
424
+ receiver_task = self._receiver_task
425
+ if (
426
+ receiver_task is not None
427
+ and receiver_task is not asyncio.current_task()
428
+ and not receiver_task.done()
429
+ ):
430
+ receiver_task.cancel()
431
+ try:
432
+ await receiver_task
433
+ except (asyncio.CancelledError, Exception):
434
+ pass
435
+
436
+ ws = self._ws
437
+ self._ws = None
438
+ if ws is not None and not ws.closed:
439
+ try:
440
+ await ws.close()
441
+ except Exception:
442
+ pass
443
+
444
+ if self._owned_session and self._session is not None:
445
+ try:
446
+ await self._session.close()
447
+ except Exception:
448
+ pass
449
+ self._session = None
450
+
451
+ # ------------------------------------------------------------------
452
+ # Internals
453
+ # ------------------------------------------------------------------
454
+
455
+ async def _connect_once(self) -> None:
456
+ if self.is_connected:
457
+ return
458
+
459
+ session = self._session
460
+ if session is None:
461
+ session = aiohttp.ClientSession(trust_env=False)
462
+ self._session = session
463
+ self._owned_session = True
464
+
465
+ url = f"{self._endpoint.rstrip('/')}/?authToken={quote(self._auth_token, safe='')}"
466
+ headers = {"User-Agent": self._user_agent}
467
+ try:
468
+ ws = await session.ws_connect(url, headers=headers)
469
+ except Exception:
470
+ # Do not leak a freshly-created session on a failed connect;
471
+ # close it so callers do not accumulate resources.
472
+ if self._owned_session and session is self._session:
473
+ await session.close()
474
+ self._session = None
475
+ raise
476
+
477
+ self._ws = ws
478
+ if self._heartbeat_interval is not None:
479
+ self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
480
+ await self._fire(self._connect_handlers)
481
+
482
+ def _ensure_receiver(self) -> None:
483
+ if self._receiver_task is None or self._receiver_task.done():
484
+ self._receiver_task = asyncio.create_task(self._receive_loop())
485
+
486
+ async def _receive_loop(self) -> None:
487
+ ws = self._ws
488
+ try:
489
+ async for message in ws:
490
+ if message.type == aiohttp.WSMsgType.TEXT:
491
+ if not await self._dispatch_message(message.data):
492
+ break
493
+ elif message.type == aiohttp.WSMsgType.ERROR:
494
+ exc = ws.exception()
495
+ raise RuntimeError(f"websocket error: {exc}")
496
+ elif message.type in (
497
+ aiohttp.WSMsgType.CLOSE,
498
+ aiohttp.WSMsgType.CLOSING,
499
+ aiohttp.WSMsgType.CLOSED,
500
+ ):
501
+ break
502
+ # BINARY and other frames are ignored.
503
+ except asyncio.CancelledError:
504
+ raise
505
+ except Exception as exc:
506
+ await self._fire(self._error_handlers, exc)
507
+ finally:
508
+ await self._fire(self._disconnect_handlers)
509
+ if not self._auto_reconnect:
510
+ self._put_end()
511
+
512
+ async def _dispatch_message(self, raw: str) -> bool:
513
+ """Decode and dispatch one text frame.
514
+
515
+ Returns ``False`` when the connection should be closed (a server
516
+ error message was received); ``True`` otherwise.
517
+ """
518
+ try:
519
+ obj = json.loads(raw)
520
+ except (ValueError, TypeError) as exc:
521
+ await self._fire(self._error_handlers, exc)
522
+ return True
523
+ if not isinstance(obj, dict):
524
+ await self._fire(
525
+ self._error_handlers,
526
+ ValueError(f"unexpected pipeline message shape: {type(obj).__name__}"),
527
+ )
528
+ return True
529
+
530
+ # Server error messages look like {"err": "..."} and the connection
531
+ # is closed right after they are sent.
532
+ if "err" in obj:
533
+ await self._fire(
534
+ self._error_handlers, VRChatWebSocketError(str(obj["err"]), raw=raw)
535
+ )
536
+ return False
537
+
538
+ msg_type = obj.get("type")
539
+ raw_content = obj.get("content")
540
+ content: Any = None
541
+
542
+ if msg_type in _NO_CONTENT_EVENTS:
543
+ raw_content = None
544
+ elif msg_type in _STRING_CONTENT_EVENTS:
545
+ content = raw_content
546
+ elif raw_content is not None:
547
+ try:
548
+ content = json.loads(raw_content)
549
+ except (ValueError, TypeError):
550
+ await self._fire(
551
+ self._error_handlers,
552
+ VRChatWebSocketError(
553
+ f"failed to decode content for {msg_type!r}", raw=raw
554
+ ),
555
+ )
556
+ return True
557
+
558
+ event = VRChatEvent(
559
+ type=msg_type, content=content, raw=raw, raw_content=raw_content
560
+ )
561
+ for handler in list(self._handlers.get(msg_type, ())):
562
+ await self._invoke(handler, event)
563
+ for handler in list(self._event_handlers):
564
+ await self._invoke(handler, event)
565
+ self._queue.put_nowait(event)
566
+ return True
567
+
568
+ async def _heartbeat_loop(self) -> None:
569
+ try:
570
+ while True:
571
+ await asyncio.sleep(self._heartbeat_interval)
572
+ ws = self._ws
573
+ if ws is None or ws.closed:
574
+ return
575
+ payload = json.dumps(
576
+ {
577
+ "type": "heartbeat",
578
+ "connected": True,
579
+ "nonce": str(uuid.uuid4()),
580
+ }
581
+ )
582
+ try:
583
+ await ws.send_str(payload)
584
+ except Exception:
585
+ return
586
+ except asyncio.CancelledError:
587
+ raise
588
+
589
+ async def _wait_before_reconnect(self, attempt: int) -> bool:
590
+ """Sleep with backoff before reconnecting; ``False`` stops the loop."""
591
+ if self._closed or not self._auto_reconnect:
592
+ return False
593
+ delay = min(self._reconnect_max_delay, 2.0 ** attempt)
594
+ await self._fire(self._reconnect_handlers, delay)
595
+ try:
596
+ await asyncio.sleep(delay)
597
+ except asyncio.CancelledError:
598
+ return False
599
+ return True
600
+
601
+ async def _teardown(self) -> None:
602
+ heartbeat_task = self._heartbeat_task
603
+ self._heartbeat_task = None
604
+ if (
605
+ heartbeat_task is not None
606
+ and heartbeat_task is not asyncio.current_task()
607
+ and not heartbeat_task.done()
608
+ ):
609
+ heartbeat_task.cancel()
610
+ try:
611
+ await heartbeat_task
612
+ except (asyncio.CancelledError, Exception):
613
+ pass
614
+
615
+ ws = self._ws
616
+ self._ws = None
617
+ if ws is not None and not ws.closed:
618
+ try:
619
+ await ws.close()
620
+ except Exception:
621
+ pass
622
+
623
+ def _put_end(self) -> None:
624
+ try:
625
+ self._queue.put_nowait(_END)
626
+ except asyncio.QueueFull: # pragma: no cover - unbounded queue
627
+ pass
628
+
629
+ async def _invoke(self, handler: Callable[..., Any], *args: Any) -> None:
630
+ try:
631
+ result: Any = handler(*args)
632
+ if inspect.isawaitable(result):
633
+ await result
634
+ except asyncio.CancelledError:
635
+ raise
636
+ except Exception:
637
+ logger.exception("Unhandled error in websocket handler %r", handler)
638
+
639
+ async def _fire(
640
+ self, handlers: List[Callable[..., Any]], *args: Any
641
+ ) -> None:
642
+ for handler in list(handlers):
643
+ await self._invoke(handler, *args)
644
+
645
+ def __aiter__(self):
646
+ return self
647
+
648
+ async def __anext__(self):
649
+ item = await self._queue.get()
650
+ if item is _END:
651
+ raise StopAsyncIteration
652
+ return item