|
|
|
@@ -0,0 +1,169 @@
|
|
|
|
|
import type { LoginAccountIdentity, DeviceLoginMode } from '../../protocol/contracts/login-contract.ts';
|
|
|
|
|
import type { LoginPolicy } from '../../presentation/login-policy.ts';
|
|
|
|
|
import { generate } from '../local/visitor-account.ts';
|
|
|
|
|
import { readPhoneCode } from './phone-code.ts';
|
|
|
|
|
|
|
|
|
|
export interface LoginNativeSettings {
|
|
|
|
|
accreditlogin(provider: number): void;
|
|
|
|
|
getcompareCode(): number | string;
|
|
|
|
|
}
|
|
|
|
|
interface Options {
|
|
|
|
|
storage: {getItem(key:string):string|null;setItem(key:string,value:string):void};
|
|
|
|
|
settings?:LoginNativeSettings; random():number; now():number;
|
|
|
|
|
gameid:string|number;agentid:string|number;policy:LoginPolicy;
|
|
|
|
|
submit(account:LoginAccountIdentity,device:DeviceLoginMode):void;tip(message:string):void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** MD5 is the legacy nickname derivation, not a security primitive. */
|
|
|
|
|
export function nicknameHash(input:string):string {
|
|
|
|
|
const bytes=Array.from(new TextEncoder().encode(input));const bitLength=bytes.length*8;
|
|
|
|
|
bytes.push(128);while(bytes.length%64!==56)bytes.push(0);
|
|
|
|
|
for(let i=0;i<8;i++)bytes.push(i<4?(bitLength>>>(8*i))&255:0);
|
|
|
|
|
const shifts=[7,12,17,22,5,9,14,20,4,11,16,23,6,10,15,21];
|
|
|
|
|
let state=[0x67452301,0xefcdab89,0x98badcfe,0x10325476];
|
|
|
|
|
for(let offset=0;offset<bytes.length;offset+=64){
|
|
|
|
|
const words=Array.from({length:16},(_,j)=>bytes[offset+j*4]|bytes[offset+j*4+1]<<8|bytes[offset+j*4+2]<<16|bytes[offset+j*4+3]<<24);
|
|
|
|
|
let [a,b,c,d]=state;
|
|
|
|
|
for(let i=0;i<64;i++){
|
|
|
|
|
const round=Math.floor(i/16);let f:number,g:number;
|
|
|
|
|
if(round===0){f=(b&c)|(~b&d);g=i;}else if(round===1){f=(d&b)|(~d&c);g=(5*i+1)%16;}
|
|
|
|
|
else if(round===2){f=b^c^d;g=(3*i+5)%16;}else{f=c^(b|~d);g=(7*i)%16;}
|
|
|
|
|
const sum=(a+f+Math.floor(Math.abs(Math.sin(i+1))*4294967296)+words[g])|0;
|
|
|
|
|
const shift=shifts[round*4+i%4];const next=(b+((sum<<shift)|(sum>>>(32-shift))))|0;
|
|
|
|
|
a=d;d=c;c=b;b=next;
|
|
|
|
|
}
|
|
|
|
|
state=state.map((v,i)=>(v+[a,b,c,d][i])|0);
|
|
|
|
|
}
|
|
|
|
|
return state.map(v=>[0,8,16,24].map(s=>((v>>>s)&255).toString(16).padStart(2,'0')).join('')).join('');
|
|
|
|
|
}
|
|
|
|
|
function record(value:unknown):Record<string,unknown>{
|
|
|
|
|
if(typeof value==='string')value=JSON.parse(value);
|
|
|
|
|
if(!value||typeof value!=='object'||Array.isArray(value))throw Error('Invalid sharelogin account');
|
|
|
|
|
return value as Record<string,unknown>;
|
|
|
|
|
}
|
|
|
|
|
function accountFrom(value:unknown):LoginAccountIdentity {
|
|
|
|
|
const data=record(value);
|
|
|
|
|
for(const field of ['openid','headimgurl','nickname','city','Province'])if(typeof data[field]!=='string')throw Error(`Invalid sharelogin.${field}`);
|
|
|
|
|
if(typeof data.unionid!=='string'&&typeof data.unionid!=='number')throw Error('Invalid sharelogin.unionid');
|
|
|
|
|
const sex=data.sex?data.sex:1; // Func.sharelogin explicitly owns this default.
|
|
|
|
|
if(typeof sex!=='number'||!Number.isFinite(sex))throw Error('Invalid sharelogin.sex');
|
|
|
|
|
if(!data.nickname&&!data.headimgurl)throw Error('授权失败,请重新授权!');
|
|
|
|
|
return {openid:data.openid as string,avatar:data.headimgurl as string,nickname:data.nickname as string,
|
|
|
|
|
city:data.city as string,province:data.Province as string,unionid:data.unionid as string|number,sex};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Exact native/cached account boundary; UI never invents account or server fields. */
|
|
|
|
|
/** 账户统一入口:协议确认、缓存选择、原生授权及提交模式均由此处管理。 */
|
|
|
|
|
export class LoginAccounts {
|
|
|
|
|
readonly versionState:0|1;
|
|
|
|
|
private hiddenClicks=0;
|
|
|
|
|
/** 每次启动默认同意;按钮、缓存登录及原生回调读取同一份状态。 */
|
|
|
|
|
private agreementAccepted=true;
|
|
|
|
|
get agreed():boolean{return this.agreementAccepted;}
|
|
|
|
|
setAgreement(accepted:boolean):void{
|
|
|
|
|
if(accepted&&!this.agreementAccepted)this.options.tip('');
|
|
|
|
|
this.agreementAccepted=accepted;
|
|
|
|
|
}
|
|
|
|
|
/** 未同意时只提示,不调用授权、不写缓存、不提交请求。 */
|
|
|
|
|
requireAgreement():boolean{
|
|
|
|
|
if(this.agreementAccepted)return true;
|
|
|
|
|
this.options.tip('未同意协议');return false;
|
|
|
|
|
}
|
|
|
|
|
constructor(private readonly options:Options){
|
|
|
|
|
// Browser source explicitly uses the legacy getVersionState default of zero.
|
|
|
|
|
const state=options.settings?Number(options.settings.getcompareCode()):0;
|
|
|
|
|
if(state!==0&&state!==1)throw Error('Invalid getcompareCode result');this.versionState=state;
|
|
|
|
|
}
|
|
|
|
|
private read(key:string,cookie:boolean):Record<string,unknown>|null {
|
|
|
|
|
if(cookie&&this.versionState===0){
|
|
|
|
|
const expiry=this.options.storage.getItem('tsgame_validtime');
|
|
|
|
|
if(expiry===null)return null;
|
|
|
|
|
if(!Number.isFinite(Number(expiry)))throw Error('Invalid tsgame_validtime');
|
|
|
|
|
if(this.options.now()>=Number(expiry))return null;
|
|
|
|
|
}
|
|
|
|
|
const raw=this.options.storage.getItem(key);return raw===null?null:record(raw);
|
|
|
|
|
}
|
|
|
|
|
private save(account:LoginAccountIdentity,key:string,telphone?:string){
|
|
|
|
|
const data={openid:account.openid,headimgurl:account.avatar,nickname:account.nickname,sex:account.sex,
|
|
|
|
|
city:account.city,Province:account.province,unionid:account.unionid,...(telphone===undefined?{}:{telphone})};
|
|
|
|
|
this.options.storage.setItem('tsgame_validtime',String(this.options.now()+48*60*60*1000));
|
|
|
|
|
this.options.storage.setItem(key,JSON.stringify(data));
|
|
|
|
|
}
|
|
|
|
|
/** 保留原工程缓存优先级,游客开关先于手机模式判断。 */
|
|
|
|
|
cached():LoginAccountIdentity|null {
|
|
|
|
|
if(this.versionState!==0)return null;
|
|
|
|
|
const policy=this.options.policy;
|
|
|
|
|
const data=this.read(policy.showVisitor?'tsgame_visitorinfo':policy.deviceLogin?'tsgame_devicePlayer':'tsgame_wxinfo',true);
|
|
|
|
|
return data===null?null:accountFrom(data);
|
|
|
|
|
}
|
|
|
|
|
autoLogin():void {
|
|
|
|
|
if(!this.requireAgreement())return;
|
|
|
|
|
const account=this.cached();if(account===null)throw Error('No cached login');
|
|
|
|
|
// showVisitor selects the visitor cache above, even when the phone button is enabled.
|
|
|
|
|
if(!this.options.policy.showVisitor&&this.options.policy.deviceLogin){
|
|
|
|
|
const data=this.read('tsgame_devicePlayer',true)!;
|
|
|
|
|
if(typeof data.telphone!=='string')throw Error('Cached device account missing telphone');
|
|
|
|
|
this.options.submit(account,{enabled:true,telphone:data.telphone});
|
|
|
|
|
}else this.options.submit(account,{enabled:false});
|
|
|
|
|
}
|
|
|
|
|
authorize(provider:'wechat'|'qq'):void {
|
|
|
|
|
if(!this.requireAgreement())return;
|
|
|
|
|
if(!this.options.settings){this.options.tip('当前宿主未提供原生登录授权接口');return;}
|
|
|
|
|
if(provider==='wechat')this.options.storage.setItem(`tsgame_playerid_${this.options.gameid}${this.options.agentid}`,'');
|
|
|
|
|
this.options.settings.accreditlogin(provider==='wechat'?2:1);
|
|
|
|
|
}
|
|
|
|
|
acceptNative(data:unknown,channel:'global'|'bridge'):void {
|
|
|
|
|
if(!this.requireAgreement())return;
|
|
|
|
|
const account=accountFrom(data);
|
|
|
|
|
this.save(account,channel==='global'&&this.options.policy.showVisitor?'tsgame_visitorinfo':'tsgame_wxinfo');
|
|
|
|
|
this.options.submit(account,{enabled:false});
|
|
|
|
|
}
|
|
|
|
|
/** 仅本次手机登录要求 tel,不能依据按钮显示配置要求游客响应携带手机号。 */
|
|
|
|
|
loginSucceeded(value:unknown,mode:DeviceLoginMode):void {
|
|
|
|
|
const data=record(value);
|
|
|
|
|
if(data.state!==0)throw Error('Cannot cache an unsuccessful login');
|
|
|
|
|
if(typeof data.playerid!=='string'&&(typeof data.playerid!=='number'||!Number.isFinite(data.playerid)))throw Error('Invalid login playerid');
|
|
|
|
|
// Button availability is not the authentication mode of this request.
|
|
|
|
|
if(mode.enabled){
|
|
|
|
|
if(typeof data.tel!=='string')throw Error('Device login response missing tel');
|
|
|
|
|
const account=accountFrom({...data,headimgurl:data.avatar,Province:data.province});
|
|
|
|
|
this.save(account,'tsgame_devicePlayer',data.tel);
|
|
|
|
|
this.options.storage.setItem('tsgame_dLogin','1');
|
|
|
|
|
}
|
|
|
|
|
this.options.storage.setItem(`tsgame_playerid_${this.options.gameid}${this.options.agentid}`,String(data.playerid));
|
|
|
|
|
}
|
|
|
|
|
visitor():void {
|
|
|
|
|
if(!this.requireAgreement())return;
|
|
|
|
|
const cached=this.read('tsgame_visitorinfo',false);
|
|
|
|
|
const account=cached===null?generate(this.options.random,'tsgame_visitorinfo'):accountFrom(cached);
|
|
|
|
|
this.save(account,this.options.policy.showVisitor?'tsgame_visitorinfo':'tsgame_wxinfo');
|
|
|
|
|
this.options.submit(account,{enabled:false});
|
|
|
|
|
}
|
|
|
|
|
hiddenFirst():void{this.hiddenClicks++;}
|
|
|
|
|
phonePanelState(){return readPhoneCode(this.options.storage.getItem('sgame_phoneCode'),this.options.now());}
|
|
|
|
|
hiddenSecond():void{if(this.hiddenClicks>=5&&this.options.policy.hiddenVisitor){this.hiddenClicks=0;this.visitor();}}
|
|
|
|
|
private deviceAccount(phone?:string):LoginAccountIdentity {
|
|
|
|
|
const cached=this.read('tsgame_devicePlayer',true);if(cached)return accountFrom(cached);
|
|
|
|
|
let unionid:string;
|
|
|
|
|
if(phone!==undefined)unionid=`phone_${phone}`;
|
|
|
|
|
else {
|
|
|
|
|
const info=this.read('tsgame_phoneInfo1',false);
|
|
|
|
|
if(info&&typeof info.PhoneIMEI==='string'&&info.PhoneIMEI)unionid=info.PhoneIMEI;
|
|
|
|
|
else {
|
|
|
|
|
const sets=['0123456789','qwertyuioplkjhgfdsazxcvbnm','QWERTYUIOPLKJHGFDSAZXCVBNM'];
|
|
|
|
|
unionid=Array.from({length:25},()=>{const chars=sets[Math.round(this.options.random()*2)];return chars[Math.ceil(this.options.random()*100000000)%chars.length];}).join('');
|
|
|
|
|
this.options.storage.setItem('tsgame_phoneInfo1',JSON.stringify({...info,PhoneIMEI:unionid}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const {avatarUrl,avatarRange}=this.options.policy;
|
|
|
|
|
return {openid:unionid,unionid,nickname:`User_${nicknameHash(unionid).slice(0,6)}`,
|
|
|
|
|
avatar:avatarUrl?`${avatarUrl}${avatarRange[0]+Math.round(this.options.random()*(avatarRange[1]-avatarRange[0]))}.jpg`:'',
|
|
|
|
|
sex:Math.floor(this.options.random()*2)+1,city:'China',province:phone===undefined?'HongKong':'ShangHai'};
|
|
|
|
|
}
|
|
|
|
|
device():void{if(!this.requireAgreement())return;this.options.submit(this.deviceAccount(),{enabled:false});}
|
|
|
|
|
phone(phone:string,code:string):void {
|
|
|
|
|
if(!this.requireAgreement())return;
|
|
|
|
|
if(!phone||!code){this.options.tip('手机号与验证码均不能为空!');return;}
|
|
|
|
|
this.options.submit(this.deviceAccount(phone),{enabled:true,telphone:phone,smmcode:code});
|
|
|
|
|
}
|
|
|
|
|
}
|