├── 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 | [](https://forthebadge.com)
4 |
5 | ## 🐙 用 Telegram bot 创建 Office 账号
6 |
7 | 
8 | 
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 |
--------------------------------------------------------------------------------