├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── build-pypi.sh ├── build.bat ├── build.sh ├── cli.py ├── clubhouse ├── __init__.py └── clubhouse.py ├── icon.ico ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *_orig.py 2 | *.pyc 3 | *.ini 4 | *.spec 5 | *.dmp 6 | __pycache__/ 7 | .DS_Store 8 | Thumbs.db 9 | .idea 10 | .vscode 11 | build/ 12 | dist/ 13 | clubhouse_py.egg-info/ 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 stypr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WARNING / NOTES 2 | 3 | * ___FOR REFERENCE AND EDUCATION PURPOSES ONLY. THIS DOES NOT COME WITH ANY KINDS OF WARRANTY.___ 4 | 5 | * ___PLEASE DO NOT CREATE BOTS OR DO ANY HARMFUL THINGS TO THE SERVICE. DON'T BREAK THINGS. DON'T BE EVIL.___ 6 | 7 | * ___ANDROID OFFICIAL BUILD IS OUT. THERE WILL BE NO MORE UPDATES ON THIS PROJECT.___ 8 | 9 | ## Pull Requests / Issues 10 | 11 | I disabled PRs and issues temporarily. I will only accept requests when it is worth fixing. 12 | 13 | [Closed PRs](https://github.com/stypr/clubhouse-py/pulls?q=is%3Apr+is%3Aclosed) / [Closed Issues](https://github.com/stypr/clubhouse-py/issues?q=is%3Aissue+is%3Aclosed) 14 | 15 | Please contact by DMs through [@stereotype32](https://twitter.com/stereotype32) for any questions. Please do not spam e-mails for a request. 16 | 17 | ___All `rc_token` related requests will be rejected. (See closed issues)___ 18 | 19 | ## QnA 20 | 21 | > Are you affiliated with those guys who built the website that streamed Clubhouse rooms? 22 | 23 | No. 24 | 25 | I am not affiliated with anyone or any company with regards to Clubhouse issues. 26 | 27 | > Why did you develop this? what is your whole intention about releasing this to public? 28 | 29 | 1. There has been a lot of articles about security concerns of Clubhouse when I joined Clubhouse. 30 | * [Clubhouse And Its Privacy & Security Risk](https://medium.com/technology-hits/clubhouse-and-its-security-risk-201526fd06d1) 31 | * [Clubhouse says it will improve security after researchers raise China spying concerns](https://www.theverge.com/2021/2/14/22282772/clubhouse-improve-security-stanford-researchers-china-security) 32 | * [Clubhouse: Security and privacy in the new social media app](https://blog.avast.com/clubhouse-security-and-privacy-avast) 33 | 2. I decided to take a closer look at the application by reverse engineering the app. With this I can find out what is the truth and what isn't. 34 | 3. I found some possible security risks during the analysis. However, I will not disclose this information until things are properly and safely mitigated. 35 | 4. I was planning to destroy my work after doing the analysis, but I've decided to share the code as (i) I found out that the whole authentication flow and API base may change in the future, so this src will be priceless at some point of time (ii) I think it would be better off for Android phone users to interact with others. (iii) I wanted more people to join into conversations and have fun. 36 | 37 | > What if someone uses your code to do malicious activities? Wouldn't that be an issue? 38 | 39 | 1. Evil people with evil intentions will do bad things even if the sourcecode wasn't released. 40 | 2. There has been already numerous reports of trollers doing bad things around here and there. ([Reference](https://github.com/ai-eks/OpenClubhouse)) These trollers have also disclosed their sourcecode, so please have some time to check their source code. These guys did their stuff without even referencing other's source code. This already shows that evil people will always try to break stuff and do bad things regardless of any other helpful factors. 41 | 4. What I shared on GitHub is a very basic thing that a reverse engineer can do. It's technically not difficult to get these information snatched from the binary. 42 | 5. Clubhouse has a straightforward API with some unknown security mechanisms; They have implemented things to ban you for excessive usage. 43 | 6. DO NOT even try anything if you don't really know what you're trying to do. I have been mentioning the same message over here and there. 44 | 7. I am not liable for anything you do with this application. I already warned about this as well. 45 | 46 | > You've released API keys and secret keys. Wouldn't that severely impact the server? 47 | 48 | 1. Let me make things clear first. Those keys are NOT confidential secrets. 49 | 2. These are just identifiers for third-party services to declare that your actions are coming from the Clubhouse app. 50 | 3. These keys are used for communication, adding your instagram/twitter accounts, chat notifications, etc. 51 | 4. I wouldn't have disclosed keys if these keys were actual secrets/confidentials. 52 | 53 | > Can you disclose what you've found during an analysis? 54 | 55 | No. 56 | 57 | I will only disclose these issues to the vendor. 58 | 59 | I think issues I found seem to be already reported by other researchers as well and they might be already aware of these issues and circumstances. 60 | 61 | I've already sent a twitter DM to one of Clubhouse employees as of 2021/Feb/24, but I haven't received any messages yet. 62 | 63 | > Then, can you explain a bit on that myth about the Chinese IP thing? 64 | 65 | 1. It's fixed in the latest version. You don't have to worry about this anymore. 66 | 2. Worth reading [this technical post](https://theori.io/research/korean/analyzing-clubhouse) for more detailed information. 67 | 3. The blog post is written in Korean so please translate the page. 68 | 69 | > I heard that the app is using iOS just to prevent the voice recording. Releasing these kinds of code can possibly make it 'easier' to make voice recording. I want to hear your opinions. 70 | 71 | 1. There is literally no way to disallow users from recording the voice. Imagine some people having a "physical" recording device next to them. How will you or the Clubhouse app detect such actions? 72 | 2. Moreover, there is no way to even catch or block the user when someone records and shares your voice record anonymously. 73 | 3. I think there are much more serious risks/problems that CH developers need to take a look at. There seem to be more high priority issues than this one. (in which I assume they're already working on atm) 74 | 75 | > What do you think about the Clubhouse app? Is the app secure enough? Can you rate their security quality? 76 | 77 | From my very personal perspective as a security engineer: 78 | 79 | 1. API: Well-made, and I see developers are trying to fix some security issues here. although they still haven't fixed it, yet. 80 | 2. Notifications: LGTM. but sometimes the server goes down pretty frequently. I haven't looked deep into it. 81 | 3. Interaction with voice protocol: meh, but it looks like they're trying to work on it. I think it is more fun to dig more in but doing so will go out of the scope. 82 | 83 | > Don't you think your actions were ethically wrong? 84 | 85 | 1. I also heard that these issues were raised and discussed over several months in an open Clubhouse chatroom, and I guess I've clarified a lot of questions people had over for several months. I guess this already helped some of engineers who were pretty much concerned about things here. 86 | 2. I am pretty sure that somemone would've done this if it wasn't me anyways. At least I gave some initiative to try with good wills and share details with you guys. 87 | 88 | > I heard that the voice communication is not encrypted. is this true? 89 | 90 | As of 2021/Feb/24, 91 | 92 | 1. [This technical post](https://theori.io/research/korean/analyzing-clubhouse) already explains things really well about the current situation. 93 | 2. I was also curious and read some documentations in Agora.io ([Reference](https://docs.agora.io/en/Voice/channel_encryption_android)) 94 | 3. As mentioned in the technical post, it looks like the communication encryption is never done. 95 | 4. Also, ny looking at those documentations and my codes, you may have already noticed that the `enableEncryption` is never used here. 96 | 5. In the latest version, they have added the encryption routine but it is not yet used. It should be fixed in the upcoming releases. 97 | 98 | > I heard that the app is also using Camera permissions. I am really worried right now. 99 | 100 | You don't have to worry about this as well. There are some things to share here. 101 | 102 | 1. It may have been turned on because you tried to take a photo of yourself to put a profile image. 103 | 2. ... or the voice SDK is trying to secretly access your camera. But from my analysis, I don't see anything like that happening from the App to take photos or videos. Although they have the feature to communicate with your camera, the app does not use that part of the feature atm. (Confirmed safe as of Feb 2021) 104 | 105 | > I heard that the app is also taking your information while adding your Instagram/Twitter accounts. did you check that? 106 | 107 | Yes. You don't have to worry about this as well. 108 | 109 | Clubhouse only takes very basic part of your information just to verify that you are the owner of the given account. 110 | 111 | * For Instagram: You're allowing Clubhouse to just take your username. That's all. 112 | * For Twitter: You're allowing Clubhouse to read your profile, timeline and tweets. However, Clubhouse CANNOT read your personal DMs. This is the least permission they can ask to a user. 113 | 114 | The permission setting can also change, but in that case you will be asked again to re-authorize the application with additional permission. Don't worry so much about this part. 115 | 116 | If you're still worried about this, You can also revoke the access by doing the following action. 117 | 118 | * For Instagram: `Settings` -> `Security` -> `Apps and Websites` -> `Active` -> `Clubhouse` -> revoke access. 119 | * For Twitter: `Settings` -> `Security and account access` -> `Apps and sessions` -> `Connected apps` -> `Clubhouse` -> revoke acccess. 120 | 121 | > Do you have any plans to do further analysis if Clubhouse opens up a bug bounty programme? 122 | 123 | Very unlikely. 124 | 125 | > Is Clubhouse actually working hard to fix all kinds of security stuff? I'm really worried. 126 | 127 | Yes, but there are some reasons why developers are taking some time. 128 | 129 | 1. They probably don't want to break things while updating. Developers also need time to fix and test their own code. 130 | 2. Clubhouse is a small company with ~10 employees. You also need to consider the manpower to fix issues. 131 | 3. It may take a few days to get their updates reviewed by Apple. 132 | 4. They also need to have some time to make "best moves" in order to efficiently fix issues. 133 | 134 | > As a typical user, what do I need to be very careful about when using Clubhouse? 135 | 136 | 1. As a speaker: Always assume that someone is recording your voice. Always think multiple times before you speak. Don't speak out confidential/personal stuff. I am not saying that the Clubhouse is recording your voice. There are chances that some trolls or reporters are trying to record multiple chatrooms. 137 | 2. As a moderator: You need to be alert and make quick decisions to make your channel healthy. If someone says something weird or does something crazy, you need to make quick decisions. Move that speaker to audience or just kick the user out of the channel. Simple as that. Also, be aware that you have a lot of privileges. Do not give moderators to unknown people. Any moderator can destroy the channel. 138 | 139 | > Why did you block issues / PRs? 140 | 141 | Mainly two reasons: 142 | 143 | 1. There are some people sending me some issues without actually looking into sourcecodes and testing codes. 144 | 2. There are some people wasting their time to send worthless PRs. 145 | 146 | I will not open these for the time being. You can send me a message or make your own fork, and I will take a look whenever I'm free 147 | 148 | ## Clubhouse API written in Python 149 | 150 | `clubhouse-py` is originally developed for the sake of interoperability. 151 | 152 | Standalone client is also created with very basic features, including but not limited to the audio-chat. 153 | 154 | Please note that you may get a permanent ban for sending invalid API requests. Server's ratelimit and security mechanisms are quite strict. 155 | 156 | ## Downloads 157 | 158 | Check [Releases](https://github.com/stypr/clubhouse-py/releases). OSX(x86_64) may not be stable for use yet. 159 | 160 | ## Demo 161 | 162 | Please click the image to open a Youtube video demo. 163 | 164 | [![Demo video](https://img.youtube.com/vi/1L6bEoNKego/maxresdefault.jpg)](https://www.youtube.com/watch?v=1L6bEoNKego) 165 | 166 | ## Requirements 167 | 168 | * Windows or OSX 169 | * Python 3.7 or higher 170 | 171 | ## Installation 172 | 173 | ### By pip 174 | 175 | 1. Install by pip 176 | 177 | ```sh 178 | $ pip3 install clubhouse-py 179 | ... 180 | Successfully built clubhouse-py 181 | Installing collected packages: clubhouse-py 182 | Successfully installed clubhouse-py-304.0.1 183 | ``` 184 | 185 | 2. You need to install Agora SDK for voice communication. Refer to [Agora-Python-SDK#installation](https://github.com/AgoraIO-Community/Agora-Python-SDK#installation). 186 | 187 | ### Manual Installation 188 | 189 | 1. Clone project 190 | 191 | ```sh 192 | $ git clone https://github.com/stypr/clubhouse-py.git clubhouse 193 | $ cd clubhouse 194 | ``` 195 | 196 | 2. You need to install dependencies first. 197 | 198 | ```sh 199 | $ pip3 install -r requirements.txt 200 | ``` 201 | 202 | 3. You need to install Agora SDK for voice communication. Refer to [Agora-Python-SDK#installation](https://github.com/AgoraIO-Community/Agora-Python-SDK#installation). 203 | 204 | 205 | ## Usage 206 | 207 | * For calling APIs from other script 208 | 209 | ```python 210 | from clubhouse.clubhouse import Clubhouse 211 | 212 | ... 213 | 214 | if __name__ == "__main__": 215 | clubhouse = Clubhouse() 216 | ``` 217 | 218 | * For running a standalone client 219 | 220 | ```sh 221 | $ python3 cli.py 222 | ``` 223 | 224 | ### PubNub 225 | 226 | PubNub is used for the notification while being in a conversation. 227 | This has not been implemented yet. However, you may utilize the PubSub keys provided in the sourcecode to implement this. 228 | 229 | ## Reference / Recommended to read 230 | 231 | You may also add more endpoints and features based on the following repositories. 232 | 233 | Please note that these repositories were partially referenced to create this project. 234 | 235 | Most of things were tested and handcrafted from scratch. 236 | 237 | * https://github.com/Seia-Soto/clubhouse-api (NodeJS build) 238 | * https://github.com/grishka/Houseclub (Android build) 239 | * https://theori.io/research/korean/analyzing-clubhouse/ (Written in Korean) 240 | -------------------------------------------------------------------------------- /build-pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf dist 4 | rm -rf clubhouse_py.egg-info 5 | python setup.py sdist 6 | twine upload dist/* 7 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | python -m PyInstaller --onefile .\cli.py --icon=icon.ico 4 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf build/ dist/ 4 | rm *.pyc 5 | python3 -OO -m PyInstaller --onefile ./cli.py 6 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | cli.py 3 | 4 | Sample CLI Clubhouse Client 5 | 6 | RTC: For voice communication 7 | """ 8 | 9 | import os 10 | import sys 11 | import threading 12 | import configparser 13 | import keyboard 14 | from rich.table import Table 15 | from rich.console import Console 16 | from clubhouse.clubhouse import Clubhouse 17 | 18 | # Set some global variables 19 | try: 20 | import agorartc 21 | RTC = agorartc.createRtcEngineBridge() 22 | eventHandler = agorartc.RtcEngineEventHandlerBase() 23 | RTC.initEventHandler(eventHandler) 24 | # 0xFFFFFFFE will exclude Chinese servers from Agora's servers. 25 | RTC.initialize(Clubhouse.AGORA_KEY, None, agorartc.AREA_CODE_GLOB & 0xFFFFFFFE) 26 | # Enhance voice quality 27 | if RTC.setAudioProfile( 28 | agorartc.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO, 29 | agorartc.AUDIO_SCENARIO_GAME_STREAMING 30 | ) < 0: 31 | print("[-] Failed to set the high quality audio profile") 32 | except ImportError: 33 | RTC = None 34 | 35 | def set_interval(interval): 36 | """ (int) -> decorator 37 | 38 | set_interval decorator 39 | """ 40 | def decorator(func): 41 | def wrap(*args, **kwargs): 42 | stopped = threading.Event() 43 | def loop(): 44 | while not stopped.wait(interval): 45 | ret = func(*args, **kwargs) 46 | if not ret: 47 | break 48 | thread = threading.Thread(target=loop) 49 | thread.daemon = True 50 | thread.start() 51 | return stopped 52 | return wrap 53 | return decorator 54 | 55 | def write_config(user_id, user_token, user_device, filename='setting.ini'): 56 | """ (str, str, str, str) -> bool 57 | 58 | Write Config. return True on successful file write 59 | """ 60 | config = configparser.ConfigParser() 61 | config["Account"] = { 62 | "user_device": user_device, 63 | "user_id": user_id, 64 | "user_token": user_token, 65 | } 66 | with open(filename, 'w') as config_file: 67 | config.write(config_file) 68 | return True 69 | 70 | def read_config(filename='setting.ini'): 71 | """ (str) -> dict of str 72 | 73 | Read Config 74 | """ 75 | config = configparser.ConfigParser() 76 | config.read(filename) 77 | if "Account" in config: 78 | return dict(config['Account']) 79 | return dict() 80 | 81 | def process_onboarding(client): 82 | """ (Clubhouse) -> NoneType 83 | 84 | This is to process the initial setup for the first time user. 85 | """ 86 | print("=" * 30) 87 | print("Welcome to Clubhouse!\n") 88 | print("The registration is not yet complete.") 89 | print("Finish the process by entering your legal name and your username.") 90 | print("WARNING: THIS FEATURE IS PURELY EXPERIMENTAL.") 91 | print(" YOU CAN GET BANNED FOR REGISTERING FROM THE CLI ACCOUNT.") 92 | print("=" * 30) 93 | 94 | while True: 95 | user_realname = input("[.] Enter your legal name (John Smith): ") 96 | user_username = input("[.] Enter your username (elonmusk1234): ") 97 | 98 | user_realname_split = user_realname.split(" ") 99 | 100 | if len(user_realname_split) != 2: 101 | print("[-] Please enter your legal name properly.") 102 | continue 103 | 104 | if not (user_realname_split[0].isalpha() and 105 | user_realname_split[1].isalpha()): 106 | print("[-] Your legal name is supposed to be written in alphabets only.") 107 | continue 108 | 109 | if len(user_username) > 16: 110 | print("[-] Your username exceeds above 16 characters.") 111 | continue 112 | 113 | if not user_username.isalnum(): 114 | print("[-] Your username is supposed to be in alphanumerics only.") 115 | continue 116 | 117 | client.update_name(user_realname) 118 | result = client.update_username(user_username) 119 | if not result['success']: 120 | print(f"[-] You failed to update your username. ({result})") 121 | continue 122 | 123 | result = client.check_waitlist_status() 124 | if not result['success']: 125 | print("[-] Your registration failed.") 126 | print(f" It's better to sign up from a real device. ({result})") 127 | continue 128 | 129 | print("[-] Registration Complete!") 130 | print(" Try registering by real device if this process pops again.") 131 | break 132 | 133 | def print_channel_list(client, max_limit=20): 134 | """ (Clubhouse) -> NoneType 135 | 136 | Print list of channels 137 | """ 138 | # Get channels and print 139 | console = Console() 140 | table = Table(show_header=True, header_style="bold magenta") 141 | table.add_column("") 142 | table.add_column("channel_name", style="cyan", justify="right") 143 | table.add_column("topic") 144 | table.add_column("speaker_count") 145 | channels = client.get_feed()['items'] # ['channel'] 146 | i = 0 147 | for channel in channels: 148 | channel = channel['channel'] 149 | i += 1 150 | if i > max_limit: 151 | break 152 | _option = "" 153 | _option += "\xEE\x85\x84" if channel['is_social_mode'] or channel['is_private'] else "" 154 | table.add_row( 155 | str(_option), 156 | str(channel['channel']), 157 | str(channel['topic']), 158 | str(int(channel['num_speakers'])), 159 | ) 160 | console.print(table) 161 | 162 | def chat_main(client): 163 | """ (Clubhouse) -> NoneType 164 | 165 | Main function for chat 166 | """ 167 | max_limit = 20 168 | channel_speaker_permission = False 169 | _wait_func = None 170 | _ping_func = None 171 | 172 | def _request_speaker_permission(client, channel_name, user_id): 173 | """ (str) -> bool 174 | 175 | Raise hands for permissions 176 | """ 177 | if not channel_speaker_permission: 178 | client.audience_reply(channel_name, True, False) 179 | _wait_func = _wait_speaker_permission(client, channel_name, user_id) 180 | print("[/] You've raised your hand. Wait for the moderator to give you the permission.") 181 | 182 | @set_interval(30) 183 | def _ping_keep_alive(client, channel_name): 184 | """ (str) -> bool 185 | 186 | Continue to ping alive every 30 seconds. 187 | """ 188 | client.active_ping(channel_name) 189 | return True 190 | 191 | @set_interval(10) 192 | def _wait_speaker_permission(client, channel_name, user_id): 193 | """ (str) -> bool 194 | 195 | Function that runs when you've requested for a voice permission. 196 | """ 197 | # Get some random users from the channel. 198 | _channel_info = client.get_channel(channel_name) 199 | if _channel_info['success']: 200 | for _user in _channel_info['users']: 201 | if _user['user_id'] != user_id: 202 | user_id = _user['user_id'] 203 | break 204 | # Check if the moderator allowed your request. 205 | res_inv = client.accept_speaker_invite(channel_name, user_id) 206 | if res_inv['success']: 207 | print("[-] Now you have a speaker permission.") 208 | print(" Please re-join this channel to activate a permission.") 209 | return False 210 | return True 211 | 212 | while True: 213 | # Choose which channel to enter. 214 | # Join the talk on success. 215 | user_id = client.HEADERS.get("CH-UserID") 216 | print_channel_list(client, max_limit) 217 | channel_name = input("[.] Enter channel_name: ") 218 | channel_info = client.join_channel(channel_name) 219 | if not channel_info['success']: 220 | # Check if this channel_name was taken from the link 221 | channel_info = client.join_channel(channel_name, "link", "e30=") 222 | if not channel_info['success']: 223 | print(f"[-] Error while joining the channel ({channel_info['error_message']})") 224 | continue 225 | 226 | # List currently available users (TOP 20 only.) 227 | # Also, check for the current user's speaker permission. 228 | channel_speaker_permission = False 229 | console = Console() 230 | table = Table(show_header=True, header_style="bold magenta") 231 | table.add_column("user_id", style="cyan", justify="right") 232 | table.add_column("username") 233 | table.add_column("name") 234 | table.add_column("is_speaker") 235 | table.add_column("is_moderator") 236 | users = channel_info['users'] 237 | i = 0 238 | for user in users: 239 | i += 1 240 | if i > max_limit: 241 | break 242 | table.add_row( 243 | str(user['user_id']), 244 | "@" + str(user['username']), 245 | str(user['name']), 246 | str(user['is_speaker']), 247 | str(user['is_moderator']), 248 | ) 249 | # Check if the user is the speaker 250 | if user['user_id'] == int(user_id): 251 | channel_speaker_permission = bool(user['is_speaker']) 252 | console.print(table) 253 | 254 | # Check for the voice level. 255 | if RTC: 256 | token = channel_info['token'] 257 | RTC.joinChannel(token, channel_name, "", int(user_id)) 258 | else: 259 | print("[!] Agora SDK is not installed.") 260 | print(" You may not speak or listen to the conversation.") 261 | 262 | # Activate pinging 263 | client.active_ping(channel_name) 264 | _ping_func = _ping_keep_alive(client, channel_name) 265 | _wait_func = None 266 | 267 | # Add raise_hands key bindings for speaker permission 268 | # Sorry for the bad quality 269 | if not channel_speaker_permission: 270 | 271 | if sys.platform == "darwin": # OSX 272 | _hotkey = "9" 273 | elif sys.platform == "win32": # Windows 274 | _hotkey = "ctrl+shift+h" 275 | 276 | print(f"[*] Press [{_hotkey}] to raise your hands for the speaker permission.") 277 | keyboard.add_hotkey( 278 | _hotkey, 279 | _request_speaker_permission, 280 | args=(client, channel_name, user_id) 281 | ) 282 | 283 | input("[*] Press [Enter] to quit conversation.\n") 284 | keyboard.unhook_all() 285 | 286 | # Safely leave the channel upon quitting the channel. 287 | if _ping_func: 288 | _ping_func.set() 289 | if _wait_func: 290 | _wait_func.set() 291 | if RTC: 292 | RTC.leaveChannel() 293 | client.leave_channel(channel_name) 294 | 295 | def user_authentication(client): 296 | """ (Clubhouse) -> NoneType 297 | 298 | Just for authenticating the user. 299 | """ 300 | 301 | result = None 302 | while True: 303 | user_phone_number = input("[.] Please enter your phone number. (+818043217654) > ") 304 | result = client.start_phone_number_auth(user_phone_number) 305 | if not result['success']: 306 | print(f"[-] Error occured during authentication. ({result['error_message']})") 307 | continue 308 | break 309 | 310 | result = None 311 | while True: 312 | verification_code = input("[.] Please enter the SMS verification code (1234, 0000, ...) > ") 313 | result = client.complete_phone_number_auth(user_phone_number, verification_code) 314 | if not result['success']: 315 | print(f"[-] Error occured during authentication. ({result['error_message']})") 316 | continue 317 | break 318 | 319 | user_id = result['user_profile']['user_id'] 320 | user_token = result['auth_token'] 321 | user_device = client.HEADERS.get("CH-DeviceId") 322 | write_config(user_id, user_token, user_device) 323 | 324 | print("[.] Writing configuration file complete.") 325 | 326 | if result['is_waitlisted']: 327 | print("[!] You're still on the waitlist. Find your friends to get yourself in.") 328 | return 329 | 330 | # Authenticate user first and start doing something 331 | client = Clubhouse( 332 | user_id=user_id, 333 | user_token=user_token, 334 | user_device=user_device 335 | ) 336 | if result['is_onboarding']: 337 | process_onboarding(client) 338 | 339 | return 340 | 341 | def main(): 342 | """ 343 | Initialize required configurations, start with some basic stuff. 344 | """ 345 | # Initialize configuration 346 | client = None 347 | user_config = read_config() 348 | user_id = user_config.get('user_id') 349 | user_token = user_config.get('user_token') 350 | user_device = user_config.get('user_device') 351 | 352 | # Check if user is authenticated 353 | if user_id and user_token and user_device: 354 | client = Clubhouse( 355 | user_id=user_id, 356 | user_token=user_token, 357 | user_device=user_device 358 | ) 359 | 360 | # Check if user is still on the waitlist 361 | _check = client.check_waitlist_status() 362 | if _check['is_waitlisted']: 363 | print("[!] You're still on the waitlist. Find your friends to get yourself in.") 364 | return 365 | 366 | # Check if user has not signed up yet. 367 | _check = client.me() 368 | if not _check['user_profile'].get("username"): 369 | process_onboarding(client) 370 | 371 | chat_main(client) 372 | else: 373 | client = Clubhouse() 374 | user_authentication(client) 375 | main() 376 | 377 | if __name__ == "__main__": 378 | try: 379 | main() 380 | except Exception: 381 | # Remove dump files on exit. 382 | file_list = os.listdir(".") 383 | for _file in file_list: 384 | if _file.endswith(".dmp"): 385 | os.remove(_file) 386 | -------------------------------------------------------------------------------- /clubhouse/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stypr/clubhouse-py/fde10929d26bd6e3e4d792633617a3b5baed3d9d/clubhouse/__init__.py -------------------------------------------------------------------------------- /clubhouse/clubhouse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | #-*- coding: utf-8 -*- 3 | # pylint: disable=line-too-long,too-many-arguments,too-many-lines 4 | # pylint: disable=no-self-argument,not-callable 5 | 6 | """ 7 | clubhouse.py 8 | 9 | Developed for education purposes only. 10 | Please make sure to know what you're trying to do! 11 | Sending an odd API request could result in a permanent ban on your account. 12 | """ 13 | 14 | import uuid 15 | import random 16 | import secrets 17 | import functools 18 | import requests 19 | 20 | class Clubhouse: 21 | """ 22 | Clubhouse Class 23 | 24 | Decorators: 25 | @require_authentication: 26 | - this means that the endpoint requires authentication to access. 27 | 28 | @unstable_endpoint 29 | - This means that the endpoint is never tested. 30 | - Likely to be endpoints that were taken from a static analysis 31 | """ 32 | 33 | # App/API Information 34 | API_URL = "https://www.clubhouseapi.com/api" 35 | 36 | API_BUILD_ID_IOS = "434" 37 | API_BUILD_VERSION = "0.1.40" 38 | API_BUILD_ID_ANDROID = "3389" 39 | API_BUILD_VERSION_ANDROID= "1.0.1" 40 | 41 | API_UA_IOS = f"clubhouse/{API_BUILD_ID_IOS} (iPhone; iOS 14.4; Scale/2.00)" 42 | API_UA_ANDROID = f"clubhouse/android/{API_BUILD_ID_ANDROID}" 43 | API_UA_STATIC = f"Clubhouse/{API_BUILD_ID_IOS} CFNetwork/1220.1 Darwin/20.3.0" 44 | 45 | # Some useful information for commmunication 46 | PUBNUB_PUB_KEY = "pub-c-6878d382-5ae6-4494-9099-f930f938868b" 47 | PUBNUB_SUB_KEY = "sub-c-a4abea84-9ca3-11ea-8e71-f2b83ac9263d" 48 | PUBNUB_API_URL = "https://clubhouse.pubnubapi.com/v2" 49 | SENTRY_URL = "63d2d71e7f424c41a2ede9ad3d703960@o325556.ingest.sentry.io/5245095" 50 | 51 | # co.alphaexploration.clubhouse / 16.0.3 52 | # co.alphaexploration.clubhouse/0.1.41 iPhone/13.5.1 hw/iPhone8_2 53 | 54 | TWITTER_ID = "NyJhARWVYU1X3qJZtC2154xSI" 55 | TWITTER_SECRET = "ylFImLBFaOE362uwr4jut8S8gXGWh93S1TUKbkfh7jDIPse02o" 56 | 57 | INSTAGRAM_ID = "1352866981588597" 58 | INSTAGRAM_CALLBACK = "https://www.joinclubhouse.com/callback/instagram" 59 | 60 | AGORA_KEY = "938de3e8055e42b281bb8c6f69c21f78" 61 | INSTABUG_KEY = "4e53155da9b00728caa5249f2e35d6b3" 62 | AMPLITUDE_KEY = "9098a21a950e7cb0933fb5b30affe5be" 63 | STRIPE_PUBLISH_KEY = "63d2d71e7f424c41a2ede9ad3d703960@o325556.ingest.sentry.io/5245095" 64 | 65 | # SafetyNetClient, recaptchaClient.init(recaptchaKey) 66 | ANDROID_API_KEY = "AIzaSyDGJ877BvgHAg2Bed1sgFjZ4wJmh2RfEfU" 67 | ANDROID_API_ID = "1:1096237342636:android:c800b1b9e5ee70d1f8a409" 68 | ANDROID_RECAPTCHA_KEY = "LcNAMYaAAAAAKDxm-jPPMrJvh_VTiWyWy4D9jp3" 69 | 70 | # https://www.recaptcha.net/recaptcha/api3/iosc & iose 71 | IOS_API_ID = "co.alphaexploration.clubhouse:16.0.3" 72 | IOS_RECAPTCHA_KEY = "6LeWyKUaAAAAAA7XsHRe-JWuI1qLwoZn5p3seyoW" 73 | 74 | # Useful header information 75 | # flip headers if to use iOS 76 | HEADERS = { 77 | "CH-Languages": "en-JP,ja-JP", 78 | "CH-Locale": "en_JP", 79 | "Accept": "application/json", 80 | "Accept-Language": "en-JP;q=1, ja-JP;q=0.9", 81 | "Accept-Encoding": "gzip, deflate", 82 | "ch-keyboards": "en_US", 83 | "CH-AppBuild": f"{API_BUILD_ID_ANDROID}", 84 | "CH-AppVersion": f"{API_BUILD_VERSION_ANDROID}", 85 | "User-Agent": f"{API_UA_ANDROID}", 86 | "Connection": "close", 87 | "Content-Type": "application/json; charset=utf-8", 88 | "Cookie": f"__cfduid={secrets.token_hex(21)}{random.randint(1, 9)}" 89 | } 90 | 91 | def require_authentication(func): 92 | """ Simple decorator to check for the authentication """ 93 | @functools.wraps(func) 94 | def wrap(self, *args, **kwargs): 95 | if not (self.HEADERS.get("CH-UserID") and 96 | self.HEADERS.get("CH-DeviceId") and 97 | self.HEADERS.get("Authorization")): 98 | raise Exception('Not Authenticated') 99 | return func(self, *args, **kwargs) 100 | return wrap 101 | 102 | def unstable_endpoint(func): 103 | """ Simple decorator to warn that this endpoint is never tested at all. """ 104 | @functools.wraps(func) 105 | def wrap(self, *args, **kwargs): 106 | print("[!] This endpoint is NEVER TESTED and MAY BE UNSTABLE. BE CAREFUL!") 107 | return func(self, *args, **kwargs) 108 | return wrap 109 | 110 | def __init__(self, user_id='', user_token='', user_device='', headers=None): 111 | """ (Clubhouse, str, str, str, dict) -> NoneType 112 | Set authenticated information 113 | """ 114 | self.HEADERS = dict(self.HEADERS) 115 | if isinstance(headers, dict): 116 | self.HEADERS.update(headers) 117 | self.HEADERS['CH-UserID'] = user_id if user_id else "(null)" 118 | if user_token: 119 | self.HEADERS['Authorization'] = f"Token {user_token}" 120 | self.HEADERS['CH-DeviceId'] = user_device.upper() if user_device else str(uuid.uuid4()).upper() 121 | 122 | def __str__(self): 123 | """ (Clubhouse) -> str 124 | Get information about the given class. 125 | >>> clubhouse = Clubhouse() 126 | >>> str(clubhouse) 127 | Clubhouse(user_id=(null), user_token=None, user_device=31525f52-6b67-40de-83c0-8f9fe0f6f409) 128 | """ 129 | return "Clubhouse(user_Id={}, user_token={}, user_device={})".format( 130 | self.HEADERS.get('CH-UserID'), 131 | self.HEADERS.get('Authorization'), 132 | self.HEADERS.get('CH-DeviceId') 133 | ) 134 | 135 | def start_phone_number_auth(self, phone_number): 136 | """ (Clubhouse, str) -> dict 137 | 138 | Begin phone number authentication. 139 | Some examples for the phone number. 140 | 141 | >>> clubhouse = Clubhouse() 142 | >>> clubhouse.start_phone_number_auth("+821012341337") 143 | ... 144 | >>> clubhouse.start_phone_number_auth("+818013371221") 145 | ... 146 | """ 147 | if self.HEADERS.get("Authorization"): 148 | raise Exception('Already Authenticatied') 149 | data = { 150 | "phone_number": phone_number 151 | } 152 | req = requests.post(f"{self.API_URL}/start_phone_number_auth", headers=self.HEADERS, json=data) 153 | return req.json() 154 | 155 | @unstable_endpoint 156 | def call_phone_number_auth(self, phone_number): 157 | """ (Clubhouse, str) -> dict 158 | 159 | Call the person and send verification message. 160 | """ 161 | if self.HEADERS.get("Authorization"): 162 | raise Exception('Already Authenticatied') 163 | data = { 164 | "phone_number": phone_number 165 | } 166 | req = requests.post(f"{self.API_URL}/call_phone_number_auth", headers=self.HEADERS, json=data) 167 | return req.json() 168 | 169 | @unstable_endpoint 170 | def resend_phone_number_auth(self, phone_number): 171 | """ (Clubhouse, str) -> dict 172 | 173 | Resend the verification message 174 | """ 175 | if self.HEADERS.get("Authorization"): 176 | raise Exception('Already Authenticatied') 177 | data = { 178 | "phone_number": phone_number 179 | } 180 | req = requests.post(f"{self.API_URL}/resend_phone_number_auth", headers=self.HEADERS, json=data) 181 | return req.json() 182 | 183 | def complete_phone_number_auth(self, phone_number, verification_code, rc_token=None, safety_net_nonce=None, safety_net_response=None): 184 | """ (Clubhouse, str, str, str, str, str) -> dict 185 | 186 | Complete phone number authentication. 187 | 188 | IMPORTANT NOTE 189 | You need to also provide `rc_token`, `safety_net_nonce` and `safety_net_response` 190 | depending on the platform type. Please do not send messages for the usage of these 191 | options. Some of these features may not have a Python implementations. 192 | """ 193 | if self.HEADERS.get("Authorization"): 194 | raise Exception('Already Authenticatied') 195 | 196 | data = { 197 | "device_token": None, 198 | "rc_token": rc_token, 199 | "safety_net_nonce": safety_net_nonce, 200 | "safety_net_response": safety_net_response, 201 | "phone_number": phone_number, 202 | "verification_code": verification_code 203 | } 204 | req = requests.post(f"{self.API_URL}/complete_phone_number_auth", headers=self.HEADERS, json=data) 205 | return req.json() 206 | 207 | def check_for_update(self, is_testflight=False): 208 | """ (Clubhouse, bool) -> dict 209 | 210 | Check for app updates. 211 | 212 | >>> clubhouse = Clubhouse() 213 | >>> clubhouse.check_for_update(False) 214 | {'has_update': False, 'success': True} 215 | """ 216 | query = f"is_testflight={int(is_testflight)}" 217 | req = requests.get(f"{self.API_URL}/check_for_update?{query}", headers=self.HEADERS) 218 | return req.json() 219 | 220 | @require_authentication 221 | def logout(self): 222 | """ (Clubhouse) -> dict 223 | 224 | Logout from the app. 225 | """ 226 | data = {} 227 | req = requests.post(f"{self.API_URL}/logout", headers=self.HEADERS, json=data) 228 | return req.json() 229 | 230 | @require_authentication 231 | def get_release_notes(self): 232 | """ (Clubhouse) -> dict 233 | 234 | Get release notes. 235 | """ 236 | req = requests.post(f"{self.API_URL}/get_release_notes", headers=self.HEADERS) 237 | return req.json() 238 | 239 | @require_authentication 240 | def check_waitlist_status(self): 241 | """ (Clubhouse) -> dict 242 | 243 | Check whether you're still on a waitlist or not. 244 | """ 245 | req = requests.post(f"{self.API_URL}/check_waitlist_status", headers=self.HEADERS) 246 | return req.json() 247 | 248 | @require_authentication 249 | def add_email(self, email): 250 | """ (Clubhouse, str) -> dict 251 | 252 | Request for email verification. 253 | You only need to do this once. 254 | """ 255 | data = { 256 | "email": email 257 | } 258 | req = requests.post(f"{self.API_URL}/add_email", headers=self.HEADERS, json=data) 259 | return req.json() 260 | 261 | @require_authentication 262 | def update_photo(self, photo_filename): 263 | """ (Clubhouse, str) -> dict 264 | 265 | Update photo. Please make sure to upload a JPG format. 266 | """ 267 | files = { 268 | "file": ("image.jpg", open(photo_filename, "rb"), "image/jpeg"), 269 | } 270 | tmp = self.HEADERS['Content-Type'] 271 | self.HEADERS.pop("Content-Type") 272 | req = requests.post(f"{self.API_URL}/update_photo", headers=self.HEADERS, files=files) 273 | self.HEADERS['Content-Type'] = tmp 274 | return req.json() 275 | 276 | @require_authentication 277 | def follow(self, user_id, user_ids=None, source=4, source_topic_id=None): 278 | """ (Clubhouse, int, list, int, int) -> dict 279 | 280 | Follow a user. 281 | Different value for `source` may require different parameters to be set 282 | """ 283 | data = { 284 | "source_topic_id": source_topic_id, 285 | "user_ids": user_ids, 286 | "user_id": int(user_id), 287 | "source": source 288 | } 289 | req = requests.post(f"{self.API_URL}/follow", headers=self.HEADERS, json=data) 290 | return req.json() 291 | 292 | @require_authentication 293 | def unfollow(self, user_id): 294 | """ (Clubhouse, int) -> dict 295 | 296 | Unfollow a user. 297 | """ 298 | data = { 299 | "user_id": int(user_id) 300 | } 301 | req = requests.post(f"{self.API_URL}/unfollow", headers=self.HEADERS, json=data) 302 | return req.json() 303 | 304 | @require_authentication 305 | def block(self, user_id): 306 | """ (Clubhouse, int) -> dict 307 | 308 | Block a user. 309 | """ 310 | data = { 311 | "user_id": int(user_id) 312 | } 313 | req = requests.post(f"{self.API_URL}/block", headers=self.HEADERS, json=data) 314 | return req.json() 315 | 316 | @require_authentication 317 | def unblock(self, user_id): 318 | """ (Clubhouse, int) -> dict 319 | 320 | Unfollow a user. 321 | """ 322 | data = { 323 | "user_id": int(user_id) 324 | } 325 | req = requests.post(f"{self.API_URL}/unblock", headers=self.HEADERS, json=data) 326 | return req.json() 327 | 328 | @require_authentication 329 | def follow_multiple(self, user_ids, user_id=None, source=7, source_topic_id=None): 330 | """ (Clubhouse, list, int, int, int) -> dict 331 | 332 | Follow multiple users at once. 333 | Different value for `source` may require different parameters to be set 334 | """ 335 | data = { 336 | "source_topic_id": source_topic_id, 337 | "user_ids": user_ids, 338 | "user_id": user_id, 339 | "source": source 340 | } 341 | req = requests.post(f"{self.API_URL}/follow_multiple", headers=self.HEADERS, json=data) 342 | return req.json() 343 | 344 | @require_authentication 345 | def follow_club(self, club_id, source_topic_id=None): 346 | """ (Clubhouse, int, int) -> dict 347 | 348 | Follow a club 349 | """ 350 | data = { 351 | "club_id": int(club_id), 352 | "source_topic_id": source_topic_id 353 | } 354 | req = requests.post(f"{self.API_URL}/follow_club", headers=self.HEADERS, json=data) 355 | return req.json() 356 | 357 | @require_authentication 358 | def unfollow_club(self, club_id, source_topic_id=None): 359 | """ (Clubhouse, int, int) -> dict 360 | 361 | Unfollow a club 362 | """ 363 | data = { 364 | "club_id": int(club_id), 365 | "source_topic_id": source_topic_id 366 | } 367 | req = requests.post(f"{self.API_URL}/unfollow_club", headers=self.HEADERS, json=data) 368 | return req.json() 369 | 370 | @require_authentication 371 | def update_follow_notifications(self, user_id, notification_type=2): 372 | """ (Clubhouse, str, int) -> dict 373 | 374 | Update notification frequency for the given user. 375 | 1 = Always notify, 2 = Sometimes, 3 = Never 376 | """ 377 | data = { 378 | "user_id": int(user_id), 379 | "notification_type": int(notification_type) 380 | } 381 | req = requests.post(f"{self.API_URL}/update_follow_notifications", headers=self.HEADERS, json=data) 382 | return req.json() 383 | 384 | @require_authentication 385 | def get_suggested_follows_similar(self, user_id='', username=''): 386 | """ (Clubhouse, str, str) -> dict 387 | 388 | Get similar users based on the given user. 389 | """ 390 | data = { 391 | "user_id": int(user_id) if user_id else None, 392 | "username": username if username else None, 393 | "query_id": None, 394 | "query_result_position": None, 395 | } 396 | req = requests.post(f"{self.API_URL}/get_suggested_follows_similar", headers=self.HEADERS, json=data) 397 | return req.json() 398 | 399 | @require_authentication 400 | def get_suggested_follows_friends_only(self, club_id=None, upload_contacts=True, contacts=()): 401 | """ (Clubhouse, int, int, list of dict) -> dict 402 | 403 | Get users based on the phone number. 404 | Only seems to be used upon signup. 405 | """ 406 | data = { 407 | "club_id": club_id, 408 | "upload_contacts": upload_contacts, 409 | "contacts": contacts 410 | } 411 | req = requests.post(f"{self.API_URL}/get_suggested_follows_friends_only", headers=self.HEADERS, json=data) 412 | return req.json() 413 | 414 | @require_authentication 415 | def get_suggested_follows_all(self, in_onboarding=True, page_size=50, page=1): 416 | """ (Clubhouse, bool, int, int) -> dict 417 | 418 | Get all suggested follows. 419 | """ 420 | query = "in_onboarding={}&page_size={}&page={}".format( 421 | "true" if in_onboarding else "false", 422 | page_size, 423 | page 424 | ) 425 | req = requests.get(f"{self.API_URL}/get_suggested_follows_all?{query}", headers=self.HEADERS) 426 | return req.json() 427 | 428 | @require_authentication 429 | def ignore_suggested_follow(self, user_id): 430 | """ (Clubhouse, str) -> dict 431 | 432 | Remove user_id from the suggested follow list. 433 | """ 434 | data = { 435 | "user_id": int(user_id) 436 | } 437 | req = requests.post(f"{self.API_URL}/user_id", headers=self.HEADERS, json=data) 438 | return req.json() 439 | 440 | @require_authentication 441 | def get_event(self, event_id=None, user_ids=None, club_id=None, is_member_only=False, event_hashid=None, description=None, time_start_epoch=None, name=None): 442 | """ (Clubhouse, int, list, int, bool, int, str, int, str) -> dict 443 | 444 | Get details about the event 445 | """ 446 | data = { 447 | "user_ids": user_ids, 448 | "club_id": club_id, 449 | "is_member_only": is_member_only, 450 | "event_id": int(event_id) if event_id else None, 451 | "event_hashid": event_hashid, 452 | "description": description, 453 | "time_start_epoch": time_start_epoch, 454 | "name": name 455 | } 456 | req = requests.post(f"{self.API_URL}/get_event", headers=self.HEADERS, json=data) 457 | return req.json() 458 | 459 | @require_authentication 460 | def create_event(self, name, time_start_epoch, description, event_id=None, user_ids=(), club_id=None, is_member_only=False, event_hashid=None): 461 | """ (Clubhouse, str, int, str, int, list, int, bool, int) -> dict 462 | 463 | Create a new event 464 | """ 465 | data = { 466 | "user_ids": user_ids, 467 | "club_id": club_id, 468 | "is_member_only": is_member_only, 469 | "event_id": int(event_id) if event_id else None, 470 | "event_hashid": event_hashid, 471 | "description": description, 472 | "time_start_epoch": time_start_epoch, 473 | "name": name 474 | } 475 | req = requests.post(f"{self.API_URL}/edit_event", headers=self.HEADERS, json=data) 476 | return req.json() 477 | 478 | @require_authentication 479 | def edit_event(self, name, time_start_epoch, description, event_id=None, user_ids=(), club_id=None, is_member_only=False, event_hashid=None): 480 | """ (Clubhouse, str, int, str, int, list, int, bool, int) -> dict 481 | 482 | Edit an event. 483 | """ 484 | data = { 485 | "user_ids": user_ids, 486 | "club_id": club_id, 487 | "is_member_only": is_member_only, 488 | "event_id": int(event_id) if event_id else None, 489 | "event_hashid": event_hashid, 490 | "description": description, 491 | "time_start_epoch": time_start_epoch, 492 | "name": name 493 | } 494 | req = requests.post(f"{self.API_URL}/edit_event", headers=self.HEADERS, json=data) 495 | return req.json() 496 | 497 | @require_authentication 498 | def delete_event(self, event_id, user_ids=None, club_id=None, is_member_only=False, event_hashid=None, description=None, time_start_epoch=None, name=None): 499 | """ (Clubhouse, str, list, int, bool, int, str, int, str) -> dict 500 | 501 | Delete event. 502 | """ 503 | data = { 504 | "user_ids": user_ids, 505 | "club_id": club_id, 506 | "is_member_only": is_member_only, 507 | "event_id": int(event_id) if event_id else None, 508 | "event_hashid": event_hashid, 509 | "description": description, 510 | "time_start_epoch": time_start_epoch, 511 | "name": name 512 | } 513 | req = requests.post(f"{self.API_URL}/delete_event", headers=self.HEADERS, json=data) 514 | return req.json() 515 | 516 | @require_authentication 517 | def get_events(self, is_filtered=True, page_size=25, page=1): 518 | """ (Clubhouse, bool, int, int) -> dict 519 | 520 | Get list of upcoming events with details. 521 | """ 522 | _is_filtered = "true" if is_filtered else "false" 523 | query = "is_filtered={}&page_size={}&page={}".format( 524 | "true" if is_filtered else "false", 525 | page_size, 526 | page 527 | ) 528 | req = requests.get(f"{self.API_URL}/get_events?{query}", headers=self.HEADERS) 529 | return req.json() 530 | 531 | @require_authentication 532 | def get_club(self, club_id, source_topic_id=None): 533 | """ (Clubhouse, int, int) -> dict 534 | 535 | Get the information about the given club_id. 536 | """ 537 | data = { 538 | "club_id": int(club_id), 539 | "source_topic_id": source_topic_id, 540 | "query_id": None, 541 | "query_result_position": None, 542 | "slug": None, 543 | } 544 | req = requests.post(f"{self.API_URL}/get_club", headers=self.HEADERS, json=data) 545 | return req.json() 546 | 547 | @require_authentication 548 | def get_club_members(self, club_id, return_followers=False, return_members=True, page_size=50, page=1): 549 | """ (Clubhouse, int, bool, bool, int, int) -> dict 550 | 551 | Get list of members on the given club_id. 552 | """ 553 | query = "club_id={}&return_followers={}&return_members={}&page_size={}&page={}".format( 554 | club_id, 555 | int(return_followers), 556 | int(return_members), 557 | page_size, 558 | page 559 | ) 560 | req = requests.get(f"{self.API_URL}/get_club_members?{query}", headers=self.HEADERS) 561 | return req.json() 562 | 563 | @require_authentication 564 | def get_settings(self): 565 | """ (Clubhouse) -> dict 566 | 567 | Receive user's settings. 568 | """ 569 | req = requests.get(f"{self.API_URL}/get_settings", headers=self.HEADERS) 570 | return req.json() 571 | 572 | @require_authentication 573 | def get_welcome_channel(self): 574 | """ (Clubhouse) -> dict 575 | 576 | Seems to be called upon sign up. Does not seem to return much data. 577 | """ 578 | req = requests.get(f"{self.API_URL}/get_welcome_channel", headers=self.HEADERS) 579 | return req.json() 580 | 581 | @require_authentication 582 | def hide_channel(self, channel, hide=True): 583 | """ (Clubhouse, str, bool) -> dict 584 | 585 | Hide/unhide the channel from the channel list. 586 | """ 587 | # Join channel 588 | data = { 589 | "channel": channel, 590 | "hide": hide 591 | } 592 | req = requests.post(f"{self.API_URL}/hide_channel", headers=self.HEADERS, json=data) 593 | return req.json() 594 | 595 | @require_authentication 596 | def join_channel(self, channel, attribution_source="feed", attribution_details="eyJpc19leHBsb3JlIjpmYWxzZSwicmFuayI6MX0="): 597 | """ (Clubhouse, str, str) -> dict 598 | 599 | Join the given channel 600 | """ 601 | data = { 602 | "channel": channel, 603 | "attribution_source": attribution_source, 604 | "attribution_details": attribution_details, # base64_json 605 | # logging_context (json of some details) 606 | } 607 | req = requests.post(f"{self.API_URL}/join_channel", headers=self.HEADERS, json=data) 608 | return req.json() 609 | 610 | @require_authentication 611 | def leave_channel(self, channel): 612 | """ (Clubhouse, str) -> dict 613 | 614 | Leave the given channel 615 | """ 616 | data = { 617 | "channel": channel 618 | } 619 | req = requests.post(f"{self.API_URL}/leave_channel", headers=self.HEADERS, json=data) 620 | return req.json() 621 | 622 | @require_authentication 623 | def make_channel_public(self, channel, channel_id=None): 624 | """ (Clubhouse, str, int) -> dict 625 | 626 | Make the current channel open to public. 627 | Everyone can join the channel. 628 | """ 629 | data = { 630 | "channel": channel, 631 | "channel_id": channel_id 632 | } 633 | req = requests.post(f"{self.API_URL}/make_channel_public", headers=self.HEADERS, json=data) 634 | return req.json() 635 | 636 | @require_authentication 637 | def make_channel_social(self, channel, channel_id=None): 638 | """ (Clubhouse, str, int) -> dict 639 | 640 | Make the current channel open to public. 641 | Only people who user follows can join the channel. 642 | """ 643 | data = { 644 | "channel": channel, 645 | "channel_id": channel_id 646 | } 647 | req = requests.post(f"{self.API_URL}/make_channel_social", headers=self.HEADERS, json=data) 648 | return req.json() 649 | 650 | @require_authentication 651 | def end_channel(self, channel, channel_id=None): 652 | """ (Clubhouse, str, int) -> dict 653 | 654 | Kick everyone and close the channel. Requires moderator privilege. 655 | """ 656 | data = { 657 | "channel": channel, 658 | "channel_id": channel_id 659 | } 660 | req = requests.post(f"{self.API_URL}/end_channel", headers=self.HEADERS, json=data) 661 | return req.json() 662 | 663 | @require_authentication 664 | def make_moderator(self, channel, user_id): 665 | """ (Clubhouse, str, int) -> dict 666 | 667 | Make the given user moderator. Requires moderator privilege. 668 | """ 669 | data = { 670 | "channel": channel, 671 | "user_id": int(user_id) 672 | } 673 | req = requests.post(f"{self.API_URL}/make_moderator", headers=self.HEADERS, json=data) 674 | return req.json() 675 | 676 | @require_authentication 677 | def block_from_channel(self, channel, user_id): 678 | """ (Clubhouse, str, int) -> dict 679 | 680 | Remove the user from the channel. The user will not be able to re-join. 681 | """ 682 | data = { 683 | "channel": channel, 684 | "user_id": int(user_id) 685 | } 686 | req = requests.post(f"{self.API_URL}/block_from_channel", headers=self.HEADERS, json=data) 687 | return req.json() 688 | 689 | @require_authentication 690 | def get_profile(self, user_id='', username=''): 691 | """ (Clubhouse, str, str) -> dict 692 | 693 | Lookup someone else's profile. It is OK to one's own profile with this method. 694 | """ 695 | data = { 696 | "query_id": None, 697 | "query_result_position": 0, 698 | "user_id": int(user_id) if user_id else None, 699 | "username": username if username else None 700 | } 701 | req = requests.post(f"{self.API_URL}/get_profile", headers=self.HEADERS, json=data) 702 | return req.json() 703 | 704 | @require_authentication 705 | def me(self, return_blocked_ids=False, timezone_identifier="Asia/Tokyo", return_following_ids=False): 706 | """ (Clubhouse, bool, str, bool) -> dict 707 | 708 | Get my information 709 | """ 710 | data = { 711 | "return_blocked_ids": return_blocked_ids, 712 | "timezone_identifier": timezone_identifier, 713 | "return_following_ids": return_following_ids 714 | } 715 | req = requests.post(f"{self.API_URL}/me", headers=self.HEADERS, json=data) 716 | return req.json() 717 | 718 | @require_authentication 719 | def get_following(self, user_id, page_size=50, page=1): 720 | """ (Clubhouse, str, int, int) -> dict 721 | 722 | Get following users type2 723 | """ 724 | query = "user_id={}&page_size={}&page={}".format( 725 | user_id, 726 | page_size, 727 | page 728 | ) 729 | req = requests.get(f"{self.API_URL}/get_following?{query}", headers=self.HEADERS) 730 | return req.json() 731 | 732 | @require_authentication 733 | def get_followers(self, user_id, page_size=50, page=1): 734 | """ (Clubhouse, str, int, int) -> dict 735 | 736 | Get followers of the given user_id. 737 | """ 738 | query = "user_id={}&page_size={}&page={}".format( 739 | user_id, 740 | page_size, 741 | page 742 | ) 743 | req = requests.get(f"{self.API_URL}/get_followers?{query}", headers=self.HEADERS) 744 | return req.json() 745 | 746 | @require_authentication 747 | def get_mutual_follows(self, user_id, page_size=50, page=1): 748 | """ (Clubhouse, str, int, int) -> dict 749 | 750 | Get mutual followers between the current user and the given user_id. 751 | """ 752 | query = "user_id={}&page_size={}&page={}".format( 753 | user_id, 754 | page_size, 755 | page 756 | ) 757 | req = requests.get(f"{self.API_URL}/get_mutual_follows?{query}", headers=self.HEADERS) 758 | return req.json() 759 | 760 | @require_authentication 761 | def get_all_topics(self): 762 | """ (Clubhouse) -> dict 763 | 764 | Get list of topics, based on the server's channel selection algorithm 765 | """ 766 | req = requests.get(f"{self.API_URL}/get_all_topics", headers=self.HEADERS) 767 | return req.json() 768 | 769 | @require_authentication 770 | def get_feed(self): 771 | """ (Clubhouse) -> dict 772 | 773 | Get list of channels, current invite status, etc. 774 | """ 775 | req = requests.get(f"{self.API_URL}/get_feed?", headers=self.HEADERS) 776 | return req.json() 777 | 778 | @require_authentication 779 | def get_channels(self): 780 | """ (Clubhouse) -> dict 781 | 782 | Get list of channels, based on the server's channel selection algorithm 783 | """ 784 | req = requests.get(f"{self.API_URL}/get_channels", headers=self.HEADERS) 785 | return req.json() 786 | 787 | @require_authentication 788 | def get_channel(self, channel, channel_id=None): 789 | """ (Clubhouse, str, int) -> dict 790 | 791 | Get information of the given channel 792 | """ 793 | data = { 794 | "channel": channel, 795 | "channel_id": channel_id 796 | } 797 | req = requests.post(f"{self.API_URL}/get_channel", headers=self.HEADERS, json=data) 798 | return req.json() 799 | 800 | @require_authentication 801 | def active_ping(self, channel): 802 | """ (Clubhouse, str) -> dict 803 | 804 | Keeping the user active while being in a chatroom 805 | """ 806 | data = { 807 | "channel": channel, 808 | "chanel_id": None 809 | } 810 | req = requests.post(f"{self.API_URL}/active_ping", headers=self.HEADERS, json=data) 811 | return req.json() 812 | 813 | @require_authentication 814 | def audience_reply(self, channel, raise_hands=True, unraise_hands=False): 815 | """ (Clubhouse, str, bool, bool) -> bool 816 | 817 | Request for raise_hands. 818 | """ 819 | data = { 820 | "channel": channel, 821 | "raise_hands": raise_hands, 822 | "unraise_hands": unraise_hands 823 | } 824 | req = requests.post(f"{self.API_URL}/audience_reply", headers=self.HEADERS, json=data) 825 | return req.json() 826 | 827 | @require_authentication 828 | def change_handraise_settings(self, channel, is_enabled=True, handraise_permission=1): 829 | """ (Clubhouse, bool, int) -> dict 830 | 831 | Change handraise settings. Requires moderator privilege 832 | 833 | * handraise_permission(int) 834 | - 1: Everyone 835 | - 2: Followed by the speakers 836 | * is_enabled(bool) 837 | - True: Enable handraise 838 | - False: Disable handraise 839 | """ 840 | handraise_permission = int(handraise_permission) 841 | if not 1 <= handraise_permission <= 2: 842 | return False 843 | 844 | data = { 845 | "channel": channel, 846 | "is_enabled": is_enabled, 847 | "handraise_permission": handraise_permission 848 | } 849 | req = requests.post(f"{self.API_URL}/change_handraise_settings", headers=self.HEADERS, json=data) 850 | return req.json() 851 | 852 | @require_authentication 853 | def update_skintone(self, skintone=1): 854 | """ (Clubhouse, int) -> dict 855 | Updating skinetone for raising hands, etc. 856 | """ 857 | skintone = int(skintone) 858 | if not 1 <= skintone <= 5: 859 | return False 860 | 861 | data = { 862 | "skintone": skintone 863 | } 864 | req = requests.post(f"{self.API_URL}/update_skintone", headers=self.HEADERS, json=data) 865 | return req.json() 866 | 867 | @require_authentication 868 | def get_notifications(self, page_size=20, page=1): 869 | """ (Clubhouse, int, int) -> dict 870 | 871 | Get my notifications. 872 | """ 873 | query = f"page_size={page_size}&page={page}" 874 | req = requests.get(f"{self.API_URL}/get_notifications?{query}", headers=self.HEADERS) 875 | return req.json() 876 | 877 | @require_authentication 878 | def get_actionable_notifications(self): 879 | """ (Clubhouse, int, int) -> dict 880 | 881 | Get notifications. This may return some notifications that require some actions 882 | """ 883 | req = requests.get(f"{self.API_URL}/get_actionable_notifications", headers=self.HEADERS) 884 | return req.json() 885 | 886 | @require_authentication 887 | def get_online_friends(self): 888 | """ (Clubhouse) -> dict 889 | 890 | List all online friends. 891 | """ 892 | req = requests.post(f"{self.API_URL}/get_online_friends", headers=self.HEADERS, json={}) 893 | return req.json() 894 | 895 | @require_authentication 896 | def accept_speaker_invite(self, channel, user_id): 897 | """ (Clubhouse, str, int) -> dict 898 | 899 | Accept speaker's invitation, based on the (channel, invited_moderator) 900 | `raise_hands` needs to be called first, prior to the invitation. 901 | """ 902 | data = { 903 | "channel": channel, 904 | "user_id": int(user_id) 905 | } 906 | req = requests.post(f"{self.API_URL}/accept_speaker_invite", headers=self.HEADERS, json=data) 907 | return req.json() 908 | 909 | @require_authentication 910 | def reject_speaker_invite(self, channel, user_id): 911 | """ (Clubhouse, str, int) -> dict 912 | 913 | Reject speaker's invitation. 914 | """ 915 | data = { 916 | "channel": channel, 917 | "user_id": int(user_id) 918 | } 919 | req = requests.post(f"{self.API_URL}/reject_speaker_invite", headers=self.HEADERS, json=data) 920 | return req.json() 921 | 922 | @require_authentication 923 | def invite_speaker(self, channel, user_id): 924 | """ (Clubhouse, str, int) -> dict 925 | 926 | Move audience to speaker. Requires moderator privilege. 927 | """ 928 | data = { 929 | "channel": channel, 930 | "user_id": int(user_id) 931 | } 932 | req = requests.post(f"{self.API_URL}/invite_speaker", headers=self.HEADERS, json=data) 933 | return req.json() 934 | 935 | @require_authentication 936 | def uninvite_speaker(self, channel, user_id): 937 | """ (Clubhouse, str, int) -> dict 938 | 939 | Move speaker to audience. Requires moderator privilege. 940 | """ 941 | data = { 942 | "channel": channel, 943 | "user_id": int(user_id) 944 | } 945 | req = requests.post(f"{self.API_URL}/uninvite_speaker", headers=self.HEADERS, json=data) 946 | return req.json() 947 | 948 | @require_authentication 949 | def mute_speaker(self, channel, user_id): 950 | """ (Clubhouse, str, int) -> dict 951 | 952 | Mute speaker. Requires moderator privilege 953 | """ 954 | data = { 955 | "channel": channel, 956 | "user_id": int(user_id) 957 | } 958 | req = requests.post(f"{self.API_URL}/mute_speaker", headers=self.HEADERS, json=data) 959 | return req.json() 960 | 961 | @require_authentication 962 | def get_suggested_speakers(self, channel): 963 | """ (Clubhouse, str) -> dict 964 | 965 | Get suggested speakers from the given channel 966 | """ 967 | data = { 968 | "channel": channel 969 | } 970 | req = requests.post(f"{self.API_URL}/get_suggested_speakers", headers=self.HEADERS, json=data) 971 | return req.json() 972 | 973 | @require_authentication 974 | def create_channel(self, topic="", user_ids=(), is_private=False, is_social_mode=False): 975 | """ (Clubhouse, str, list, bool, bool) -> dict 976 | 977 | Create a new channel. Type of the room can be changed 978 | """ 979 | data = { 980 | "is_social_mode": is_social_mode, 981 | "is_private": is_private, 982 | "club_id": None, 983 | "user_ids": user_ids, 984 | "event_id": None, 985 | "topic": topic 986 | } 987 | req = requests.post(f"{self.API_URL}/create_channel", headers=self.HEADERS, json=data) 988 | return req.json() 989 | 990 | @require_authentication 991 | def get_create_channel_targets(self): 992 | """ (Clubhouse) -> dict 993 | 994 | Not sure what this does. Triggered upon channel creation 995 | """ 996 | data = {} 997 | req = requests.post(f"{self.API_URL}/get_create_channel_targets", headers=self.HEADERS, json=data) 998 | return req.json() 999 | 1000 | @require_authentication 1001 | def get_suggested_invites(self, club_id=None, upload_contacts=True, contacts=()): 1002 | """ (Clubhouse, int, bool, list of dict) -> dict 1003 | 1004 | Get invitations and user lists based on phone number. 1005 | 1006 | contacts(dict) 1007 | - example: [{"name": "Test Name", "phone_number": "+821043219876"}, ...] 1008 | """ 1009 | data = { 1010 | "club_id": club_id, 1011 | "upload_contacts": upload_contacts, 1012 | "contacts": contacts 1013 | } 1014 | req = requests.post(f"{self.API_URL}/get_suggested_invites", headers=self.HEADERS, json=data) 1015 | return req.json() 1016 | 1017 | @require_authentication 1018 | def get_suggested_club_invites(self, upload_contacts=True, contacts=()): 1019 | """ (Clubhouse, int, bool, list of dict) -> dict 1020 | 1021 | Get user lists based on phone number. For inviting clubs. 1022 | 1023 | contacts(dict) 1024 | - example: [{"name": "Test Name", "phone_number": "+821043219876"}, ...] 1025 | """ 1026 | data = { 1027 | "upload_contacts": upload_contacts, 1028 | "contacts": contacts 1029 | } 1030 | req = requests.post(f"{self.API_URL}/get_suggested_club_invites", headers=self.HEADERS, json=data) 1031 | return req.json() 1032 | 1033 | @require_authentication 1034 | def invite_to_app(self, name, phone_number, message=None): 1035 | """ (Clubhouse, str, str, str) -> dict 1036 | 1037 | Invite users to app. but this only works when you have a leftover invitation. 1038 | """ 1039 | data = { 1040 | "name": name, 1041 | "phone_number": phone_number, 1042 | "message": message 1043 | } 1044 | req = requests.post(f"{self.API_URL}/invite_to_app", headers=self.HEADERS, json=data) 1045 | return req.json() 1046 | 1047 | @require_authentication 1048 | def invite_from_waitlist(self, user_id): 1049 | """ (Clubhouse, str, str, str) -> dict 1050 | 1051 | Invite someone from the waitlist. 1052 | This is much more reliable than inviting someone by invite_to_app 1053 | """ 1054 | data = { 1055 | "user_id": int(user_id), 1056 | } 1057 | req = requests.post(f"{self.API_URL}/invite_from_waitlist", headers=self.HEADERS, json=data) 1058 | return req.json() 1059 | 1060 | @require_authentication 1061 | def search_users(self, query, followers_only=False, following_only=False, cofollows_only=False): 1062 | """ (Clubhouse, str, bool, bool, bool) -> dict 1063 | 1064 | Search users based on the given query. 1065 | """ 1066 | data = { 1067 | "cofollows_only": cofollows_only, 1068 | "following_only": following_only, 1069 | "followers_only": followers_only, 1070 | "query": query 1071 | } 1072 | req = requests.post(f"{self.API_URL}/search_users", headers=self.HEADERS, json=data) 1073 | return req.json() 1074 | 1075 | @require_authentication 1076 | def search_clubs(self, query, followers_only=False, following_only=False, cofollows_only=False): 1077 | """ (Clubhouse, str, bool, bool, bool) -> dict 1078 | 1079 | Search clubs based on the given query. 1080 | """ 1081 | data = { 1082 | "cofollows_only": cofollows_only, 1083 | "following_only": following_only, 1084 | "followers_only": followers_only, 1085 | "query": query 1086 | } 1087 | req = requests.post(f"{self.API_URL}/search_clubs", headers=self.HEADERS, json=data) 1088 | return req.json() 1089 | 1090 | @require_authentication 1091 | def get_topic(self, topic_id): 1092 | """ (Clubhouse, int) -> dict 1093 | 1094 | Get topic's information based on the given topic id. 1095 | """ 1096 | data = { 1097 | "topic_id": int(topic_id) 1098 | } 1099 | req = requests.post(f"{self.API_URL}/get_topic", headers=self.HEADERS, json=data) 1100 | return req.json() 1101 | 1102 | @require_authentication 1103 | def get_clubs_for_topic(self, topic_id, page_size=25, page=1): 1104 | """ (Clubhouse, int, int, int) -> dict 1105 | 1106 | Get list of clubs based on the given topic id. 1107 | """ 1108 | query = "topic_id={}&page_size={}&page={}".format( 1109 | topic_id, 1110 | page_size, 1111 | page 1112 | ) 1113 | req = requests.get(f"{self.API_URL}/get_clubs_for_topic?{query}", headers=self.HEADERS) 1114 | return req.json() 1115 | 1116 | @require_authentication 1117 | def get_clubs(self, is_startable_only): 1118 | """ (Clubhouse, bool) -> dict 1119 | 1120 | Get list of clubs the user's in. 1121 | """ 1122 | data = { 1123 | "is_startable_only": is_startable_only 1124 | } 1125 | req = requests.post(f"{self.API_URL}/get_clubs", headers=self.HEADERS, json=data) 1126 | return req.json() 1127 | 1128 | @require_authentication 1129 | def get_users_for_topic(self, topic_id, page_size=25, page=1): 1130 | """ (Clubhouse, int, int, int) -> dict 1131 | 1132 | Get list of users based on the given topic id. 1133 | """ 1134 | query = "topic_id={}&page_size={}&page={}".format( 1135 | topic_id, 1136 | page_size, 1137 | page 1138 | ) 1139 | req = requests.get(f"{self.API_URL}/get_users_for_topic?{query}", headers=self.HEADERS) 1140 | return req.json() 1141 | 1142 | @require_authentication 1143 | def invite_to_existing_channel(self, channel, user_id): 1144 | """ (Clubhouse, str, int) -> dict 1145 | 1146 | Invite someone to a currently joined channel. 1147 | It will send a ping notification to the given user_id. 1148 | """ 1149 | data = { 1150 | "channel": channel, 1151 | "user_id": int(user_id) 1152 | } 1153 | req = requests.post(f"{self.API_URL}/invite_to_existing_channel", headers=self.HEADERS, json=data) 1154 | return req.json() 1155 | 1156 | @require_authentication 1157 | def update_username(self, username): 1158 | """ (Clubhouse, str) -> dict 1159 | 1160 | Change username. YOU HAVE LIMITED NUMBER OF TRIALS TO CHANGE YOUR USERNAME. 1161 | """ 1162 | data = { 1163 | "username": username, 1164 | } 1165 | req = requests.post(f"{self.API_URL}/update_username", headers=self.HEADERS, json=data) 1166 | return req.json() 1167 | 1168 | @require_authentication 1169 | def update_name(self, name): 1170 | """ (Clubhouse, str) -> dict 1171 | 1172 | Change your legal name. Be careful of what you're trying to enter. 1173 | (1) Upon registration 1174 | (2) Changing your legal name. YOU CAN ONLY DO THIS ONCE. 1175 | """ 1176 | data = { 1177 | "name": name, 1178 | } 1179 | req = requests.post(f"{self.API_URL}/update_name", headers=self.HEADERS, json=data) 1180 | return req.json() 1181 | 1182 | @unstable_endpoint 1183 | @require_authentication 1184 | def update_twitter_username(self, username, twitter_token, twitter_secret): 1185 | """ (Clubhouse, str, str, str) -> dict 1186 | 1187 | Change Twitter username based on Twitter Token. 1188 | 1189 | >>> client.update_twitter_username(None, None, None) # Clear username 1190 | >>> client.update_twitter_username("stereotype32", "...", "...") # Set username 1191 | """ 1192 | data = { 1193 | "username": username, 1194 | "twitter_token": twitter_token, 1195 | "twitter_secret": twitter_secret 1196 | } 1197 | req = requests.post(f"{self.API_URL}/update_twitter_username", headers=self.HEADERS, json=data) 1198 | return req.json() 1199 | 1200 | @unstable_endpoint 1201 | @require_authentication 1202 | def update_instagram_username(self, code): 1203 | """ (Clubhouse, str) -> dict 1204 | 1205 | Change Twitter username based on Instagram token. 1206 | 1207 | >>> client.update_instagram_username(None) # Clear username 1208 | >>> client.update_instagram_username("...") # Set username 1209 | """ 1210 | data = { 1211 | "code": code 1212 | } 1213 | req = requests.post(f"{self.API_URL}/update_instagram_username", headers=self.HEADERS, json=data) 1214 | return req.json() 1215 | 1216 | @require_authentication 1217 | def update_displayname(self, name): 1218 | """ (Clubhouse, str) -> dict 1219 | 1220 | Change your nickname. YOU CAN ONLY DO THIS ONCE. 1221 | """ 1222 | data = { 1223 | "name": name, 1224 | } 1225 | req = requests.post(f"{self.API_URL}/update_name", headers=self.HEADERS, json=data) 1226 | return req.json() 1227 | 1228 | @require_authentication 1229 | def refresh_token(self, refresh_token): 1230 | """ (Clubhouse, str) -> dict 1231 | 1232 | Refresh the JWT token. returns both access and refresh token. 1233 | """ 1234 | data = { 1235 | "refresh": refresh_token 1236 | } 1237 | req = requests.post(f"{self.API_URL}/refresh_token", headers=self.HEADERS, json=data) 1238 | return req.json() 1239 | 1240 | @require_authentication 1241 | def update_bio(self, bio): 1242 | """ (Clubhouse, str) -> dict 1243 | 1244 | Update bio on your profile 1245 | """ 1246 | data = { 1247 | "bio": bio 1248 | } 1249 | req = requests.post(f"{self.API_URL}/update_bio", headers=self.HEADERS, json=data) 1250 | return req.json() 1251 | 1252 | @require_authentication 1253 | def record_action_trails(self, action_trails=()): 1254 | """ (Clubhouse, list of dict) -> dict 1255 | 1256 | Recording actions of the user interactions while using the app. 1257 | action_trails: [{"blob_data":{}, "trail_type": "...", ...}, ...] 1258 | """ 1259 | data = { 1260 | "action_trails": action_trails 1261 | } 1262 | req = requests.post(f"{self.API_URL}/update_bio", headers=self.HEADERS, json=data) 1263 | return req.json() 1264 | 1265 | @require_authentication 1266 | def add_user_topic(self, club_id=None, topic_id=None): 1267 | """ (Clubhouse, int, int) -> dict 1268 | 1269 | Add user's interest. 1270 | 1271 | Some interesting flags for Language has been shared in the following link. 1272 | Reference: https://github.com/grishka/Houseclub/issues/24 1273 | """ 1274 | data = { 1275 | "club_id": int(club_id) if club_id else None, 1276 | "topic_id": int(topic_id) if topic_id else None 1277 | } 1278 | req = requests.post(f"{self.API_URL}/add_user_topic", headers=self.HEADERS, json=data) 1279 | return req.json() 1280 | 1281 | @require_authentication 1282 | def remove_user_topic(self, club_id, topic_id): 1283 | """ (Clubhouse, int, int) -> dict 1284 | 1285 | Remove user's interest 1286 | """ 1287 | data = { 1288 | "club_id": int(club_id) if club_id else None, 1289 | "topic_id": int(topic_id) if topic_id else None 1290 | } 1291 | req = requests.post(f"{self.API_URL}/remove_user_topic", headers=self.HEADERS, json=data) 1292 | return req.json() 1293 | 1294 | @unstable_endpoint 1295 | @require_authentication 1296 | def report_incident(self, user_id, channel, incident_type, incident_description, email): 1297 | """ (Clubhouse, int, str, unknown, str, str) -> dict 1298 | 1299 | Report incident 1300 | There seemed to be a field for attachment, need to trace this later 1301 | """ 1302 | data = { 1303 | "user_id": int(user_id), 1304 | "channel": channel, 1305 | "incident_type": incident_type, 1306 | "incident_description": incident_description, 1307 | "email": email 1308 | } 1309 | req = requests.post(f"{self.API_URL}/report_incident", headers=self.HEADERS, json=data) 1310 | return req.json() 1311 | 1312 | @unstable_endpoint 1313 | @require_authentication 1314 | def reject_welcome_channel(self): 1315 | """ (Clubhouse) -> dict 1316 | 1317 | Unknown 1318 | """ 1319 | req = requests.get(f"{self.API_URL}/reject_welcome_channel", headers=self.HEADERS) 1320 | return req.json() 1321 | 1322 | @unstable_endpoint 1323 | @require_authentication 1324 | def update_channel_flags(self, channel, visibility, flag_title, unflag_title): 1325 | """ (Clubhouse, str, bool, unknown, unknown) -> dict 1326 | 1327 | Unknown 1328 | """ 1329 | data = { 1330 | "channel": channel, 1331 | "visibility": visibility, 1332 | "flag_title": flag_title, 1333 | "unflag_title": unflag_title, 1334 | } 1335 | req = requests.post(f"{self.API_URL}/update_channel_flags", headers=self.HEADERS, json=data) 1336 | return req.json() 1337 | 1338 | @unstable_endpoint 1339 | @require_authentication 1340 | def ignore_actionable_notification(self, actionable_notification_id): 1341 | """ (Clubhouse, int) -> dict 1342 | 1343 | Ignore the actionable notification. 1344 | """ 1345 | data = { 1346 | "actionable_notification_id": actionable_notification_id 1347 | } 1348 | req = requests.post(f"{self.API_URL}/ignore_actionable_notification", headers=self.HEADERS, json=data) 1349 | return req.json() 1350 | 1351 | @unstable_endpoint 1352 | @require_authentication 1353 | def invite_to_new_channel(self, user_id, channel): 1354 | """ (Clubhouse, int, str) -> dict 1355 | 1356 | Invite someone to the channel 1357 | """ 1358 | data = { 1359 | "user_id": int(user_id), 1360 | "channel": channel 1361 | } 1362 | req = requests.post(f"{self.API_URL}/invite_to_new_channel", headers=self.HEADERS, json=data) 1363 | return req.json() 1364 | 1365 | @unstable_endpoint 1366 | @require_authentication 1367 | def accept_new_channel_invite(self, channel_invite_id): 1368 | """ (Clubhouse, int) -> dict 1369 | 1370 | Accept Channel Invitation 1371 | """ 1372 | data = { 1373 | "channel_invite_id": channel_invite_id 1374 | } 1375 | req = requests.post(f"{self.API_URL}/accept_new_channel_invite", headers=self.HEADERS, json=data) 1376 | return req.json() 1377 | 1378 | @unstable_endpoint 1379 | @require_authentication 1380 | def reject_new_channel_invite(self, channel_invite_id): 1381 | """ (Clubhouse, int) -> dict 1382 | 1383 | Reject Channel Invitation 1384 | """ 1385 | data = { 1386 | "channel_invite_id": channel_invite_id 1387 | } 1388 | req = requests.post(f"{self.API_URL}/reject_new_channel_invite", headers=self.HEADERS, json=data) 1389 | return req.json() 1390 | 1391 | @unstable_endpoint 1392 | @require_authentication 1393 | def cancel_new_channel_invite(self, channel_invite_id): 1394 | """ (Clubhouse, int) -> dict 1395 | 1396 | Cancel Channel Invitation 1397 | """ 1398 | data = { 1399 | "channel_invite_id": channel_invite_id 1400 | } 1401 | req = requests.post(f"{self.API_URL}/cancel_new_channel_invite", headers=self.HEADERS, json=data) 1402 | return req.json() 1403 | 1404 | @require_authentication 1405 | def add_club_admin(self, club_id, user_id): 1406 | """ (Clubhouse, int, int) -> dict 1407 | 1408 | Add Club Admin. Requires privilege. 1409 | """ 1410 | data = { 1411 | "club_id": int(club_id), 1412 | "user_id": int(user_id) 1413 | } 1414 | req = requests.post(f"{self.API_URL}/add_club_admin", headers=self.HEADERS, json=data) 1415 | return req.json() 1416 | 1417 | @require_authentication 1418 | def remove_club_admin(self, club_id, user_id): 1419 | """ (Clubhouse, int, int) -> dict 1420 | 1421 | Remove Club admin. Requires privilege. 1422 | """ 1423 | data = { 1424 | "club_id": int(club_id) if club_id else None, 1425 | "user_id": int(user_id) 1426 | } 1427 | req = requests.post(f"{self.API_URL}/remove_club_admin", headers=self.HEADERS, json=data) 1428 | return req.json() 1429 | 1430 | @require_authentication 1431 | def remove_club_member(self, club_id, user_id): 1432 | """ (Clubhouse, int, int) -> dict 1433 | 1434 | Remove Club member. Requires privilege. 1435 | """ 1436 | data = { 1437 | "club_id": int(club_id) if club_id else None, 1438 | "user_id": int(user_id) 1439 | } 1440 | req = requests.post(f"{self.API_URL}/remove_club_member", headers=self.HEADERS, json=data) 1441 | return req.json() 1442 | 1443 | @require_authentication 1444 | def accept_club_member_invite(self, club_id, source_topic_id=None, invite_code=None): 1445 | """ (Clubhouse, int, int, str) -> dict 1446 | 1447 | Accept Club member invite. 1448 | """ 1449 | data = { 1450 | "club_id": int(club_id) if club_id else None, 1451 | "invite_code": invite_code, 1452 | "query_id": None, 1453 | "query_result_position": None, 1454 | "slug": None, 1455 | "source_topic_id": source_topic_id 1456 | } 1457 | req = requests.post(f"{self.API_URL}/accept_club_member_invite", headers=self.HEADERS, json=data) 1458 | return req.json() 1459 | 1460 | @require_authentication 1461 | def add_club_member(self, club_id, user_id, name, phone_number, message, reason): 1462 | """ (Clubhouse, int, int, str, str, str, unknown) -> dict 1463 | 1464 | Add club member 1465 | """ 1466 | data = { 1467 | "club_id": int(club_id), 1468 | "user_id": int(user_id), 1469 | "name": name, 1470 | "phone_number": phone_number, 1471 | "message": message, 1472 | "reason": reason 1473 | } 1474 | req = requests.post(f"{self.API_URL}/add_club_member", headers=self.HEADERS, json=data) 1475 | return req.json() 1476 | 1477 | @require_authentication 1478 | def get_club_nominations(self, club_id, source_topic_id): 1479 | """ (Club, int, int) -> dict 1480 | 1481 | Get club nomination list 1482 | """ 1483 | data = { 1484 | "club_id": int(club_id), 1485 | "source_topic_id": source_topic_id 1486 | } 1487 | req = requests.post(f"{self.API_URL}/get_club_nominations", headers=self.HEADERS, json=data) 1488 | return req.json() 1489 | 1490 | @require_authentication 1491 | def approve_club_nomination(self, club_id, source_topic_id, invite_nomination_id): 1492 | """ (Club, int, int) -> dict 1493 | 1494 | Approve club nomination 1495 | """ 1496 | data = { 1497 | "club_id": int(club_id), 1498 | "source_topic_id": source_topic_id, 1499 | "invite_nomination_id": invite_nomination_id 1500 | } 1501 | req = requests.post(f"{self.API_URL}/approve_club_nomination", headers=self.HEADERS, json=data) 1502 | return req.json() 1503 | 1504 | @require_authentication 1505 | def reject_club_nomination(self, club_id, source_topic_id, invite_nomination_id): 1506 | """ (Club, int, int) -> dict 1507 | 1508 | Reject club nomination 1509 | """ 1510 | data = { 1511 | "club_id": int(club_id), 1512 | "source_topic_id": source_topic_id, 1513 | "invite_nomination_id": invite_nomination_id 1514 | } 1515 | req = requests.post(f"{self.API_URL}/approve_club_nomination", headers=self.HEADERS, json=data) 1516 | return req.json() 1517 | 1518 | @require_authentication 1519 | def add_club_topic(self, club_id, topic_id): 1520 | """ (Club, int, int) -> dict 1521 | 1522 | Add club topic 1523 | """ 1524 | data = { 1525 | "club_id": int(club_id), 1526 | "topic_id": int(topic_id) 1527 | } 1528 | req = requests.post(f"{self.API_URL}/add_club_topic", headers=self.HEADERS, json=data) 1529 | return req.json() 1530 | 1531 | @require_authentication 1532 | def remove_club_topic(self, club_id, topic_id): 1533 | """ (Club, int, int) -> dict 1534 | 1535 | Remove club topic 1536 | """ 1537 | data = { 1538 | "club_id": int(club_id), 1539 | "topic_id": int(topic_id) 1540 | } 1541 | req = requests.post(f"{self.API_URL}/remove_club_topic", headers=self.HEADERS, json=data) 1542 | return req.json() 1543 | 1544 | @require_authentication 1545 | def get_events_to_start(self): 1546 | """ (Clubhouse) -> dict 1547 | 1548 | Get events to start 1549 | """ 1550 | req = requests.get(f"{self.API_URL}/get_events_to_start", headers=self.HEADERS) 1551 | return req.json() 1552 | 1553 | @require_authentication 1554 | def update_is_follow_allowed(self, club_id, is_follow_allowed=True): 1555 | """ (Clubhouse, int, bool) -> dict 1556 | 1557 | Update follow button of the given Club 1558 | """ 1559 | data = { 1560 | "club_id": int(club_id), 1561 | "is_follow_allowed": is_follow_allowed 1562 | } 1563 | req = requests.post(f"{self.API_URL}/update_is_follow_allowed", headers=self.HEADERS, json=data) 1564 | return req.json() 1565 | 1566 | @require_authentication 1567 | def update_is_membership_private(self, club_id, is_membership_private=False): 1568 | """ (Clubhouse, int, bool) -> dict 1569 | 1570 | Update club membership status of the given Club 1571 | If True, member list will not be shown to public. 1572 | """ 1573 | data = { 1574 | "club_id": int(club_id), 1575 | "is_membership_private": is_membership_private 1576 | } 1577 | req = requests.post(f"{self.API_URL}/update_is_membership_private", headers=self.HEADERS, json=data) 1578 | return req.json() 1579 | 1580 | @require_authentication 1581 | def update_is_community(self, club_id, is_community=False): 1582 | """ (Clubhouse, int, bool) -> dict 1583 | 1584 | Change room start permission. If set False, Admins can only start club rooms. 1585 | """ 1586 | data = { 1587 | "club_id": int(club_id), 1588 | "is_community": is_community 1589 | } 1590 | req = requests.post(f"{self.API_URL}/update_is_community", headers=self.HEADERS, json=data) 1591 | return req.json() 1592 | 1593 | @require_authentication 1594 | def update_club_description(self, club_id, description): 1595 | """ (Clubhouse, int, str) -> dict 1596 | 1597 | Update description of the given Club 1598 | """ 1599 | data = { 1600 | "club_id": int(club_id), 1601 | "description": description 1602 | } 1603 | req = requests.post(f"{self.API_URL}/update_club_description", headers=self.HEADERS, json=data) 1604 | return req.json() 1605 | 1606 | @require_authentication 1607 | def update_club_rules(self, club_id='', rules=()): 1608 | """ (Clubhouse, str, list) -> dict 1609 | 1610 | Update Club's rules (Maximum upto 3 rules) 1611 | rules: [{'desc': "text", "title": "text"}, ...] 1612 | """ 1613 | data = { 1614 | "club_id": int(club_id), 1615 | "rules": rules if rules else [], 1616 | } 1617 | req = requests.post(f"{self.API_URL}/update_club_rules", headers=self.HEADERS, json=data) 1618 | return req.json() 1619 | 1620 | @require_authentication 1621 | def get_events_for_user(self, user_id='', page_size=25, page=1): 1622 | """ (Clubhouse, str, int, int) -> dict 1623 | 1624 | Get events for the specific user. 1625 | """ 1626 | query = f"user_id={user_id}&page_size={page_size}&page={page}" 1627 | req = requests.get(f"{self.API_URL}/get_events_for_user?{query}", headers=self.HEADERS) 1628 | return req.json() 1629 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stypr/clubhouse-py/fde10929d26bd6e3e4d792633617a3b5baed3d9d/icon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyboard 2 | requests 3 | rich 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -u 2 | #-*- coding: utf-8 -*- 3 | """ 4 | setup.py 5 | For pypi 6 | """ 7 | from setuptools import setup 8 | 9 | def _requires_from_file(filename): 10 | return open(filename).read().splitlines() 11 | 12 | 13 | with open("README.md", "r") as fh: 14 | long_description = fh.read() 15 | 16 | setup( 17 | name="clubhouse-py", 18 | packages=["clubhouse"], 19 | version="434.1", 20 | license="MIT", 21 | description=("Clubhouse API written in Python. Standalone client included." + 22 | "For reference and education purposes only."), 23 | author="Harold Kim", 24 | author_email="root@stypr.com", 25 | long_description=long_description, 26 | long_description_content_type="text/markdown", 27 | url="https://github.com/stypr/clubhouse-py", 28 | download_url="https://github.com/stypr/clubhouse-py/archive/v434.tar.gz", 29 | keywords=[ 30 | "clubhouse", 31 | "voice-chat", 32 | "clubhouse-client", 33 | "clubhouse-api", 34 | "clubhouse-lib", 35 | ], 36 | install_requires=_requires_from_file("requirements.txt"), 37 | classifiers=[ 38 | "Development Status :: 5 - Production/Stable", 39 | "Intended Audience :: Developers", 40 | "License :: OSI Approved :: MIT License", 41 | "Operating System :: MacOS", 42 | "Operating System :: Microsoft :: Windows :: Windows 10", 43 | "Programming Language :: Python :: 3", 44 | "Programming Language :: Python :: 3.7", 45 | "Programming Language :: Python :: 3.8", 46 | "Programming Language :: Python :: 3.9", 47 | ], 48 | ) 49 | --------------------------------------------------------------------------------