Browse Source

Implement unverified game reporting

pull/10109/head
dolfies 2 years ago
parent
commit
5832e8bfdf
  1. 70
      discord/application.py
  2. 69
      discord/client.py
  3. 25
      discord/enums.py
  4. 8
      discord/file.py
  5. 42
      discord/http.py
  6. 6
      discord/types/application.py
  7. 5
      docs/api.rst

70
discord/application.py

@ -27,6 +27,7 @@ from __future__ import annotations
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Collection,
List,
@ -94,6 +95,7 @@ if TYPE_CHECKING:
Manifest as ManifestPayload,
ManifestLabel as ManifestLabelPayload,
PartialApplication as PartialApplicationPayload,
UnverifiedApplication as UnverifiedApplicationPayload,
WhitelistedUser as WhitelistedUserPayload,
)
from .types.user import PartialUser as PartialUserPayload
@ -117,6 +119,7 @@ __all__ = (
'PartialApplication',
'Application',
'IntegrationApplication',
'UnverifiedApplication',
)
MISSING = utils.MISSING
@ -1282,7 +1285,7 @@ class ApplicationBuild(Hashable):
for url in urls:
file = id_files.get(url['id'])
if file:
await self._state.http.upload_to_cloud(url['url'], file, file.md5 if hash else None)
await self._state.http.upload_to_cloud(url['url'], file, file.b64_md5 if hash else None)
async def publish(self) -> None:
"""|coro|
@ -3681,3 +3684,68 @@ class IntegrationApplication(Hashable):
app_id = self.id
data = await state.http.get_app_activity_statistics(app_id)
return [ApplicationActivityStatistics(data=activity, state=state, application_id=app_id) for activity in data]
class UnverifiedApplication:
"""Represents an unverified application (a game not detected by the Discord client) that has been reported to Discord.
.. container:: operations
.. describe:: x == y
Checks if two applications are equal.
.. describe:: x != y
Checks if two applications are not equal.
.. describe:: hash(x)
Return the application's hash.
.. describe:: str(x)
Returns the application's name.
.. versionadded:: 2.1
Attributes
-----------
name: :class:`str`
The name of the application.
hash: :class:`str`
The hash of the application.
missing_data: List[:class:`str`]
Data missing from the unverified application report.
.. note::
:meth:`Client.report_unverified_application` will automatically
upload the unverified application's icon, if missing.
"""
__slots__ = ('name', 'hash', 'missing_data')
def __init__(self, *, data: UnverifiedApplicationPayload):
self.name: str = data['name']
self.hash: str = data['hash']
self.missing_data: List[str] = data.get('missing_data', [])
def __repr__(self) -> str:
return f'<UnverifiedApplication name={self.name!r} hash={self.hash!r}>'
def __hash__(self) -> int:
return hash(self.hash)
def __str__(self) -> str:
return self.name
def __eq__(self, other: Any) -> bool:
if isinstance(other, UnverifiedApplication):
return self.hash == other.hash
return NotImplemented
def __ne__(self, other: Any) -> bool:
if isinstance(other, UnverifiedApplication):
return self.hash != other.hash
return NotImplemented

69
discord/client.py

@ -70,7 +70,7 @@ from .utils import MISSING
from .object import Object, OLDEST_OBJECT
from .backoff import ExponentialBackoff
from .webhook import Webhook
from .application import Application, ApplicationActivityStatistics, Company, EULA, PartialApplication
from .application import Application, ApplicationActivityStatistics, Company, EULA, PartialApplication, UnverifiedApplication
from .stage_instance import StageInstance
from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@ -102,11 +102,12 @@ if TYPE_CHECKING:
from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .billing import BillingAddress
from .enums import PaymentGateway, RequiredActionType
from .enums import Distributor, OperatingSystem, PaymentGateway, RequiredActionType
from .metadata import MetadataObject
from .permissions import Permissions
from .read_state import ReadState
from .tutorial import Tutorial
from .file import File
from .types.snowflake import Snowflake as _Snowflake
PrivateChannel = Union[DMChannel, GroupChannel]
@ -780,7 +781,6 @@ class Client:
aiohttp.ClientError,
asyncio.TimeoutError,
) as exc:
self.dispatch('disconnect')
if not reconnect:
await self.close()
@ -4883,3 +4883,66 @@ class Client:
Leaving the active developer program failed.
"""
await self._connection.http.unenroll_active_developer()
async def report_unverified_application(
self,
name: str,
*,
icon: File,
os: OperatingSystem,
executable: str = MISSING,
publisher: str = MISSING,
distributor: Distributor = MISSING,
sku: str = MISSING,
) -> UnverifiedApplication:
"""|coro|
Reports an unverified application (a game not detected by the Discord client) to Discord.
If missing, this also uploads the application icon to Discord.
.. versionadded:: 2.1
Parameters
-----------
name: :class:`str`
The name of the application.
icon: :class:`.File`
The icon of the application.
os: :class:`.OperatingSystem`
The operating system the application is found on.
executable: :class:`str`
The executable of the application.
publisher: :class:`str`
The publisher of the application.
distributor: :class:`.Distributor`
The distributor of the application SKU.
sku: :class:`str`
The SKU of the application.
Raises
-------
HTTPException
Reporting the unverified application failed.
Returns
-------
:class:`.UnverifiedApplication`
The reported unverified application.
"""
state = self._connection
data = await state.http.report_unverified_application(
name=name,
icon_hash=icon.md5.hexdigest(),
os=str(os),
executable=executable,
publisher=publisher,
distributor=str(distributor) if distributor else None,
sku=sku,
)
app = UnverifiedApplication(data=data)
if 'icon' in app.missing_data:
icon_data = utils._bytes_to_base64_data(icon.fp.read())
await state.http.upload_unverified_application_icon(app.name, app.hash, icon_data)
return app

