kopia lustrzana https://gitlab.com/marnanel/chapeau
Porównaj commity
3 Commity
6b5e47807e
...
963a8b2234
Autor | SHA1 | Data |
---|---|---|
Marnanel Thurman | 963a8b2234 | |
Marnanel Thurman | 7f675c7b00 | |
Marnanel Thurman | 6817216d52 |
|
@ -1,12 +1,16 @@
|
|||
from kepi.daemon import Daemon
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
|
||||
import kepi.validate
|
||||
|
||||
logger = logging.getLogger('kepi')
|
||||
logging.basicConfig(
|
||||
level = logging.INFO,
|
||||
stream = sys.stdout,
|
||||
)
|
||||
|
||||
def daemonise(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||
|
||||
|
@ -36,47 +40,48 @@ def daemonise(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|||
|
||||
logger.info("Running at PID %s", os.getpid())
|
||||
|
||||
def get_config():
|
||||
def load_message(name):
|
||||
if name=='-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = open(name, 'r')
|
||||
|
||||
result = json.load(f)
|
||||
|
||||
if f!=sys.stdin:
|
||||
f.close()
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='send or receive ActivityPub messages')
|
||||
parser.add_argument(
|
||||
'--incoming', '-I', action='store_true',
|
||||
help='read an incoming message (rather than a message to send)')
|
||||
parser.add_argument(
|
||||
'--fifo', '-F', default='/var/run/kepi/kepi.fifo',
|
||||
help='filename for control pipe')
|
||||
parser.add_argument(
|
||||
'--pidfile', '-P', default='/var/run/kepi/kepi.pid',
|
||||
help='filename for process ID')
|
||||
|
||||
parser.add_argument(
|
||||
'--spool', '-S', default='/var/spool/kepi',
|
||||
help='directory to store the messages')
|
||||
parser.add_argument(
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest = 'command',
|
||||
required = True,
|
||||
)
|
||||
|
||||
validate_parser = subparsers.add_parser('validate')
|
||||
|
||||
validate_parser.add_argument(
|
||||
'input',
|
||||
help=(
|
||||
'the file to read ("-" for stdin)'
|
||||
),
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.input=='-':
|
||||
f = sys.stdin
|
||||
if args.command=='validate':
|
||||
message = load_message(args.input)
|
||||
kepi.validate.validate(message)
|
||||
else:
|
||||
f = open(args.input, 'r')
|
||||
|
||||
result = dict(args._get_kwargs())
|
||||
result['message'] = json.load(f)
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
config = get_config()
|
||||
|
||||
daemon = Daemon(
|
||||
config = config,
|
||||
)
|
||||
|
||||
logger.info("Process ended normally.")
|
||||
raise NotImplementedError()
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import os
|
||||
import glob
|
||||
import json
|
||||
import copy
|
||||
|
||||
JSON_EXT = '.json'
|
||||
TEMP_EXT = '.1'
|
||||
|
||||
PRIVATE_KEY_FIELD = 'private-key'
|
||||
|
||||
class User:
|
||||
def __init__(self,
|
||||
filename):
|
||||
self.filename = filename
|
||||
self.details = None
|
||||
self.private_key = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return os.path.splitext(os.path.basename(self.filename))[0]
|
||||
|
||||
def _load_details(self):
|
||||
if self.details is not None:
|
||||
return
|
||||
|
||||
with open(self.filename, 'r') as f:
|
||||
self.details = json.load(f)
|
||||
|
||||
def _save_details(self):
|
||||
if self.details is not None:
|
||||
return
|
||||
|
||||
with open(self.filename+TEMP_EXT, 'w') as f:
|
||||
json.dump(self.details, f)
|
||||
|
||||
os.path.move(
|
||||
self.filename+TEMP_EXT,
|
||||
self.filename,
|
||||
)
|
||||
|
||||
def __getitem__(self, name):
|
||||
|
||||
if name==PRIVATE_KEY_FIELD:
|
||||
if self.private_key is None:
|
||||
|
||||
username = os.path.splitext(
|
||||
os.path.basename(self.filename))[1]
|
||||
|
||||
with open(os.path.join(
|
||||
os.path.basename(self.filename),
|
||||
'private',
|
||||
'username',
|
||||
), 'r') as f:
|
||||
self.private_key = f.read()
|
||||
|
||||
return self.private_key
|
||||
|
||||
self._load_details()
|
||||
return self.details[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
|
||||
if name==PRIVATE_KEY_FIELD:
|
||||
raise KeyError(f"You can't set {PRIVATE_KEY_FIELD}")
|
||||
|
||||
self._load_details()
|
||||
self.details[name] = value
|
||||
self._save_details()
|
||||
|
||||
def as_dict(self):
|
||||
self._load_details()
|
||||
|
||||
result = copy.copy(self.details)
|
||||
return result
|
||||
|
||||
class Users:
|
||||
def __init__(self,
|
||||
users_dir,
|
||||
):
|
||||
self.users_dir = users_dir
|
||||
|
||||
def __getitem__(self, name):
|
||||
|
||||
if '/' in name or '\\' in name:
|
||||
raise ValueError("Forbidden characters in name.")
|
||||
|
||||
filename = os.path.join(
|
||||
self.users_dir,
|
||||
name,
|
||||
) + JSON_EXT
|
||||
|
||||
try:
|
||||
os.stat(filename)
|
||||
except FileNotFoundError:
|
||||
raise KeyError()
|
||||
|
||||
return User(filename)
|
||||
|
||||
def __iter__(self):
|
||||
for someone in glob.glob(os.path.join(
|
||||
self.users_dir,
|
||||
'*'+JSON_EXT,
|
||||
)):
|
||||
name = os.path.splitext(
|
||||
os.path.basename(someone)
|
||||
)[0]
|
||||
yield self[name]
|
|
@ -16,6 +16,10 @@ def validate(
|
|||
Validates a message.
|
||||
"""
|
||||
|
||||
message = dict([
|
||||
(k.lower(),v) for k,v in message.items()
|
||||
])
|
||||
|
||||
if 'body' not in message:
|
||||
raise ValueError("message must contain a body")
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
These are public/private key pairs used in testing.
|
|
@ -0,0 +1,48 @@
|
|||
{"@context":["https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
|
||||
"sensitive":"as:sensitive",
|
||||
"movedTo":{"@id":"as:movedTo",
|
||||
"@type":"@id"},
|
||||
"alsoKnownAs":{"@id":"as:alsoKnownAs",
|
||||
"@type":"@id"},
|
||||
"Hashtag":"as:Hashtag",
|
||||
"ostatus":"http://ostatus.org#",
|
||||
"atomUri":"ostatus:atomUri",
|
||||
"inReplyToAtomUri":"ostatus:inReplyToAtomUri",
|
||||
"conversation":"ostatus:conversation",
|
||||
"toot":"http://joinmastodon.org/ns#",
|
||||
"Emoji":"toot:Emoji",
|
||||
"focalPoint":{"@container":"@list",
|
||||
"@id":"toot:focalPoint"},
|
||||
"featured":{"@id":"toot:featured",
|
||||
"@type":"@id"},
|
||||
"schema":"http://schema.org#",
|
||||
"PropertyValue":"schema:PropertyValue",
|
||||
"value":"schema:value"}],
|
||||
"id":"https://local.example.org/users/fred",
|
||||
"type":"Person",
|
||||
"following":"https://local.example.org/users/fred/following",
|
||||
"followers":"https://local.example.org/users/fred/followers",
|
||||
"inbox":"https://local.example.org/users/fred/inbox",
|
||||
"outbox":"https://local.example.org/users/fred/outbox",
|
||||
"featured":"https://local.example.org/users/fred/collections/featured",
|
||||
"preferredUsername":"fred",
|
||||
"name":"",
|
||||
"summary":"I am not a basset hound.",
|
||||
"url":"https://local.example.org/@fred",
|
||||
"manuallyApprovesFollowers":false,
|
||||
"publicKey":{"id":"https://local.example.org/users/fred#main-key",
|
||||
"owner":"https://local.example.org/users/fred",
|
||||
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJtAZt9hgB/7bNozWtv9Er+Pnf\num97oM8cxRlqRqUXZk0wuST9A0eY5EUsN8j3qc6msZjDPSDQELr/U/o+zJLp/B8s\n7x3iHAHGD4LcQ9AbyDqbhX9JZkmwGx6PIVmbMDANmppqLik36V7cov6BuHz1gFpD\nP+iPjem4mph/KLwugQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"tag":[],
|
||||
"attachment":[],
|
||||
"endpoints":{"sharedInbox":"https://local.example.org/inbox"},
|
||||
"icon":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/avatars/000/015/322/original/data.png"},
|
||||
"image":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/headers/000/015/322/original/data.png"}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
{"@context":["https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
|
||||
"sensitive":"as:sensitive",
|
||||
"movedTo":{"@id":"as:movedTo",
|
||||
"@type":"@id"},
|
||||
"alsoKnownAs":{"@id":"as:alsoKnownAs",
|
||||
"@type":"@id"},
|
||||
"Hashtag":"as:Hashtag",
|
||||
"ostatus":"http://ostatus.org#",
|
||||
"atomUri":"ostatus:atomUri",
|
||||
"inReplyToAtomUri":"ostatus:inReplyToAtomUri",
|
||||
"conversation":"ostatus:conversation",
|
||||
"toot":"http://joinmastodon.org/ns#",
|
||||
"Emoji":"toot:Emoji",
|
||||
"focalPoint":{"@container":"@list",
|
||||
"@id":"toot:focalPoint"},
|
||||
"featured":{"@id":"toot:featured",
|
||||
"@type":"@id"},
|
||||
"schema":"http://schema.org#",
|
||||
"PropertyValue":"schema:PropertyValue",
|
||||
"value":"schema:value"}],
|
||||
"id":"https://local.example.org/users/jim",
|
||||
"type":"Person",
|
||||
"following":"https://local.example.org/users/jim/following",
|
||||
"followers":"https://local.example.org/users/jim/followers",
|
||||
"inbox":"https://local.example.org/users/jim/inbox",
|
||||
"outbox":"https://local.example.org/users/jim/outbox",
|
||||
"featured":"https://local.example.org/users/jim/collections/featured",
|
||||
"preferredUsername":"jim",
|
||||
"name":"",
|
||||
"summary":"His friends were very good to him.",
|
||||
"url":"https://local.example.org/@jim",
|
||||
"manuallyApprovesFollowers":false,
|
||||
"publicKey":{"id":"https://local.example.org/users/jim#main-key",
|
||||
"owner":"https://local.example.org/users/jim",
|
||||
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPPJ6uNXzpocC9Z1rue3sgZl/W\nnYHjbtkfQCUpdV9lgmtbOgpZrQos5sIB5QxUx+yRAXmdSRsD2q1Kaeeew5T+pv3h\nJKH4XMNZd2mZf1KAuHjPFBjCRGMUwdEEozSy8ZpDAg+jQ2ro8E7wgZ+wsYatSLbQ\n9SIkceGWqxyhabyIqwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"tag":[],
|
||||
"attachment":[],
|
||||
"endpoints":{"sharedInbox":"https://local.example.org/inbox"},
|
||||
"icon":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/avatars/000/015/322/original/data.png"},
|
||||
"image":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/headers/000/015/322/original/data.png"}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQDJtAZt9hgB/7bNozWtv9Er+Pnfum97oM8cxRlqRqUXZk0wuST9
|
||||
A0eY5EUsN8j3qc6msZjDPSDQELr/U/o+zJLp/B8s7x3iHAHGD4LcQ9AbyDqbhX9J
|
||||
ZkmwGx6PIVmbMDANmppqLik36V7cov6BuHz1gFpDP+iPjem4mph/KLwugQIDAQAB
|
||||
AoGBAI6sRaQAYCkBzTeWC8E0HmwhN/Z2NKdZL0clb/3JrLtphI5DWBOT/0/5n6hQ
|
||||
aVouBdvJYcowcgZa3zr+FtPW9s9EKswd4M6VEg7Kb7yvd7iD+6Hl/KY5YkpRutJF
|
||||
ZVBt20iJi3xi+5D0BvMImD3nE/Zl2MgAJWIBlRywfDOuKS7BAkEA4hIoq0RAbSgf
|
||||
VBzzWucpZa4o2Ll35tG9X5zQSmEjWuuIxipsiwcuyHURTEG+45TU/AasyDTvqqm6
|
||||
HhG4Z9SpaQJBAORoAzTfuF6Z7I3THSDLQjqWiE10K0qSkrcPf7fMlTdzheQdhS5T
|
||||
A4liwuAkpoRKxpvBw+OvCkgaqr34+Oqo4VkCQQC19kPBxofM1HSS8VJ3IoTRkOLT
|
||||
vkTiBoPUx5VnqNQaRGasik0fgkKHmqK3rFuHNq5PxNehteoKhd6GgWDaQfOxAkEA
|
||||
1KV9rsFGrlSR5qyRJtH11AQH3Ex2bZQuod39I0qF9b1I/0r4jltdJJBdLD8TBIF1
|
||||
jNeGH7j8Uor5QarFW/tk6QJBAMEYab7c1YTagny4nrKoddPfNyjUF2a7HNHTnCQn
|
||||
rbOt963SPr1/dv602FwOzKHAWVw401PfaHWkclEROZTwwEc=
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXgIBAAKBgQDPPJ6uNXzpocC9Z1rue3sgZl/WnYHjbtkfQCUpdV9lgmtbOgpZ
|
||||
rQos5sIB5QxUx+yRAXmdSRsD2q1Kaeeew5T+pv3hJKH4XMNZd2mZf1KAuHjPFBjC
|
||||
RGMUwdEEozSy8ZpDAg+jQ2ro8E7wgZ+wsYatSLbQ9SIkceGWqxyhabyIqwIDAQAB
|
||||
AoGBAMMfZqDMh+JKhHlROVLWPOYSviYKg2Oq2RANi2/vrXScSYzJpzksLip80yqJ
|
||||
iQTCgME/TEyFqsQEP6mS8ZyQtlUjoz8j6q/9TFvQrWWHRNgiD9bdXFQr0+F9kxr9
|
||||
hdm1h7F5Q0JTGCPwBmL8GXCO8KmbZUMejITNqsIxx3bvzUoRAkEA1Eul1Mwkb/6C
|
||||
cBuG0TPfXwluWdzS37XY+ckuQqgA5Jhj+vQTWbGovA/99W5GHHWsHJwGwcqqJJet
|
||||
c4slKN3GBwJBAPnmXpAO1NiQMlIVohTfjCusypsSiHJXocigJG/DeEuMIHUJebuH
|
||||
r6M7D2ENk7xh/kr3qA6369WokYT+K5Bqnz0CQQDPvApUVUIeeNwoWTcuBOVBeNgL
|
||||
hOKv16Cuo6bpwL3G8jt7OFSrAwZKqBdojvR6Ksc045RVEzw0PFuU4YaGG6UHAkBZ
|
||||
Q9LvfnzFRuzSqWuWLSwyxawxrHMU9PyTX7DkQ1yLD+jgJZxYQmWY1xXtQx5Momxl
|
||||
dwWPDF+vmGEysl/5XDy5AkEAwbbjj3YstPYPnBsAbI0XYHfXxu7aZ+bwPNtH48r3
|
||||
3YPAqM+HO0i2ffHjMJCYfbttWiRJRFVHyp3QO/Wtww4DQQ==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQCsGqRZbZV2nljTOW2b77fzpkx9+iqcNgEnlhdgSIjCPvhzwj6c
|
||||
b0O5RVqPu6krvv+Dgiy89Mb0nregdOstUXzn40Yicg1OOHMitrpAQ+4MsotqspfF
|
||||
ZF/Q9qSFom3PxDA55lIHYJJmusVM6bSlrdY8msAsL1BieW7065gtjzv6RQIDAQAB
|
||||
AoGACURx/yLIfpeuPsmD3na9GBCnY805yCmcTE5nudaODq+nX0xhZLkVE3/pjX3U
|
||||
cTeauLEkyZQAtqFpT+mb1Ffj+t3exqK68k7UwCUI23Gtbr5dRUOivWuN75Sf4xFo
|
||||
7vQeSwIot/1PyU7JYXZ9Tq9WMBHcFocCdxu85QSBS40cPIECQQC60bIlj+bN7fvU
|
||||
HoPyQbj1vfjbz6vcoeR6v+YGCiSFrzawOEuJ1xUC7c/uivgJUvKumhrOFfwLac+1
|
||||
Zay2t7khAkEA69X4ft06mfVNo1oM8ly33CO5TKlpFlVPBxEdKMHyZ2ZO1vIK6DsF
|
||||
N67YwYI9OoncTvBucZ+Dy2GU0xUeqtSopQJANhtnsjNUUI49onjYFEDutdW4jsk9
|
||||
6F/HEbokf9lOLJ3LhAw57Ikrn7aKw3biUakBeopNeySo5BFYRBxXgnABoQJBANlf
|
||||
MVoNo1QAy/zCpahGWZlovASzKY9SNjM3TP8iNMGlhQmNswv2SorWeCd0Wec45n1E
|
||||
EyhbdOjjGn+sucWPmZkCQD8ekBtzToRvNZjocFJhZdcWMIAo96XsnTAxREKSmbnG
|
||||
pFHXBWokjDO/rTRbqANocLr0GwFR8UBD70CJLLOCaEg=
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,47 @@
|
|||
{"@context":["https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
|
||||
"sensitive":"as:sensitive",
|
||||
"movedTo":{"@id":"as:movedTo",
|
||||
"@type":"@id"},
|
||||
"alsoKnownAs":{"@id":"as:alsoKnownAs",
|
||||
"@type":"@id"},
|
||||
"Hashtag":"as:Hashtag",
|
||||
"ostatus":"http://ostatus.org#",
|
||||
"atomUri":"ostatus:atomUri",
|
||||
"inReplyToAtomUri":"ostatus:inReplyToAtomUri",
|
||||
"conversation":"ostatus:conversation",
|
||||
"toot":"http://joinmastodon.org/ns#",
|
||||
"Emoji":"toot:Emoji",
|
||||
"focalPoint":{"@container":"@list",
|
||||
"@id":"toot:focalPoint"},
|
||||
"featured":{"@id":"toot:featured",
|
||||
"@type":"@id"},
|
||||
"schema":"http://schema.org#",
|
||||
"PropertyValue":"schema:PropertyValue",
|
||||
"value":"schema:value"}],
|
||||
"id":"https://local.example.org/users/sheila",
|
||||
"type":"Person",
|
||||
"following":"https://local.example.org/users/sheila/following",
|
||||
"followers":"https://local.example.org/users/sheila/followers",
|
||||
"inbox":"https://local.example.org/users/sheila/inbox",
|
||||
"outbox":"https://local.example.org/users/sheila/outbox",
|
||||
"featured":"https://local.example.org/users/sheila/collections/featured",
|
||||
"preferredUsername":"sheila",
|
||||
"name":"",
|
||||
"summary":"What do you think?",
|
||||
"url":"https://local.example.org/@sheila",
|
||||
"manuallyApprovesFollowers":false,
|
||||
"publicKey":{"id":"https://local.example.org/users/sheila#main-key",
|
||||
"owner":"https://local.example.org/users/sheila",
|
||||
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsGqRZbZV2nljTOW2b77fzpkx9\n+iqcNgEnlhdgSIjCPvhzwj6cb0O5RVqPu6krvv+Dgiy89Mb0nregdOstUXzn40Yi\ncg1OOHMitrpAQ+4MsotqspfFZF/Q9qSFom3PxDA55lIHYJJmusVM6bSlrdY8msAs\nL1BieW7065gtjzv6RQIDAQAB\n-----END PUBLIC KEY-----\n"},
|
||||
"tag":[],
|
||||
"attachment":[],
|
||||
"endpoints":{"sharedInbox":"https://local.example.org/inbox"},
|
||||
"icon":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/avatars/000/015/322/original/data.png"},
|
||||
"image":{"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url":"https://body.local.example.org/media/accounts/headers/000/015/322/original/data.png"}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
from test import *
|
||||
from kepi.users import Users
|
||||
|
||||
USERS_DIR = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"example-users",
|
||||
)
|
||||
|
||||
FRED_OUTBOX = 'https://local.example.org/users/fred/outbox'
|
||||
|
||||
def test_users_simple():
|
||||
u = Users(users_dir = USERS_DIR)
|
||||
|
||||
fred = u['fred']
|
||||
assert fred['summary']=='I am not a basset hound.'
|
||||
|
||||
def test_users_dir():
|
||||
u = Users(users_dir = USERS_DIR)
|
||||
|
||||
assert sorted([user.name for user in u])==['fred', 'jim', 'sheila']
|
||||
|
||||
def test_users_dict():
|
||||
u = Users(users_dir = USERS_DIR)
|
||||
fred = u['fred']
|
||||
|
||||
assert fred['outbox'] == FRED_OUTBOX
|
||||
|
||||
fred_dict = fred.as_dict()
|
||||
assert not isinstance(fred, dict)
|
||||
assert isinstance(fred_dict, dict)
|
||||
|
||||
assert fred_dict['outbox'] == FRED_OUTBOX
|
Ładowanie…
Reference in New Issue