This commit is contained in:
Athena Funderburg
2026-05-25 07:05:17 +00:00
commit 4b463a3432
682 changed files with 47796 additions and 0 deletions
View File
+118
View File
@@ -0,0 +1,118 @@
from typing import List, Tuple, Any, Optional
import bisect
from time import time as time_builtin
from datetime import datetime, timedelta
from functools import total_ordering
from util.hash import gen_salt
from .db import LoginToken
from .conn import Conn
def GenTokenStr(trim: int = 20) -> str:
return gen_salt(trim)
class LoginAuthService:
def __init__(self, conn: Conn) -> None:
self._conn = conn
def create_token(self, purpose: str, data: List[Any], *, token: Optional[str] = None, lifetime: int = 30) -> Tuple[str, datetime]:
with self._conn.session() as sess:
logintoken = sess.query(LoginToken).filter(LoginToken.token == token, LoginToken.purpose == purpose).one_or_none()
assert logintoken is None
token = GenTokenStr() if token is None else token
logintoken = LoginToken(
token=token, purpose=purpose,
data=data, expiry=datetime.utcnow() + timedelta(seconds=lifetime),
)
sess.add(logintoken)
return logintoken.token, logintoken.expiry
def get_token(self, purpose: str, token: str) -> Optional[List[Any]]:
with self._conn.session() as sess:
logintoken = sess.query(LoginToken).filter(LoginToken.token == token, LoginToken.purpose == purpose).one_or_none()
if logintoken is None or logintoken.expiry <= datetime.utcnow():
return None
return logintoken.data
def remove_expired(self) -> None:
with self._conn.session() as sess:
sess.query(LoginToken).filter(LoginToken.expiry <= datetime.utcnow()).delete()
class AuthService:
def __init__(self, time: Optional[Any] = None) -> None:
if time is None:
time = time_builtin
self._time = time
self._ordered = []
self._bytoken = {}
self._idxbase = 0
def create_token(self, purpose: str, data: Any, *, token: Optional[str] = None, lifetime: int = 30) -> Tuple[str, int]:
self._remove_expired()
token = GenTokenStr() if token is None else token
td = TokenData(purpose, data, self._time() + lifetime, token)
assert token not in self._bytoken
idx = bisect.bisect_left(self._ordered, td)
self._ordered.insert(idx, td)
self._bytoken[td.token] = idx + self._idxbase
return td.token, td.expiry
def pop_token(self, purpose: str, token: str) -> Optional[Any]:
self._remove_expired()
idx = self._bytoken.pop(token, None)
if idx is None:
return None
idx -= self._idxbase
td = self._ordered[idx]
if not td.validate(purpose, token, self._time()):
return None
return td.data
def get_token(self, purpose: str, token: str) -> Optional[Any]:
self._remove_expired()
idx = self._bytoken.get(tuple(token)) if isinstance(token, list) else self._bytoken.get(token)
if idx is None:
return None
idx -= self._idxbase
td = self._ordered[idx]
if not td.validate(purpose, token, self._time()):
return None
return td.data
def get_token_expiry(self, purpose: str, token: str) -> Optional[Any]:
self._remove_expired()
idx = self._bytoken.get(token)
if idx is None:
return None
idx -= self._idxbase
td = self._ordered[idx]
if not td.validate(purpose, token, self._time()):
return None
return td.expiry
def _remove_expired(self) -> None:
if not self._ordered:
return
now = self._time()
dummy = TokenData('', None, now, '')
idx = bisect.bisect(self._ordered, dummy)
if idx < 1:
return
self._idxbase += idx
for td in self._ordered[:idx]:
self._bytoken.pop(td.token, None)
self._ordered = self._ordered[idx:]
@total_ordering
class TokenData:
def __init__(self, purpose: str, data: Any, expiry: int, token: str) -> None:
self.token = token
self.purpose = purpose
self.expiry = expiry
self.data = data
def __le__(self, other: 'TokenData') -> bool:
return self.expiry <= other.expiry
def validate(self, purpose: str, token: str, now: int) -> bool:
return self.expiry > now and self.purpose == purpose and self.token == token
+1604
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
from typing import Dict, Tuple, Any
class Client:
__slots__ = ('program', 'version', 'via', '_tuple', '_hash')
program: str
version: str
via: str
_tuple: Tuple[str, str, str]
_hash: int
@classmethod
def FromJSON(cls, json: Dict[str, str]) -> 'Client':
return Client(json['program'], json['version'], json.get('via') or 'direct')
@classmethod
def ToJSON(cls, client: 'Client') -> Dict[str, str]:
return {
'program': client.program,
'version': client.version,
'via': client.via,
}
def __init__(self, program: str, version: str, via: str) -> None:
self.program = program
self.version = version
self.via = via
self._tuple = (program, version, via)
self._hash = hash(self._tuple)
def __setattr__(self, attr: str, value: Any) -> Any:
if getattr(self, '_hash', None) is None:
super().__setattr__(attr, value)
return
raise AttributeError("Immutable")
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Client):
return False
return self._tuple == other._tuple
def __hash__(self) -> int:
return self._hash
+37
View File
@@ -0,0 +1,37 @@
from typing import Any, Iterator
from contextlib import contextmanager
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker
class Conn:
__slots__ = ('engine', 'session_factory', '_session', '_depth')
engine: Any
session_factory: Any
_session: Any
_depth: int
def __init__(self, conn_str: str) -> None:
self.engine = sa.create_engine(conn_str)
self.session_factory = sessionmaker(bind = self.engine)
self._session = None
self._depth = 0
@contextmanager
def session(self) -> Iterator[Any]:
if self._depth > 0:
yield self._session
return
sess = self.session_factory()
self._session = sess
self._depth += 1
try:
yield sess
sess.commit()
except:
sess.rollback()
raise
finally:
sess.close()
self._session = None
self._depth -= 1
+220
View File
@@ -0,0 +1,220 @@
from typing import Any, Iterator
from datetime import datetime
from contextlib import contextmanager
import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
from util.json_type import JSONType
import settings
def Col(*args: Any, **kwargs: Any) -> sa.Column:
if 'nullable' not in kwargs:
kwargs['nullable'] = False
return sa.Column(*args, **kwargs)
class Base(declarative_base()): # type: ignore
__abstract__ = True
class WithFrontData(Base):
__abstract__ = True
# Data specific to front-ends; e.g. different types of password hashes
# E.g. front_data = { 'msn': { ... }, 'ymsg': { ... }, ... }
_front_data = Col(JSONType, name = 'front_data', default = {})
def set_front_data(self, frontend: str, key: str, value: Any) -> None:
fd = self._front_data or {}
if frontend not in fd:
fd[frontend] = {}
fd[frontend][key] = value
# As a side-effect, this also makes `._front_data` into a new object,
# so SQLAlchemy picks up the fact that it's been changed.
# (SQLAlchemy only does shallow comparisons on fields by default.)
self._front_data = _simplify_json_data(fd)
def get_front_data(self, frontend: str, key: str) -> Any:
fd = self._front_data
if not fd: return None
fd = fd.get(frontend)
if not fd: return None
return fd.get(key)
class User(WithFrontData):
__tablename__ = 'user'
id = Col(sa.Integer, primary_key = True)
date_created = Col(sa.DateTime, default = datetime.utcnow)
date_login = Col(sa.DateTime, nullable = True)
uuid = Col(sa.String(50), unique = True)
email = Col(sa.String(80, collation='utf8mb4_unicode_ci'), nullable=False, unique=True)
username = Col(sa.String(40, collation='utf8mb4_unicode_ci'), nullable=False, unique=True)
first_name = Col(sa.String(50), default = 'John')
middle_name = Col(sa.String(50), nullable = True)
last_name = Col(sa.String(50), default = 'Doe')
# specifically used for social features/member directory
nickname = Col(sa.String(60), nullable = True)
uin = Col(sa.BigInteger, nullable = True)
# verified for logging in. all existing users pre-email verification have this set to true, until a certain date, unless they verified their email (account_verified), then it's always true. this is set to false on all new users post-verification unless they've also verified their e-mail address
verified_to_login = Col(sa.Boolean)
# Roaming name - can be null and (in theory) stays constant to what the user sets it to
name = Col(sa.String(60), nullable = True)
name_last_modified = Col(sa.DateTime, default = datetime.utcnow)
# Friendly name set during IM sessions. It cannot be null and is more prone to being overwritten than the roaming name
friendly_name = Col(sa.String(60))
# Roaming message
message = Col(sa.String(255), nullable = True)
message_last_modified = Col(sa.DateTime, default = datetime.utcnow)
password = Col(sa.String(250))
groups = Col(JSONType)
settings = Col(JSONType)
suspended = Col(sa.Boolean)
is_tester = Col(sa.Boolean)
is_mvp = Col(sa.Boolean)
show_in_dir = Col(sa.Boolean)
evil_permanent = Col(sa.Integer, default = 0)
evil_temporary = Col(sa.Integer, default = 0)
alias_active = Col(sa.Boolean, default=False)
did_firsttime_email_change = Col(sa.Boolean, default=False)
account_verified = Col(sa.Boolean, default=False) # for e-mail address verification
lang = Col(sa.String(10), nullable = True)
avatar = Col(sa.String(16), nullable = True)
profile = relationship("UserProfile", backref="user", uselist=False, cascade="all, delete-orphan")
__table_args__ = (sa.Index('email_ci_index', sa.text('(LOWER(email))'), unique = True), sa.Index('username_ci_index', sa.text('(LOWER(username))'), unique = True))
class UserContact(WithFrontData):
__tablename__ = 'user_contact'
user_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True)
contact_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True)
user_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) # = User(self.user_id).uuid
uuid = Col(sa.String(50), sa.ForeignKey('user.uuid')) # = User(self.contact_id).uuid
name = Col(sa.String(100))
lists = Col(sa.Integer)
pending = Col(sa.Boolean, default = False)
groups = Col(JSONType)
is_messenger_user = Col(sa.Boolean)
index_id = Col(sa.String(50))
birthdate = Col(sa.DateTime, nullable = True)
anniversary = Col(sa.DateTime, nullable = True)
notes = Col(sa.String(255), nullable = True)
first_name = Col(sa.String(50), nullable = True)
middle_name = Col(sa.String(50), nullable = True)
last_name = Col(sa.String(50), nullable = True)
nickname = Col(sa.String(80), nullable = True)
primary_email_type = Col(sa.String(10), nullable = True)
personal_email = Col(sa.String(80), nullable = True)
work_email = Col(sa.String(80), nullable = True)
im_email = Col(sa.String(80), nullable = True)
other_email = Col(sa.String(80), nullable = True)
home_phone = Col(sa.String(50), nullable = True)
work_phone = Col(sa.String(50), nullable = True)
fax_phone = Col(sa.String(50), nullable = True)
pager_phone = Col(sa.String(50), nullable = True)
mobile_phone = Col(sa.String(50), nullable = True)
other_phone = Col(sa.String(50), nullable = True)
personal_website = Col(sa.String(80), nullable = True)
business_website = Col(sa.String(80), nullable = True)
locations = Col(JSONType, default = {})
class UserProfile(WithFrontData):
__tablename__ = 'user_profile'
user_id = Col(sa.Integer, sa.ForeignKey('user.id'), primary_key = True)
bio = Col(sa.Text, nullable = True)
pronouns = Col(sa.String(40), nullable = True)
website = Col(sa.String(100), nullable = True)
socials = Col(JSONType, nullable = True)
streetaddr = Col(sa.String(100), nullable = True)
city = Col(sa.String(80), nullable = True)
state = Col(sa.String(80), nullable = True)
zip = Col(sa.Integer, nullable = True)
country = Col(sa.String(60), nullable = True)
language = Col(sa.String(50), nullable = True)
interests = Col(JSONType, nullable = True, default = {})
visibility = Col(sa.String(25), default='public')
class Circle(Base):
__tablename__ = 'circle'
id = Col(sa.Integer, primary_key = True)
chat_id = Col(sa.String(50), unique = True)
name = Col(sa.String(100))
owner_id = Col(sa.Integer, sa.ForeignKey('user.id'))
owner_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid'))
owner_friendly = Col(sa.String(100))
membership_access = Col(sa.Integer)
request_membership_option = Col(sa.Integer)
class CircleMembership(Base):
__tablename__ = 'circle_membership'
id = Col(sa.Integer, primary_key = True)
chat_id = Col(sa.String(50), sa.ForeignKey('circle.chat_id'))
member_id = Col(sa.Integer, sa.ForeignKey('user.id'))
member_uuid = Col(sa.String(50), sa.ForeignKey('user.uuid'))
role = Col(sa.Integer)
state = Col(sa.Integer)
blocking = Col(sa.Boolean)
inviter_uuid = Col(sa.String(50), nullable = True)
inviter_email = Col(sa.String(100), nullable = True)
inviter_name = Col(sa.String(100), nullable = True)
invite_message = Col(sa.String(250), nullable = True)
class Sound(Base):
__tablename__ = 'sound'
hash = Col(sa.String(50), primary_key = True)
title = Col(sa.String(100))
category = Col(sa.Integer)
language = Col(sa.Integer)
is_public = Col(sa.Boolean)
hits = Col(sa.Integer, default = 0)
class LoginToken(Base):
__tablename__ = 'login_token'
id = Col(sa.Integer, primary_key = True)
token = Col(sa.String(100))
purpose = Col(sa.String(25))
data = Col(JSONType)
expiry = Col(sa.DateTime)
def _simplify_json_data(data: Any) -> Any:
if isinstance(data, dict):
d = {}
for k, v in data.items():
v = _simplify_json_data(v)
if v is not None:
d[k] = v
if not d:
return None
return d
if isinstance(data, (list, tuple)):
return [_simplify_json_data(x) for x in data]
return data
engine = sa.create_engine(settings.DB, echo='debug' if settings.DEBUG and settings.DEBUG_FULL and settings.DEBUG_LOG_SQL_QUERIES else None)
session_factory = sessionmaker(bind = engine)
@contextmanager
def Session() -> Iterator[Any]:
if Session._depth > 0: # type: ignore
yield Session._global # type: ignore
return
session = session_factory()
Session._global = session # type: ignore
Session._depth += 1 # type: ignore
try:
yield session
session.commit()
except:
session.rollback()
raise
finally:
session.close()
Session._global = None # type: ignore
Session._depth -= 1 # type: ignore
Session._global = None # type: ignore
Session._depth = 0 # type: ignore
+83
View File
@@ -0,0 +1,83 @@
class ClientError(Exception):
pass
class ServerError(Exception):
pass
class GroupNameTooLong(ClientError):
pass
class GroupDoesNotExist(ClientError):
pass
class GroupAlreadyExists(ClientError):
pass
class CannotRemoveSpecialGroup(ClientError):
pass
class ContactDoesNotExist(ClientError):
pass
class ContactAlreadyOnContactList(ClientError):
pass
class NicknameExceedsLengthLimit(ClientError):
pass
class SpecialMessageNotSentWithDType(ClientError):
pass
class EmptyDomainInXXL(ClientError):
pass
class InvalidXXLPayload(ClientError):
pass
class ContactNotOnContactList(ClientError):
pass
class UserDoesNotExist(ClientError):
pass
class ContactNotOnline(ClientError):
pass
class AuthFail(ClientError):
pass
class NotAllowedWhileHDN(ClientError):
pass
class NotAllowedToJoinCircle(ClientError):
pass
class MemberDoesntHaveSufficientCircleRole(ClientError):
pass
class CircleDoesNotExist(ClientError):
pass
class MemberAlreadyInCircle(ClientError):
pass
class MemberAlreadyInvitedToCircle(ClientError):
pass
class CircleMemberIsPending(ClientError):
pass
class CantLeaveCircle(ClientError):
pass
class MemberNotInCircle(ClientError):
pass
class ContactListIsFull(ClientError):
pass
class DataTooLargeToSend(ServerError):
pass
class MemberIsSuspended(ClientError):
pass
+157
View File
@@ -0,0 +1,157 @@
from typing import TYPE_CHECKING, Optional, Dict, Any, List
from abc import ABCMeta, abstractmethod
from .models import User, Contact, OIM, Circle, CircleRole, MessageData, TextWithData, Substatus, LoginOption
from util.misc import MultiDict
if TYPE_CHECKING:
from .backend import BackendSession, Chat, ChatSession
class BackendEventHandler(metaclass = ABCMeta):
__slots__ = ('bs',)
bs: 'BackendSession'
# Note to subclassers, regarding `__init__`:
# `bs` is assigned in `Backend.login`, before `BackendEventHandler.on_open` is called,
# because of circular references.
# Therefore, your `__init__` should be conspicuously missing an assignment to `bs`.
def on_open(self) -> None:
pass
def on_close(self) -> None:
pass
def on_maintenance_message(self, *args: Any, **kwargs: Any) -> None:
pass
def on_client_alert(self, icon_url: Optional[str], url: Optional[str], message: str = '') -> None:
pass
def on_maintenance_boot(self) -> None:
pass
@abstractmethod
def on_presence_notification(
self, ctc: Contact, on_contact_add: bool, old_substatus: Substatus, *,
trid: Optional[str] = None, update_status: bool = True, update_info_other: bool = True,
send_status_on_bl: bool = False, sess_id: Optional[int] = None, updated_phone_info: Optional[Dict[str, Any]] = None,
) -> None: pass
@abstractmethod
def on_presence_self_notification(self, old_substatus: Substatus, *, update_status: bool = True, update_info: bool = True) -> None: pass
@abstractmethod
def on_chat_invite(
self, chat: 'Chat', inviter: User, *,
circle: bool = False, inviter_id: Optional[str] = None, invite_msg: str = '',
) -> None: pass
@abstractmethod
def on_declined_chat_invite(self, chat: 'Chat', circle: bool = False) -> None: pass
# `user` added me to their FL, and they're now on my RL.
@abstractmethod
def on_added_me(self, user: User, *, adder_id: Optional[str] = None, message: Optional[TextWithData] = None) -> None: pass
@abstractmethod
def on_removed_me(self, user: User) -> None: pass
# `user` didn't accept contact request
@abstractmethod
def on_contact_request_denied(self, user_added: User, message: str, *, contact_id: Optional[str] = None) -> None: pass
@abstractmethod
def on_login_elsewhere(self, option: LoginOption) -> None: pass
@abstractmethod
def on_oim_sent(self, oim: OIM) -> None: pass
@abstractmethod
def on_circle_created(self, circle: Circle) -> None: pass
@abstractmethod
def on_circle_invite_revoked(self, chat_id: str) -> None: pass
@abstractmethod
def on_accepted_circle_invite(self, circle: Circle) -> None: pass
@abstractmethod
def on_circle_updated(self, circle: Circle) -> None: pass
@abstractmethod
def on_left_circle(self, circle: Circle) -> None: pass
@abstractmethod
def on_circle_role_updated(self, chat_id: str, role: CircleRole) -> None: pass
# TODO: Make these non-frontend-specific to allow interop
def msn_on_oim_deletion(self, oims_deleted: int) -> None:
pass
def msn_on_uun_sent(
self, sender: User, type: int, data: Optional[bytes], *,
pop_id_sender: Optional[str] = None, pop_id: Optional[str] = None,
) -> None:
pass
def msn_on_notify_ab(self) -> None:
pass
def msn_on_notify_circle_ab(self, chat_id: str) -> None:
pass
def ymsg_on_p2p_msg_request(self, sess_id: int, yahoo_data: MultiDict[bytes, bytes]) -> None:
pass
def ymsg_on_xfer_init(self, sess_id: int, yahoo_data: MultiDict[bytes, bytes]) -> None:
pass
def ymsg_on_sent_ft_http(self, yahoo_id_sender: str, url_path: str, upload_time: float, message: str) -> None:
pass
def ymsg_on_upload_file_ft(self, recipient: str, message: str) -> None:
pass
class ChatEventHandler(metaclass = ABCMeta):
__slots__ = ('cs',)
cs: 'ChatSession'
# Note to subclassers, regarding `__init__`:
# `cs` is assigned in `Chat.join`, before `ChatEventHandler.on_open` is called,
# because of circular references.
# Therefore, your `__init__` should be conspicuously missing an assignment to `cs`.
def on_open(self) -> None:
pass
def on_close(self) -> None:
pass
@abstractmethod
def on_participant_joined(self, cs_other: 'ChatSession', first_pop: bool, initial_join: bool) -> None: pass
@abstractmethod
def on_participant_left(self, cs_other: 'ChatSession', last_pop: bool) -> None: pass
@abstractmethod
def on_chat_invite_declined(
self, chat: 'Chat', invitee: User, *,
invitee_id: Optional[str] = None, message: Optional[str] = None, circle: bool = False,
) -> None: pass
@abstractmethod
def on_chat_updated(self) -> None: pass
@abstractmethod
def on_chat_roster_updated(self) -> None: pass
@abstractmethod
def on_participant_status_updated(self, cs_other: 'ChatSession', first_pop: bool, initial: bool, old_substatus: Substatus) -> None: pass
@abstractmethod
def on_message(self, data: MessageData) -> None: pass
+192
View File
@@ -0,0 +1,192 @@
import asyncio, secrets, ssl, jinja2, json, mimetypes, random, base64, settings, util.misc
from typing import Any, Dict, Optional
from aiohttp import web, ClientSession, ClientTimeout, ClientError
from pathlib import Path
from util.misc import AIOHTTPRunner
from util.avatar import get_avatar_data
from core.backend import Backend
from brutus import Brutus
MISC_TMPL_DIR = "core/tmpl/misc/"
AD_TMPL_DIR = "core/tmpl/ads"
def register(loop: asyncio.AbstractEventLoop, backend: Backend, *, devmode: bool = False) -> web.Application:
ssl_context: Optional[ssl.SSLContext]
http_host = '0.0.0.0'
http_port = settings.HTTP_PORT
if devmode:
ssl_context = Brutus('CrossTalk').create_ssl_context()
else:
ssl_context = None
app = create_app(loop, backend)
backend.add_runner(AIOHTTPRunner(http_host, http_port, app, ssl_context=ssl_context, service='HTTP'))
app.router.add_get('/', handle_misc_conns)
app.router.add_post('/', handle_blankok)
app.router.add_static('/static', 'core/static')
app.router.add_get('/ads/txt', handle_textad)
app.router.add_get('/ads/banner', handle_bannerad)
app.router.add_get('/ads/banner-lg', lambda req: handle_bannerad(req, yuge=True))
app.router.add_get('/ads/banner-sq', lambda req: handle_bannerad(req, sq = True))
app.router.add_get('/ads/GetMSNAdImage.asmx', handle_msnbannerimg)
app.router.add_get('/svcs/mms/adxml_main.asp', lambda req: handle_bannerad(req, msnxml=True))
app.router.add_get('/avatar/{uuid}/static', handle_avatar)
app.router.add_get('/avatar/{uuid}/small', lambda req: handle_avatar(req, small=True))
app.router.add_get('/avatar/{uuid}/msn', lambda req: handle_avatar(req, msn=True))
app.router.add_route('*', '/{path:.*}', handle_notfound)
return app
def create_app(loop: asyncio.AbstractEventLoop, backend: Backend) -> Any:
app = web.Application(loop=loop)
app['backend'] = backend
app['jinja_env'] = jinja2.Environment(
loader=jinja2.PrefixLoader({}, delimiter=':'),
autoescape=jinja2.select_autoescape(default=False),
)
app.on_response_prepare.append(on_response_prepare)
util.misc.add_to_jinja_env(app, 'misc', MISC_TMPL_DIR)
return app
async def on_response_prepare(req: web.Request, res: web.StreamResponse) -> None:
res.headers['X-Azul-Version'] = settings.VERSION
if not settings.DEBUG:
return
if settings.DEBUG:
ip_address = req.headers.get('X-Forwarded-For', req.remote)
print("[HTTP] <Debug> (IP: {}) [Client] {} {}://{}{}".format(
ip_address, req.method, req.scheme, req.host, req.path_qs))
if settings.DEBUG_FULL:
for header, value in req.headers.items():
print(f"{header}: {value}")
body = await req.read()
if body:
print(body.decode('utf-8', errors='replace'))
print(f"\n[HTTP] <Debug> (IP: {ip_address}) [Server]: {res.status} {res.reason}")
if settings.DEBUG_FULL:
for header, value in res.headers.items():
print(f"{header}: {value}")
if isinstance(res, web.Response):
if res.body:
print(res.body.decode(res.charset or 'utf-8', errors='replace'))
async def handle_misc_conns(req: web.Request) -> web.Response:
return render(req, 'misc:miscconns.html', {
'settings': settings
}, status=400)
async def handle_notfound(req: web.Request) -> web.Response:
return render(req, 'misc:miscconns.html', {
'settings': settings
}, status=404)
async def handle_bannerad(req: web.Request, yuge: bool = False, sq: bool = False, msnxml: bool = False) -> web.Response:
with open('config/big-bannerimages.json' if yuge else 'config/sq-bannerimages.json' if sq else 'config/bannerimages.json', 'r') as json_file:
data = json.load(json_file)
random_entry = random.choice(data)
image_path = f"core/{random_entry['image']}"
image_link = random_entry['link']
id = random_entry['id']
urlparams = req.rel_url.query
version = urlparams.get('Version')
content_type, _ = mimetypes.guess_type(image_path)
headers = {
'Content-Type': 'text/xml' if msnxml else 'text/html',
}
html_response = f'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html style="-ms-overflow-style: none;"><body style="-ms-overflow-style: none;overflow: hidden;padding:0;margin:0;overflow:hidden;-ms-scroll-limit: 0 0 0 0;" scroll=no><a href="{image_link}" style="text-decoration:none;" target="_blank"><img border="0" src="http://{settings.STATIC_HOST}/svc/ads/ct/{random_entry["image"]}"></a></body></html>'
msn_response = f'<?xml version="1.0"?><ADRSP V="{version}"><image IMG="http://{settings.AD_HOST}/ads/GetMSNAdImage.asmx?id={id}" ALT="Advertisement" HEIGHT="60" WIDTH="234"/><click CLK="{image_link}" TARGET="_NEW"/></ADRSP>'
return web.HTTPOk(body=msn_response if msnxml else html_response, headers=headers)
async def handle_msnbannerimg(req: web.Request) -> web.Response:
with open("config/bannerimages.json", "r", encoding="utf-8") as f:
data = json.load(f)
id_in_url = req.rel_url.query.get("id")
entry = None
if id_in_url is not None:
wanted = int(id_in_url)
for e in data:
if e.get("id") == wanted:
entry = e
break
else:
entry = random.choice(data)
image_url = f"http://{settings.STATIC_HOST}/svc/ads/ct/{entry['image']}"
async with ClientSession() as sess:
async with sess.get(image_url) as resp:
body = await resp.read()
content_type = resp.headers.get("Content-Type")
if not content_type:
content_type = mimetypes.guess_type(entry["image"])[0] or "application/octet-stream"
return web.Response(body=body, content_type=content_type)
async def handle_textad(req: web.Request) -> web.Response:
textad = ''
# Use 'rb' to make UTF-8 text load properly
with open('config/textads.json', 'rb') as f:
textads = json.loads(f.read())
f.close()
if len(textads) > 0:
if len(textads) > 1:
ad = textads[secrets.randbelow(len(textads))]
else:
ad = textads[0]
with open(AD_TMPL_DIR + '/text-msn.xml') as fh:
textad = fh.read()
textad = textad.format(caption=ad['caption'], url=ad['url'])
return web.HTTPOk(content_type='text/xml', text=textad)
async def handle_blankok(req: web.Request) -> web.Response:
# MSN counts the login server as a "key port" by POSTing to the root of the server with no content.
return web.Response(status = 200)
async def handle_avatar(req: web.Request, small: bool = False, msn: bool = False) -> web.Response:
uuid = req.match_info['uuid']
backend: Backend = req.app['backend']
user = backend.user_service.get(uuid)
avatar_md5 = user.avatar if user else None
result = get_avatar_data(uuid, avatar_md5, small=small, msn=msn)
if result is not None:
data, content_type = result
return web.Response(status=200, body=data, content_type=content_type)
return await _get_default_avatar(small)
async def _get_default_avatar(small: bool) -> web.Response:
default_url = f"https://{settings.STATIC_HOST}/svc/userstore/avatar/default-sm.png" if small else f"https://{settings.STATIC_HOST}/svc/userstore/avatar/default.png"
timeout = ClientTimeout(total=5)
try:
async with ClientSession(timeout=timeout) as session:
async with session.get(default_url) as resp:
if resp.status != 200:
raise web.HTTPNotFound()
data = await resp.read()
content_type = resp.headers.get("Content-Type", "image/png")
return web.Response(status=200, body=data, content_type=content_type)
except ClientError:
raise
def render(req: web.Request, tmpl_name: str, ctxt: Optional[Dict[str, Any]] = None, status: int = 200) -> web.Response:
if tmpl_name.endswith('.xml'):
content_type = 'text/xml'
else:
content_type = 'text/html'
tmpl = req.app['jinja_env'].get_template(tmpl_name)
content = tmpl.render(**(ctxt or {}))
return web.Response(status=status, content_type=content_type, text=content)
+1
View File
@@ -0,0 +1 @@
from .entry import register
+913
View File
@@ -0,0 +1,913 @@
from typing import Tuple, Optional, Iterable, List, Any, Callable, Dict
from disposable_email_domains import blocklist as disposable_emails
import asyncio, io, hashlib, base64, json, traceback
import sqlalchemy as sa
from sqlalchemy.orm.attributes import flag_modified
from core.backend import Backend
from core.models import CircleRole
from core import error
from util import misc, hash
import core.db as db
import settings
class INSCtrl:
__slots__ = (
'logger', 'reader', 'writer', 'peername', 'close_callback', 'closed', 'transport',
'authenticated', 'current_challenge', 'backend', #'alive', 'alive_task',
)
logger: misc.Logger
reader: 'INSReader'
writer: 'INSWriter'
peername: Tuple[str, int]
close_callback: Optional[Callable[[], None]]
closed: bool
transport: Optional[asyncio.WriteTransport]
authenticated: bool
current_challenge: Optional[str]
#alive: bool
#alive_task: Optional[misc.VoidTaskType]
backend: Backend
def __init__(self, logger: misc.Logger, via: str, backend: Backend) -> None:
self.logger = logger
self.reader = INSReader(logger)
self.writer = INSWriter(logger)
self.peername = ('0.0.0.0', 4309)
self.close_callback = None
self.closed = False
self.transport = None
self.authenticated = False
self.current_challenge = None
#self.alive = True
#self.alive_task = None
self.backend = backend
def _m_linksrv(self, password: str, identifier: Optional[str]) -> None:
backend = self.backend
bytes_enc = identifier.encode("ascii")
base64_dec = base64.b64decode(bytes_enc)
identifier_dec = base64_dec.decode("ascii")
valid_identifiers = ['WEB', 'AzuL-SERV', 'WTV', 'uMaIL', 'sBOOK', 'NOTBAY', 'CrossBot', 'SEARCH', 'WebTalk']
if identifier_dec not in valid_identifiers:
self.logger.info(f'InterService identification failed, invalid identifier "{identifier_dec}".')
self.send_numeric(Err.AuthenticationFailed)
self.close()
else:
hashed_pw = hashlib.sha256(settings.INS_LINK_PASSWORD.encode()).hexdigest()
if password == hashed_pw:
self.authenticated = True
backend._linked = True
# TODO: Re-implement in V2
#self.alive_task = backend.loop.create_task(self._ping_conn())
self.logger.info(f'New InterService session established (identifier: {identifier_dec})')
self.send_reply('LINKSRV', 'OK')
else:
self.send_numeric(Err.AuthenticationFailed)
self.close()
#def _m_pong(self, challenge: str) -> None:
# if self.alive: return
# if not self.current_challenge or challenge != self.current_challenge:
# self.close()
# return
# self.alive = True
# self.current_challenge = None
def _m_circle(self, ts: str, chat_id: str, action: str, *args: str) -> None:
backend = self.backend
if not self.authenticated:
self.send_numeric(Err.NotAuthenticated)
self.close()
return
circle = backend.user_service.get_circle(chat_id)
if circle is None:
self.send_numeric(Err.CircleDoesNotExist, ':CIRCLE {}'.format(ts))
return
if action == 'INCHAT':
if len(args) < 1:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
uuid = args[0]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
try:
in_chat = backend.util_user_online_in_circle(circle, user)
self.send_reply('CIRCLE', ts, 'INCHAT', uuid, str(in_chat))
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
elif action == 'ACCEPT':
if len(args) < 1:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
uuid = args[0]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
try:
backend.util_accept_circle_invite(circle, user)
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
except error.MemberAlreadyInCircle:
self.send_numeric(Err.MemberAlreadyInCircle, ':CIRCLE {}'.format(ts))
return
elif action == 'DECLINE':
if len(args) < 1:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
uuid = args[0]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
try:
backend.util_decline_circle_invite(circle, user)
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
except error.MemberAlreadyInCircle:
self.send_numeric(Err.MemberAlreadyInCircle, ':CIRCLE {}'.format(ts))
return
elif action == 'REVOKE':
if len(args) < 1:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
uuid = args[0]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
try:
backend.util_revoke_circle_invite(circle, user)
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
except error.MemberAlreadyInCircle:
self.send_numeric(Err.MemberAlreadyInCircle, ':CIRCLE {}'.format(ts))
return
elif action == 'ROLE':
if len(args) < 2:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
user_self = None
uuid = args[0]
role_num = args[1]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
if len(args) >= 3:
uuid_self = args[2]
user_self = backend._load_user_record(uuid_self)
if user_self is None:
self.send_numeric(Err.UserNotInDB, ':{}'.format(ts))
return
try:
role = CircleRole(int(role_num))
if user_self is not None and role is not CircleRole.Admin: raise ValueError()
backend.util_change_circle_membership_role(circle, user, role, user_self)
except ValueError:
self.send_numeric(Err.CircleRoleInvalid, ':CIRCLE {}'.format(ts))
return
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
except error.CircleMemberIsPending:
self.send_numeric(Err.CircleMemberIsPending, ':CIRCLE {}'.format(ts))
return
except error.MemberDoesntHaveSufficientCircleRole:
self.send_numeric(Err.DoesntHaveSufficientPermissions, ':CIRCLE {}'.format(ts))
return
elif action == 'REMOVE':
if len(args) < 1:
self.send_numeric(Err.TooFewArguments, ':CIRCLE {}'.format(ts))
return
uuid = args[0]
user = backend._load_user_record(uuid)
if user is None:
self.send_numeric(Err.UserNotInDB, ':CIRCLE {}'.format(ts))
return
try:
backend.util_remove_user_from_circle(circle, user)
except error.MemberNotInCircle:
self.send_numeric(Err.CircleMemberInvalid, ':CIRCLE {}'.format(ts))
return
except error.CantLeaveCircle:
self.send_numeric(Err.CantLeaveCircle, ':CIRCLE {}'.format(ts))
return
else:
self.send_numeric(Err.InvalidArgument, ':CIRCLE {}'.format(ts))
return
self.send_numeric(StatusCode.CircleActionSuccessful, ':CIRCLE {}'.format(ts))
def _m_alert(self, ts: str, type: str, content: str = '', url: str = '', targets: str = 'all', icon: str = '') -> None:
def _quote_circumcision(s: str) -> str:
if not s or len(s) < 2:
return s
if (s[0] == s[-1]) and s[0] in ('"', "'"):
quote = s[0]
inner = s[1:-1]
inner = inner.replace('\\\\', '\\').replace('\\' + quote, quote)
return inner
return s
if not content:
return self.send_numeric(Err.InvalidArgument, ts)
if type == 'MAINTENANCE':
try:
mt_mins = int(content)
except ValueError:
return self.send_numeric(Err.TooFewArguments, ts)
if self.backend.maintenance_mode or self.backend.notify_maintenance:
return self.send_numeric(Err.ServerInModeAlready, ts)
self.backend.push_maintenance_message(1, mt_mins)
return self.send_reply('ALERT', ts, 'OK')
else:
content = content.strip()
content_circumcised = _quote_circumcision(content)
if not content_circumcised:
return self.send_numeric(Err.InvalidArgument, ts)
sessions = set()
if targets.strip() == 'all':
sessions = set(self.backend._sc.iter_sessions())
else:
for t in [t.strip() for t in targets.split(',') if t.strip()]:
uuid = self.backend.util_get_uuid_from_email(t)
if uuid:
user = self.backend._load_user_record(uuid)
if user:
for s in self.backend.util_get_sessions_by_user(user):
sessions.add(s)
continue
for bs in self.backend._sc.iter_sessions():
if str(id(bs)) == t:
sessions.add(bs)
for bs in sessions:
bs.evt.on_client_alert(icon, url, content_circumcised)
self.send_reply('ALERT', ts, 'OK')
def _m_user(self, ts: str, action: str, uuid: str, field: str = '', *args: str) -> None:
def _parse_value_for_column(col, value_str):
py_type = getattr(col.type, 'python_type', None)
if py_type is not None:
try:
if py_type is bool:
v = value_str.strip().lower()
return v in ('1', 'true', 'yes', 'on')
if py_type is int:
return int(value_str)
if py_type is float:
return float(value_str)
return py_type(value_str)
except Exception:
return value_str
tname = col.type.__class__.__name__.lower()
if 'json' in tname or 'json' in str(col.type).lower():
try:
return json.loads(value_str)
except Exception:
return value_str
if 'boolean' in tname:
v = value_str.strip().lower()
return v in ('1', 'true', 'yes', 'on')
if 'integer' in tname or 'int' in tname:
try:
return int(value_str)
except Exception:
return value_str
return value_str
def set_passwords_and_prune(user_obj, pw: str, flags_list: List[str]):
if 'oldmsn' in flags_list:
user_obj.set_front_data('msn', 'pw_md5', hash.hasher_md5.encode(pw))
else:
if user_obj._front_data and 'msn' in user_obj._front_data:
user_obj._front_data.pop('msn', None)
flag_modified(user_obj, '_front_data')
if 'yahoo' in flags_list:
user_obj.set_front_data('ymsg', 'pw_md5_unsalted', hash.hasher_md5.encode(pw, salt=''))
user_obj.set_front_data('ymsg', 'pw_md5crypt', hash.hasher_md5crypt.encode(pw, salt='$1$_2S43d5f'))
else:
if user_obj._front_data and 'ymsg' in user_obj._front_data:
user_obj._front_data.pop('ymsg', None)
flag_modified(user_obj, '_front_data')
if 'oldaim' in flags_list:
pw_md5_encoded = hash.hasher_md5.encode(pw, identifier='AOL Instant Messenger (SM)')
pw_md5_salt = hash.hasher_md5.extract_salt(pw_md5_encoded)
user_obj.set_front_data('aim', 'pw_md5', pw_md5_encoded)
user_obj.set_front_data('aim', 'pw_md5_v5', hash.hasher_md5.encode_aim5(pw, salt=pw_md5_salt))
else:
if user_obj._front_data and 'aim' in user_obj._front_data:
user_obj._front_data.pop('aim', None)
flag_modified(user_obj, '_front_data')
if 'msim' in flags_list:
user_obj.set_front_data('msim', 'pw_sha1', hash.hasher_sha1.encode(pw))
else:
if user_obj._front_data and 'msim' in user_obj._front_data:
user_obj._front_data.pop('msim', None)
flag_modified(user_obj, '_front_data')
user_obj.password = hash.hasher.encode(pw)
if action == 'CREATE':
if len(args) < 4:
return self.send_numeric(Err.TooFewArguments, ts)
email = uuid
username = field
first_name = args[0]
last_name = args[1]
try:
uin = int(args[2])
except Exception:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
password_b64 = args[3]
flags = [f.lower() for f in args[4:]]
try:
pw_bytes = base64.b64decode(password_b64, validate=True)
password = pw_bytes.decode('utf-8')
except Exception:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
if len(username) > 32:
return self.send_numeric(Err.TooManyCharactersUsername, f':USER {ts}')
if len(email) > 254:
return self.send_numeric(Err.TooManyCharactersEmail, f':USER {ts}')
if username and not username.isalnum():
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
try:
with db.Session() as sess:
user_row = sess.query(db.User).filter_by(email=email).one_or_none()
if user_row is not None:
set_passwords_and_prune(user_row, password, flags)
sess.add(user_row)
created_uuid = user_row.uuid
else:
with open('config/restricted-usernames.json', 'r') as file:
restricted_usernames = json.load(file)
if username.lower() in restricted_usernames:
return self.send_numeric(Err.RestrictedUseranme, f':USER {ts}')
with open('config/restricted-emails.json', 'r') as file:
restricted_emails = json.load(file)
email_domain = email.lower().split('@')[-1] if '@' in email else ''
if email_domain in restricted_emails:
return self.send_numeric(Err.RestrictedEmail, f':USER {ts}')
if email_domain in disposable_emails:
return self.send_numeric(Err.RestrictedEmail, f':USER {ts}')
username_conflict = sess.query(db.User).filter_by(username=username).one_or_none()
email_conflict = sess.query(db.User).filter_by(email=email).one_or_none()
if username_conflict:
return self.send_numeric(Err.UsernameTaken, f':USER {ts}')
elif email_conflict:
return self.send_numeric(Err.EmailTaken, f':USER {ts}')
new_user = db.User(
uuid=misc.gen_uuid(),
email=email,
first_name=first_name,
last_name=last_name,
username=username,
uin=uin,
verified_to_login=False,
account_verified=False,
alias_active=False,
friendly_name=email,
groups={},
settings={},
suspended=False,
is_tester=False,
is_mvp=False,
show_in_dir=False
)
set_passwords_and_prune(new_user, password, flags)
sess.add(new_user)
self.logger.info(f"New user created")
self.logger.info(f"UUID: {new_user.uuid}")
self.logger.info(f"Username: {new_user.username}")
self.logger.info(f"E-mail: {new_user.email}")
self.logger.info(f"UIN: {new_user.uin}")
self.logger.info(f"Flags: {flags}")
sess.commit()
created_uuid = new_user.uuid
except Exception as e:
self.logger.error(e)
return self.send_numeric(Err.UserCreationFailed, f':USER {ts}')
try:
domain_user = self.backend._load_user_record(created_uuid)
if domain_user is not None:
try:
detail = self.backend._load_detail(domain_user)
except Exception:
detail = None
self.backend._mark_modified(domain_user, detail=detail)
except Exception as e:
self.logger.error(e)
self.logger.info("post-create backend processing failed")
self.send_reply('USER', 'CREATE', ts, created_uuid, 'OK')
return
elif action == 'UPDATE':
new_raw_value = ' '.join(args)
try:
domain_user = self.backend._load_user_record(uuid)
if domain_user:
for bs in self.backend.util_get_sessions_by_user(domain_user):
try:
bs.close()
except Exception:
pass
except Exception:
traceback.print_exc()
if field == 'alias_active':
try:
self.backend._handle_worklist_notify()
self.backend._worklist_notify.clear()
except Exception:
traceback.print_exc()
try:
with db.Session() as sess:
user_row = sess.query(db.User).filter_by(uuid=uuid).one_or_none()
if user_row is None:
return self.send_numeric(Err.UserNotInDB, ts)
if field == 'password':
if len(args) < 1:
return self.send_numeric(Err.TooFewArguments, ts)
password_b64 = args[0]
flags = [f.lower() for f in args[1:]]
try:
pw_bytes = base64.b64decode(password_b64, validate=True)
password = pw_bytes.decode('utf-8')
except Exception:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
set_passwords_and_prune(user_row, password, flags)
sess.add(user_row)
try:
self.backend._user_by_uuid.pop(uuid, None)
except Exception as e:
self.logger.error(e)
self.logger.info("Failed to nuke stale user cache")
#elif field not in ('account_verified', 'verified_to_login') and not user_row.verified_to_login:
# return self.send_numeric(Err.EmailNotVerified, ts)
elif field in ('alias_active') and not user_row.account_verified:
return self.send_numeric(Err.EmailNotVerified, ts)
elif field in ('show_in_dir', 'alias_active', 'account_verified' 'verified_to_login', 'suspended', 'is_tester', 'is_mvp'):
col = user_row.__table__.columns[field]
parsed_val = _parse_value_for_column(col, new_raw_value)
setattr(user_row, field, parsed_val)
sess.add(user_row)
elif field.startswith('profile.'):
prof_field = field.split('.', 1)[1]
profile = sess.query(db.UserProfile).filter_by(user_id=user_row.id).one_or_none()
if not profile:
profile = db.UserProfile(user_id=user_row.id)
sess.add(profile)
sess.flush()
if prof_field == 'interests':
try:
interests_json = json.loads(new_raw_value)
if not isinstance(interests_json, list):
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
total_len = 0
for item in interests_json:
if not isinstance(item, str):
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
if len(item) > 50:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
total_len += len(item)
if total_len > 200:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
profile.interests = interests_json
except json.JSONDecodeError:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
elif prof_field == 'pronouns':
if len(new_raw_value) > 16:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
col = profile.__table__.columns[prof_field]
val = _parse_value_for_column(col, new_raw_value)
setattr(profile, prof_field, val)
elif prof_field == 'website':
if len(new_raw_value) > 75:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
col = profile.__table__.columns[prof_field]
val = _parse_value_for_column(col, new_raw_value)
setattr(profile, prof_field, val)
elif prof_field == 'bio':
if len(new_raw_value) > 200:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
col = profile.__table__.columns[prof_field]
val = _parse_value_for_column(col, new_raw_value)
setattr(profile, prof_field, val)
elif prof_field not in profile.__table__.columns:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
else:
col = profile.__table__.columns[prof_field]
val = _parse_value_for_column(col, new_raw_value)
setattr(profile, prof_field, val)
sess.add(profile)
else:
if field.endswith('_uuid'):
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
if '.' in field:
top, sub = field.split('.', 1)
if top not in user_row.__table__.columns:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
col = user_row.__table__.columns[top]
tname = col.type.__class__.__name__.lower()
if 'json' in tname or 'json' in str(col.type).lower():
orig = getattr(user_row, top) or {}
try:
subval = json.loads(new_raw_value)
except Exception:
subval = new_raw_value
orig[sub] = subval
setattr(user_row, top, orig)
sess.add(user_row)
else:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
else:
username_conflict = sess.query(db.User).filter_by(username=new_raw_value).one_or_none()
email_conflict = sess.query(db.User).filter_by(email=new_raw_value).one_or_none()
if field not in user_row.__table__.columns:
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
if field == 'username' and len(new_raw_value) > 32:
return self.send_numeric(Err.TooManyCharactersUsername, f':USER {ts}')
if field == 'email' and len(new_raw_value) > 254:
return self.send_numeric(Err.TooManyCharactersEmail, f':USER {ts}')
if field == 'username' and new_raw_value and not new_raw_value.isalnum():
return self.send_numeric(Err.InvalidArgument, f':USER {ts}')
if field == 'username' and username_conflict:
return self.send_numeric(Err.UsernameTaken, f':USER {ts}')
if field == 'email' and email_conflict:
return self.send_numeric(Err.EmailTaken, f':USER {ts}')
with open('config/restricted-usernames.json', 'r') as file:
restricted_usernames = json.load(file)
if field == 'username' and new_raw_value.lower() in restricted_usernames:
return self.send_numeric(Err.RestrictedUseranme, f':USER {ts}')
if field == 'email':
with open('config/restricted-emails.json', 'r') as file:
restricted_emails = json.load(file)
email_domain = new_raw_value.lower().split('@')[-1] if '@' in new_raw_value else ''
if email_domain in restricted_emails:
return self.send_numeric(Err.RestrictedEmail, f':USER {ts}')
if email_domain in disposable_emails:
return self.send_numeric(Err.RestrictedEmail, f':USER {ts}')
col = user_row.__table__.columns[field]
val = _parse_value_for_column(col, new_raw_value)
setattr(user_row, field, val)
sess.add(user_row)
except Exception:
traceback.print_exc()
return self.send_numeric(Err.ServerUnknownError, ts)
try:
domain_user = self.backend._load_user_record(uuid)
except Exception:
domain_user = None
if field == 'email':
try:
if domain_user:
domain_user.email = new_raw_value
except Exception as e:
self.logger.error(e)
self.logger.info("Failed to update cached email")
if field == 'alias_active':
try:
if domain_user:
v = new_raw_value.strip().lower()
domain_user.alias_active = v in ('1', 'true', 'yes', 'on')
except Exception as e:
self.logger.error(e)
self.logger.info("Failed to update cached alias status")
try:
if domain_user:
try:
detail = self.backend._load_detail(domain_user)
except Exception:
detail = None
self.backend._mark_modified(domain_user, detail=detail)
except Exception as e:
self.logger.error(e)
self.logger.info("Failed while post-processing USER UPDATE")
if domain_user is not None:
output_value = "[REDACTED]" if field == "password" else new_raw_value
self.logger.info(f"{domain_user.username} ({domain_user.email}) has changed user attribute {field} to {output_value}")
else:
output_value = "[REDACTED]" if field == "password" else new_raw_value
self.logger.info(f"User with UUID {uuid} (unresolved) has changed user attribute {field} to {output_value}")
self.send_reply('USER', 'UPDATE', ts, uuid, 'OK')
return
elif action == 'DELETE':
try:
with db.Session() as sess:
user_row = sess.query(db.User).filter_by(uuid=uuid).one_or_none()
if user_row is None:
return self.send_numeric(Err.UserNotInDB, ts)
sess.delete(user_row)
sess.flush()
try:
domain_user = self.backend._load_user_record(uuid)
if domain_user:
for bs in self.backend.util_get_sessions_by_user(domain_user):
try:
bs.close()
except Exception:
pass
self.backend._mark_modified(domain_user, deleted=True)
except Exception as e:
self.logger.error(e)
self.logger.info("Failed while post-processing USER DELETE")
if domain_user is not None:
self.logger.info(f"{domain_user.username} ({domain_user.email}) has deleted their account")
else:
self.logger.info(f"User with UUID {uuid} (unresolved) has deleted their account")
self.send_reply('USER', 'DELETE', ts, uuid, 'OK')
return
except Exception as e:
self.logger.error(e)
self.logger.info("USER DELETE failed")
return self.send_numeric(Err.ServerUnknownError, ts)
else:
return self.send_numeric(Err.InvalidArgument, ts)
def _m_allthesessions(self, ts: str, email_filter: str = '') -> None:
# TODO: make this a payload command
sessions_info = []
for bs in self.backend._sc.iter_sessions():
if email_filter and bs.user.email != email_filter:
continue
info = f"{id(bs)}|{bs.user.uuid}|{bs.user.email}|{bs.client.ToJSON(bs.client)}"
sessions_info.append(info)
self.send_reply('ALLTHESESSIONS', ts, *sessions_info)
def _m_session(self, ts: str, sess_id: str, method: str) -> None:
target = None
for bs in self.backend._sc.iter_sessions():
if str(id(bs)) == sess_id:
target = bs
break
if not target:
return self.send_numeric(e, ts)
if not method:
return self.send_numeric(105, ts)
if method.upper() == 'GET':
data = (
sess_id,
target.user.username,
target.user.uuid,
target.user.email,
str(target.user.uin),
target.client.ToJSON(target.client),
str(target.chat_enabled),
)
self.send_reply('SESSION', ts, *data)
elif method.upper() == 'KILL':
target.close(sess_id=sess_id)
self.send_reply('SESSION', ts, 'KILLED')
else:
self.send_numeric(104, ts)
def _m_quit(self) -> None:
self.send_reply('QUIT')
self.close()
#async def _ping_conn(self) -> None:
# while True:
# await asyncio.sleep(60)
# if self.closed or not self.alive:
# if not self.alive:
# self.close()
# break
# self.alive = False
# self.current_challenge = hash.gen_salt()
# self.send_reply('PING', ':{}'.format(self.current_challenge))
def data_received(self, transport: asyncio.BaseTransport, data: bytes) -> None:
self.peername = transport.get_extra_info('peername')
for m in self.reader.data_received(data):
try:
f = getattr(self, '_m_{}'.format(m[0].lower()))
f(*m[1:])
except Exception as ex:
self.logger.error(ex)
def send_numeric(self, n: int, *m: str) -> None:
self.send_reply('{:03}'.format(n), *m)
def send_reply(self, *m: str) -> None:
self.writer.write(m)
transport = self.transport
if transport is not None:
transport.write(self.flush())
def flush(self) -> bytes:
return self.writer.flush()
def close(self) -> None:
if self.closed: return
self.closed = True
#if self.alive_task is not None and not self.alive_task.cancelled:
# self.alive_task.cancel()
if self.authenticated:
self.backend._linked = False
if self.close_callback:
self.close_callback()
class INSReader:
__slots__ = ('_logger', '_data')
_logger: misc.Logger
_data: bytes
def __init__(self, logger: misc.Logger) -> None:
self._logger = logger
self._data = b''
def data_received(self, data: bytes) -> Iterable[List[str]]:
if self._data:
self._data += data
else:
self._data = data
while self._data:
m = self._read()
if m is None:
break
self._logger.debug('[Client]', *m)
yield m
def _read(self) -> Optional[List[str]]:
try:
i = self._data.index(b'\r\n')
except ValueError:
return None
chunk = self._data[:i].decode('utf-8', errors='replace')
self._data = self._data[i+2:]
if '\r' in chunk or '\n' in chunk:
self._logger.info('Embedded CR/LF in incoming line; whack his peepee')
chunk = chunk.replace('\r', ' ').replace('\n', ' ')
toks = []
while True:
chunk = chunk.lstrip(' ')
if not chunk:
break
if chunk[:1] == ':':
toks.append(chunk[1:])
break
if chunk[0] in ('"', "'"):
quote_char = chunk[0]
j = 1
escaped = False
found = False
while j < len(chunk):
ch = chunk[j]
if ch == '\\' and not escaped:
escaped = True
j += 1
continue
if ch == quote_char and not escaped:
found = True
break
escaped = False
j += 1
if found:
raw_tok = chunk[:j+1]
chunk = chunk[j+1:]
inner = raw_tok[1:-1]
inner = inner.replace('\\' + quote_char, quote_char).replace('\\\\', '\\')
if '\r' in inner or '\n' in inner:
self._logger.info('Satanizing') # intentional
inner = inner.replace('\r', ' ').replace('\n', ' ')
toks.append(inner)
else:
inner = chunk[1:].replace('\\' + quote_char, quote_char).replace('\\\\', '\\')
if '\r' in inner or '\n' in inner:
self._logger.info('Satanizing but quotes')
inner = inner.replace('\r', ' ').replace('\n', ' ')
toks.append(inner)
break
continue
k = chunk.find(' ')
if k < 0:
tok = chunk
chunk = ''
else:
tok = chunk[:k]
chunk = chunk[k+1:]
if tok:
if '\r' in tok or '\n' in tok:
self._logger.info('Satanizing CR/LF from token')
tok = tok.replace('\r', ' ').replace('\n', ' ')
toks.append(tok)
if not chunk:
break
return toks
class INSWriter:
__slots__ = ('_logger', '_buf')
_logger: misc.Logger
_buf: io.BytesIO
def __init__(self, logger: misc.Logger) -> None:
self._logger = logger
self._buf = io.BytesIO()
def write(self, m: Iterable[Any]) -> None:
safe_parts = []
for part in m:
s = str(part)
if '\r' in s or '\n' in s:
self._logger.info('Satanizing before sending')
s = s.replace('\r', ' ').replace('\n', ' ')
if ' ' in s or s.startswith(':') or any(ord(c) < 32 for c in s):
escaped = s.replace('\\', '\\\\').replace('"', '\\"')
s = f'"{escaped}"'
safe_parts.append(s)
self._logger.debug('[Server]', *safe_parts)
self._buf.write(' '.join(safe_parts).encode('utf-8'))
self._buf.write(b'\r\n')
def flush(self) -> bytes:
data = self._buf.getvalue()
if data:
self._buf = io.BytesIO()
return data
# `1xx`: Generic codes; `2xx`: Circle codes; `3xx`: Server operations codes; `4xx`: User service status codes
class Err:
AuthenticationFailed = 101
NotAuthenticated = 102
InvalidArgument = 104
TooFewArguments = 105
CircleDoesNotExist = 200
CircleRoleInvalid = 202
CircleMemberInvalid = 203
MemberAlreadyInCircle = 204
CircleMemberIsPending = 205
DoesntHaveSufficientPermissions = 206
CantLeaveCircle = 207
ServerUnknownError = 300
ServerInModeAlready = 310
UsernameTaken = 410
EmailTaken = 411
RestrictedUseranme = 415
RestrictedEmail = 416
EmailNotVerified = 419
TooManyCharactersUsername = 425
TooManyCharactersEmail = 426
UserCreationFailed = 427
UserNotInDB = 428
class StatusCode:
CircleActionSuccessful = 201
+52
View File
@@ -0,0 +1,52 @@
from typing import Optional, Callable
import asyncio
from core.backend import Backend
from util.misc import Logger
import settings
from .ctrl import INSCtrl
def register(loop: asyncio.AbstractEventLoop, backend: Backend) -> None:
from util.misc import ProtocolRunner
backend.add_runner(ProtocolRunner('0.0.0.0', 4309, ListenerINS, args = ['InterService', backend, INSCtrl], service = 'InterService'))
class ListenerINS(asyncio.Protocol):
logger: Logger
backend: Backend
controller: INSCtrl
transport: Optional[asyncio.WriteTransport]
def __init__(self, logger_prefix: str, backend: Backend, controller_factory: Callable[[Logger, str, Backend], INSCtrl]) -> None:
super().__init__()
self.logger = Logger(logger_prefix, self)
self.backend = backend
self.controller = controller_factory(self.logger, 'direct', backend)
self.controller.close_callback = self._on_close
self.transport = None
def connection_made(self, transport: asyncio.BaseTransport) -> None:
assert isinstance(transport, asyncio.WriteTransport)
self.transport = transport
self.logger.log_connect()
def connection_lost(self, exc: Optional[Exception]) -> None:
self.controller.close()
self.logger.log_disconnect()
self.transport = None
def data_received(self, data: bytes) -> None:
transport = self.transport
assert transport is not None
if self.backend.maintenance_mode:
transport.close()
return
self.controller.transport = None
self.controller.data_received(transport, data)
transport.write(self.controller.flush())
self.controller.transport = transport
def _on_close(self) -> None:
if self.transport is None: return
self.transport.close()
+657
View File
@@ -0,0 +1,657 @@
from datetime import datetime
from typing import Dict, Optional, Set, List, Tuple, Any, TypeVar
from enum import Enum, IntEnum, IntFlag
class User:
__slots__ = (
'id',
'uuid',
'email',
'username',
'first_name',
'last_name',
'uin',
'verified_to_login',
'account_verified',
'alias_active',
'status',
'detail',
'settings',
'date_created',
'date_login',
'suspended',
'is_tester',
'is_mvp',
'show_in_dir',
'evil_permanent',
'evil_temporary',
'avatar',
'profile'
)
id: int
uuid: str
email: str
username: str
first_name: str
last_name: str
uin: int
verified_to_login: bool
account_verified: bool
alias_active: bool
status: 'UserStatus'
detail: Optional['UserDetail']
settings: Dict[str, Any]
date_created: datetime
date_login: datetime
suspended: bool
is_tester: bool
is_mvp: bool
show_in_dir: bool
evil_permanent: int
evil_temporary: int
avatar: str
profile: 'UserProfile'
def __init__(
self, id: int, uuid: str, email: str, username: str, first_name: str, last_name: str, uin: int, verified_to_login: bool, account_verified: bool, alias_active: bool, status: 'UserStatus',
settings: Dict[str, Any], date_created: datetime, date_login: datetime, suspended: bool, is_tester: bool, is_mvp: bool, show_in_dir: bool, evil_permanent: int, evil_temporary: int, avatar: str, profile: 'UserProfile'
) -> None:
self.id = id
self.uuid = uuid
self.email = email
self.username = username
self.first_name = first_name
self.last_name = last_name
self.uin = uin
self.verified_to_login = verified_to_login
self.account_verified = account_verified
self.alias_active = alias_active
# `status`: true status of user
self.status = status
self.detail = None
self.settings = settings
self.date_created = date_created
self.date_login = date_login
self.suspended = suspended
self.is_tester = is_tester
self.is_mvp = is_mvp
self.show_in_dir = show_in_dir
self.evil_permanent = evil_permanent
self.evil_temporary = evil_temporary
self.avatar = avatar
self.profile = profile
class Contact:
__slots__ = ('head', '_groups', 'lists', 'status', 'is_messenger_user', 'pending', 'detail')
head: User
_groups: Set['ContactGroupEntry']
lists: 'ContactList'
status: 'UserStatus'
is_messenger_user: bool
pending: bool
detail: 'ContactDetail'
def __init__(
self, user: User, groups: Set['ContactGroupEntry'], lists: 'ContactList', status: 'UserStatus', detail: 'ContactDetail', *,
is_messenger_user: Optional[bool] = None, pending: Optional[bool] = None,
) -> None:
self.head = user
self._groups = groups
self.lists = lists
# `status`: status as known by the contact
self.status = status
self.is_messenger_user = _default_if_none(is_messenger_user, True)
self.pending = _default_if_none(pending, False)
self.detail = detail
def compute_visible_status(self, to_user: User) -> None:
# Set Contact.status based on BLP and Contact.lists
# If not blocked, Contact.status == Contact.head.status
if self.head.detail is None or _is_blocking(self.head, to_user):
self.status.substatus = Substatus.Offline
return
true_status = self.head.status
self.status.substatus = true_status.substatus
self.status.name = true_status.name
self.status.message = true_status.message
self.status.media = true_status.media
def is_in_group_id(self, group_id: str) -> bool:
for group in self._groups:
if group.id == group_id:
return True
return False
def group_in_entry(self, grp: 'Group') -> bool:
for group in self._groups:
if group.id == grp.id or group.uuid == grp.uuid:
return True
return False
def add_group_to_entry(self, grp: 'Group') -> None:
self._groups.add(ContactGroupEntry(
self.head.uuid, grp.id, grp.uuid,
))
def remove_from_group(self, grp: 'Group') -> None:
found_group = None
for group in self._groups:
if group.id == grp.id or group.uuid == grp.uuid:
found_group = group
break
if found_group is not None:
self._groups.discard(group)
def _is_blocking(blocker: User, blockee: User) -> bool:
detail = blocker.detail
assert detail is not None
contact = detail.contacts.get(blockee.uuid)
lists = (contact and contact.lists or 0)
if lists & ContactList.BL: return True
if lists & ContactList.AL: return False
return (blocker.settings.get('BLP', 'AL') == 'BL')
class ContactDetail:
__slots__ = (
'index_id', 'birthdate', 'anniversary', 'notes', 'first_name', 'middle_name', 'last_name',
'nickname', 'primary_email_type', 'personal_email', 'work_email', 'im_email', 'other_email',
'home_phone', 'work_phone', 'fax_phone', 'pager_phone', 'mobile_phone', 'other_phone',
'personal_website', 'business_website', 'locations',
)
index_id: str
birthdate: Optional[datetime]
anniversary: Optional[datetime]
notes: Optional[str]
first_name: Optional[str]
middle_name: Optional[str]
last_name: Optional[str]
nickname: Optional[str]
primary_email_type: Optional[str]
personal_email: Optional[str]
work_email: Optional[str]
im_email: Optional[str]
other_email: Optional[str]
home_phone: Optional[str]
work_phone: Optional[str]
fax_phone: Optional[str]
pager_phone: Optional[str]
mobile_phone: Optional[str]
other_phone: Optional[str]
personal_website: Optional[str]
business_website: Optional[str]
locations: Dict[str, 'ContactLocation']
def __init__(
self, index_id: str, *, birthdate: Optional[datetime] = None, anniversary: Optional[datetime] = None,
notes: Optional[str] = None, first_name: Optional[str] = None, middle_name: Optional[str] = None,
last_name: Optional[str] = None, nickname: Optional[str] = None, primary_email_type: Optional[str] = None,
personal_email: Optional[str] = None, work_email: Optional[str] = None, im_email: Optional[str] = None,
other_email: Optional[str] = None, home_phone: Optional[str] = None, work_phone: Optional[str] = None,
fax_phone: Optional[str] = None, pager_phone: Optional[str] = None, mobile_phone: Optional[str] = None,
other_phone: Optional[str] = None, personal_website: Optional[str] = None, business_website: Optional[str] = None,
):
self.index_id = index_id
self.birthdate = birthdate
self.anniversary = anniversary
self.notes = notes
self.first_name = first_name
self.middle_name = middle_name
self.last_name = last_name
self.nickname = nickname
self.primary_email_type = primary_email_type
self.personal_email = personal_email
self.work_email = work_email
self.im_email = im_email
self.other_email = other_email
self.home_phone = home_phone
self.work_phone = work_phone
self.fax_phone = fax_phone
self.pager_phone = pager_phone
self.mobile_phone = mobile_phone
self.other_phone = other_phone
self.personal_website = personal_website
self.business_website = business_website
self.locations = {}
class ContactGroupEntry:
__slots__ = ('contact_uuid', 'id', 'uuid')
contact_uuid: str
id: str
uuid: str
def __init__(self, contact_uuid: str, id: str, uuid: str) -> None:
self.contact_uuid = contact_uuid
self.id = id
self.uuid = uuid
class ContactLocation:
__slots__ = ('type', 'name', 'street', 'city', 'state', 'country', 'zip_code')
type: str
name: Optional[str]
street: Optional[str]
city: Optional[str]
state: Optional[str]
country: Optional[str]
zip_code: Optional[str]
def __init__(
self, type: str, *, name: Optional[str] = None, street: Optional[str] = None, city: Optional[str] = None,
state: Optional[str] = None, country: Optional[str] = None, zip_code: Optional[str] = None,
) -> None:
self.type = type
self.name = name
self.street = street
self.city = city
self.state = state
self.country = country
self.zip_code = zip_code
class UserProfile:
__slots__ = ('user_id', 'bio', 'pronouns', 'website', 'socials', 'streetaddr', 'city', 'state', 'zip', 'country', 'interests', 'visibility')
user_id: User
bio: Optional[str]
pronouns: Optional[str]
website: Optional[str]
socials: Optional[Dict[str, Any]]
streetaddr: Optional[str]
city: Optional[str]
state: Optional[str]
zip: Optional[int]
country: Optional[str]
interests: Optional[Dict[str, Any]]
visibility: Optional[str]
def __init__(
self, user_id: User, bio: Optional[str], pronouns: Optional[str], website: Optional[str],
socials: Optional[Dict[str, Any]], streetaddr: Optional[str], city: Optional[str], state: Optional[str],
zip: Optional[int], country: Optional[str], interests: Optional[Dict[str, Any]], visibility: Optional[str]
):
self.user_id = user_id
self.bio = bio
self.pronouns = pronouns
self.website = website
self.socials = socials
self.streetaddr = streetaddr
self.city = city
self.state = state
self.zip = zip
self.country = country
self.interests = interests
self.visibility = visibility
class UserStatus:
__slots__ = ('substatus', 'name', 'message', 'media')
substatus: 'Substatus'
name: str
message: str
media: Optional[Any]
def __init__(self, name: str) -> None:
self.substatus = Substatus.Offline
self.name = name
self.message = ''
self.media = None
def is_offlineish(self) -> bool:
return self.substatus is Substatus.Offline or self.substatus is Substatus.Invisible
class UserDetail:
__slots__ = ('_groups_by_id', '_groups_by_uuid', 'contacts')
_groups_by_id: Dict[str, 'Group']
_groups_by_uuid: Dict[str, 'Group']
contacts: Dict[str, 'Contact']
def __init__(self) -> None:
self._groups_by_id = {}
self._groups_by_uuid = {}
self.contacts = {}
def get_contacts_by_list(self, lst: 'ContactList') -> Tuple[Contact, ...]:
return tuple([ctc for ctc in self.contacts.values() if ctc.lists & lst])
def insert_group(self, grp: 'Group') -> None:
self._groups_by_id[grp.id] = grp
self._groups_by_uuid[grp.uuid] = grp
def get_group_by_id(self, id: str) -> Optional['Group']:
group = None
group = self._groups_by_id.get(id)
if group is None:
group = self._groups_by_uuid.get(id)
return group
def get_groups_by_name(self, name: str) -> List['Group']:
groups = [] # type: ContactList[Group]
for group in self._groups_by_id.values():
if group.name == name:
if group not in groups: groups.append(group)
for group in self._groups_by_uuid.values():
if group.name == name:
if group not in groups: groups.append(group)
return groups
def delete_group(self, grp: 'Group') -> None:
if grp.id in self._groups_by_id:
del self._groups_by_id[grp.id]
if grp.uuid in self._groups_by_uuid:
del self._groups_by_uuid[grp.uuid]
class Group:
__slots__ = ('id', 'uuid', 'name', 'is_favorite')
id: str
uuid: str
name: str
is_favorite: bool
def __init__(self, id: str, uuid: str, name: str, is_favorite: bool) -> None:
self.id = id
self.uuid = uuid
self.name = name
self.is_favorite = is_favorite
class MessageType(Enum):
Chat = object()
Nudge = object()
Typing = object()
TypingDone = object()
Webcam = object()
Ink = object()
MSNP2P = object()
MSNP2PInvite = object()
MSNP2PTransBody = object()
Emoticon = object()
Ignored = object()
Other = object()
class MessageData:
__slots__ = ('sender', 'sender_pop_id', 'type', 'text', 'front_cache')
sender: User
sender_pop_id: Optional[str]
type: MessageType
text: Optional[str]
front_cache: Dict[str, Any]
def __init__(self, *, sender: User, sender_pop_id: Optional[str] = None, type: MessageType, text: Optional[str] = None) -> None:
self.sender = sender
self.sender_pop_id = sender_pop_id
self.type = type
self.text = text
self.front_cache = {}
class TextWithData:
__slots__ = ('text', 'yahoo_utf8')
text: str
yahoo_utf8: Any
def __init__(self, text: str, yahoo_utf8: Any) -> None:
self.text = text
self.yahoo_utf8 = yahoo_utf8
class RoamingInfo:
__slots__ = ('name', 'name_last_modified', 'message', 'message_last_modified')
name: Optional[str]
name_last_modified: datetime
message: Optional[str]
message_last_modified: datetime
def __init__(self, name: Optional[str], name_last_modified: datetime, message: Optional[str], message_last_modified: datetime) -> None: #, avatar: Optional[str]) -> None:
self.name = name
self.name_last_modified = name_last_modified
self.message = message
self.message_last_modified = message_last_modified
class Circle:
__slots__ = (
'chat_id', 'name', 'owner_id', 'owner_uuid', 'owner_friendly', 'membership_access',
'request_membership_option', 'memberships',
)
chat_id: str
name: str
owner_id: int
owner_uuid: str
owner_friendly: str
membership_access: int
request_membership_option: int
memberships: Dict[str, 'CircleMembership']
def __init__(
self, chat_id: str, name: str, owner_id: int, owner_uuid: str, owner_friendly: str,
membership_access: int, request_membership_option: int,
) -> None:
self.chat_id = chat_id
self.name = name
self.owner_id = owner_id
self.owner_uuid = owner_uuid
self.owner_friendly = owner_friendly
self.membership_access = membership_access
self.request_membership_option = request_membership_option
self.memberships = {}
class Chatroom:
__slots__ = (
'chat_id', 'name', 'owner', 'topic', 'role', 'is_public'
)
chat_id: str
name: str
owner: User
topic: Optional[str]
role: 'ChatroomRole' # will be used for setting IRC user modes, etc
is_public: bool
def __init__(
self, chat_id: str, name: str, owner: User, role: 'ChatroomRole', topic: Optional[str] = None, is_public: bool = False
):
self.chat_id = chat_id
self.name = name
self.owner = owner
self.topic = topic
self.role = role
self.is_public = is_public
class CircleMembership:
__slots__ = (
'chat_id', 'head', 'role', 'state', 'blocking', 'inviter_uuid', 'inviter_email', 'inviter_name', 'invite_message',
)
chat_id: str
head: User
role: 'CircleRole'
state: 'CircleState'
blocking: bool
inviter_uuid: Optional[str]
inviter_email: Optional[str]
inviter_name: Optional[str]
invite_message: Optional[str]
def __init__(
self, chat_id: str, head: User, role: 'CircleRole', state: 'CircleState', *,
blocking: bool = False, inviter_uuid: Optional[str] = None, inviter_email: Optional[str] = None,
inviter_name: Optional[str] = None, invite_message: Optional[str] = None,
):
self.chat_id = chat_id
self.head = head
self.role = role
self.state = state
self.blocking = blocking
self.inviter_uuid = inviter_uuid
self.inviter_email = inviter_email
self.inviter_name = inviter_name
self.invite_message = invite_message
class OIM:
__slots__ = (
'uuid', 'run_id', 'from_email', 'from_username', 'from_friendly', 'from_friendly_encoding', 'from_friendly_charset',
'from_user_id', 'to_email', 'sent', 'origin_ip', 'oim_proxy', 'headers', 'message', 'utf8',
)
uuid: str
run_id: str
from_email: str
from_username: str
from_friendly: str
from_friendly_encoding: str
from_friendly_charset: str
from_user_id: Optional[str]
to_email: str
sent: datetime
origin_ip: Optional[str]
oim_proxy: Optional[str]
headers: Dict[str, str]
message: str
utf8: bool
def __init__(
self, uuid: str, run_id: str, from_email: str, from_username: str, from_friendly: str, to_email: str, sent: datetime,
message: str, utf8: bool, *, headers: Optional[Dict[str, str]] = None, from_friendly_encoding: Optional[str] = None,
from_friendly_charset: Optional[str] = None, from_user_id: Optional[str] = None, origin_ip: Optional[str] = None,
oim_proxy: Optional[str] = None,
) -> None:
self.uuid = uuid
self.run_id = run_id
self.from_email = from_email
self.from_username = from_username
self.from_friendly = from_friendly
self.from_friendly_encoding = _default_if_none(from_friendly_encoding, 'B')
self.from_friendly_charset = _default_if_none(from_friendly_charset, 'utf-8')
self.from_user_id = from_user_id
self.to_email = to_email
self.sent = sent
self.origin_ip = origin_ip
self.oim_proxy = oim_proxy
self.headers = _default_if_none(headers, {})
self.message = message
self.utf8 = utf8
T = TypeVar('T')
def _default_if_none(x: Optional[T], default: T) -> T:
if x is None: return default
return x
class Substatus(Enum):
Offline = object()
Online = object()
Busy = object()
Idle = object()
BRB = object()
Away = object()
OnPhone = object()
OutToLunch = object()
Invisible = object()
NotAtHome = object()
NotAtDesk = object()
NotInOffice = object()
OnVacation = object()
SteppedOut = object()
def is_offlineish(self) -> bool:
return self is Substatus.Offline or self is Substatus.Invisible
# TODO: Put this in the ContactList class
MembershipLabels = {
# From further discovery, `FL` isn't used officially in any of the membership SOAPs. Skip to `AL`.
0x02: "Allow",
0x04: "Block",
0x08: "Reverse",
0x10: "Pending"
}
class ContactList(IntFlag):
Empty = 0x00
FL = 0x01
AL = 0x02
BL = 0x04
RL = 0x08
PL = 0x10
def __init__(self, id: int) -> None:
super().__init__()
self.label = MembershipLabels.get(id, "Undefined")
@classmethod
def Parse(cls, label: str) -> Optional['ContactList']:
if not hasattr(cls, '_MAP'):
label_map = {v.lower(): k for k, v in MembershipLabels.items()}
setattr(cls, '_MAP', label_map)
return cls._MAP.get(label.lower())
class NetworkID(IntEnum):
WINDOWS_LIVE = 0x01
OFFICE_COMMUNICATOR = 0x02 # E-mail in Skype too
ALIAS = 0x03
TELEPHONE = 0x04
DOMAIN = 0x05
SINK = 0x06
CONTACT = 0x07
MNI = 0x08 # Skype + Mobile Network Interop, used by Vodafone
CIRCLE = 0x09
TEMPORARYGROUP = 0x0A
CID = 0x0B
APPID = 0x0C
CONNECTUSER = 0x0D
CONNECTNETWORKS = 0x0E
SMTP = 0x10 # Jaguire, Japanese mobile interop
LIVEIDSINK = 0x11
MULTICAST = 0x12
THREAD = 0x13 # https://github.com/msndevs/protocol-docs/wiki/Threads-(Groupchats)
SHORTCIRCUIT = 0x14 # Unknown what this was used for
ONETOONETEXT = 0x15
GROUPTEXT = 0x16
BOT = 0x1C
YAHOO = 0x20
PUBSUBTOPIC = 0x21
PUBSUBSUBSCRIBER = 0x22
WNSSID = 0x23
class CircleRole(IntEnum):
Empty = 0
Admin = 1
AssistantAdmin = 2
Member = 3
StatePendingOutbound = 4
class ChatroomRole(IntEnum):
Member = 0
RoomOwner = 1
Admin = 2
Moderator = 3
Bot = 4 # maybe add user flags service-wide instead of doing this?
class CircleState(IntEnum):
Empty = 0
WaitingResponse = 1
Left = 2
Accepted = 3
Rejected = 4
class RelationshipType(IntEnum):
Circle = 5
class Service:
__slots__ = ('host', 'port')
host: str
port: int
def __init__(self, host: str, port: int) -> None:
self.host = host
self.port = port
class LoginOption(Enum):
BootOthers = object()
NotifyOthers = object()
Duplicate = object()
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<colorBlue>blau</colorBlue>
<colorWhite>weiß</colorWhite>
<cmdHelp>Hilfe</cmdHelp>
<cmdRestart>Neustart</cmdRestart>
<cmdRoll>Würfeln</cmdRoll>
<cmdSwitchTurn>Zug weitergeben</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} möchte das Spiel neu starten, geht das in Ordnung?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>getrennt</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Datenfehler; Die Daten können nicht gesendet werden. Spiel gestoppt.</errorDataSend>
<errorDisconnected>Getrennt.</errorDisconnected>
<errorUserLeft>{0} hat das Spiel verlassen. Das Spiel wurde beendet.</errorUserLeft>
<infoBearOff>Alle Ihrer Spielsteine sind im Homeboard, beginnen Sie mit dem Auswürfeln.</infoBearOff>
<infoCheckerHit>Ein Spielstein wurde geschlagen und landet auf der Bar.</infoCheckerHit>
<infoFromBarFirst>Sie müssen die Bar zuerst leeren.</infoFromBarFirst>
<infoLastMove>Dies ist Ihr letzter Zug.</infoLastMove>
<infoMovesLeft>Sie haben {0} Züge übrig.</infoMovesLeft>
<infoNoRestart>Kein Neustart.</infoNoRestart>
<infoRequestRestart>Um Neustart ersuchen..</infoRequestRestart>
<infoRestartDenied>{0} möchte das Spiel nicht neu starten.</infoRestartDenied>
<infoRoll>Würfeln</infoRoll>
<infoSelectPiece>Wählen Sie den Spielstein, den Sie bewegen möchten.</infoSelectPiece>
<infoSelectTarget>Wählen Sie das Zielfeld aus oder klicken Sie erneut auf den Spielstein um ihn abzuwählen.</infoSelectTarget>
<infoTurnLost>Sie haben Ihren Zug verloren.</infoTurnLost>
<infoTurnLostDetail>Sie haben Ihren Zug verloren, es sind keine Züge möglich. Klicken Sie auf 'Zug weitergeben' um fortzufahren.</infoTurnLostDetail>
<lblBroughtToYouBy>Wird Ihnen präsentiert von {0}</lblBroughtToYouBy>
<lblConnectionType>Verbindungsart</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Das Spiel wurde beendet.</stateGameOver>
<stateGameOverWon>Gratulation, Sie haben gewonnen! Klicken Sie auf 'Neustart' für ein neues Spiel.</stateGameOverWon>
<stateGameOverLost>Was für ein Pech! Sie haben verloren.</stateGameOverLost>
<stateInitializing>Starte Spiel...</stateInitializing>
<stateMyTurn>Sie sind dran.</stateMyTurn>
<stateRemoteTurn>Bitte warten Sie bis {0} einen Zug gemacht hat.</stateRemoteTurn>
<stateStartMyTurn>Spiel gestartet. Sie spielen mit {0}. Sie sind dran.</stateStartMyTurn>
<stateStartRemoteTurn>Spiel gestartet. Sie spielen mit {0}. Bitte warten Sie bis {1} einen Zug gemacht hat.</stateStartRemoteTurn>
<stateWaitOnJoin>Bitte warten Sie bis die Verbindung mit Ihrem Gegner hergestellt ist.</stateWaitOnJoin>
</words>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<colorBlue>blue</colorBlue>
<colorWhite>white</colorWhite>
<cmdHelp>Help</cmdHelp>
<cmdRestart>Restart</cmdRestart>
<cmdRoll>Roll the dice</cmdRoll>
<cmdSwitchTurn>Switch turn</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} wants to restart this game, are you ok with that?</confirmRemoteWantsRestart>
<connectionDirect>direct</connectionDirect>
<connectionDisconnected>disconnected</connectionDisconnected>
<connectionIndirect>indirect</connectionIndirect>
<errorDataSend>Data error; unable to send data. Game stopped.</errorDataSend>
<errorDisconnected>Disconnected.</errorDisconnected>
<errorUserLeft>{0} has left the game. The game is over.</errorUserLeft>
<infoBearOff>All your checkers are in the home board, commence bearing off.</infoBearOff>
<infoCheckerHit>A checker has been hit and placed on the bar.</infoCheckerHit>
<infoFromBarFirst>Remember, you must clear the bar first.</infoFromBarFirst>
<infoLastMove>This is your last move.</infoLastMove>
<infoMovesLeft>You have {0} moves left.</infoMovesLeft>
<infoNoRestart>No restart.</infoNoRestart>
<infoRequestRestart>Requesting restart..</infoRequestRestart>
<infoRestartDenied>{0} doesn't want to restart this game.</infoRestartDenied>
<infoRoll>Roll the dice</infoRoll>
<infoSelectPiece>Select the checker you want to move.</infoSelectPiece>
<infoSelectTarget>Now select the target point or deselect by clicking the selected checker.</infoSelectTarget>
<infoTurnLost>You've lost your turn.</infoTurnLost>
<infoTurnLostDetail>You've lost your turn, there are no valid moves left. Click 'Switch turn' to continue.</infoTurnLostDetail>
<lblBroughtToYouBy>Brought to you by {0}</lblBroughtToYouBy>
<lblConnectionType>Connection type</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Game over.</stateGameOver>
<stateGameOverWon>Congratulations, you have won! Click 'Restart' to play again.</stateGameOverWon>
<stateGameOverLost>Too bad, you lost.</stateGameOverLost>
<stateInitializing>Initializing game...</stateInitializing>
<stateMyTurn>It's your turn.</stateMyTurn>
<stateRemoteTurn>Waiting for {0} to make a move.</stateRemoteTurn>
<stateStartMyTurn>Game started, you are {0}. It's your turn.</stateStartMyTurn>
<stateStartRemoteTurn>Game started, you are {0}. Waiting for {1} to complete his/her turn.</stateStartRemoteTurn>
<stateWaitOnJoin>Waiting for your opponent to join</stateWaitOnJoin>
</words>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<colorBlue>Azul</colorBlue>
<colorWhite>Blanco</colorWhite>
<cmdHelp>Ayuda</cmdHelp>
<cmdRestart>Reiniciar</cmdRestart>
<cmdRoll>Tira el dado</cmdRoll>
<cmdSwitchTurn>Cambiar turno</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} quiere reiniciar el juego, ¿estás de acuerdo?</confirmRemoteWantsRestart>
<connectionDirect>directa</connectionDirect>
<connectionDisconnected>Desconectar</connectionDisconnected>
<connectionIndirect>indirecta</connectionIndirect>
<errorDataSend>Error; Imposible enviar datos. Se ha parado el juego.</errorDataSend>
<errorDisconnected>Desconectado.</errorDisconnected>
<errorUserLeft>{0} ha dejado el juego. El juego ha acabado.</errorUserLeft>
<infoBearOff>Todas tus fichas están en el tablero, comienza a sacar fichas.</infoBearOff>
<infoCheckerHit>Una ficha ha sido golpeada y colocada sobre la barra,</infoCheckerHit>
<infoFromBarFirst>Recuerda, debes limpiar la barra primero.</infoFromBarFirst>
<infoLastMove>Este es tu último movimiento.</infoLastMove>
<infoMovesLeft>Te quedan {0} movimientos.</infoMovesLeft>
<infoNoRestart>No reiniciar.</infoNoRestart>
<infoRequestRestart>Comenzando a reiniciar..</infoRequestRestart>
<infoRestartDenied>{0} no quiere reiniciar el juego.</infoRestartDenied>
<infoRoll>Tira el dado</infoRoll>
<infoSelectPiece>Selecciona la ficha con la que quieres jugar</infoSelectPiece>
<infoSelectTarget>Ahora selecciona tu objetivo o no selecciones haciendo clic sobre la ficha seleccionada.</infoSelectTarget>
<infoTurnLost>Has perdido tu turno.</infoTurnLost>
<infoTurnLostDetail>Has perdido tu turno, no te quedan movimientos validos. Haz clic en 'Cambiar turno' para continuar.</infoTurnLostDetail>
<lblBroughtToYouBy>Juego patrocinado por {0}</lblBroughtToYouBy>
<lblConnectionType>Tipo de conexión</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Final del juego.</stateGameOver>
<stateGameOverWon>Enhorabuena, Has ganado! Haz clic en 'Reiniciar' para jugar de nuevo.</stateGameOverWon>
<stateGameOverLost>Has perdido.</stateGameOverLost>
<stateInitializing>Iniciando juego...</stateInitializing>
<stateMyTurn>Es tu turno.</stateMyTurn>
<stateRemoteTurn>Esperando por {0} para realizar movimiento.</stateRemoteTurn>
<stateStartMyTurn>El juego ha empezado, tu eres el color {0}. Es tu turno.</stateStartMyTurn>
<stateStartRemoteTurn>El juego ha empezado, tu eres el color {0}. Esperando por {1} para completar su turno.</stateStartRemoteTurn>
<stateWaitOnJoin>Esperando a tu oponente</stateWaitOnJoin>
</words>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<colorBlue>bleu</colorBlue>
<colorWhite>blanc</colorWhite>
<cmdHelp>Aide</cmdHelp>
<cmdRestart>Redémarrer</cmdRestart>
<cmdRoll>Lancer les dés</cmdRoll>
<cmdSwitchTurn>Switch turn</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} veut redémarrer la partie, êtes-vous d'accord ?</confirmRemoteWantsRestart>
<connectionDirect>direct</connectionDirect>
<connectionDisconnected>déconnecté</connectionDisconnected>
<connectionIndirect>indirect</connectionIndirect>
<errorDataSend>Erreur de données; impossible d'envoyer les données. Arrêt du jeu.</errorDataSend>
<errorDisconnected>Déconnecté.</errorDisconnected>
<errorUserLeft>{0} a quitté la partie. Fin du jeu.</errorUserLeft>
<infoBearOff>Tous vos pions sont dans le home board, commence bearing off.</infoBearOff>
<infoCheckerHit>Un pion has been hit and placed on the bar.</infoCheckerHit>
<infoFromBarFirst>Rappellez-vous que vous devez supprimer la barre avant tout.</infoFromBarFirst>
<infoLastMove>C'est votre dernier déplacement.</infoLastMove>
<infoMovesLeft>Il vous reste {0} déplacements.</infoMovesLeft>
<infoNoRestart>Pas de redémarrage.</infoNoRestart>
<infoRequestRestart>Demande de redémarrage...</infoRequestRestart>
<infoRestartDenied>{0} ne veut pas relancer la partie.</infoRestartDenied>
<infoRoll>Lancer les dés</infoRoll>
<infoSelectPiece>Choisissez le pion que vous voulez déplacer.</infoSelectPiece>
<infoSelectTarget>Maintenant, choisissez un point-cible ou déselectionnez en cliquant sur le pion choisi.</infoSelectTarget>
<infoTurnLost>Vous avez perdu votre tour.</infoTurnLost>
<infoTurnLostDetail>Vous avez perdu votre tour, il n'y a pas de déplacement valide. Cliquez sur 'Switch turn' pour continuer.</infoTurnLostDetail>
<lblBroughtToYouBy>Offert par {0}</lblBroughtToYouBy>
<lblConnectionType>Type de connexion</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Fin de la partie.</stateGameOver>
<stateGameOverWon>Félicitations, vous avez gagné ! Cliquez sur 'Redémarrer' pour rejouer.</stateGameOverWon>
<stateGameOverLost>Dommage, vous avez perdu.</stateGameOverLost>
<stateInitializing>Initialisation du jeu...</stateInitializing>
<stateMyTurn>A vous de jouer.</stateMyTurn>
<stateRemoteTurn>En attente d'un déplacement de {0}</stateRemoteTurn>
<stateStartMyTurn>Début de la partie, vous êtes {0}. A vous de jouer.</stateStartMyTurn>
<stateStartRemoteTurn>Début de la partie, vous êtes {0}. Attente de la fin du tour de {1}.</stateStartRemoteTurn>
<stateWaitOnJoin>Attente de votre adversaire</stateWaitOnJoin>
</words>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<colorBlue>kék</colorBlue>
<colorWhite>fehér</colorWhite>
<cmdHelp>Súgó</cmdHelp>
<cmdRestart>Újrakezdés</cmdRestart>
<cmdRoll>Dobd a kockát</cmdRoll>
<cmdSwitchTurn>Forduló csere</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} újra szeretné kezdeni a játékot, egyetért ezzel?</confirmRemoteWantsRestart>
<connectionDirect>közvetlen</connectionDirect>
<connectionDisconnected>megszakadt</connectionDisconnected>
<connectionIndirect>közvetett</connectionIndirect>
<errorDataSend>Adat hiba; nem lehet adatot küldeni. A játék megállt.</errorDataSend>
<errorDisconnected>Megszakadt.</errorDisconnected>
<errorUserLeft>{0} elhagyta a játékot. A játék véget ért.</errorUserLeft>
<infoBearOff>Az összes dámád a háznál van.</infoBearOff>
<infoCheckerHit>A dáma le lett ütve és el lett helyezve a pulton.</infoCheckerHit>
<infoFromBarFirst>Emlékezz, elöször meg kell tisztítanod a pultot.</infoFromBarFirst>
<infoLastMove>Ez az utolsó lépésed.</infoLastMove>
<infoMovesLeft>{0} lépésed van hátra.</infoMovesLeft>
<infoNoRestart>Nincs újrakezdés.</infoNoRestart>
<infoRequestRestart>Újrakezdési kérelem..</infoRequestRestart>
<infoRestartDenied>{0} nem szeretné újrakezdeni a játékot.</infoRestartDenied>
<infoRoll>Kocka eldobása</infoRoll>
<infoSelectPiece>Válaszd ki a dámát, amit el akarsz mozdítani.</infoSelectPiece>
<infoSelectTarget>Most válassz egy kiindulópontot vagy tedd le úgy, hogy a kijelölt dámára kattintasz.</infoSelectTarget>
<infoTurnLost>Vesztésre állsz, te jössz.</infoTurnLost>
<infoTurnLostDetail>Vesztésre állsz, te jössz, nincsen több lépés. Kattints a 'Forduló csere' gombra a folytatáshoz.</infoTurnLostDetail>
<lblBroughtToYouBy>Kihívó: {0}</lblBroughtToYouBy>
<lblConnectionType>Kapcsolat típusa</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Vége.</stateGameOver>
<stateGameOverWon>Gratulálok, nyertél! Kattints az 'Újrakezdés' gombra az új játékhoz.</stateGameOverWon>
<stateGameOverLost>Túl rossz, vesztettél.</stateGameOverLost>
<stateInitializing>Játék betöltése...</stateInitializing>
<stateMyTurn>Te jössz.</stateMyTurn>
<stateRemoteTurn>Várakozás {0} lépésére.</stateRemoteTurn>
<stateStartMyTurn>A játék elindult, te vagy a {0}. Te jössz.</stateStartMyTurn>
<stateStartRemoteTurn>A játék elindult, te vagy a {0}. Várakozás a {1} játékosra.</stateStartRemoteTurn>
<stateWaitOnJoin>Várakozás az ellenfél kapcsolódására</stateWaitOnJoin>
</words>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<colorBlue>blauw</colorBlue>
<colorWhite>wit</colorWhite>
<cmdHelp>Help</cmdHelp>
<cmdRestart>Herstart</cmdRestart>
<cmdRoll>Gooi de dobbelstenen</cmdRoll>
<cmdSwitchTurn>Geef beurt over</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} wil het spel opnieuw beginnen. Vind je dat goed?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>verbroken</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Data fout; kan geen gegevens versturen. Het spel is gestopt.</errorDataSend>
<errorDisconnected>Verbroken.</errorDisconnected>
<errorUserLeft>{0} heeft het spel verlaten. Het spel is gestopt.</errorUserLeft>
<infoBearOff>Alle stenen staan in het eindvak, je kunt nu gaan afspelen.</infoBearOff>
<infoCheckerHit>Een steen is op de balk geplaatst.</infoCheckerHit>
<infoFromBarFirst>Je moet eerst de balk leeghalen.</infoFromBarFirst>
<infoLastMove>Dit is de laatste steen.</infoLastMove>
<infoMovesLeft>Je kunt nog {0} stenen verplaatsen.</infoMovesLeft>
<infoNoRestart>Geen herstart.</infoNoRestart>
<infoRequestRestart>Herstart aanvragen..</infoRequestRestart>
<infoRestartDenied>{0} wil dit spel niet opnieuw beginnen.</infoRestartDenied>
<infoRoll>Gooi de dobbelstenen</infoRoll>
<infoSelectPiece>Selecteer de steen die je wilt verplaatsen.</infoSelectPiece>
<infoSelectTarget>Selecteer het vak waar de steen moet komen te staan of de-selecteer de steen door erop te klikken.</infoSelectTarget>
<infoTurnLost>Je beurt is voorbij.</infoTurnLost>
<infoTurnLostDetail>Je beurt is voorbij, er zijn geen stenen meer die verplaatst kunnen worden. Klik op 'Geef beurt over' om door te gaan.</infoTurnLostDetail>
<lblBroughtToYouBy>Aangeboden door {0}</lblBroughtToYouBy>
<lblConnectionType>Connectie type</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Spel over.</stateGameOver>
<stateGameOverWon>Gefeliciteerd, je hebt gewonnen! Klik 'Herstart' om opnieuw te spelen.</stateGameOverWon>
<stateGameOverLost>Jammer, je hebt verloren.</stateGameOverLost>
<stateInitializing>Spel wordt geïnitialiseerd...</stateInitializing>
<stateMyTurn>Het is jouw beurt.</stateMyTurn>
<stateRemoteTurn>Wacht op de beurt van {0}.</stateRemoteTurn>
<stateStartMyTurn>Het spel is gestart, jij bent {0}. Het is jouw beurt.</stateStartMyTurn>
<stateStartRemoteTurn>Het spel is gestart, jij bent {0}. Wacht op de beurt van {1}.</stateStartRemoteTurn>
<stateWaitOnJoin>Wacht op de tegenstander om mee te doen</stateWaitOnJoin>
</words>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<colorBlue>azul</colorBlue>
<colorWhite>branco</colorWhite>
<cmdHelp>Ajuda</cmdHelp>
<cmdRestart>Recomeçar</cmdRestart>
<cmdRoll>Atire o dado</cmdRoll>
<cmdSwitchTurn>Mude de turno</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} quer recomeçar o jogo, está de acordo?</confirmRemoteWantsRestart>
<connectionDirect>directo</connectionDirect>
<connectionDisconnected>desconectar</connectionDisconnected>
<connectionIndirect>indirecto</connectionIndirect>
<errorDataSend>Erro de comunicação. Jogo parado.</errorDataSend>
<errorDisconnected>Desconectado.</errorDisconnected>
<errorUserLeft>{0} saiu. O jogo acabou.</errorUserLeft>
<infoBearOff>Todas as suas peças estão no tabuleiro de inicio, começe a jogar para fora.</infoBearOff>
<infoCheckerHit>Uma peça foi atingida e colocada na barra.</infoCheckerHit>
<infoFromBarFirst>Lembre-se, tem de limpar a barra primeiro.</infoFromBarFirst>
<infoLastMove>Esta é a sua ultima jogada.</infoLastMove>
<infoMovesLeft>Tem {0} jogadas.</infoMovesLeft>
<infoNoRestart>Sem recomeço.</infoNoRestart>
<infoRequestRestart>A pedir um recomeço..</infoRequestRestart>
<infoRestartDenied>{0} não quer recomeçar o jogo.</infoRestartDenied>
<infoRoll>Atire o dado</infoRoll>
<infoSelectPiece>Selecione a peça que deseja mover.</infoSelectPiece>
<infoSelectTarget>Agora escolha o destino ou selecione outra peça.</infoSelectTarget>
<infoTurnLost>Perdeu a sua vez.</infoTurnLost>
<infoTurnLostDetail>Perdeu a sua vez, não existem jogadas válidas. Clique em 'Mude de turno' para continuar.</infoTurnLostDetail>
<lblBroughtToYouBy>Produzido por {0}</lblBroughtToYouBy>
<lblConnectionType>Tipo de ligação</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Jogo Terminado.</stateGameOver>
<stateGameOverWon>Parabéns, você ganhou! Clique em 'Recomeçar' para jogar de novo.</stateGameOverWon>
<stateGameOverLost>Você perdeu.</stateGameOverLost>
<stateInitializing>A iniciar jogo...</stateInitializing>
<stateMyTurn>É a sua vez.</stateMyTurn>
<stateRemoteTurn>À espera que {0} jogue.</stateRemoteTurn>
<stateStartMyTurn>Jogo começado, é a sua vez.</stateStartMyTurn>
<stateStartRemoteTurn>Jogo começado, à espera que {0} jogue.</stateStartRemoteTurn>
<stateWaitOnJoin>À espera que o seu adversário chegue.</stateWaitOnJoin>
</words>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<colorBlue>moder</colorBlue>
<colorWhite>bel</colorWhite>
<cmdHelp>Pomoč</cmdHelp>
<cmdRestart>Reset</cmdRestart>
<cmdRoll>Vrži kocke</cmdRoll>
<cmdSwitchTurn>Predaj potezo</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} želi resetirati to igro, se strinjaš?</confirmRemoteWantsRestart>
<connectionDirect>posredno</connectionDirect>
<connectionDisconnected>prekinjeno</connectionDisconnected>
<connectionIndirect>neposredno</connectionIndirect>
<errorDataSend>Podatkovna napaka; pošiljanje podatkov ni možno. Igra ustavljena.</errorDataSend>
<errorDisconnected>Prekinjeno.</errorDisconnected>
<errorUserLeft>{0} je zapustil igro. Igra je končana.</errorUserLeft>
<infoBearOff>Vsi tvoji žetoni so na domači tabli.</infoBearOff>
<infoCheckerHit>Žeton je bil zadet in postavljen na drog.</infoCheckerHit>
<infoFromBarFirst>Zapomni si, najprej moraš sprazniti drog.</infoFromBarFirst>
<infoLastMove>To je tvoj zadnji premik.</infoLastMove>
<infoMovesLeft>Imaš še {0} premikov.</infoMovesLeft>
<infoNoRestart>Brez reseta.</infoNoRestart>
<infoRequestRestart>Zahtevam reset..</infoRequestRestart>
<infoRestartDenied>{0} ne želi resetirati te igre.</infoRestartDenied>
<infoRoll>Vrži kocke</infoRoll>
<infoSelectPiece>Izberi žeton, katerega želiš premakniti.</infoSelectPiece>
<infoSelectTarget>Sedaj izberi destinacijo ali še enkrat klikni na žeton.</infoSelectTarget>
<infoTurnLost>Izgubil si potezo.</infoTurnLost>
<infoTurnLostDetail>Izgubil si potezo, nobenega premika ni možno izvesti. Klikni 'Predaj potezo' za nadaljevanje.</infoTurnLostDetail>
<lblBroughtToYouBy>Vam na uslugo: {0}</lblBroughtToYouBy>
<lblConnectionType>Vrsta povezave</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Igra je končana.</stateGameOver>
<stateGameOverWon>Čestitam, zmagal si! Klikni 'Reset' za novo igro.</stateGameOverWon>
<stateGameOverLost>Škoda, izgubil si.</stateGameOverLost>
<stateInitializing>Nalagam igro...</stateInitializing>
<stateMyTurn>Ti si na vrsti.</stateMyTurn>
<stateRemoteTurn>Čakaš, da {0} naredi potezo.</stateRemoteTurn>
<stateStartMyTurn>Igra se je začela, ti si {0}. Ti si na vrsti.</stateStartMyTurn>
<stateStartRemoteTurn>Igra se je začela, ti si {0}. Čakaš, da {1} napravi potezo.</stateStartRemoteTurn>
<stateWaitOnJoin>Čakaš na nasprotnika da se pridruži</stateWaitOnJoin>
</words>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<colorBlue>blå</colorBlue>
<colorWhite>vit</colorWhite>
<cmdHelp>Hjälp</cmdHelp>
<cmdRestart>Starta om</cmdRestart>
<cmdRoll>Rulla tärningarna</cmdRoll>
<cmdSwitchTurn>Ändra tur</cmdSwitchTurn>
<confirmRemoteWantsRestart>{0} vill starta om omgången, är det okej med dig?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>anslutning bruten</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Data fel; omöjligt att skicka data. Spelet har stoppats.</errorDataSend>
<errorDisconnected>Anslutning bruten.</errorDisconnected>
<errorUserLeft>{0} har lämnat spelet. Spelet är över.</errorUserLeft>
<infoBearOff>Alla dina pjäser är på hemmaplan, spelet kan börja.</infoBearOff>
<infoCheckerHit>En pjäs har blivit träffad och har placerats på baren.</infoCheckerHit>
<infoFromBarFirst>Kom ihåg, du måste rensa baren först.</infoFromBarFirst>
<infoLastMove>Det här är ditt sista drag.</infoLastMove>
<infoMovesLeft>Du har {0} drag kvar.</infoMovesLeft>
<infoNoRestart>Ingen omstart.</infoNoRestart>
<infoRequestRestart>Begär omstart..</infoRequestRestart>
<infoRestartDenied>{0} vill inte starta om spelet.</infoRestartDenied>
<infoRoll>Rulla tärningarna</infoRoll>
<infoSelectPiece>Välj pjäsen du vill flytta.</infoSelectPiece>
<infoSelectTarget>Välj platsen dit du vill flytta eller avmarkera genom att klicka på den markerade pjäsen.</infoSelectTarget>
<infoTurnLost>Du förlorade din tur.</infoTurnLost>
<infoTurnLostDetail>Du förlorade din tur, det är inga giltliga drag kvar. Klicka på 'Ändra tur' för att fortsätta.</infoTurnLostDetail>
<lblBroughtToYouBy>Framtaget till dig av {0}</lblBroughtToYouBy>
<lblConnectionType>Anslutnings typ</lblConnectionType>
<lblVersion>Backgammon {0}</lblVersion>
<stateGameOver>Spelet över.</stateGameOver>
<stateGameOverWon>Grattis, du har vunnit! Klicka på 'Starta om' för att spela igen.</stateGameOverWon>
<stateGameOverLost>För dåligt, du förlorade.</stateGameOverLost>
<stateInitializing>Påbörjar spelet...</stateInitializing>
<stateMyTurn>Det är din tur.</stateMyTurn>
<stateRemoteTurn>Väntar på att {0} ska göra sitt drag.</stateRemoteTurn>
<stateStartMyTurn>Spelet har startat, du är {0}. Det är din tur.</stateStartMyTurn>
<stateStartRemoteTurn>Spelet har startat, du är {0}. Väntar på att {1} ska slutföra hans/hennes tur.</stateStartRemoteTurn>
<stateWaitOnJoin>Väntar på att motståndaren ska ansluta</stateWaitOnJoin>
</words>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
<html>
<head>
<title>Backgammon</title>
<style type="text/css"> body{font-family:Tahoma;font-size:8pt}
table{font-family:Tahoma;font-size:8pt;border-style:solid;border-width:1px;border-color:buttonface;border-collapse:collapse}
.btn{font-family:Tahoma;font-size:8pt;border-style:solid;border-width:1px;border-color:buttonface;background-color:white}
</style>
</head>
<body>
<P>No help yet.</P>
<p>For a detailed explanation of the game look <a href="http://www.bkgm.com/rules.html">
here</a></p>
<p>Direction of movement:</p>
<img src="direction.gif">
<p><b>Known bug:</b><br>
When either number of a roll can be played, but not both, a player is not forced to play the larger number.
</p>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<boat0>Flugzeugträger</boat0>
<boat1>Schlachtschiff</boat1>
<boat2>Kreuzer</boat2>
<boat3>U-Boot</boat3>
<boat4>Zerstörer</boat4>
<cmdDone>Fertig</cmdDone>
<cmdHelp>Hilfe</cmdHelp>
<cmdPlaceRandomly>Schiffe zufällig platzieren</cmdPlaceRandomly>
<cmdRestart>Neustart</cmdRestart>
<confirmRemoteWantsRestart>{0} möchte das Spiel neu starten, geht das in Ordnung?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>getrennt</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Datenfehler; Die Daten können nicht gesendet werden. Spiel gestoppt.</errorDataSend>
<errorDisconnected>Getrennt.</errorDisconnected>
<errorUserLeft>{0} hat das Spiel verlassen. Das Spiel wurde beendet.</errorUserLeft>
<infoHit>Treffer!</infoHit>
<infoMiss>Daneben.</infoMiss>
<infoLostBoat>Ein {0} wurde versenkt.</infoLostBoat>
<infoNoRestart>Kein Neustart.</infoNoRestart>
<infoPlaceShips>Platzieren Sie Ihre Schiffe durch Klicken auf 'Schiffe zufällig platzieren'. Wenn Sie mit der Position Ihrer Flotte nicht einverstanden sind klicken Sie noch einmal. Klicken Sie auf 'Fertig' um mit dem Spiel zu beginnen.</infoPlaceShips>
<infoRequestRestart>Um Neustart ersuchen..</infoRequestRestart>
<infoRemoteTurn>Bitte warten Sie bis Ihr Gegner einen Zug gemacht hat.</infoRemoteTurn>
<infoRestartDenied>{0} möchte das Spiel nicht neu starten.</infoRestartDenied>
<infoSunkBoat>Ein {0} wurde versenkt.</infoSunkBoat>
<infoWaitPlacing>Bitte warten Sie bis Ihr Gegner seine Flotte platziert hat.</infoWaitPlacing>
<infoYourTurn>Sie sind dran. Klicken Sie auf ein freies Feld Ihres Gegners und versuchen Sie seine Schiffe zu treffen.</infoYourTurn>
<lblBroughtToYouBy>Wird Ihnen präsentiert von {0}</lblBroughtToYouBy>
<lblConnectionType>Verbindungsart</lblConnectionType>
<lblEnemyNavy>Gegnerische Flotte</lblEnemyNavy>
<lblVersion>Schiffe Versenken {0}</lblVersion>
<lblYourNavy>Ihre Flotte</lblYourNavy>
<stateGameOverLost>Was für ein Pech! Sie haben verloren.</stateGameOverLost>
<stateGameOverWon>Gratulation, Sie haben gewonnen!</stateGameOverWon>
<stateInitializing>Starte Spiel...</stateInitializing>
<stateMyTurn>Sie sind dran.</stateMyTurn>
<statePlacingShips>Platzieren Sie Ihre Schiffe</statePlacingShips>
<stateRemoteTurn>Bitte warten Sie bis {0} einen Zug gemacht hat.</stateRemoteTurn>
<stateStartMyTurn>Spiel gestartet. Sie sind dran.</stateStartMyTurn>
<stateStartRemoteTurn>Spiel gestartet. Bitte warten Sie bis {0} einen Zug gemacht hat.</stateStartRemoteTurn>
<stateWaitOnJoin>Bitte warten Sie bis die Verbindung mit Ihrem Gegner hergestellt ist.</stateWaitOnJoin>
<stateWaitOnPlacing>Bitte warten Sie auf Ihren Gegner...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<boat0>Aircraft Carrier</boat0>
<boat1>Battleship</boat1>
<boat2>Cruiser</boat2>
<boat3>Submarine</boat3>
<boat4>Destroyer</boat4>
<cmdDone>Done</cmdDone>
<cmdHelp>Help</cmdHelp>
<cmdPlaceRandomly>Place ships randomly</cmdPlaceRandomly>
<cmdRestart>Restart</cmdRestart>
<confirmRemoteWantsRestart>{0} wants to restart this game, are you ok with that?</confirmRemoteWantsRestart>
<connectionDirect>direct</connectionDirect>
<connectionDisconnected>disconnected</connectionDisconnected>
<connectionIndirect>indirect</connectionIndirect>
<errorDataSend>Data error; unable to send data. Game stopped.</errorDataSend>
<errorDisconnected>Disconnected.</errorDisconnected>
<errorUserLeft>{0} has left the game. The game is over.</errorUserLeft>
<infoHit>A hit!</infoHit>
<infoMiss>A miss.</infoMiss>
<infoLostBoat>You just lost {0}.</infoLostBoat>
<infoNoRestart>No restart.</infoNoRestart>
<infoPlaceShips>Place your ships by clicking on the 'Place ships randomly' button. If you're not satisfied with the position of your fleet click again. Click 'Done' to start playing.</infoPlaceShips>
<infoRequestRestart>Requesting restart..</infoRequestRestart>
<infoRemoteTurn>Waiting for your opponent to make a move.</infoRemoteTurn>
<infoRestartDenied>{0} doesn't want to restart this game.</infoRestartDenied>
<infoSunkBoat>You sank {0}.</infoSunkBoat>
<infoWaitPlacing>Waiting for your opponent to finish placing it's fleet.</infoWaitPlacing>
<infoYourTurn>It's your turn. Click a free square on the opponent's board and try to sink his ships.</infoYourTurn>
<lblBroughtToYouBy>Brought to you by {0}</lblBroughtToYouBy>
<lblConnectionType>Connection type</lblConnectionType>
<lblEnemyNavy>Enemy's navy</lblEnemyNavy>
<lblVersion>Battleships {0}</lblVersion>
<lblYourNavy>Your navy</lblYourNavy>
<stateGameOverLost>Game over, you have lost!</stateGameOverLost>
<stateGameOverWon>Game over, you have won!</stateGameOverWon>
<stateInitializing>Initializing game...</stateInitializing>
<stateMyTurn>It's your turn.</stateMyTurn>
<statePlacingShips>Place your ships</statePlacingShips>
<stateRemoteTurn>Waiting for {0} to make a move.</stateRemoteTurn>
<stateStartMyTurn>Game started, it's your turn.</stateStartMyTurn>
<stateStartRemoteTurn>Game started, waiting for {0} to make a move.</stateStartRemoteTurn>
<stateWaitOnJoin>Waiting for your opponent to join</stateWaitOnJoin>
<stateWaitOnPlacing>Waiting for your opponent...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<boat0>Portaaviones</boat0>
<boat1>Acorazado</boat1>
<boat2>Crucero</boat2>
<boat3>Submarino</boat3>
<boat4>Destructor</boat4>
<cmdDone>Listo</cmdDone>
<cmdHelp>Ayuda</cmdHelp>
<cmdPlaceRandomly>Situar barcos aleatoriamente</cmdPlaceRandomly>
<cmdRestart>Reiniciar</cmdRestart>
<confirmRemoteWantsRestart>{0} quiere reiniciar el juego, ¿estás de acuerdo?</confirmRemoteWantsRestart>
<connectionDirect>directa</connectionDirect>
<connectionDisconnected>desconectado</connectionDisconnected>
<connectionIndirect>indirecta</connectionIndirect>
<errorDataSend>Se ha producido un error; imposible enviar datos. El juego se ha interrumpido.</errorDataSend>
<errorDisconnected>Desconectado.</errorDisconnected>
<errorUserLeft>{0} ha abandonado el juego. La partida ha finalizado.</errorUserLeft>
<infoHit>Tocado</infoHit>
<infoMiss>Agua</infoMiss>
<infoLostBoat>Has perdido {0}.</infoLostBoat>
<infoNoRestart>No reiniciar.</infoNoRestart>
<infoPlaceShips>Coloca tus barcos haciendo clic en el botón 'Situar barcos aleatoriamente'. Puedes pulsar tantas veces como quieras hasta que estés satisfecho con la colocación de tu flota. Pulsa 'Listo' para comenzar la partida.</infoPlaceShips>
<infoRequestRestart>Solicitando reiniciar...</infoRequestRestart>
<infoRemoteTurn>Esperando a que tu oponente mueva.</infoRemoteTurn>
<infoRestartDenied>{0} no quiere reiniciar la partida.</infoRestartDenied>
<infoSunkBoat>Has hundido un {0}.</infoSunkBoat>
<infoWaitPlacing>Esperando a que tu oponente termine de situar su flota.</infoWaitPlacing>
<infoYourTurn>Es tu turno. Pulsa sobre un espacio libre en el tablero de tu oponente y trata de hundir sus barcos.</infoYourTurn>
<lblBroughtToYouBy>Juego patrocinado por {0}</lblBroughtToYouBy>
<lblConnectionType>Tipo de conexión</lblConnectionType>
<lblEnemyNavy>Flota enemiga</lblEnemyNavy>
<lblVersion>Hundir la flota {0}</lblVersion>
<lblYourNavy>Tu flota</lblYourNavy>
<stateGameOverLost>El juego ha finalizado. Has perdido.</stateGameOverLost>
<stateGameOverWon>El juego ha finalizado. Has ganado.</stateGameOverWon>
<stateInitializing>Iniciando juego...</stateInitializing>
<stateMyTurn>Es tu turno.</stateMyTurn>
<statePlacingShips>Sitúa tus barcos</statePlacingShips>
<stateRemoteTurn>Esperando a que {0} mueva.</stateRemoteTurn>
<stateStartMyTurn>El juego ha comenzado, es tu turno.</stateStartMyTurn>
<stateStartRemoteTurn>El juego ha comenzado, esperando a que {0} mueva.</stateStartRemoteTurn>
<stateWaitOnJoin>Esperando a que se una tu oponente</stateWaitOnJoin>
<stateWaitOnPlacing>Esperando a tu oponente...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<boat0>Porte-avions</boat0>
<boat1>Cuirassé</boat1>
<boat2>Croiseur</boat2>
<boat3>Sous-marin</boat3>
<boat4>Destroyer</boat4>
<cmdDone>Terminé</cmdDone>
<cmdHelp>Aide</cmdHelp>
<cmdPlaceRandomly>Placer les bateaux de façon aléatoire</cmdPlaceRandomly>
<cmdRestart>Redémarrer</cmdRestart>
<confirmRemoteWantsRestart>{0} veut relancer la partie, êtes-vous d'accord ?</confirmRemoteWantsRestart>
<connectionDirect>direct</connectionDirect>
<connectionDisconnected>déconnecté</connectionDisconnected>
<connectionIndirect>indirect</connectionIndirect>
<errorDataSend>Erreur de données; impossible d'envoyer les données. Arrêt du jeu.</errorDataSend>
<errorDisconnected>Déconnecté.</errorDisconnected>
<errorUserLeft>{0} a quitté la partie. Fin du jeu.</errorUserLeft>
<infoHit>Touché !</infoHit>
<infoMiss>Raté.</infoMiss>
<infoLostBoat>Vous venez de perdre {0}.</infoLostBoat>
<infoNoRestart>Pas de redémarrage.</infoNoRestart>
<infoPlaceShips>Placez vos bateaux en cliquant sur le bouton 'Placer les bateaux de façon aléatoire'. Si ces positions ne vous conviennent pas, recliquez. Cliquez sur 'Terminé' pour commencer à jouer.</infoPlaceShips>
<infoRequestRestart>Demande d'un redémarrage...</infoRequestRestart>
<infoRemoteTurn>Attente d'un mouvement de votre adversaire.</infoRemoteTurn>
<infoRestartDenied>{0} ne veut pas relancer cette partie.</infoRestartDenied>
<infoSunkBoat>Vous avez coulé {0}.</infoSunkBoat>
<infoWaitPlacing>Attente du placement de la flotte de votre adversaire.</infoWaitPlacing>
<infoYourTurn>A vous de jouer. Cliquez sur un carré libre sur l'écran de votre adversaire et tentez de couler ses bateaux.</infoYourTurn>
<lblBroughtToYouBy>Offert par {0}</lblBroughtToYouBy>
<lblConnectionType>Type de connexion</lblConnectionType>
<lblEnemyNavy>Marine de l'ennemi</lblEnemyNavy>
<lblVersion>Bataille navale {0}</lblVersion>
<lblYourNavy>Votre marine</lblYourNavy>
<stateGameOverLost>Partie terminée, vous avez perdu !</stateGameOverLost>
<stateGameOverWon>Partie terminée, vous avez gagné !</stateGameOverWon>
<stateInitializing>Initialisation du jeu...</stateInitializing>
<stateMyTurn>A vous de jouer.</stateMyTurn>
<statePlacingShips>Placez vos bateaux</statePlacingShips>
<stateRemoteTurn>Attente d'un mouvement de {0}</stateRemoteTurn>
<stateStartMyTurn>Début du jeu, à vous de jouer.</stateStartMyTurn>
<stateStartRemoteTurn>Début du jeu, attente d'un mouvement de {0}</stateStartRemoteTurn>
<stateWaitOnJoin>Attente de votre adversaire</stateWaitOnJoin>
<stateWaitOnPlacing>Attente des placements de votre adversaire...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<boat0>Repülõgép-anyahajó</boat0>
<boat1>Csatahajó</boat1>
<boat2>Cirkáló</boat2>
<boat3>Tengeralattjáró</boat3>
<boat4>Romboló</boat4>
<cmdDone>Rendben</cmdDone>
<cmdHelp>Súgó</cmdHelp>
<cmdPlaceRandomly>Helyezd el a hajóidat</cmdPlaceRandomly>
<cmdRestart>Újrakezdés</cmdRestart>
<confirmRemoteWantsRestart>{0} újra szeretné kezdeni a játékot, egyet ért ezzel?</confirmRemoteWantsRestart>
<connectionDirect>közvetett</connectionDirect>
<connectionDisconnected>megszakadt</connectionDisconnected>
<connectionIndirect>közvetlen</connectionIndirect>
<errorDataSend>Adat hiba; nem lehet adatot küldeni. A játék leállt.</errorDataSend>
<errorDisconnected>Megszakadt.</errorDisconnected>
<errorUserLeft>{0} elhagyta a játékot. A játék véget ért.</errorUserLeft>
<infoHit>Találat!</infoHit>
<infoMiss>Elhibázás.</infoMiss>
<infoLostBoat>Vesztettél, {0}.</infoLostBoat>
<infoNoRestart>Nincs újrakezdés.</infoNoRestart>
<infoPlaceShips>Helyezd el a hajóidat a 'Helyezd el a hajóidat' gombra kattintva. Ha nem vagy megelégedve a hajók elrendezésével, akkor kattints újra a hajóra, és tedd máshová. Kattints a 'Kész' gombra a játékhoz.</infoPlaceShips>
<infoRequestRestart>Újrakezdés kérése..</infoRequestRestart>
<infoRemoteTurn>Várakozás az ellenfél lépésére.</infoRemoteTurn>
<infoRestartDenied>{0} nem szeretné újrakezdeni a játékot.</infoRestartDenied>
<infoSunkBoat>Elsülyedtél, {0}.</infoSunkBoat>
<infoWaitPlacing>Várakozás az ellenfélre, hogy elhelyezze a flottáját.</infoWaitPlacing>
<infoYourTurn>Te jössz. Kattints a szabad részre az ellenfél tábláján, és próbálj elsülyeszteni egy hajót.</infoYourTurn>
<lblBroughtToYouBy>Kihívó: {0}</lblBroughtToYouBy>
<lblConnectionType>Kapcsolat típusa</lblConnectionType>
<lblEnemyNavy>Ellenfél flottája</lblEnemyNavy>
<lblVersion>Csatahajók {0}</lblVersion>
<lblYourNavy>Flottád</lblYourNavy>
<stateGameOverLost>Vége, vesztettél!</stateGameOverLost>
<stateGameOverWon>Vége, nyertél!</stateGameOverWon>
<stateInitializing>Játék betöltése...</stateInitializing>
<stateMyTurn>Te jössz.</stateMyTurn>
<statePlacingShips>Helyezd ela hajókat</statePlacingShips>
<stateRemoteTurn>Várakozás {0} lépésére.</stateRemoteTurn>
<stateStartMyTurn>A játék elindult, te jössz.</stateStartMyTurn>
<stateStartRemoteTurn>A játék elindult, várakozás {0} lépésére.</stateStartRemoteTurn>
<stateWaitOnJoin>Várakozás az ellenfélre</stateWaitOnJoin>
<stateWaitOnPlacing>Várakozás az ellenfélre...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<boat0>het vliegdekschip</boat0>
<boat1>de kruiser</boat1>
<boat2>de torpedojager</boat2>
<boat3>de onderzeeboot</boat3>
<boat4>de mijnenveger</boat4>
<cmdDone>Klaar</cmdDone>
<cmdHelp>Help</cmdHelp>
<cmdPlaceRandomly>Plaats de vloot</cmdPlaceRandomly>
<cmdRestart>Herstart</cmdRestart>
<confirmRemoteWantsRestart>{0} wil het spel opnieuw beginnen. Vind je dat goed?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>verbroken</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Data fout; kan geen gegevens versturen. Het spel is gestopt.</errorDataSend>
<errorDisconnected>Verbroken.</errorDisconnected>
<errorUserLeft>{0} heeft het spel verlaten. Het spel is gestopt.</errorUserLeft>
<infoHit>Raak!</infoHit>
<infoMiss>Mis.</infoMiss>
<infoLostBoat>O, o, {0} is gezonken.</infoLostBoat>
<infoNoRestart>Geen herstart.</infoNoRestart>
<infoPlaceShips>Plaats jouw vloot door te klikken op 'Plaats de vloot'. Als je niet tevreden bent over de positie van je schepen, klik opnieuw. Klik 'Klaar' om te beginnen met spelen.</infoPlaceShips>
<infoRequestRestart>Herstart aanvragen..</infoRequestRestart>
<infoRemoteTurn>Wacht op de beurt van je tegenstander.</infoRemoteTurn>
<infoRestartDenied>{0} wil dit spel niet opnieuw beginnen.</infoRestartDenied>
<infoSunkBoat>Je hebt {0} van je tegenstander laten zinken.</infoSunkBoat>
<infoWaitPlacing>Wacht tot je tegenstander zijn vloot heeft geplaatst.</infoWaitPlacing>
<infoYourTurn>Het is jouw beurt. Klik op een leeg vak van je tegenstander en probeer de schepen te laten zinken.</infoYourTurn>
<lblBroughtToYouBy>Aangeboden door {0}</lblBroughtToYouBy>
<lblConnectionType>Connectie type</lblConnectionType>
<lblEnemyNavy>Vloot van de tegenstander</lblEnemyNavy>
<lblVersion>Zeeslag {0}</lblVersion>
<lblYourNavy>Jouw vloot</lblYourNavy>
<stateGameOverLost>Spel over, je hebt verloren!</stateGameOverLost>
<stateGameOverWon>Spel over, je hebt gewonnen!</stateGameOverWon>
<stateInitializing>Spel wordt geïnitialiseerd...</stateInitializing>
<stateMyTurn>Het is jouw beurt.</stateMyTurn>
<statePlacingShips>Plaats je vloot</statePlacingShips>
<stateRemoteTurn>Wacht op de beurt van {0}.</stateRemoteTurn>
<stateStartMyTurn>Spel gestart, het is jouw beurt.</stateStartMyTurn>
<stateStartRemoteTurn>Spel gestart, wacht op de beurt van {0}.</stateStartRemoteTurn>
<stateWaitOnJoin>Wacht op de tegenstander om mee te doen</stateWaitOnJoin>
<stateWaitOnPlacing>Wacht op de tegenstander...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<boat0>Porta-aviões</boat0>
<boat1>Navio de Guerra</boat1>
<boat2>Cruzeiro</boat2>
<boat3>Submarino</boat3>
<boat4>Barco</boat4>
<cmdDone>Feito</cmdDone>
<cmdHelp>Ajuda</cmdHelp>
<cmdPlaceRandomly>Dispôr os navios aleatóriamente</cmdPlaceRandomly>
<cmdRestart>Recomeçar</cmdRestart>
<confirmRemoteWantsRestart>{0} quer recomeçar o jogo, está de acordo?</confirmRemoteWantsRestart>
<connectionDirect>directo</connectionDirect>
<connectionDisconnected>desconectar</connectionDisconnected>
<connectionIndirect>indirecto</connectionIndirect>
<errorDataSend>Erro de comunicação. Jogo parado.</errorDataSend>
<errorDisconnected>Desconectado.</errorDisconnected>
<errorUserLeft>{0} saiu. O jogo acabou.</errorUserLeft>
<infoHit>Acertou!</infoHit>
<infoMiss>Falhou.</infoMiss>
<infoLostBoat>Acabou de perder {0}.</infoLostBoat>
<infoNoRestart>Sem recomeçar.</infoNoRestart>
<infoPlaceShips>Posicione os seus navios clicando no botão 'Dispôr os navios aleatóriamente'. Se não ficar satisfeito com as posições clique de novo. Por fim, clique em 'Feito' para começar a jogar.</infoPlaceShips>
<infoRequestRestart>A pedir o recomeço..</infoRequestRestart>
<infoRemoteTurn>À espera que o adversário jogue.</infoRemoteTurn>
<infoRestartDenied>{0} não quer recomeçar o jogo.</infoRestartDenied>
<infoSunkBoat>Você afundou {0}.</infoSunkBoat>
<infoWaitPlacing>À espera que o adversário posicione a sua frota.</infoWaitPlacing>
<infoYourTurn>É a sua vez. Clique num quadrado livre no tabuleiro do adversário e tente afundar os navios dele.</infoYourTurn>
<lblBroughtToYouBy>Produzido por {0}</lblBroughtToYouBy>
<lblConnectionType>Tipo de ligação</lblConnectionType>
<lblEnemyNavy>Marinha do Adversário</lblEnemyNavy>
<lblVersion>Batalha Naval {0}</lblVersion>
<lblYourNavy>Sua Marinha</lblYourNavy>
<stateGameOverLost>Jogo terminado, você perceu!</stateGameOverLost>
<stateGameOverWon>Jogo terminado, você ganhou!</stateGameOverWon>
<stateInitializing>A iniciar o jogo...</stateInitializing>
<stateMyTurn>É a sua vez.</stateMyTurn>
<statePlacingShips>Posicione os seus navios.</statePlacingShips>
<stateRemoteTurn>À espera que {0} jogue.</stateRemoteTurn>
<stateStartMyTurn>Jogo começado, é a sua vez.</stateStartMyTurn>
<stateStartRemoteTurn>Jogo começado, à espera que {0} jogue.</stateStartRemoteTurn>
<stateWaitOnJoin>À espera que o seu adversário chegue.</stateWaitOnJoin>
<stateWaitOnPlacing>Á espera do seu adversario...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<words>
<boat0>Letalonosilko</boat0>
<boat1>Bojno ladjo</boat1>
<boat2>Križarko</boat2>
<boat3>Podmornico</boat3>
<boat4>Uničevalca</boat4>
<cmdDone>Postavljeno</cmdDone>
<cmdHelp>Pomoč</cmdHelp>
<cmdPlaceRandomly>Postavi ladjice</cmdPlaceRandomly>
<cmdRestart>Reset</cmdRestart>
<confirmRemoteWantsRestart>{0} želi resetirati to igro, se strinjaš?</confirmRemoteWantsRestart>
<connectionDirect>posredno</connectionDirect>
<connectionDisconnected>prekinjeno</connectionDisconnected>
<connectionIndirect>neposredno</connectionIndirect>
<errorDataSend>Podatkovna napaka; pošiljanje podatkov ni možno. Igra ustavljena.</errorDataSend>
<errorDisconnected>Prekinjeno.</errorDisconnected>
<errorUserLeft>{0} je zapustil igro. Igra je končana.</errorUserLeft>
<infoHit>Zadetek!</infoHit>
<infoMiss>Zgrešeno.</infoMiss>
<infoLostBoat>Pravkar si izgubil {0}.</infoLostBoat>
<infoNoRestart>Brez reseta.</infoNoRestart>
<infoPlaceShips>Postavi svoje ladjice s klikom na 'Postavi ladjice'. Če nisi zadovoljen s pozicijo svojih ladjic klikni ponovno. Klikni 'Postavljeno' za začetek igre.</infoPlaceShips>
<infoRequestRestart>Zahtevam reset..</infoRequestRestart>
<infoRemoteTurn>Čakaš na nasprotnika da napravi potezo.</infoRemoteTurn>
<infoRestartDenied>{0} ne želi resetirati te igre.</infoRestartDenied>
<infoSunkBoat>Potopil si {0}.</infoSunkBoat>
<infoWaitPlacing>Čakaš da nasprotnik postavi svoje ladjice.</infoWaitPlacing>
<infoYourTurn>Ti si na vrsti. Klikni na prost kvadratek na nasprotnikovi tabli in poskusi potopiti njegove ladje.</infoYourTurn>
<lblBroughtToYouBy>Vam na uslugo: {0}</lblBroughtToYouBy>
<lblConnectionType>Vrsta povezave</lblConnectionType>
<lblEnemyNavy>Sovražnikova mornarica</lblEnemyNavy>
<lblVersion>Potaplanje ladjic {0}</lblVersion>
<lblYourNavy>Tvoja mornarica</lblYourNavy>
<stateGameOverLost>Konec igre, izgubil si!</stateGameOverLost>
<stateGameOverWon>Konec igre, zmagal si!</stateGameOverWon>
<stateInitializing>Nalagam igro...</stateInitializing>
<stateMyTurn>Ti si na vrsti.</stateMyTurn>
<statePlacingShips>Postavi ladjice</statePlacingShips>
<stateRemoteTurn>Čakaš, da {0} naredi potezo.</stateRemoteTurn>
<stateStartMyTurn>Igra se je začela, ti si na potezi.</stateStartMyTurn>
<stateStartRemoteTurn>Igra se je začela, čakaš da {0} naredi potezo.</stateStartRemoteTurn>
<stateWaitOnJoin>Čakaš na nasprotnika, da se pridruži.</stateWaitOnJoin>
<stateWaitOnPlacing>Čakaš na nasprotnika...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<words>
<boat0>Flygplans bärare</boat0>
<boat1>Stridsskepp</boat1>
<boat2>Stridsbåt</boat2>
<boat3>Ubåt</boat3>
<boat4>Förstörare</boat4>
<cmdDone>Klart</cmdDone>
<cmdHelp>Hjälp</cmdHelp>
<cmdPlaceRandomly>Placera skeppen slumpmässigt</cmdPlaceRandomly>
<cmdRestart>Starta om</cmdRestart>
<confirmRemoteWantsRestart>{0} vill starta om den här omgången, är det ok?</confirmRemoteWantsRestart>
<connectionDirect>direkt</connectionDirect>
<connectionDisconnected>anslutning bruten</connectionDisconnected>
<connectionIndirect>indirekt</connectionIndirect>
<errorDataSend>Data fel; omöjligt att skicka data. Spelet har stoppats.</errorDataSend>
<errorDisconnected>Anslutning bruten.</errorDisconnected>
<errorUserLeft>{0} har lämnat spelet. Spelet är slut.</errorUserLeft>
<infoHit>En träff!</infoHit>
<infoMiss>En miss.</infoMiss>
<infoLostBoat>Du förlorade {0}.</infoLostBoat>
<infoNoRestart>Ingen omstart.</infoNoRestart>
<infoPlaceShips>Placera skeppen genom att klicka på knappen "Placera skeppen slumpmässigt". Om du inte är nöjd med positionerna på din flotta klicka igen. Klicka på 'Klart' för att börja spela.</infoPlaceShips>
<infoRequestRestart>Begär omstart..</infoRequestRestart>
<infoRemoteTurn>Väntar på att motståndaren ska göra sitt drag.</infoRemoteTurn>
<infoRestartDenied>{0} vill inte starta om omgången.</infoRestartDenied>
<infoSunkBoat>Du sänkte {0}.</infoSunkBoat>
<infoWaitPlacing>Väntar på att motståndaren ska bli klar med att placera ut sin flotta.</infoWaitPlacing>
<infoYourTurn>Det är din tur. Klicka på en ledig ruta på moståndarens bräda för att försöka sänka hans skepp.</infoYourTurn>
<lblBroughtToYouBy>Framtaget till dig av {0}</lblBroughtToYouBy>
<lblConnectionType>Anslutnings typ</lblConnectionType>
<lblEnemyNavy>Fiendens flotta</lblEnemyNavy>
<lblVersion>Stridsskepp {0}</lblVersion>
<lblYourNavy>Din flotta</lblYourNavy>
<stateGameOverLost>Spelet slut, du har förlorat!</stateGameOverLost>
<stateGameOverWon>Spelet slut, du har vunnit!</stateGameOverWon>
<stateInitializing>Påbörjar spelet...</stateInitializing>
<stateMyTurn>Det är din tur.</stateMyTurn>
<statePlacingShips>Placera dina skepp</statePlacingShips>
<stateRemoteTurn>Väntar på att {0} ska göra sitt drag.</stateRemoteTurn>
<stateStartMyTurn>Spelet har startat, det är din tur.</stateStartMyTurn>
<stateStartRemoteTurn>Spelet har startat, väntar på att {0} ska göra sitt drag.</stateStartRemoteTurn>
<stateWaitOnJoin>Väntar på att din motståndare ska ansluta</stateWaitOnJoin>
<stateWaitOnPlacing>Väntar på din motståndare...</stateWaitOnPlacing>
</words>
@@ -0,0 +1,693 @@
<html>
<!--
Copyright by Koen, 2003,2004,2005
http://games.mess.be
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<title>Battleships</title>
<style type="text/css">
.btn{font-family:Tahoma;font-size:8pt;border-style:none;background-image:url(images/playerbg.gif);height:17px}
.header{font-weight:bold;color:white}
table{font-family:Tahoma;font-size:8pt}
</style>
<script language="javascript" src="../common/common.js"></script>
<script language="javascript">
var VERSION = "v2.0";
/// Data commands
var CMD_RESTART = "res";
var CMD_NORESTART = "nor";
var CMD_RESTARTOK = "rok";
var CMD_DONE = "done";
/// GAME_STATE
var GS_INITIALIZING = 0;
var GS_WAITFORJOIN = 1;
var GS_WAITFORPLACING = 2;
var GS_PLACINGSHIPS = 3;
var GS_MYTURN = 4;
var GS_REMOTETURN = 5;
var GS_GAMEOVER = 6;
var GS_ERROR = 7;
var BOAT_CARRIER = 0;
var BOAT_BATTLESHIP = 1;
var BOAT_CRUISER = 2;
var BOAT_SUB = 3;
var BOAT_DESTROYER = 4;
var BOAT_NONE = 10;
var BOAT_HIT = 11;
var BOAT_MISS = 12;
var m_remoteUserName;
var m_gameState = GS_INITIALIZING;
var m_isInitialized = false;
var m_requestRestart = false;
var m_iStart;
var m_remoteDonePlacing = false;
var m_myBoard = new Array(10);
var m_remoteBoard = new Array(10);
var m_myBoats = new Array(5);
var m_remoteBoats = new Array(5);
m_myBoats[BOAT_CARRIER] = new Array(5);
m_myBoats[BOAT_BATTLESHIP] = new Array(4);
m_myBoats[BOAT_CRUISER] = new Array(3);
m_myBoats[BOAT_SUB] = new Array(3);
m_myBoats[BOAT_DESTROYER] = new Array(2);
var m_preloadDone = false;
var m_remoteLoaded = false;
///
/// Global event handlers
///
function OnLoad(){
// load language
loadLanguage("battleships");
applyLanguage();
m_remoteUserName = getRemoteUserName();
m_iStart = iAmInviter();
initBoard(true);
initBoard(false);
updateGameState(GS_INITIALIZING, getWord("stateInitializing"));
updateConnectionType();
m_isInitialized = true;
window.external.Channel.Initialize();
preloadImages(new Array("bigbg.gif", "boat-bottom.gif", "boat-left.gif", "boat-middle-h.gif",
"boat-middle-v.gif", "boat-right.gif", "boat-top.gif", "ct_direct.gif", "ct_disconnected.gif",
"ct_indirect.gif", "empty.gif", "help.gif", "hit.gif", "miss.gif", "playerbg.gif"),
OnPreloadProgress, OnPreloadComplete);
}
function OnUnload(){
}
///
/// Preloading stuff
///
function OnPreloadProgress(progress){
divProgress.style.width = progress + "%";
}
function OnPreloadComplete(){
m_preloadDone = true;
initialize();
}
function initialize(){
if (m_preloadDone && m_remoteLoaded){
tblPreload.style.display = "none";
tblGame.style.display = "";
startGame();
}
}
function applyLanguage(){
lblBy.innerHTML = getWord("lblBroughtToYouBy", "<b>games.mess.be</b>");
lblVersion.innerText = getWord("lblVersion", VERSION);
lblYourNavy.innerText = getWord("lblYourNavy");
lblEnemyNavy.innerText = getWord("lblEnemyNavy");
btnHelp.title = getWord("cmdHelp");
btnRestart.innerText = getWord("cmdRestart");
btnPlaceRnd.innerText = getWord("cmdPlaceRandomly");
btnDone.innerText = getWord("cmdDone");
}
///
/// Channel event handlers
///
///
/// Occurs when the remote user closes the application, the conversation window or signs out of Messenger.
///
function Channel_OnAppClose(){
m_remoteLoaded = false;
updateGameState(GS_ERROR, getWord("errorUserLeft", getDisplayName(m_remoteUserName)));
}
///
/// Occurs when a data send has failed, inspect Channel.Error for details.
///
function Channel_OnDataError(){
updateGameState(GS_ERROR, getWord("errorDataSend"));
}
///
/// Occurs when new data has been received. The data is available in window.external.Channel.Data.
///
function Channel_OnDataReceived(){
var data = window.external.Channel.Data;
if (data == CMD_RESTART){
if (m_gameState == GS_MYTURN || m_gameState == GS_REMOTETURN){
var restart = confirm(getWord("confirmRemoteWantsRestart", getDisplayName(m_remoteUserName)));
var cmd = restart ? CMD_RESTARTOK : CMD_NORESTART;
window.external.Channel.SendData(cmd);
if (restart)
startGame();
}
else if (m_gameState == GS_WAITFORJOIN){
window.external.Channel.SendData(CMD_RESTARTOK); //confirm
startGame();
}
}
else if (data == CMD_RESTARTOK){
m_requestRestart = false;
startGame();
}
else if (data == CMD_NORESTART){
m_requestRestart = false;
var msg = getWord("infoNoRestart") + " ";
if (m_gameState == GS_MYTURN)
msg += getWord("stateMyTurn");
else if (m_gameState == GS_REMOTETURN)
msg += getWord("stateRemoteTurn", getDisplayName(m_remoteUserName));
statusText.innerText = msg;
alert(getWord("infoRestartDenied", getDisplayName(m_remoteUserName)));
}
else if (data.substr(0, CMD_DONE.length) == CMD_DONE){
handleFleetInfo(data);
if (m_gameState == GS_WAITFORPLACING)
startGamePlay();
}
else{
if (m_gameState == GS_REMOTETURN)
doMove(new point(data), false);
}
}
///
/// Occurs when the remote user closes the window that contains the app.
///
function Channel_OnRemoteAppClosed(){
m_remoteLoaded = false;
updateGameState(GS_ERROR, getWord("errorUserLeft", getDisplayName(m_remoteUserName)));
}
///
/// Occurs when the remote app has been initialized. Both apps are now initialized.
///
function Channel_OnRemoteAppLoaded(){
m_remoteLoaded = true;
initialize();
}
///
/// Occurs when the connection between conversants changes from direct to indirect or disconnected.
/// The type is available in window.external.Channel.Type
///
function Channel_OnTypeChanged(){
updateConnectionType();
}
///
/// Helpers functions
///
function updateConnectionType(){
var icon, descr;
switch(window.external.Channel.Type){
case CT_DIRECT:
icon = "ct_direct.gif"
descr = getWord("connectionDirect");
break;
case CT_DISCONNECTED:
icon = "ct_disconnected.gif"
descr = getWord("connectionDisconnected");
if (m_isInitialized)
updateGameState(GS_ERROR, getWord("errorDisconnected"));
else
updateGameState(GS_INITIALIZING, getWord("stateInitializing"));
break;
case CT_INDIRECT:
icon = "ct_indirect.gif"
descr = getWord("connectionIndirect");
break;
}
statusIcon.src = "images/" + icon;
statusIcon.title = getWord("lblConnectionType") + ": " + descr;
}
///
/// Game specific objects
///
function point(){
if (arguments.length == 1){
var args = arguments[0].split(",");
this.x = parseInt(args[0]);
this.y = parseInt(args[1]);
}
else{
this.x = arguments[0];
this.y = arguments[1];
}
}
point.prototype.toString = function(){
return this.x + "," + this.y;
}
///
/// Game specific functions
///
function startGame(){
document.body.disabled = false;
initBoard(true);
initBoard(false);
m_remoteDonePlacing = false;
updateGameState(GS_PLACINGSHIPS, getWord("statePlacingShips"));
}
function startGamePlay(){
btnRestart.disabled = false;
if (m_iStart)
updateGameState(GS_MYTURN, getWord("stateStartMyTurn"));
else
updateGameState(GS_REMOTETURN, getWord("stateStartRemoteTurn", getDisplayName(m_remoteUserName)));
}
function initBoard(me){
var board = myBoard;
var boardArr = m_myBoard;
var fleet = m_myBoats;
if (!me){
board = remoteBoard;
boardArr = m_remoteBoard;
fleet = m_remoteBoats;
}
if (board.rows.length != 0){
for(var i = 0; i < 10; i++){
board.deleteRow(0);
}
}
for(var i = 0; i < 10; i++){
boardArr[i] = new Array(10);
var row = board.insertRow();
row.height = "21";
for(var j = 0; j < 10; j++){
var cell = row.insertCell();
cell.innerHTML = "<img src='images/empty.gif'>"
cell.width = "21";
boardArr[i][j] = BOAT_NONE;
}
}
for(var i = 0; i < fleet.length; i++){
var boatInfo = fleet[i];
if (boatInfo){
for(var j = 0; j < boatInfo.length; j++){
boatInfo[j] = null;
}
}
}
}
function drawBoat(boatInfo, myBoat){
var board = (myBoat) ? myBoard : remoteBoard;
var horizontal = (boatInfo[0].y == boatInfo[1].y);
for(var i = 0; i < boatInfo.length; i++){
var p = boatInfo[i];
var img;
if (i == 0){
if (horizontal)
img = "boat-left.gif";
else
img = "boat-top.gif";
}
else if (i == boatInfo.length - 1){
if (horizontal)
img = "boat-right.gif";
else
img = "boat-bottom.gif";
}
else{
if (horizontal)
img = "boat-middle-h.gif";
else
img = "boat-middle-v.gif";
}
board.rows[p.y].cells[p.x].style.backgroundImage = "url(images/" + img + ")";
}
}
function placeBoat(boat, startPoint, horizontal){
var oldBoatInfo = m_myBoats[boat];
var newBoatInfo = new Array(oldBoatInfo.length);
var p = startPoint;
for(var i = 0; i < newBoatInfo.length; i++){
if (p.x > 9 || p.y > 9 || m_myBoard[p.x][p.y] != BOAT_NONE)
return false;
else{
newBoatInfo[i] = p;
if (horizontal)
p = new point(p.x + 1, p.y);
else
p = new point(p.x, p.y + 1);
}
}
for(var i = 0; i < newBoatInfo.length; i++){
// clear old position
if (oldBoatInfo[i]){
var oldX = oldBoatInfo[i].x;
var oldY = oldBoatInfo[i].y;
m_myBoard[oldX][oldY] = BOAT_NONE;
myBoard.rows[oldY].cells[oldX].style.backgroundImage = "url(images/empty.gif)";
oldBoatInfo[i] = null;
}
var x = newBoatInfo[i].x;
var y = newBoatInfo[i].y;
m_myBoard[x][y] = boat;
oldBoatInfo[i] = newBoatInfo[i];
}
drawBoat(newBoatInfo, true);
return true;
}
function placeBoatsRnd(){
for(var i = 0; i < 5; i++){
var placed = false;
while(!placed){
placed = placeBoat(i, new point(Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)), (Math.random() < .5))
}
}
}
function updateGameState(gameState, msg){
var infoMsg = "";
if (gameState == GS_ERROR){
document.body.disabled = true;
}
else if (gameState == GS_PLACINGSHIPS){
infoMsg = getWord("infoPlaceShips");
btnDone.disabled = true;
btnRestart.disabled = true;
placePnl.style.display = "";
}
else if (gameState == GS_WAITFORPLACING){
btnRestart.disabled = true;
infoMsg = getWord("infoWaitPlacing");
}
else if (gameState == GS_MYTURN){
infoMsg = getWord("infoYourTurn");
}
else if (gameState == GS_REMOTETURN){
infoMsg = getWord("infoRemoteTurn");
}
pnlInfo.innerText = infoMsg;
m_gameState = gameState;
statusText.innerText = msg;
}
function pointFromElement(element){
var td;
if (element.tagName == "TD")
td = event.srcElement;
else if (element.tagName == "IMG")
td = event.srcElement.parentElement;
else
return null;
var x = td.cellIndex;
var y = td.parentElement.rowIndex;
return new point(x, y);
}
function handleFleetInfo(data){
data = data.substr(4);
var boatInfo = data.split("$");
m_remoteBoats = new Array(boatInfo.length);
for(var i = 0; i < boatInfo.length; i++){
var boatPos = boatInfo[i].split("|");
m_remoteBoats[i] = new Array(boatPos.length);
for(var j = 0; j < boatPos.length; j++){
var p = new point(boatPos[j]);
m_remoteBoard[p.x][p.y] = i;
m_remoteBoats[i][j] = p;
}
}
m_remoteDonePlacing = true;
}
function sendFleetInfo(){
var data = CMD_DONE;
for(var i = 0; i < 5; i++){
if (i > 0)
data += "$";
var boatInfo = m_myBoats[i];
for (var j = 0; j < boatInfo.length; j++){
if (j > 0)
data += "|";
data += boatInfo[j].toString();
}
}
window.external.Channel.SendData(data);
}
function doMove(p, isMyMove){
var board = (isMyMove) ? remoteBoard : myBoard;
var boardArr = (isMyMove) ? m_remoteBoard : m_myBoard;
var msg;
if (boardArr[p.x][p.y] == BOAT_NONE){
boardArr[p.x][p.y] = BOAT_MISS;
board.rows[p.y].cells[p.x].firstChild.src = "images/miss.gif";
msg = getWord("infoMiss");
}
else{
boardArr[p.x][p.y] = BOAT_HIT;
board.rows[p.y].cells[p.x].firstChild.src = "images/hit.gif"
msg = getWord("infoHit");
var boatID = boatSunk(p, isMyMove)
if (boatID != -1){
var boat = getWord("boat" + boatID);
msg = ((isMyMove) ? getWord("infoSunkBoat", boat) : getWord("infoLostBoat", boat)) +"!";
drawBoat((isMyMove) ? m_remoteBoats[boatID] : m_myBoats[boatID], !isMyMove);
}
}
if (isGameOver()){
if (isFleetGone(true)){
msg = getWord("stateGameOverLost");
m_iStart = false;
// reveal enemy fleet
for(var i = 0; i < m_remoteBoats.length; i++)
drawBoat(m_remoteBoats[i], false);
}
else{
msg = getWord("stateGameOverWon");
m_iStart = true;
}
updateGameState(GS_GAMEOVER, msg);
}
else{
if (m_gameState == GS_MYTURN){
msg += " " + getWord("stateRemoteTurn", getDisplayName(m_remoteUserName));
updateGameState(GS_REMOTETURN, getWord("stateRemoteTurn", getDisplayName(m_remoteUserName)));
}
else{
msg += " " + getWord("stateMyTurn");
updateGameState(GS_MYTURN, getWord("stateMyTurn"));
}
}
pnlInfo.innerText = msg;
}
function isFleetGone(me){
var fleet = (me) ? m_myBoats : m_remoteBoats;
var boardArr = (me) ? m_myBoard : m_remoteBoard;
for(var i = 0; i < fleet.length; i++){
var boatInfo = fleet[i];
for(var j = 0; j < boatInfo.length; j++){
var p = boatInfo[j];
if (boardArr[p.x][p.y] != BOAT_HIT)
return false;
}
}
return true;
}
function boatSunk(p, isMyMove){
var fleet = (isMyMove) ? m_remoteBoats : m_myBoats;
var boardArr = (isMyMove) ? m_remoteBoard : m_myBoard;
for(var i = 0; i < fleet.length; i++){
var boatInfo = fleet[i];
for(var j = 0; j < boatInfo.length; j++){
var boatPos = boatInfo[j];
if (boatPos.x == p.x && boatPos.y == p.y){
// Find out if entire boat sunk
for(var k = 0; k < boatInfo.length; k++){
var boatPos2 = boatInfo[k];
if (boardArr[boatPos2.x][boatPos2.y] != BOAT_HIT)
return -1;
}
return i;
}
}
}
return -1;
}
function isGameOver(){
return isFleetGone(true) || isFleetGone(false);
}
///
/// GUI event handlers
///
function btnPlaceRnd_onclick(){
placeBoatsRnd();
btnDone.disabled = false;
}
function btnDone_onclick(){
placePnl.style.display = "none";
sendFleetInfo();
if (m_remoteDonePlacing)
startGamePlay();
else
updateGameState(GS_WAITFORPLACING, getWord("stateWaitOnPlacing"));
}
function myBoard_onmouseover(){
//NYI
}
function myBoard_onclick(){
//NYI
}
function remoteBoard_onmouseover(){
}
function remoteBoard_onclick(){
if (m_gameState == GS_MYTURN){
var p = pointFromElement(event.srcElement);
if (p){
if (m_remoteBoard[p.x][p.y] == BOAT_HIT || m_remoteBoard[p.x][p.y] == BOAT_MISS){
// ignore
return;
}
doMove(p, true);
window.external.Channel.SendData(p.toString());
}
}
}
function btnRestart_onclick(){
if (m_gameState == GS_GAMEOVER){
updateGameState(GS_WAITFORJOIN, getWord("stateWaitOnJoin"));
window.external.Channel.SendData(CMD_RESTART);
}
else if ((m_gameState == GS_MYTURN || m_gameState == GS_REMOTETURN) && !m_requestRestart){
m_requestRestart = true;
statusText.innerText = getWord("infoRequestRestart");
window.external.Channel.SendData(CMD_RESTART);
}
}
function help_onclick(){
showHelpFile("help/battleships-help.htm");
}
</script>
</head>
<body onload="OnLoad();" onunload="OnUnload();" style="margin:0" scroll="no" style="background-color:#516CA1">
<xml id="words"></xml>
<table id="tblPreload" width="100%" height="100%">
<tr height="20%">
<td>&nbsp;</td>
</tr>
<tr height="40%">
<td align="center"><img src="images/battleships-logo.jpg"><div id="lblBy" style="color:#D4DEF4"></div>
</td>
</tr>
<tr valign="top" height="30%">
<td align="center"><div align="left" style="width:100px;height:16px;color:#D4DEF4;border-style:solid;border-width:1px"><div id="divProgress" style="background-color:#D4DEF4;width:0%;height:100%"></div>
</div>
</td>
</tr>
</table>
<table id="tblGame" width="100%" height="100%" style="display:none">
<tr class="header">
<td id="lblYourNavy" align="center"></td>
<td id="lblEnemyNavy" align="center"></td>
</tr>
<tr>
<td align="center">
<table id="myBoard" onmouseover="myBoard_onmouseover();" onclick="myBoard_onclick();" border="0" borderColorDark="black" borderColorLight="black" cellpadding="0" cellspacing="0" style="background-image:url(images/blanksquare.gif);table-layout:fixed;border-collapse:collapse">
</table>
</td>
<td align="center">
<table id="remoteBoard" onmouseover="remoteBoard_onmouseover();" onclick="remoteBoard_onclick();" border="0" borderColorDark="black" borderColorLight="black" cellpadding="0" cellspacing="0" style="background-image:url(images/blanksquare.gif);table-layout:fixed;border-collapse:collapse">
</table>
</td>
</tr>
<tr id="placePnl" height="30" valign="bottom" style="display:none">
<td colspan="2" align="center">
<button id="btnPlaceRnd" class="btn" onclick="btnPlaceRnd_onclick();" style="width:150"></button>
&nbsp;
<button id="btnDone" class="btn" onclick="btnDone_onclick();" style="width:150"></button>
</td>
</tr>
<tr>
<td colspan="2">
<div id="pnlInfo" style="margin:10px;padding:5px;height:50px;background-image:url(images/bigbg.gif);border-style:solid;border-width:1px;border-color:black;padding:2px">
</div>
</td>
</tr>
<tr height="100%">
<td colspan="2">&nbsp;</td>
</tr>
<tr>
<td colspan="2" align="center">
<button id="btnRestart" onclick="btnRestart_onclick();" class="btn"></button>
</td>
</tr>
<tr>
<td colspan="2" align="right" style="font-size:7pt"><a id="lblVersion" href="http://games.mess.be" target="_blank" style="color:white"></a> <a href="javascript:help_onclick();"><img id="btnHelp" align="absmiddle" border="0" src="images/help.gif"></a></td>
</tr>
<tr valign="bottom" height="17px">
<td colspan="2">
<div id="statusBar" style="border-style:solid;border-width:0px;border-color:buttonface;height:17px;background-image:url(images/playerbg.gif)">
<img id="statusIcon" width="16" height="16" align="absmiddle" style="unselectable;margin:0px 5px" src="images/empty.gif">
<span id="statusText" unselectable style="cursor:default"></span>
</div>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,62 @@
<html>
<head>
<title>Sea Battle</title>
<style type="text/css">
body{font-family:Tahoma;font-size:8pt}
table{font-family:Tahoma;font-size:8pt;border-style:solid;border-width:1px;border-color:buttonface;border-collapse:collapse}
.btn{font-family:Tahoma;font-size:8pt;border-style:solid;border-width:1px;border-color:buttonface;background-color:white}
</style>
</head>
<body>
<b><font size="5">The Rules of Sea Battle</font></b>
<br>
<i>Written by bno
<p>
Before I start this guide, I just thought I would say that this game also goes under the name of <b>Battle Ships</b>.
<p>
Well, let us start with giving the general idea of the game.</i>
<p>
The idea is to (well, win obviously) blow up all of your enemy's ships before they blow up all of yours.
<p><br>
<b>The start</b>
<p>
<img src="seabattle1.png">
<br>
Above is a picture of something similar to what you will get at the beginning. Press the 'Place ships Randomly' to randomly place your ships on your board. Keep pressing this button until you are happy with where your ships are located.
<p>
<img src="seabattle2.png">
<br>
Next up, is to try and attempt to blow up your opponent's ships before they bow up yours!
<br>
Click squares to guess where you think they could be. A whirlpool, as shown in the above picture, shows as a miss. The explosion, shows as a hit. Once you fully sink their ship, it will appear as what ship it was with the explosions. You will not be able to see where there other ships are located until you have fully bombed the ship (unless of course you are either playing yourself, or someone RIGHT next to you... lol)
<p>
You are allowed one guess each turn, taking it in turn to guess where the ships are located. Once you get a hit, guess around that square (not diagonal) to try and blow up the whole ship.
<p>
Below is a guide to what each ship is named and how many of each ship there are, the smallest being two, and the largest being five.<P></P>
<br>
<table border="1" ID="Table1">
<tr bgcolor="buttonface"><td width="100"><b>Name</b></td><td><b>Number of squares</b></td><td><b>Amount of ships</b></td></tr>
<tr><td>Destroyer</td><td>2</td><td>1</td></tr>
<tr><td>Submarine</td><td>3</td><td>2</td></tr>
<tr><td>Battleship</td><td>4</td><td>1</td></tr>
<tr><td>Aircraft Carrier</td><td>5</td><td>1</td></tr>
</table>
<p>
<br>
When the game has finished it will read 'Game over! Congratulations, you have won!' if you won, or 'Game over! Too bad, you have lost.' if you lost.
<p><br><BR>
<b>I hope this guide has made it easier for you to understand the game. Of course, another thing to help you get good at any game is to PRACTISE.
<p>
This guide has been brought to you by: bno
<p>
I would also like to thank everyone who has made this game possible with MSN. <font color="green">THANKYOU</font></b>
<p>
<br>
<BR>
<hr>
This guide was written for 'Sea Battle v0.1'
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Some files were not shown because too many files have changed in this diff Show More