25
discord/enums.py

@ -1330,21 +1330,23 @@ class SKUGenre(Enum):
# There are tons of different operating system/client enums in the API,
# so we try to unify them here
# They're normalized as the numbered enum, and converted from the stringified enums
# They're normalized as the numbered enum, and converted from the stringified enum(s)
class OperatingSystem(Enum):
windows = 1
macos = 2
linux = 3
android = -1
ios = -1
unknown = -1
ios = -2
unknown = -3
@classmethod
def from_string(cls, value: str) -> Self:
lookup = {
'windows': cls.windows,
'win32': cls.windows,
'macos': cls.macos,
'darwin': cls.macos,
'linux': cls.linux,
'android': cls.android,
'ios': cls.ios,
@ -1352,6 +1354,20 @@ class OperatingSystem(Enum):
}
return lookup.get(value, create_unknown_value(cls, value))
def to_string(self):
lookup = {
OperatingSystem.windows: 'win32',
OperatingSystem.macos: 'darwin',
OperatingSystem.linux: 'linux',
OperatingSystem.android: 'android',
OperatingSystem.ios: 'ios',
OperatingSystem.unknown: 'unknown',
}
return lookup[self]
def __str__(self):
return self.to_string()
class ContentRatingAgency(Enum):
esrb = 1
@ -1454,6 +1470,9 @@ class Distributor(Enum):
epic_games = 'epic'
google_play = 'google_play'
def __str__(self):
return self.value
class EntitlementType(Enum):
purchase = 1

8
discord/file.py

@ -162,12 +162,16 @@ class File(_FileBase):
super().__init__(filename, spoiler=spoiler, description=description)
@cached_slot_property('_cs_md5')
def md5(self) -> str:
def md5(self):
try:
return b64encode(md5(self.fp.read()).digest()).decode('utf-8')
return md5(self.fp.read())
finally:
self.reset()
@property
def b64_md5(self) -> str:
return b64encode(self.md5.digest()).decode('ascii')
@cached_slot_property('_cs_size')
def size(self) -> int:
return os.fstat(self.fp.fileno()).st_size

42
discord/http.py

@ -3483,7 +3483,7 @@ class HTTPClient:
file.filename = id
data = {'id': file.filename}
if hash:
data['md5_hash'] = file.md5
data['md5_hash'] = file.b64_md5
payload['files'].append(data)
@ -4559,6 +4559,46 @@ class HTTPClient:
)
)
# Unverified Applications
def report_unverified_application(
self,
name: str,
icon_hash: str,
os: str,
*,
executable: Optional[str] = None,
publisher: Optional[str] = None,
distributor: Optional[str] = None,
sku: Optional[str] = None,
) -> Response[application.UnverifiedApplication]:
payload = {
'report_version': 3,
'name': name,
'icon': icon_hash,
'os': os,
}
if executable is not None:
payload['executable'] = executable
if publisher:
payload['publisher'] = publisher
if distributor:
payload['distributor_application'] = {
'distributor': distributor,
'sku': sku or '',
}
return self.request(Route('POST', '/unverified-applications'), json=payload)
def upload_unverified_application_icon(self, name: str, hash: str, icon: str) -> Response[None]:
payload = {
'application_name': name,
'application_hash': hash,
'icon': icon,
}
return self.request(Route('POST', '/unverified-applications/icons'), json=payload)
# Recent Mentions
def get_recent_mentions(

6
discord/types/application.py

@ -284,3 +284,9 @@ class PartialRoleConnection(TypedDict):
class RoleConnection(PartialRoleConnection):
application: RoleConnectionApplication
application_metadata: List[RoleConnectionMetadata]
class UnverifiedApplication(TypedDict):
name: str
hash: str
missing_data: List[str]

5
docs/api.rst

@ -6705,6 +6705,11 @@ Application
.. autoclass:: EmbeddedActivityConfig()
:members:
.. attributetable:: UnverifiedApplication
.. autoclass:: UnverifiedApplication()
:members:
ApplicationBranch
~~~~~~~~~~~~~~~~~

Loading…
Cancel
Save