Stiglo je novo izdanje. Osvježi stranu.
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest
Izvor
4d7883a894b3
- server.js
- lib/auth.js
- lib/build.js
- lib/cards.js
- lib/db.js
- lib/gradovi.js
- lib/icons.js
- lib/imena.js
- lib/jezik.js
- lib/kartapodaci.js
- lib/lekcije.js
- lib/md.js
- lib/mehanika.js
- lib/mreza.js
- lib/pages.js
- lib/registar.js
- lib/routes.js
- lib/schema2.js
- lib/views.js
- seed/content.mjs
- seed/seed.mjs
- public/app.js
- public/style.css
1 import crypto from 'node:crypto';
2 import {dinar} from './db.js';
3 import {EDITION,EDITIONS} from './cards.js';
4 import {sha,token} from './auth.js';
5 import {validOrigin} from './imena.js';
6 export const DAY=86400000;
7 export function level(db,u,explorer=false,now=Date.now()){if(!u)return explorer?1:0;if(db.get('SELECT 1 FROM mandates WHERE user_id=? AND starts_at<=? AND ends_at>?',u.id,now,now)||db.get('SELECT 1 FROM mirrors WHERE owner_id=?',u.id))return 4;if(u.abuse_contact&&db.get("SELECT 1 FROM lesson_done WHERE user_id=? AND lesson='pravila'",u.id)&&(db.get('SELECT 1 FROM pages WHERE owner_id=? AND published=1',u.id)||db.get('SELECT 1 FROM listings WHERE seller_id=?',u.id)))return 3;return 2;}
8 export function mechanics(db,reg){
9 const activity=(id,kind,ref='')=>db.tx(()=>{db.run('INSERT INTO activities(user_id,kind,ref,at) VALUES(?,?,?,?)',id,kind,String(ref),Date.now());const done=progress(id);if(done.every(Boolean)&&!db.get("SELECT 1 FROM card_instances WHERE owner_id=? AND edition='signal-zlatna'",id))mint(id,'signal-zlatna','nagrada');});
10 function progress(id){return [!!db.get("SELECT 1 FROM activities WHERE user_id=? AND kind='razrijeseno'",id),!!db.get("SELECT 1 FROM activities WHERE user_id=? AND kind='provjera'",id),!!db.get('SELECT 1 FROM pages WHERE owner_id=? AND published=1',id),!!db.get('SELECT 1 FROM users WHERE id=? AND city_id IS NOT NULL',id)];}
11 const count=(key,n=1)=>db.run('INSERT INTO counters VALUES(?,?,?) ON CONFLICT(key,day) DO UPDATE SET n=n+excluded.n',key,new Date().toISOString().slice(0,10),n);
12 function reward(id,n,reason,ref){if(db.get('SELECT 1 FROM ledger WHERE user_id=? AND reason=? AND ref=?',id,reason,String(ref)))return;dinar(db,id,n,reason,String(ref));const treasury=db.get("SELECT id FROM users WHERE handle='blagajna'");dinar(db,treasury.id,-n,reason,String(ref));if(reason==='akcija'&&db.get("SELECT COUNT(*) n FROM ledger WHERE user_id=? AND reason='akcija'",id).n>=10&&!db.get("SELECT 1 FROM card_instances WHERE owner_id=? AND edition='udarnik'",id))mint(id,'udarnik','nagrada');}
13 function mint(owner,slug,source='kupovina',serial=null,at=Date.now()){const e=EDITION[slug];if(!e)throw Error('Nepoznato izdanje.');const next=serial??db.get('SELECT COALESCE(MAX(serial),0)+1 n FROM card_instances WHERE edition=?',slug).n;if(e.supply&&next>e.supply)throw Error('Izdanje je iscrpljeno.');const id=Number(db.run('INSERT INTO card_instances(edition,serial,owner_id,minted_at,source,sig,key_id) VALUES(?,?,?,?,?,?,?)',slug,next,owner,at,source,'',reg.key().key_id).lastInsertRowid);const sig=reg.sign({card_id:id,edition:slug,serial:next,minted_at:at});db.run('UPDATE card_instances SET sig=? WHERE id=?',sig,id);return db.get('SELECT * FROM card_instances WHERE id=?',id);}
14 function buy(owner,slug){return db.tx(()=>{const e=EDITION[slug];if(!e||e.earned||!e.price)throw Error('Ovo izdanje se dobija radom.');reg.charge(owner,e.price,'slicica');return mint(owner,slug);});}
15 function pack(owner,now=Date.now()){return db.tx(()=>{if(db.get("SELECT COUNT(*) n FROM ledger WHERE user_id=? AND reason='kesica' AND delta<0 AND created_at>=?",owner,Math.floor(now/DAY)*DAY).n>=5)throw Error('Najviše pet kesica dnevno.');const pool=EDITIONS.filter(e=>!e.earned&&(!e.supply||db.get('SELECT COUNT(*) n FROM card_instances WHERE edition=?',e.slug).n<e.supply));reg.charge(owner,60,'kesica');const result=[];for(let i=0;i<3;i++){const available=pool.filter(e=>!e.supply||db.get('SELECT COUNT(*) n FROM card_instances WHERE edition=?',e.slug).n<e.supply),weights={'obična':70,'rijetka':22,folija:7,hologram:1},rarities=Object.keys(weights).filter(r=>available.some(e=>e.rarity===r)),total=rarities.reduce((n,r)=>n+weights[r],0);if(!total)throw Error('Nema preostalih sličica.');let roll=crypto.randomInt(total),rarity=rarities.at(-1);for(const r of rarities){roll-=weights[r];if(roll<0){rarity=r;break;}}const choices=available.filter(e=>e.rarity===rarity),pick=choices[crypto.randomInt(choices.length)];result.push(mint(owner,pick.slug,'kesica'));}activity(owner,'kesica');return result;});}
16 function owned(owner,ids){if(ids.length<1||ids.length>3||new Set(ids).size!==ids.length)throw Error('Izaberite jednu do tri sličice.');return ids.map(id=>{const c=db.get('SELECT * FROM card_instances WHERE id=?',id);if(!c||c.owner_id!==owner||EDITION[c.edition].earned)throw Error('Sličica nije prenosiva ili više nije vaša.');return c;});}
17 function transfer(c,from,to,kind,trade=null){if(!db.get('SELECT 1 FROM users WHERE id=? AND deleted=0',to)||from===to)throw Error('Primalac nije dostupan.');db.run('UPDATE card_instances SET owner_id=?,source=? WHERE id=?',to,kind,c.id);db.run('INSERT INTO card_transfers(card_id,from_id,to_id,kind,trade_id,at) VALUES(?,?,?,?,?,?)',c.id,from,to,kind,trade,Date.now());db.run('INSERT INTO notifications(user_id,kind,actor_id,created_at) VALUES(?,?,?,?)',to,kind,from,Date.now());}
18 function gift(from,to,id){return db.tx(()=>{const [c]=owned(from,[id]);transfer(c,from,to,'poklon');activity(from,'poklon',id);});}
19 function offer(from,to,give,want,amount=0){if(!Number.isInteger(amount)||amount<0||amount>100)throw Error('Dodatak je od 0 do 100 DINAR.');return db.tx(()=>{owned(from,give);owned(to,want);if(from===to)throw Error('Izaberite drugog člana.');return Number(db.run("INSERT INTO trades(from_id,to_id,give_json,want_json,dinar,status,created_at) VALUES(?,?,?,?,?,'otvoreno',?)",from,to,JSON.stringify(give),JSON.stringify(want),amount,Date.now()).lastInsertRowid);});}
20 function accept(to,id,yes=true){return db.tx(()=>{const o=db.get('SELECT * FROM trades WHERE id=?',id);if(!o||o.to_id!==to||o.status!=='otvoreno'||o.created_at+7*DAY<Date.now())throw Error('Ponuda više ne važi.');if(yes){const a=owned(o.from_id,JSON.parse(o.give_json)),b=owned(to,JSON.parse(o.want_json));if(db.get('SELECT dinar FROM users WHERE id=?',o.from_id).dinar<o.dinar)throw Error('Nema dovoljno probnih bodova.');a.forEach(c=>transfer(c,o.from_id,to,'zamjena',id));b.forEach(c=>transfer(c,to,o.from_id,'zamjena',id));if(o.dinar){dinar(db,o.from_id,-o.dinar,'zamjena',String(id));dinar(db,to,o.dinar,'zamjena',String(id));}activity(to,'zamjena',id);}db.run('UPDATE trades SET status=?,decided_at=? WHERE id=?',yes?'prihvaceno':'odbijeno',Date.now(),id);});}
21 function order(buyer,id){return db.tx(()=>{const l=db.get('SELECT * FROM listings WHERE id=?',id);if(!l||l.status!=='otvoreno'||l.stock<1||l.seller_id===buyer||!db.get('SELECT 1 FROM users WHERE id=? AND deleted=0',l.seller_id))throw Error('Oglas nije dostupan.');if(db.get('SELECT dinar FROM users WHERE id=?',buyer).dinar<l.price_dinar)throw Error('Nema dovoljno probnih bodova.');const oid=Number(db.run("INSERT INTO orders(listing_id,buyer_id,seller_id,amount,status,created_at,updated_at,release_at) VALUES(?,?,?,?,'polog',?,?,NULL)",id,buyer,l.seller_id,l.price_dinar,Date.now(),Date.now()).lastInsertRowid);dinar(db,buyer,-l.price_dinar,'polog',String(oid));db.run('UPDATE listings SET stock=stock-1 WHERE id=?',id);activity(buyer,'narudzba',oid);return oid;});}
22 function release(o){if(!['polog','poslano'].includes(o.status))throw Error('Narudžba nije spremna za prijem.');dinar(db,o.seller_id,o.amount,'primljeno',String(o.id));db.run("UPDATE orders SET status='primljeno',updated_at=? WHERE id=?",Date.now(),o.id);activity(o.buyer_id,'primljeno',o.id);}
23 function orderAction(user,id,action){return db.tx(()=>{const o=db.get('SELECT * FROM orders WHERE id=?',id);if(!o)throw Error('Narudžba ne postoji.');if(action==='poslano'&&user===o.seller_id&&o.status==='polog')db.run("UPDATE orders SET status='poslano',updated_at=?,release_at=? WHERE id=?",Date.now(),Date.now()+7*DAY,id);else if(action==='primljeno'&&user===o.buyer_id)release(o);else if(action==='spor'&&user===o.buyer_id&&['polog','poslano'].includes(o.status)){db.run("UPDATE orders SET status='spor',updated_at=? WHERE id=?",Date.now(),id);dispute(user,'narudzba',String(id),'Spor oko probne narudžbe.',o.seller_id);}else throw Error('Radnja nije dozvoljena.');});}
24 function autoRelease(now=Date.now()){db.tx(()=>{for(const o of db.all("SELECT * FROM orders WHERE status='poslano' AND release_at<=?",now))release(o);});}
25 function rate(user,id,score,note){return db.tx(()=>{const o=db.get('SELECT * FROM orders WHERE id=?',id);if(!o||o.status!=='primljeno'||![o.buyer_id,o.seller_id].includes(user)||!Number.isInteger(score)||score<1||score>5)throw Error('Ocjena nije dozvoljena.');db.run('INSERT INTO ratings VALUES(?,?,?,?,?,?) ON CONFLICT(order_id,from_id) DO UPDATE SET score=excluded.score,note=excluded.note,at=excluded.at',id,user,user===o.buyer_id?o.seller_id:o.buyer_id,score,note.slice(0,500),Date.now());});}
26 function vote(user,id,choice,now=Date.now()){return db.tx(()=>{const p=db.get('SELECT * FROM proposals WHERE id=?',id),u=db.get('SELECT * FROM users WHERE id=?',user);if(!p||p.status!=='otvoreno'||p.opens_at>now||p.closes_at<=now||!JSON.parse(p.options).includes(choice))throw Error('Glasanje nije otvoreno.');if(p.kind!=='rasprava'&&u.created_at>now-3*DAY)throw Error('Zaštita od zloupotrebe: nalog mora biti star najmanje 72 h.');db.run('INSERT INTO ballots VALUES(?,?,?,?) ON CONFLICT(proposal_id,user_id) DO UPDATE SET choice=excluded.choice,at=excluded.at',id,user,choice,now);reward(user,5,'glas',id);activity(user,'glas',id);});}
27 function result(id){const p=db.get('SELECT * FROM proposals WHERE id=?',id),rows=db.all('SELECT choice,COUNT(*) n FROM ballots WHERE proposal_id=? GROUP BY choice',id),n=rows.reduce((s,r)=>s+r.n,0),yes=rows.find(r=>r.choice==='za')?.n||0;const ranked=[...rows].sort((a,b)=>b.n-a.n);return {n,yes,passed:n>=p.quorum&&(p.kind==='izbori'?!!ranked.length&&(!ranked[1]||ranked[0].n>ranked[1].n):p.kind==='izmjena-povelje'?yes*3>=n*2:yes>n/2),tally:rows};}
28 function closeProposal(id,now=Date.now()){return db.tx(()=>{const p=db.get('SELECT * FROM proposals WHERE id=?',id);if(!p||p.status!=='otvoreno'||p.closes_at>now)throw Error('Glasanje još traje.');const r=result(id);db.run('UPDATE proposals SET status=?,result=? WHERE id=?',r.passed?'usvojeno':'odbijeno',JSON.stringify(r),id);if(p.kind==='izbori'&&r.n>=p.quorum){const candidates=db.all('SELECT * FROM candidates WHERE proposal_id=?',id),ranked=[...r.tally].sort((a,b)=>b.n-a.n);if(ranked.length&&(!ranked[1]||ranked[0].n>ranked[1].n)){const winner=candidates.find(c=>db.get('SELECT handle FROM users WHERE id=?',c.user_id)?.handle===ranked[0].choice);if(winner){db.run('UPDATE mandates SET ends_at=? WHERE seat=? AND ends_at>?',now,winner.seat,now);db.run('INSERT INTO mandates(user_id,seat,starts_at,ends_at,proposal_id) VALUES(?,?,?,?,?)',winner.user_id,winner.seat,now,now+90*DAY,id);db.run("UPDATE proposals SET status='usvojeno' WHERE id=?",id);}}}if(r.passed&&p.kind==='izmjena-povelje'){const old=db.get("SELECT * FROM docs WHERE slug='povelja'");db.run('INSERT OR REPLACE INTO docs VALUES(?,?,?,?,?)','povelja-v'+old.version,old.title,old.body,old.version,old.updated_at);db.run("UPDATE docs SET body=?,version=version+1,updated_at=? WHERE slug='povelja'",p.body,now);}return r;});}
29 function dispute(user,kind,ref,statement,respondent,appeal=null,redact=0){if(appeal){const old=db.get('SELECT * FROM disputes WHERE id=?',appeal);if(!old||old.appeal_of||!['usvojena','odbijena'].includes(old.status)||old.opener_id!==user||db.get('SELECT 1 FROM disputes WHERE appeal_of=?',appeal))throw Error('Žalba nije dozvoljena.');}return Number(db.run("INSERT INTO disputes(kind,ref,opener_id,respondent_id,statement,response,status,deadline_at,appeal_of,redact,created_at) VALUES(?,?,?,?,?,'','u toku',?,?,?,?)",kind,ref,user,respondent,statement,Date.now()+3*DAY,appeal,redact,Date.now()).lastInsertRowid);}
30 function decide(reviewer,id,decision,now=Date.now()){return db.tx(()=>{const d=db.get('SELECT * FROM disputes WHERE id=?',id),u=db.get('SELECT * FROM users WHERE id=?',reviewer);if(!d||!['usvojena','odbijena'].includes(decision)||d.status!=='u toku'||!db.get('SELECT 1 FROM mandates WHERE user_id=? AND starts_at<=? AND ends_at>?',reviewer,now,now)||[d.opener_id,d.respondent_id].includes(reviewer)||d.deadline_at>now)throw Error('Potreban je nezavisan povjerenik nakon roka za odgovor.');if(d.appeal_of&&db.get('SELECT reviewer_id FROM disputes WHERE id=?',d.appeal_of)?.reviewer_id===reviewer)throw Error('Žalbu razmatra drugi povjerenik.');db.run('UPDATE disputes SET reviewer_id=?,decision=?,status=?,decided_at=? WHERE id=?',reviewer,decision,decision,now,id);if(d.kind==='narudzba'){const o=db.get('SELECT * FROM orders WHERE id=?',Number(d.ref));if(o?.status==='spor'){dinar(db,decision==='usvojena'?o.buyer_id:o.seller_id,o.amount,'odluka',String(o.id));db.run('UPDATE orders SET status=?,updated_at=? WHERE id=?',decision==='usvojena'?'vraceno':'primljeno',now,o.id);}}db.run('INSERT INTO mod_log(actor_id,action,target,note,created_at) VALUES(?,?,?,?,?)',reviewer,'odluka',String(id),decision,now);});}
31 function approval(user,subject,ref){return db.tx(()=>{const u=db.get('SELECT * FROM users WHERE id=?',user);if(!db.get('SELECT 1 FROM mandates WHERE user_id=? AND starts_at<=? AND ends_at>?',user,Date.now(),Date.now()))throw Error('Potreban je mandat povjerenika.');if(!['kljuc','opoziv','provjera','suspenzija'].includes(subject))throw Error('Nepoznata radnja.');const reviewed=['provjera','opoziv'].includes(subject)?db.get('SELECT * FROM names WHERE name=?',ref):null;if(['provjera','opoziv'].includes(subject)&&!reviewed)throw Error('Ime ne postoji.');if(subject==='provjera'&&reviewed.owner_id===user)throw Error('Pregled mora biti nezavisan.');const action=subject+':'+ref;if(db.get("SELECT 1 FROM mod_log WHERE action='odobreno' AND target=?",action))throw Error('Radnja je već izvršena.');db.run('INSERT OR IGNORE INTO approvals(subject,subject_id,steward_id,at) VALUES(?,?,?,?)',subject,ref,user,Date.now());const n=db.get('SELECT COUNT(*) n FROM approvals a WHERE subject=? AND subject_id=? AND EXISTS(SELECT 1 FROM mandates m WHERE m.user_id=a.steward_id AND m.starts_at<=? AND m.ends_at>?)',subject,ref,Date.now(),Date.now()).n;if(n>=2){if(subject==='kljuc')reg.rotate();else if(subject==='suspenzija'){db.run('UPDATE users SET banned=1 WHERE id=?',Number(ref));db.run('DELETE FROM sessions WHERE user_id=?',Number(ref));}else {const seq=reg.log(subject,ref,user);db.run('UPDATE names SET status=?,trust=?,sequence=? WHERE name=?',subject==='opoziv'?'opozvano':'izdato',subject==='provjera'?'provjereno':'neprovjereno',seq,ref);reg.seal(ref);if(subject==='provjera'&&reviewed.status==='pregled'){for(const a of db.all('SELECT DISTINCT steward_id FROM approvals WHERE subject=? AND subject_id=?',subject,ref)){if(db.get("SELECT COUNT(*) n FROM ledger WHERE user_id=? AND reason='akcija' AND created_at>=?",a.steward_id,Math.floor(Date.now()/DAY)*DAY).n<3)reward(a.steward_id,10,'akcija','pregled:'+ref);}}}db.run('INSERT INTO mod_log(actor_id,action,target,note,created_at) VALUES(?,?,?,?,?)',user,'odobreno',action,'Dva različita povjerenika.',Date.now());}return n;});}
32 function publish(user,name,title,body,origin=''){return db.tx(()=>{const n=db.get('SELECT * FROM names WHERE name=?',name);if(!n||n.owner_id!==user||n.kind!=='licno')throw Error('Ime nije vaše.');if(!['izdato','rezervisano'].includes(n.status)||n.expires_at<Date.now())throw Error('Ime nije spremno za objavu.');if(n.status==='rezervisano')reg.change(user,name,'izdaj');if(origin&&!validOrigin(origin))throw Error('Porijeklo mora biti javna HTTPS adresa.');db.run('INSERT INTO pages VALUES(?,?,?,?,1,?) ON CONFLICT(name) DO UPDATE SET title=excluded.title,body=excluded.body,published=1,updated_at=excluded.updated_at',name,user,title,body,Date.now());const seq=reg.log('objava',name,user);db.run("UPDATE names SET origin=?,status='izdato',trust=?,sequence=? WHERE name=?",origin||'https://www.yugoslavia.forum/s/'+encodeURIComponent(name.slice(0,-3)),origin?'neprovjereno':'provjereno',seq,name);reg.seal(name);activity(user,'objava',name);});}
33 function exportUser(user){const profile=db.get('SELECT handle,display,member_no,city_id,pismo,bio,created_at,dinar FROM users WHERE id=?',user);const payload={profile,names:db.all('SELECT * FROM names WHERE owner_id=?',user),pages:db.all('SELECT * FROM pages WHERE owner_id=?',user),cards:db.all('SELECT * FROM card_instances WHERE owner_id=?',user),orders:db.all('SELECT * FROM orders WHERE buyer_id=? OR seller_id=?',user,user),ratings:db.all('SELECT * FROM ratings WHERE from_id=? OR to_id=?',user,user),ballots:db.all('SELECT proposal_id,at FROM ballots WHERE user_id=?',user),posts:db.all('SELECT * FROM posts WHERE user_id=?',user),ledger:db.all('SELECT * FROM ledger WHERE user_id=?',user)};count('izvoz');return reg.signed(payload);}
34 function remove(user,removePosts){return db.tx(()=>{const u=db.get('SELECT * FROM users WHERE id=?',user),code=token(24),payload={id:user,handle:u.handle,display:u.display,member_no:u.member_no,names:db.all('SELECT name FROM names WHERE owner_id=?',user).map(r=>r.name)};db.run('INSERT OR REPLACE INTO returns VALUES(?,?,?,?,?)',u.handle,sha(code),u.member_no,JSON.stringify(payload),Date.now()+90*DAY);db.run("UPDATE users SET deleted=1,pass_hash='!',display='',bio='',city='',city_id=NULL,avatar=NULL,abuse_contact='' WHERE id=?",user);db.run('DELETE FROM sessions WHERE user_id=?',user);db.run('UPDATE card_shares SET revoked=1 WHERE user_id=?',user);db.run('UPDATE pages SET published=0 WHERE owner_id=?',user);db.run("UPDATE listings SET status='zatvoreno' WHERE seller_id=?",user);db.run("UPDATE trades SET status='odbijeno',decided_at=? WHERE status='otvoreno' AND (from_id=? OR to_id=?)",Date.now(),user,user);if(removePosts){db.run('UPDATE posts SET deleted=1,body=? WHERE user_id=?','Poruka je uklonjena na zahtjev autora.',user);db.run('DELETE FROM search WHERE post_id IN (SELECT id FROM posts WHERE user_id=?)',user);db.run('UPDATE threads SET deleted=1 WHERE user_id=? AND NOT EXISTS(SELECT 1 FROM posts p WHERE p.thread_id=threads.id AND p.user_id!=? AND p.deleted=0)',user,user);}for(const n of payload.names){db.run("UPDATE names SET status='opozvano',sequence=? WHERE name=?",reg.log('odlazak',n,user),n);reg.seal(n);}return code;});}
35 function restore(handle,code,passwordHash,now=Date.now()){return db.tx(()=>{const r=db.get('SELECT * FROM returns WHERE handle=?',handle);if(!r||r.expires_at<now||sha(code)!==r.code_hash)throw Error('Povratnica nije ispravna ili je istekla.');const p=JSON.parse(r.payload);db.run('UPDATE users SET deleted=0,handle=?,display=?,pass_hash=?,member_no=? WHERE id=?',p.handle,p.display,passwordHash,p.member_no,p.id);for(const name of p.names){db.run("UPDATE names SET status='izdato',expires_at=?,sequence=? WHERE name=? AND owner_id=?",now+365*DAY,reg.log('povratak',name,p.id),name,p.id);reg.seal(name);}db.run('DELETE FROM returns WHERE handle=?',handle);return p.id;});}
36 function expireReturns(now=Date.now()){return db.tx(()=>{for(const r of db.all('SELECT * FROM returns WHERE expires_at<?',now)){const p=JSON.parse(r.payload);for(const n of db.all('SELECT name FROM names WHERE owner_id=?',p.id)){reg.log('istek-povratnice',n.name,p.id);db.run('DELETE FROM pages WHERE name=?',n.name);db.run('DELETE FROM names WHERE name=?',n.name);}db.run("UPDATE users SET handle=?,display='' WHERE id=? AND deleted=1",'otisli_'+p.id+'_'+token(4),p.id);db.run('DELETE FROM returns WHERE handle=?',r.handle);}});}
37 return {activity,progress,count,reward,expireReturns,mint,buy,pack,gift,offer,accept,order,orderAction,autoRelease,rate,vote,result,closeProposal,dispute,decide,approval,publish,exportUser,remove,restore};
38 }
39