├── readme ├── adeff432.png └── ea7870bb.png ├── requirements.txt ├── code.py ├── LICENSE ├── README.MD ├── office_user.py └── bot.py /readme/adeff432.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realByg/office-user-bot/HEAD/readme/adeff432.png -------------------------------------------------------------------------------- /readme/ea7870bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realByg/office-user-bot/HEAD/readme/ea7870bb.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.12.5 2 | chardet==4.0.0 3 | idna==2.10 4 | pyTelegramBotAPI==3.7.7 5 | requests==2.25.1 6 | tinydb==4.4.0 7 | urllib3==1.26.4 8 | -------------------------------------------------------------------------------- /code.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from tinydb import TinyDB, where 3 | 4 | 5 | class Code: 6 | 7 | def __init__(self): 8 | db = TinyDB('codes.json') 9 | self.tb = db.table('Codes') 10 | 11 | @staticmethod 12 | def _code_gen(): 13 | return secrets.token_urlsafe(5) 14 | 15 | def gen(self, amount: int): 16 | codes = [] 17 | for i in range(amount): 18 | code = self._code_gen() 19 | self.tb.insert({ 20 | 'code': code 21 | }) 22 | codes.append(code) 23 | return codes 24 | 25 | def check(self, code): 26 | return self.tb.get(where('code') == code) 27 | 28 | def del_code(self, code): 29 | self.tb.remove( 30 | where('code') == code 31 | ) 32 | 33 | 34 | if __name__ == '__main__': 35 | c = Code() 36 | c.gen(10) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Byg 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 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 🍼 `office-user-bot` 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) 4 | 5 | ## 🐙 用 Telegram bot 创建 Office 账号 6 | 7 | ![](readme/ea7870bb.png) 8 | ![](readme/adeff432.png) 9 | 10 | ### 🥼 环境 11 | 12 | ``` 13 | python 3.6+ 14 | ``` 15 | 16 | ### 💢 用法 17 | 18 | #### 1. 在 @botfather 新建 bot 19 | 20 | #### 2. 下载项目 21 | 22 | ``` 23 | git clone https://github.com/zayabighead/office-user-bot.git 24 | cd office-user-bot 25 | pip install -r requirements.txt 26 | ``` 27 | 28 | #### 3. 同目录下新建 `config.json` 并如下编辑 29 | 30 | ``` 31 | { 32 | "bot": { 33 | "admin": bot 管理员,填 tg 的用户 id, 34 | "notify": true / false 是否接受新建账号时的推送, 35 | "token": "bot 的 token,从 botfather 获取" 36 | }, 37 | "aad": { 38 | "clientId": "aad app 的 client id", 39 | "clientSecret": "aad app 的 client secret", 40 | "tenantId": "aad app 的 tenant id" 41 | }, 42 | "office": { 43 | "subscriptions": [ 44 | { 45 | "name": "订阅显示名称", 46 | "sku": "订阅 id" 47 | } 48 | ], 49 | "domains": [ 50 | { 51 | "display": "@******.org 仅供显示域名", 52 | "value": "@abcde.org 实际域名" 53 | } 54 | ] 55 | }, 56 | "banned": { 57 | "tgId": [要拉黑的 tg 用户的 id], 58 | "officeUsername": [ 59 | "admin", 60 | "用户名黑名单" 61 | ] 62 | } 63 | } 64 | ``` 65 | 66 | #### 4. 运行 67 | 68 | ``` 69 | python bot.py 70 | ``` 71 | 72 | 输入任意消息 bot 即会回复 73 | 74 | 生成的激活码保存在同目录下的 `codes.json` -------------------------------------------------------------------------------- /office_user.py: -------------------------------------------------------------------------------- 1 | import json 2 | import secrets 3 | import requests 4 | from time import time 5 | 6 | 7 | class OfficeUser: 8 | 9 | def __init__(self, client_id: str, tenant_id: str, client_secret: str): 10 | self._client_id = client_id 11 | self._client_secret = client_secret 12 | self._tenant_id = tenant_id 13 | self._token = None 14 | self._token_expires = None 15 | 16 | @staticmethod 17 | def _password_gen(): 18 | return secrets.token_urlsafe(8) 19 | 20 | def _refresh_token(self): 21 | if self._token is None or \ 22 | time() > self._token_expires: 23 | self._get_token() 24 | 25 | def _get_token(self): 26 | r = requests.post( 27 | url=f'https://login.microsoftonline.com/{self._tenant_id}/oauth2/v2.0/token', 28 | headers={ 29 | 'Content-Type': 'application/x-www-form-urlencoded' 30 | }, 31 | data={ 32 | 'grant_type': 'client_credentials', 33 | 'client_id': self._client_id, 34 | 'client_secret': self._client_secret, 35 | 'scope': 'https://graph.microsoft.com/.default' 36 | } 37 | ) 38 | data = r.json() 39 | 40 | if r.status_code != 200 or \ 41 | 'error' in data: 42 | raise Exception(json.dumps(data)) 43 | 44 | self._token = data['access_token'] 45 | self._token_expires = int(data['expires_in']) + int(time()) 46 | 47 | def _assign_license(self, email: str, sku_id: str): 48 | self._refresh_token() 49 | 50 | r = requests.post( 51 | url=f'https://graph.microsoft.com/v1.0/users/{email}/assignLicense', 52 | headers={ 53 | 'Authorization': f'Bearer {self._token}', 54 | 'Content-Type': 'application/json' 55 | }, 56 | json={ 57 | 'addLicenses': [{ 58 | 'disabledPlans': [], 59 | 'skuId': sku_id 60 | }], 61 | 'removeLicenses': [] 62 | } 63 | ) 64 | data = r.json() 65 | 66 | if r.status_code != 200 or \ 67 | 'error' in data: 68 | raise Exception(json.dumps(data)) 69 | 70 | def _create_user( 71 | self, 72 | display_name: str, 73 | username: str, 74 | password: str, 75 | domain: str, 76 | location: str = 'CN' 77 | ): 78 | self._refresh_token() 79 | 80 | r = requests.post( 81 | url='https://graph.microsoft.com/v1.0/users', 82 | headers={ 83 | 'Authorization': f'Bearer {self._token}', 84 | 'Content-Type': 'application/json' 85 | }, 86 | json={ 87 | 'accountEnabled': True, 88 | 'displayName': display_name, 89 | 'mailNickname': username, 90 | 'passwordPolicies': 'DisablePasswordExpiration, DisableStrongPassword', 91 | 'passwordProfile': { 92 | 'password': password, 93 | 'forceChangePasswordNextSignIn': True 94 | }, 95 | 'userPrincipalName': username + domain, 96 | 'usageLocation': location 97 | } 98 | ) 99 | data = r.json() 100 | 101 | if r.status_code != 201 or \ 102 | 'error' in data: 103 | raise Exception(json.dumps(data)) 104 | 105 | def create_account( 106 | self, 107 | username: str, 108 | domain: str, 109 | sku_id: str, 110 | display_name: str = None, 111 | password: str = None, 112 | location: str = 'CN' 113 | ): 114 | if display_name is None: 115 | display_name = username 116 | 117 | if password is None: 118 | password = self._password_gen() 119 | 120 | email = username + domain 121 | 122 | self._create_user( 123 | display_name=display_name, 124 | username=username, 125 | password=password, 126 | domain=domain, 127 | location=location 128 | ) 129 | self._assign_license( 130 | email=email, 131 | sku_id=sku_id 132 | ) 133 | 134 | return { 135 | 'email': email, 136 | 'password': password 137 | } 138 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import logging 4 | import traceback 5 | 6 | # noinspection PyPackageRequirements 7 | import telebot 8 | # noinspection PyPackageRequirements 9 | from telebot import types 10 | 11 | from code import Code 12 | from office_user import OfficeUser 13 | 14 | config = json.load(open('config.json')) 15 | 16 | bot = telebot.TeleBot( 17 | token=config['bot']['token'], 18 | parse_mode='HTML' 19 | ) 20 | 21 | user_dict = { 22 | # 'user_id': { 23 | # 'selected_sub': {}, 24 | # 'selected_domain': '', 25 | # 'username': '', 26 | # 'code': '' 27 | # } 28 | } 29 | 30 | OU = OfficeUser( 31 | client_id=config['aad']['clientId'], 32 | tenant_id=config['aad']['tenantId'], 33 | client_secret=config['aad']['clientSecret'] 34 | ) 35 | C = Code() 36 | 37 | 38 | def start(m): 39 | if m.from_user.id == config['bot']['admin']: 40 | bot.send_message( 41 | text='欢迎使用 Office User Bot\n\n' 42 | '可用的命令有:\n' 43 | '/create 创建 Office 账号\n' 44 | '/gen 10 生成十个激活码\n' 45 | '/about 关于 Bot', 46 | chat_id=m.from_user.id 47 | ) 48 | 49 | else: 50 | bot.send_message( 51 | text='欢迎使用 Office User Bot\n\n' 52 | '可用的命令有:\n' 53 | '/create 创建 Office 账号\n' 54 | '/about 关于 Bot', 55 | chat_id=m.from_user.id 56 | ) 57 | 58 | 59 | def gen(m): 60 | if m.from_user.id == config['bot']['admin']: 61 | amount = int(str(m.text).strip().split('/gen')[1].strip()) 62 | codes = C.gen(amount) 63 | bot.send_message( 64 | text='\n'.join(codes), 65 | chat_id=m.from_user.id 66 | ) 67 | 68 | 69 | def about(m): 70 | bot.send_message( 71 | text='Office User Bot', 72 | chat_id=m.from_user.id 73 | ) 74 | 75 | 76 | def create(m): 77 | buttons = [types.KeyboardButton( 78 | text=sub['name'] 79 | ) for sub in config['office']['subscriptions']] 80 | 81 | markup = types.ReplyKeyboardMarkup(row_width=1) 82 | markup.add(*buttons) 83 | msg = bot.send_message( 84 | text='欢迎创建 Office 账号\n\n请选择订阅:', 85 | chat_id=m.from_user.id, 86 | reply_markup=markup 87 | ) 88 | bot.register_next_step_handler(msg, select_subscription) 89 | 90 | 91 | def select_subscription(m): 92 | selected_sub = next( 93 | (sub for sub in config['office']['subscriptions'] if sub['name'] == m.text), 94 | None 95 | ) 96 | if selected_sub is None: 97 | msg = bot.send_message( 98 | text='订阅不存在,请重新回复:', 99 | chat_id=m.from_user.id, 100 | ) 101 | bot.register_next_step_handler(msg, select_subscription) 102 | return 103 | user_dict[m.from_user.id] = {} 104 | user_dict[m.from_user.id]['selected_sub'] = selected_sub 105 | 106 | markup = types.ReplyKeyboardRemove(selective=False) 107 | msg = bot.send_message( 108 | text='请回复想要的用户名:', 109 | chat_id=m.from_user.id, 110 | reply_markup=markup 111 | ) 112 | bot.register_next_step_handler(msg, input_username) 113 | 114 | 115 | def input_username(m): 116 | username = str(m.text).strip() 117 | if username in config['banned']['officeUsername'] or \ 118 | not re.match(r'^[a-zA-Z0-9\-]+$', username): 119 | msg = bot.send_message( 120 | text='用户名含有特殊字符或在黑名单中,请重新回复:', 121 | chat_id=m.from_user.id, 122 | ) 123 | bot.register_next_step_handler(msg, input_username) 124 | return 125 | user_dict[m.from_user.id]['username'] = username 126 | 127 | buttons = [types.KeyboardButton( 128 | text=d['display'] 129 | ) for d in config['office']['domains']] 130 | markup = types.ReplyKeyboardMarkup(row_width=1) 131 | markup.add(*buttons) 132 | msg = bot.send_message( 133 | text='请选择账号后缀:', 134 | chat_id=m.from_user.id, 135 | reply_markup=markup 136 | ) 137 | bot.register_next_step_handler(msg, select_domain) 138 | 139 | 140 | def select_domain(m): 141 | selected_domain = next( 142 | (d for d in config['office']['domains'] if d['display'] == m.text), 143 | None 144 | ) 145 | if selected_domain is None: 146 | msg = bot.send_message( 147 | text='后缀不存在,请重新回复:', 148 | chat_id=m.from_user.id, 149 | ) 150 | bot.register_next_step_handler(msg, select_domain) 151 | return 152 | user_dict[m.from_user.id]['selected_domain'] = selected_domain 153 | 154 | markup = types.ReplyKeyboardRemove(selective=False) 155 | msg = bot.send_message( 156 | text='请回复激活码:', 157 | chat_id=m.from_user.id, 158 | reply_markup=markup 159 | ) 160 | bot.register_next_step_handler(msg, input_code) 161 | 162 | 163 | def input_code(m): 164 | code = str(m.text).strip() 165 | if not C.check(code): 166 | bot.send_message( 167 | text='激活码无效!', 168 | chat_id=m.from_user.id, 169 | ) 170 | return 171 | # todo: lock code 172 | user_dict[m.from_user.id]['code'] = code 173 | 174 | selected_sub_name = user_dict[m.from_user.id]['selected_sub']['name'] 175 | selected_domain_display = user_dict[m.from_user.id]['selected_domain']['display'] 176 | username = user_dict[m.from_user.id]['username'] 177 | 178 | markup = types.InlineKeyboardMarkup(row_width=2) 179 | markup.add( 180 | types.InlineKeyboardButton(text='取消', callback_data='cancel'), 181 | types.InlineKeyboardButton(text='确认', callback_data='create'), 182 | ) 183 | bot.send_message( 184 | text=f'{selected_sub_name}\n' 185 | f'{username}@{selected_domain_display}\n\n' 186 | '激活码有效,确认创建账号吗?', 187 | chat_id=m.from_user.id, 188 | reply_markup=markup 189 | ) 190 | 191 | 192 | def notify_admin(call): 193 | if config['bot']['notify']: 194 | user_id = call.from_user.id 195 | 196 | selected_sub_name = user_dict[user_id]['selected_sub']['name'] 197 | selected_domain_value = user_dict[user_id]['selected_domain']['value'] 198 | username = user_dict[user_id]['username'] 199 | code = user_dict[user_id]['code'] 200 | tg_name = f'{call.from_user.first_name or ""} {call.from_user.last_name or ""}'.strip() 201 | 202 | bot.send_message( 203 | text=f'{tg_name} 刚刚用激活码 {code} 创建了 ' 204 | f'{username}{selected_domain_value} ({selected_sub_name})', 205 | chat_id=config['bot']['admin'] 206 | ) 207 | 208 | 209 | def create_account(call): 210 | user_id = call.from_user.id 211 | msg_id = call.message.message_id 212 | chat_id = call.from_user.id 213 | 214 | if user_dict.get(user_id) is None: 215 | return 216 | 217 | bot.edit_message_text( 218 | chat_id=chat_id, 219 | text='创建账号中,请稍等...', 220 | message_id=msg_id 221 | ) 222 | 223 | try: 224 | account = OU.create_account( 225 | username=user_dict[user_id]['username'], 226 | domain=user_dict[user_id]['selected_domain']['value'], 227 | sku_id=user_dict[user_id]['selected_sub']['sku'], 228 | display_name=f'{call.from_user.first_name or ""} {call.from_user.last_name or ""}'.strip(), 229 | ) 230 | C.del_code(user_dict[user_id]['code']) 231 | 232 | selected_sub_name = user_dict[user_id]['selected_sub']['name'] 233 | bot.send_message( 234 | text='账号创建成功\n' 235 | '===========\n\n' 236 | f'订阅: {selected_sub_name}\n' 237 | f'邮箱: {account["email"]}\n' 238 | f'初始密码: {account["password"]}\n\n' 239 | f'登录地址: https://office.com', 240 | chat_id=chat_id 241 | ) 242 | 243 | notify_admin(call) 244 | del user_dict[user_id] 245 | 246 | except Exception as e: 247 | error = json.loads(str(e)) 248 | if 'userPrincipalName already exists' in error['error']['message']: 249 | text = '用户名已存在,请换个用户名重新创建账号' 250 | 251 | else: 252 | text = '哎呀出错了' 253 | 254 | bot.send_message( 255 | text=text, 256 | chat_id=chat_id 257 | ) 258 | 259 | 260 | @bot.message_handler(content_types=['text']) 261 | def handle_text(m): 262 | # noinspection PyBroadException 263 | try: 264 | if m.from_user.id in config['banned']['tgId']: 265 | return 266 | 267 | text = str(m.text).strip() 268 | 269 | bot.send_chat_action( 270 | chat_id=m.from_user.id, 271 | action='typing' 272 | ) 273 | if text == '/create': 274 | create(m) 275 | 276 | elif text == '/about': 277 | about(m) 278 | 279 | elif text.startswith('/gen'): 280 | gen(m) 281 | 282 | else: 283 | start(m) 284 | 285 | except Exception: 286 | traceback.print_exc() 287 | 288 | 289 | @bot.callback_query_handler(func=lambda call: True) 290 | def handle_callback(call): 291 | if call.data == 'create': 292 | create_account(call) 293 | 294 | elif call.data == 'cancel': 295 | bot.edit_message_text( 296 | chat_id=call.from_user.id, 297 | text='已取消', 298 | message_id=call.message.message_id 299 | ) 300 | 301 | 302 | logger = telebot.logger 303 | telebot.logger.setLevel(logging.DEBUG) 304 | 305 | bot.polling(none_stop=True) 306 | --------------------------------------------------------------------------------