misc: add DateString class
This commit is contained in:
parent
eb359ed53b
commit
9be0387eed
321
izzylib/activitypub.py
Normal file
321
izzylib/activitypub.py
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
import json, mimetypes
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from functools import partial
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .dotdict import DotDict
|
||||||
|
from .misc import DateString, Url
|
||||||
|
|
||||||
|
|
||||||
|
pubstr = 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
actor_types = ['Application', 'Group', 'Organization', 'Person', 'Service']
|
||||||
|
activity_types = [
|
||||||
|
'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike',
|
||||||
|
'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen',
|
||||||
|
'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeAccept',
|
||||||
|
'TentativeReject', 'Travel', 'Undo', 'Update', 'View'
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
link_types = ['Mention']
|
||||||
|
object_types = [
|
||||||
|
'Article', 'Audio', 'Document', 'Event', 'Image', 'Note', 'Page', 'Place',
|
||||||
|
'Profile', 'Relationship', 'Tombstone', 'Video'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_privacy_level(to: list=[], cc: list=[]):
|
||||||
|
if to == [pubstr] and len(cc) == 1:
|
||||||
|
return 'public'
|
||||||
|
|
||||||
|
elif to and self.actor in to[0] and not cc:
|
||||||
|
return 'unlisted'
|
||||||
|
|
||||||
|
elif pubstr not in to:
|
||||||
|
return 'private'
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.warning('Not sure what this privacy level is')
|
||||||
|
logging.debug(f'to: {json.dumps(to)}')
|
||||||
|
logging.debug(f'cc: {json.dumps(cc)}')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_privacy_fields(privacy='public'):
|
||||||
|
if privacy == 'public':
|
||||||
|
return ([pubstr])
|
||||||
|
|
||||||
|
|
||||||
|
class Object(DotDict):
|
||||||
|
@property
|
||||||
|
def privacy_level(self):
|
||||||
|
return parse_privacy_level(self.get('to', []), self.get('cc', []))
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_inbox(self):
|
||||||
|
try: return self.endpoints.shared_inbox
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pubkey(self):
|
||||||
|
try: return self.publicKey.publicKeyPem
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handle(self):
|
||||||
|
return self.get('preferredUsername')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self):
|
||||||
|
return self.get('name')
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_note(cls, id, url, actor, content, **kwargs):
|
||||||
|
assert False not in map(isinstance, [id, actor, url], [Url])
|
||||||
|
|
||||||
|
date = kwargs.get('date', DateString.now('activitypub'))
|
||||||
|
|
||||||
|
return cls({
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
{
|
||||||
|
"ostatus": "http://ostatus.org#",
|
||||||
|
"atomUri": "ostatus:atomUri",
|
||||||
|
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||||
|
"conversation": "ostatus:conversation",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"votersCount": "toot:votersCount",
|
||||||
|
"litepub": "http://litepub.social/ns#",
|
||||||
|
"directMessage": "litepub:directMessage"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": id,
|
||||||
|
"type": "Note",
|
||||||
|
"summary": kwargs.get('summary'),
|
||||||
|
"inReplyTo": kwargs.get('replyto'),
|
||||||
|
"published": date,
|
||||||
|
"url": url,
|
||||||
|
"attributedTo": actor,
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"cc": [
|
||||||
|
f'{actor}/followers'
|
||||||
|
],
|
||||||
|
"sensitive": kwargs.get('sensitive', False),
|
||||||
|
"atomUri": id,
|
||||||
|
"inReplyToAtomUri": kwargs.get('replyto_id'),
|
||||||
|
"conversation": f'tag:{actor.host},{date.dump_to_string("activitypub-date")}:objectId=490:objectType=Conversation',
|
||||||
|
"content": content,
|
||||||
|
"contentMap": {
|
||||||
|
"en": content
|
||||||
|
},
|
||||||
|
"attachment": [],
|
||||||
|
"tag": [],
|
||||||
|
"replies": {
|
||||||
|
"id": f"{id}/replies",
|
||||||
|
"type": "Collection",
|
||||||
|
"first": {
|
||||||
|
"type": "CollectionPage",
|
||||||
|
"next": f"{id}/replies?only_other_accounts=true&page=true",
|
||||||
|
"partOf": f"{id}/replies",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# not complete
|
||||||
|
@classmethod
|
||||||
|
def new_actor(cls, actor, handle, pubkey, published=None, table={}, full=True, **kwargs):
|
||||||
|
actor_type = kwargs.get('type', 'Person').title()
|
||||||
|
|
||||||
|
assert actor_type in actor_types
|
||||||
|
|
||||||
|
actor = Url(actor)
|
||||||
|
data = cls({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{
|
||||||
|
'schema': 'http://schema.org',
|
||||||
|
'toot': 'https://joinmastodon.org/ns#',
|
||||||
|
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
|
||||||
|
'PropertyValue': 'schema:PropertyValue',
|
||||||
|
'value': 'schema:value',
|
||||||
|
'IdentityProof': 'toot:IdentityProof',
|
||||||
|
'discoverable': 'toot:discoverable',
|
||||||
|
'Device': 'toot:Device',
|
||||||
|
'Ed25519Signature': 'toot:Ed25519Signature',
|
||||||
|
'Ed25519Key': 'toot:Ed25519Key',
|
||||||
|
'Curve25519Key': 'toot:Curve25519Key',
|
||||||
|
'EncryptedMessage': 'toot:EncryptedMessage',
|
||||||
|
'publicKeyBase64': 'toot:publicKeyBase64',
|
||||||
|
'deviceId': 'toot:deviceId',
|
||||||
|
'messageFranking': 'toot:messageFranking',
|
||||||
|
'messageType': 'toot:messageType',
|
||||||
|
'cipherText': 'toot:cipherText',
|
||||||
|
'suspended': 'toot:suspended',
|
||||||
|
'Emoji': 'toot:Emoji',
|
||||||
|
"featured": {
|
||||||
|
"@id": "toot:featured",
|
||||||
|
"@type": "@id"
|
||||||
|
},
|
||||||
|
"featuredTags": {
|
||||||
|
"@id": "toot:featuredTags",
|
||||||
|
"@type": "@id"
|
||||||
|
},
|
||||||
|
"alsoKnownAs": {
|
||||||
|
"@id": "as:alsoKnownAs",
|
||||||
|
"@type": "@id"
|
||||||
|
},
|
||||||
|
"movedTo": {
|
||||||
|
"@id": "as:movedTo",
|
||||||
|
"@type": "@id"
|
||||||
|
},
|
||||||
|
"claim": {
|
||||||
|
"@type": "@id",
|
||||||
|
"@id": "toot:claim"
|
||||||
|
},
|
||||||
|
"fingerprintKey": {
|
||||||
|
"@type": "@id",
|
||||||
|
"@id": "toot:fingerprintKey"
|
||||||
|
},
|
||||||
|
"identityKey": {
|
||||||
|
"@type": "@id",
|
||||||
|
"@id": "toot:identityKey"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"@type": "@id",
|
||||||
|
"@id": "toot:devices"
|
||||||
|
},
|
||||||
|
"focalPoint": {
|
||||||
|
"@container": "@list",
|
||||||
|
"@id": "toot:focalPoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'id': actor,
|
||||||
|
'type': actor_type,
|
||||||
|
'following': f'{actor}/following',
|
||||||
|
'followers': f'{actor}/followers',
|
||||||
|
'inbox': kwargs.get('inbox', f'{actor}'),
|
||||||
|
'outbox': f'{actor}/outbox',
|
||||||
|
'featured': f'{actor}/collections/featured',
|
||||||
|
'featuredTags': f'{actor}/collections/tags',
|
||||||
|
'preferredUsername': handle,
|
||||||
|
'name': kwargs.get('display_name', handle),
|
||||||
|
'summary': kwargs.get('bio'),
|
||||||
|
'url': kwargs.get('url', actor),
|
||||||
|
'manuallyApprovesFollowers': kwargs.get('locked', False),
|
||||||
|
'discoverable': kwargs.get('discoverable', False),
|
||||||
|
'published': published or DateString.now('activitypub'),
|
||||||
|
'devices': f'{actor}/collections/devices',
|
||||||
|
'publicKey': {
|
||||||
|
'id': f'{actor}#main-key',
|
||||||
|
'owner': actor,
|
||||||
|
'publicKeyPem': pubkey
|
||||||
|
},
|
||||||
|
'tag': [],
|
||||||
|
'attachment': [],
|
||||||
|
'endpoints': {
|
||||||
|
'sharedInbox': kwargs.get('shared_inbox', f'https://{actor.host}/inbox')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for key, value in table.items():
|
||||||
|
data.attachment.append(PropertyValue(key, value))
|
||||||
|
|
||||||
|
if kwargs.get('avatar_url'):
|
||||||
|
data.icon = Object.new_icon(kwargs.get('avatar_url'), kwargs.get('avatar_type'))
|
||||||
|
|
||||||
|
# need to add data when "full" is true
|
||||||
|
if not full:
|
||||||
|
del data.featured
|
||||||
|
del data.featuredTags
|
||||||
|
del data.devices
|
||||||
|
del data.following
|
||||||
|
del data.followers
|
||||||
|
del data.outbox
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_activity(cls, id: str, type: str, actor_src: Union[str, dict], object: Union[str, dict], to: list=[pubstr], cc: list=[]):
|
||||||
|
assert type in activity_types
|
||||||
|
|
||||||
|
cls({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'type': type,
|
||||||
|
'to': to,
|
||||||
|
'cc': cc,
|
||||||
|
'object': object,
|
||||||
|
'id': id,
|
||||||
|
'actor': actor_src
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_emoji(cls, id, name, url, image):
|
||||||
|
return cls({
|
||||||
|
'id': id,
|
||||||
|
'type': 'Emoji',
|
||||||
|
'name': name,
|
||||||
|
'updated': updated or DateTime.now('activitypub'),
|
||||||
|
'icon': image
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_image(cls, url, type=None):
|
||||||
|
return cls({
|
||||||
|
'type': 'Image',
|
||||||
|
'mediaType': type or mimetypes.guess_type(url)[0] or 'image/png',
|
||||||
|
'url': url
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class Collection(Object):
|
||||||
|
@classmethod
|
||||||
|
def new_replies(cls, id):
|
||||||
|
{
|
||||||
|
"id": f"{id}/replies",
|
||||||
|
"type": "Collection",
|
||||||
|
"first": {
|
||||||
|
"type": "CollectionPage",
|
||||||
|
"next": f"{id}/replies?only_other_accounts=true&page=true",
|
||||||
|
"partOf": f"{id}/replies",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyValue(DotDict):
|
||||||
|
def __init__(self, key, value):
|
||||||
|
super().__init__({
|
||||||
|
'type': 'PropertyValue',
|
||||||
|
'name': key,
|
||||||
|
'value': value
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
key = key.lower()
|
||||||
|
|
||||||
|
assert key in ['type', 'name', 'value']
|
||||||
|
assert type(value) == str
|
||||||
|
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_pair(self, key, value):
|
||||||
|
self.name = key
|
||||||
|
self.value = value
|
|
@ -1,6 +1,6 @@
|
||||||
import grp, hashlib, os, platform, random, shlex, signal, socket, statistics, string, time, timeit
|
import grp, hashlib, os, platform, random, shlex, signal, socket, statistics, string, time, timeit
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from getpass import getpass, getuser
|
from getpass import getpass, getuser
|
||||||
from importlib import util
|
from importlib import util
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
@ -12,7 +12,6 @@ from .path import Path
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ap_date',
|
|
||||||
'boolean',
|
'boolean',
|
||||||
'catch_kb_interrupt',
|
'catch_kb_interrupt',
|
||||||
'get_current_user_info',
|
'get_current_user_info',
|
||||||
|
@ -31,32 +30,16 @@ __all__ = [
|
||||||
'time_function_pprint',
|
'time_function_pprint',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'var_name',
|
'var_name',
|
||||||
|
'DateString',
|
||||||
'Url'
|
'Url'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def ap_date(date=None, alt=False):
|
datetime_formats = {
|
||||||
'''
|
'http': '%a, %d %b %Y %H:%M:%S GMT',
|
||||||
Takes a datetime object and returns it as an ActivityPub-friendly string
|
'activitypub': '%Y-%m-%dT%H:%M:%SZ',
|
||||||
|
'activitypub-date': '%Y-%m-%d'
|
||||||
Arguments:
|
}
|
||||||
date (datetime): The datetime object to be converted. It not set, will create a new datetime object with the current date and time
|
|
||||||
alt (bool): If True, the returned string will be in the Mastodon API format
|
|
||||||
|
|
||||||
Return:
|
|
||||||
str: The date in an ActivityPub-friendly format
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not date:
|
|
||||||
date = datetime.utcnow()
|
|
||||||
|
|
||||||
elif type(date) == int:
|
|
||||||
date = datetime.fromtimestamp(date)
|
|
||||||
|
|
||||||
elif type(date) != datetime:
|
|
||||||
raise TypeError(f'Unsupported object type for ApDate: {type(date)}')
|
|
||||||
|
|
||||||
return date.strftime('%a, %d %b %Y %H:%M:%S GMT' if alt else '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
|
|
||||||
|
|
||||||
def boolean(v, return_value=False):
|
def boolean(v, return_value=False):
|
||||||
|
@ -486,6 +469,68 @@ def var_name(single=True, **kwargs):
|
||||||
return key[0] if single else keys
|
return key[0] if single else keys
|
||||||
|
|
||||||
|
|
||||||
|
class DateString(str):
|
||||||
|
tz_utc = timezone.utc
|
||||||
|
tz_local = datetime.now(tz_utc).astimezone().tzinfo
|
||||||
|
dt = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, string, format):
|
||||||
|
assert format in datetime_formats
|
||||||
|
|
||||||
|
self.dt = datetime.strptime(string, datetime_formats[format]).replace(tzinfo=self.tz_utc)
|
||||||
|
|
||||||
|
|
||||||
|
def __new__(cls, string, format):
|
||||||
|
date = str.__new__(cls, string)
|
||||||
|
return date
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
return getattr(self.dt, key)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_datetime(cls, date, format):
|
||||||
|
assert format in datetime_formats
|
||||||
|
return cls(date.astimezone(cls.tz_utc).strftime(datetime_formats[format]), format)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_timestamp(cls, timestamp, format):
|
||||||
|
return cls.from_datetime(datetime.fromtimestamp(timestamp), format)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls, format):
|
||||||
|
return cls.from_datetime(datetime.now(cls.tz_utc), format)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def http(self):
|
||||||
|
return DateString(self.dump_to_string('http'), 'http')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activitypub(self):
|
||||||
|
return DateString(self.dump_to_string('activitypub'), 'activitypub')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def utc(self):
|
||||||
|
return self.dt.astimezone(self.tz_utc)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local(self):
|
||||||
|
return self.dt.astimezone(self.tz_local)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_to_string(self, format):
|
||||||
|
assert format in datetime_formats
|
||||||
|
return self.dt.strftime(datetime_formats[format])
|
||||||
|
|
||||||
|
|
||||||
class Url(str):
|
class Url(str):
|
||||||
protocols = {
|
protocols = {
|
||||||
'http': 80,
|
'http': 80,
|
||||||
|
@ -495,8 +540,6 @@ class Url(str):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
str.__new__(Url, url)
|
|
||||||
|
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
|
|
||||||
self.__parsed = parsed
|
self.__parsed = parsed
|
||||||
|
|
Loading…
Reference in a new issue