├── api ├── easygoogletranslate │ ├── __init__.py │ └── easygoogletranslate.py ├── templates │ ├── index.html │ ├── error.html │ ├── demo.html │ └── auth.html └── index.py ├── .gitattributes ├── LICENSE ├── requirements.txt ├── vercel.json └── README.md /api/easygoogletranslate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under CC BY-NC 4.0. 2 | https://creativecommons.org/licenses/by-nc/4.0/ 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.3 2 | requests==2.32.0 3 | urllib3==2.2.3 4 | easygoogletranslate==0.0.4 5 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/(.*)", "destination": "/api/index" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /api/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Riot Auth 7 | 8 | 9 | https://github.com/v1bt/riot-qr-auth 10 | 11 | 12 | -------------------------------------------------------------------------------- /api/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ error_message }} 7 | 8 | 9 |

{{ status_code }}

10 |

{{ error_message }}

11 |
{{ message }}
12 | 13 | -------------------------------------------------------------------------------- /api/templates/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Riot Auth 5 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /api/easygoogletranslate/easygoogletranslate.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import requests 3 | import re 4 | import html 5 | import urllib.parse 6 | 7 | # Source: https://github.com/ahmeterenodaci/easygoogletranslate 8 | 9 | class EasyGoogleTranslate: 10 | def __init__(self, source_language='auto', target_language='en', timeout=5): 11 | self.source_language = source_language 12 | self.target_language = target_language 13 | self.timeout = timeout 14 | self.pattern = r'(?s)class="(?:t0|result-container)">(.*?)<' 15 | 16 | def make_request(self, target_language, source_language, text, timeout): 17 | escaped_text = urllib.parse.quote(text.encode('utf8')) 18 | url = 'https://translate.google.com/m?tl=%s&sl=%s&q=%s'%(target_language, source_language, escaped_text) 19 | response = requests.get(url, timeout=timeout) 20 | result = response.text.encode('utf8').decode('utf8') 21 | result = re.findall(self.pattern, result) 22 | 23 | return html.unescape(result[0]) 24 | 25 | def translate(self, text, target_language='', source_language='', timeout=''): 26 | if not target_language: 27 | target_language = self.target_language 28 | if not source_language: 29 | source_language = self.source_language 30 | if not timeout: 31 | timeout = self.timeout 32 | 33 | if type(target_language) is list: 34 | with concurrent.futures.ThreadPoolExecutor() as executor: 35 | futures = [executor.submit(self.make_request, target, source_language, text, timeout) for target in target_language] 36 | return_value = [f.result() for f in futures] 37 | return return_value 38 | return self.make_request(target_language, source_language, text, timeout) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!warning] 2 | > No longer managed 3 | > 4 | > Use https://github.com/RiisDev/RadiantConnect 5 | 6 | # Riot Auth Manager 7 | 8 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fv1bt%2Friot-auth-manager%2F&project-name=riot-auth&repository-name=riot-auth-manager) 9 | 10 | An SDK that helps you obtain Riot access tokens via QR authentication. 11 | 12 | ## Examples 13 | 14 | ### Backend (Python) 15 | Get Token and Cookies 16 | ```python 17 | import requests 18 | import urllib.parse 19 | 20 | BASE_URL = 'https://riot-auth.vercel.app' 21 | 22 | headers = {'country-code': 'en-US'} # This header is optional and can be set to 'auto'. 23 | response = requests.post(f'{BASE_URL}/login_url', headers=headers).json() 24 | login_url = response.get('login_url') 25 | 26 | session_cookies = response.get('session_cookies') 27 | sdk_sid = response.get('sdk_sid') 28 | 29 | enurl = urllib.parse.quote(login_url, safe=':/') 30 | print('Scan QR code:', f'https://api.qrserver.com/v1/create-qr-code/?size=512x512&data={enurl}') 31 | print('\nOr visit:', login_url) 32 | 33 | input('\nPress Enter after auth..') 34 | 35 | try: 36 | token_response = requests.post( 37 | f'{BASE_URL}/get_token', 38 | headers=headers, 39 | json={ 40 | 'session_cookies': session_cookies, 41 | 'sdk_sid': sdk_sid 42 | } 43 | ).json() 44 | except requests.exceptions.JSONDecodeError: 45 | print('Error: Invalid response from server. Please try again.') 46 | exit(1) 47 | 48 | if token_response.get('type') == 'success': 49 | token = token_response['access_token'] 50 | print('\nAccess Token:', token) 51 | 52 | print('\nUser Info:', requests.get('https://auth.riotgames.com/userinfo', 53 | headers={'Authorization': f'Bearer {token}'}).json()) 54 | 55 | print('\nCookies:') 56 | if 'cookies' in token_response: 57 | for cookie_name, cookie_value in token_response['cookies'].items(): 58 | print(f'{cookie_name}: {cookie_value}') 59 | else: 60 | print('No cookies') 61 | else: 62 | print('Error:', token_response.get('error', 'Unknown error occurred')) 63 | ``` 64 |
65 | 66 | Cookie Reauth 67 | 68 | https://github.com/techchrism/riot-auth-test 69 | ```python 70 | import requests 71 | 72 | BASE_URL = 'https://riot-auth.vercel.app' 73 | 74 | ssid = '' 75 | 76 | headers = { 77 | 'ssid': ssid 78 | } 79 | 80 | try: 81 | response = requests.post( 82 | f'{BASE_URL}/cookie_reauth', 83 | headers=headers 84 | ).json() 85 | 86 | if 'access_token' in response: 87 | token = response['access_token'] 88 | print('\nAccess Token:', token) 89 | 90 | user_info = requests.get( 91 | 'https://auth.riotgames.com/userinfo', 92 | headers={'Authorization': f'Bearer {token}'} 93 | ).json() 94 | print('\nUser Info:', user_info) 95 | else: 96 | print('Error:', response.get('error', 'Unknown error')) 97 | 98 | except Exception as e: 99 | print('Error:', str(e)) 100 | ``` 101 | ### With iFrame 102 | [`Check out this demo`](https://riot-auth.vercel.app/demo/) 103 | 104 | [`Use for store (Only 'kr')`](https://valstore.vercel.app/) 105 | ```html 106 | 107 | 108 | 109 | Riot Auth 110 | 117 | 118 | 119 | 120 |
121 | 122 | 130 | 131 | 132 | ``` 133 | > [!NOTE] 134 | > This project includes the source code of the [`easygoogletranslate`](https://github.com/ahmeterenodaci/easygoogletranslate) module to minimize external dependencies. 135 | -------------------------------------------------------------------------------- /api/index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, jsonify 2 | import requests 3 | from urllib.parse import unquote, parse_qs, urlparse 4 | from easygoogletranslate.easygoogletranslate import EasyGoogleTranslate 5 | import uuid 6 | from concurrent.futures import ThreadPoolExecutor 7 | import re 8 | 9 | app = Flask(__name__) 10 | 11 | LANGUAGE_TO_REGION = { 12 | 'en-US': 'NA', 13 | 'ko-KR': 'KR', 14 | 'ja-JP': 'JP', 15 | 'zh-CN': 'CN', 16 | 'zh-TW': 'TW', 17 | 'es-ES': 'EUW', 18 | 'fr-FR': 'EUW', 19 | 'de-DE': 'EUW', 20 | 'ru-RU': 'RU', 21 | 'ar-SA': 'TR', 22 | 'th-TH': 'TH', 23 | 'vi-VN': 'VN', 24 | 'id-ID': 'ID', 25 | 'ms-MY': 'MY', 26 | 'pl-PL': 'EUN', 27 | 'tr-TR': 'TR', 28 | 'ro-RO': 'EUN', 29 | 'hu-HU': 'EUN', 30 | 'el-GR': 'EUN', 31 | 'cs-CZ': 'EUN', 32 | 'pt-BR': 'BR', 33 | 'it-IT': 'EUW' 34 | } 35 | 36 | def get_user_language(): 37 | return request.accept_languages.best_match(LANGUAGE_TO_REGION.keys()) or 'en-US' 38 | 39 | def validate_language(lang): 40 | return lang in LANGUAGE_TO_REGION 41 | 42 | def translate_text(text, language): 43 | if language == "ko-KR": 44 | return text 45 | try: 46 | translator = EasyGoogleTranslate( 47 | source_language='ko', 48 | target_language=language.split('-')[0], 49 | timeout=10 50 | ) 51 | translated = translator.translate(text) 52 | return re.sub(r'&#\d+;', '', translated) 53 | except Exception as e: 54 | raise Exception(f"Translation failed: {str(e)}") 55 | 56 | def handle_error(error_code, message): 57 | user_lang = get_user_language() 58 | try: 59 | error_message = translate_text("오류가 발생했습니다", user_lang) 60 | return render_template('error.html', 61 | status_code=error_code, 62 | message=message, 63 | error_message=error_message, 64 | lang=user_lang), error_code 65 | except: 66 | return render_template('error.html', 67 | status_code=error_code, 68 | message=message, 69 | error_message="An error occurred", 70 | lang='en-US'), error_code 71 | 72 | def parallel_translate_texts(texts, language): 73 | with ThreadPoolExecutor() as executor: 74 | translated_texts = list(executor.map(lambda text: translate_text(text, language), texts)) 75 | return translated_texts 76 | 77 | def new_session(): 78 | session = requests.Session() 79 | return session, str(uuid.uuid4()) 80 | 81 | def get_region_and_language(lang_code): 82 | region = LANGUAGE_TO_REGION.get(lang_code) 83 | language = lang_code.replace('-', '_') 84 | return region, language 85 | 86 | def login_url(country_code): 87 | session, sdk_sid = new_session() 88 | 89 | region, language = get_region_and_language(country_code) 90 | 91 | trace_id = uuid.uuid4().hex 92 | parent_id = uuid.uuid4().hex[:16] 93 | traceparent = f'00-{trace_id}-{parent_id}-00' 94 | 95 | headers1 = { 96 | 'Host': 'clientconfig.rpg.riotgames.com', 97 | 'user-agent': 'RiotGamesApi/24.9.1.4445 client-config (Windows;10;;Professional, x64) riot_client/0', 98 | 'Accept-Encoding': 'deflate, gzip, zstd', 99 | 'Accept': 'application/json', 100 | 'Connection': 'keep-alive', 101 | 'baggage': f'sdksid={sdk_sid}', 102 | 'traceparent': traceparent, 103 | 'country-code': country_code 104 | } 105 | 106 | url1 = 'https://clientconfig.rpg.riotgames.com/api/v1/config/public' 107 | params = { 108 | 'os': 'windows', 109 | 'region': region, 110 | 'app': 'Riot Client', 111 | 'version': '97.0.1.2366', 112 | 'patchline': 'KeystoneFoundationLiveWin' 113 | } 114 | 115 | session.get(url1, headers=headers1, params=params) 116 | headers2 = { 117 | 'Host': 'auth.riotgames.com', 118 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-auth (Windows;10;;Professional, x64) riot_client/0', 119 | 'Accept-Encoding': 'deflate, gzip, zstd', 120 | 'Accept': 'application/json', 121 | 'Connection': 'keep-alive', 122 | 'baggage': f'sdksid={sdk_sid}', 123 | 'traceparent': traceparent, 124 | 'country-code': country_code 125 | } 126 | 127 | session.get('https://auth.riotgames.com/.well-known/openid-configuration', headers=headers2) 128 | 129 | login_data = { 130 | "client_id": "riot-client", 131 | "language": language, 132 | "platform": "windows", 133 | "remember": False, 134 | "type": "auth", 135 | "qrcode": {} 136 | } 137 | 138 | headers3 = { 139 | 'Host': 'authenticate.riotgames.com', 140 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-authenticator (Windows;10;;Professional, x64) riot_client/0', 141 | 'Accept-Encoding': 'deflate, gzip, zstd', 142 | 'Accept': 'application/json', 143 | 'Connection': 'keep-alive', 144 | 'Content-Type': 'application/json', 145 | 'baggage': f'sdksid={sdk_sid}', 146 | 'traceparent': traceparent, 147 | 'country-code': country_code 148 | } 149 | 150 | response = session.post('https://authenticate.riotgames.com/api/v1/login', headers=headers3, json=login_data) 151 | response_json = response.json() 152 | 153 | cluster = response_json.get("cluster") 154 | suuid = response_json.get("suuid") 155 | timestamp = response_json.get("timestamp") 156 | 157 | if not cluster or not suuid or not timestamp: 158 | return None, "Required data is missing from the response." 159 | 160 | login_url = f'https://qrlogin.riotgames.com/riotmobile?cluster={cluster}&suuid={suuid}×tamp={timestamp}&utm_source=riotclient&utm_medium=client&utm_campaign=qrlogin-riotmobile' 161 | 162 | return { 163 | 'login_url': login_url, 164 | 'session': session, 165 | 'sdk_sid': sdk_sid, 166 | 'cluster': cluster, 167 | 'suuid': suuid, 168 | 'timestamp': timestamp 169 | }, None 170 | 171 | def get_login_token(session, sdk_sid, country_code): 172 | traceparent = f'00-{uuid.uuid4().hex}-{uuid.uuid4().hex[:16]}-00' 173 | check_headers = { 174 | 'Host': 'authenticate.riotgames.com', 175 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-authenticator (Windows;10;;Professional, x64) riot_client/0', 176 | 'Accept-Encoding': 'deflate, gzip, zstd', 177 | 'Accept': 'application/json', 178 | 'Connection': 'keep-alive', 179 | 'Content-Type': 'application/json', 180 | 'baggage': f'sdksid={sdk_sid}', 181 | 'traceparent': traceparent, 182 | 'country-code': country_code 183 | } 184 | 185 | response = session.get('https://authenticate.riotgames.com/api/v1/login', headers=check_headers) 186 | if response.status_code == 200: 187 | return response.json() 188 | return None 189 | 190 | def get_access_token(login_token): 191 | session = requests.Session() 192 | sdk_sid = str(uuid.uuid4()) 193 | traceparent = f'00-{uuid.uuid4().hex}-{uuid.uuid4().hex[:16]}-00' 194 | 195 | headers1 = { 196 | 'Host': 'auth.riotgames.com', 197 | 'user-agent': 'RiotGamesApi/24.10.1.4471 rso-auth (Windows;10;;Professional, x64) riot_client/0', 198 | 'Accept-Encoding': 'deflate, gzip, zstd', 199 | 'Accept': 'application/json', 200 | 'Connection': 'keep-alive', 201 | 'Content-Type': 'application/json', 202 | 'baggage': f'sdksid={sdk_sid}', 203 | 'traceparent': traceparent 204 | } 205 | 206 | data1 = { 207 | "authentication_type": None, 208 | "code_verifier": "", 209 | "login_token": login_token, 210 | "persist_login": False 211 | } 212 | 213 | response1 = session.post('https://auth.riotgames.com/api/v1/login-token', headers=headers1, json=data1) 214 | 215 | if response1.status_code != 204: 216 | return None, "Login token submission failed", None 217 | 218 | headers2 = { 219 | 'Host': 'auth.riotgames.com', 220 | 'user-agent': 'RiotGamesApi/24.10.1.4471 rso-auth (Windows;10;;Professional, x64) riot_client/0', 221 | 'Accept-Encoding': 'deflate, gzip, zstd', 222 | 'Accept': 'application/json', 223 | 'Connection': 'keep-alive', 224 | 'Content-Type': 'application/json', 225 | 'baggage': f'sdksid={sdk_sid}', 226 | 'traceparent': traceparent 227 | } 228 | 229 | data2 = { 230 | "acr_values": "", 231 | "claims": "", 232 | "client_id": "riot-client", 233 | "code_challenge": "", 234 | "code_challenge_method": "", 235 | "nonce": str(uuid.uuid4()), 236 | "redirect_uri": "http://localhost/redirect", 237 | "response_type": "token id_token", 238 | "scope": "openid link ban lol_region account" 239 | } 240 | 241 | response2 = session.post('https://auth.riotgames.com/api/v1/authorization', headers=headers2, json=data2) 242 | 243 | if response2.status_code != 200: 244 | return None, "Authorization failed", None 245 | 246 | cookies = dict(response2.cookies) 247 | 248 | response_data = response2.json() 249 | if 'response' in response_data and 'parameters' in response_data['response']: 250 | return response_data['response']['parameters']['uri'], None, cookies 251 | 252 | return None, "Failed to get access token URI", None 253 | 254 | current_session_data = None 255 | 256 | @app.route('/login_url', methods=['POST']) 257 | def login_url_route(): 258 | country_code = request.headers.get('country-code') 259 | 260 | if not country_code or country_code.lower() == 'auto': 261 | accept_language = request.headers.get('Accept-Language', '') 262 | if ',' in accept_language: 263 | accept_language = accept_language.split(',')[0] 264 | country_code = accept_language.split(';')[0] 265 | if not country_code: 266 | country_code = 'en-US' 267 | 268 | result, error = login_url(country_code) 269 | if error: 270 | return jsonify({'error': error}), 400 271 | 272 | return jsonify({ 273 | 'login_url': result['login_url'], 274 | 'cluster': result['cluster'], 275 | 'suuid': result['suuid'], 276 | 'timestamp': result['timestamp'], 277 | 'session_cookies': dict(result['session'].cookies), 278 | 'sdk_sid': result['sdk_sid'] 279 | }) 280 | 281 | @app.route('/') 282 | def index(): 283 | return render_template('index.html') 284 | 285 | @app.route('/demo/') 286 | def demo(): 287 | return render_template('demo.html') 288 | 289 | @app.after_request 290 | def after_request(response): 291 | response.headers.remove('X-Frame-Options') 292 | response.headers['Access-Control-Allow-Origin'] = '*' 293 | response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' 294 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type' 295 | return response 296 | 297 | @app.route('/auth//') 298 | def auth(lang): 299 | try: 300 | language = unquote(lang) 301 | 302 | if language.lower() == 'auto': 303 | accept_language = request.headers.get('Accept-Language', '') 304 | if ',' in accept_language: 305 | accept_language = accept_language.split(',')[0] 306 | language = accept_language.split(';')[0] 307 | else: 308 | if not validate_language(language): 309 | return handle_error(404, f"Unsupported language: {language}") 310 | 311 | texts = [ 312 | '로그인', '라이엇 모바일을 통해 로그인', '로그인 Url 생성중..', 313 | '로그인 Url 생성 실패', '모바일 환경에서 바로 로그인하기', 314 | 'QR코드를 스캔하거나 Url에 방문해주세요.', '로그인 Url만료 새 Url을 생성합니다.', 315 | '남은 시간', '토큰 확인 중 오류 발생', '로그인 완료', 316 | '고객지원', '개인정보 처리방침', '서비스 약관', '쿠키 설정', '언어를 선택하세요.', '소스 코드' 317 | ] 318 | 319 | translated_texts = parallel_translate_texts(texts, language) 320 | 321 | return render_template( 322 | 'auth.html', 323 | title=translated_texts[0], 324 | dis=translated_texts[1], 325 | wait=translated_texts[2], 326 | fail=translated_texts[3], 327 | md=translated_texts[4], 328 | plzscan=translated_texts[5], 329 | end=translated_texts[6], 330 | rm=translated_texts[7], 331 | tf=translated_texts[8], 332 | sus=translated_texts[9], 333 | gg=translated_texts[10], 334 | pp=translated_texts[11], 335 | sp=translated_texts[12], 336 | cs=translated_texts[13], 337 | cl=translated_texts[14], 338 | ss=translated_texts[15], 339 | lang=language.split('-')[0] 340 | ) 341 | except Exception as e: 342 | return handle_error(500, str(e)) 343 | 344 | 345 | @app.route('/get_token', methods=['POST']) 346 | def fetch_token(): 347 | session_cookies = request.json.get('session_cookies') 348 | sdk_sid = request.json.get('sdk_sid') 349 | country_code = request.headers.get('country-code', 'en-US') 350 | 351 | if not session_cookies or not sdk_sid: 352 | return jsonify({'error': 'Session data required'}), 400 353 | 354 | session = requests.Session() 355 | session.cookies.update(session_cookies) 356 | 357 | token_data = get_login_token(session, sdk_sid, country_code) 358 | 359 | if not token_data: 360 | return jsonify({'error': 'Token expired'}) 361 | 362 | if token_data.get('type') == 'success': 363 | login_token = token_data['success']['login_token'] 364 | uri, error, cookies = get_access_token(login_token) 365 | if error: 366 | return jsonify({'error': error}), 400 367 | 368 | try: 369 | access_token = uri.split('#access_token=')[1].split('&')[0] 370 | token_data['access_token'] = access_token 371 | except: 372 | return jsonify({'error': 'Failed to extract access token'}), 400 373 | 374 | token_data['cookies'] = cookies 375 | token_data['uri'] = uri 376 | 377 | return jsonify(token_data) 378 | 379 | 380 | @app.route('/cookie_reauth', methods=['POST']) 381 | def cookie_reauth(): 382 | ssid = request.headers.get('ssid') 383 | if not ssid: 384 | return jsonify({'error': 'Cookie is required'}), 400 385 | 386 | params = { 387 | 'redirect_uri': 'https://playvalorant.com/opt_in', 388 | 'client_id': 'play-valorant-web-prod', 389 | 'response_type': 'token id_token', 390 | 'nonce': '1', 391 | 'scope': 'account openid' 392 | } 393 | 394 | try: 395 | response = requests.get( 396 | 'https://auth.riotgames.com/authorize', 397 | params=params, 398 | cookies={'ssid': ssid}, 399 | allow_redirects=False 400 | ) 401 | 402 | location = response.headers.get('Location', '') 403 | if 'access_token' not in location: 404 | return jsonify({'error': 'Invalid Cookie'}), 401 405 | 406 | fragment = urlparse(location).fragment 407 | tokens = parse_qs(fragment) 408 | access_token = tokens.get('access_token', [None])[0] 409 | 410 | if not access_token: 411 | return jsonify({'error': 'Failed to get access token'}), 500 412 | 413 | return jsonify({'access_token': access_token}) 414 | 415 | except Exception as e: 416 | return jsonify({'error': str(e)}), 500 417 | 418 | 419 | @app.errorhandler(404) 420 | def not_found_error(error): 421 | return handle_error(404, "Page not found") 422 | 423 | @app.errorhandler(500) 424 | def internal_error(error): 425 | return handle_error(500, "Internal server error") 426 | 427 | # if __name__ == '__main__': 428 | # app.run(host='0.0.0.0', port=5000, debug=True) 429 | -------------------------------------------------------------------------------- /api/templates/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 | 45 |
46 |
47 |
48 |
49 |
50 |
51 |
{{dis}}
52 |
53 | 54 |
55 |
56 |
57 |
75 |
76 |
77 |
AccessToken:
78 |

 79 |                                 
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 139 |
140 |
141 |
142 | 310 | 311 | 312 | --------------------------------------------------------------------------------