init
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
from .entry import register
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 800 B |
|
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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 340 B |
|
After Width: | Height: | Size: 511 B |
|
After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 307 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 428 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 514 B |
|
After Width: | Height: | Size: 915 B |
|
After Width: | Height: | Size: 919 B |
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 392 B |
|
After Width: | Height: | Size: 802 B |
|
After Width: | Height: | Size: 479 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 900 B |
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 163 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 146 B |
|
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> </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>
|
||||
|
||||
<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"> </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>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 681 B |