Verweis auf Microsoft Exchange Web Services Managed API fehlt

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Werniman
User
Beiträge: 4
Registriert: Montag 18. Februar 2019, 13:37

Hallo,
ich hab hier ein ziemliches Problem. Wir nutzen in der Firma lauter Lancom-Geräte (Router, Switches usw). Nun hat unser Chef ein paar ePaper-Displays als Raumbeschilderung angeschafft, die ich einrichten soll. Die Dinger werden im Grunde über eine eigene Serversoftware angesteuert,welche Ihrerseits die Daten vom Exchange-Server holen soll, auf dem das zugehörige Raumpostfach liegt. Wer Lancom kennt, weiß, dass diese Firma prinzipiell IMMER den für den User schlechtesten Weg einschlägt, bei Lancom ist *gar nichts* einfach. Entsprechend dieser Richtlinie ist auch die Kommunikation zwischen ePaper-Server und Exchange-Server aufgebaut.
Heißt im vorliegenden Fall: Ein Script für IronPython dient als Schnittstelle zwischen beiden Servern und schaut regelmäßig auf dem ePaper-Server nach neuen Daten für die Displays. Warum auch ein schönes einfaches Programm nutzen,wenn man den Nutzer auch mit ellenlangen Skripten quälen kann ? Dumm nur,wenn derjenige, der den Kram einrichten soll, von Python ungefähr soviel Ahnung hat wie ein Walross vom jodeln. Ich bräuchte also mal eure Hilfe.
Ich hab hier also 2 Skripte, von denen eins scheinbar permanent den Exchange-Server ausliest und die Daten an Script 2 übergibt, welches es an den ePaper-Server gibt.

Hier mal das erste Script, weil schon das mein Problem ist:

Code: Alles auswählen

# coding: utf-8
#
# Update LANCOM Wireless Displays with room reservations from Microsoft Exchange
# written by Martin Gordziel for LANCOM Systems GmbH in 2014
#
# prerequisites:
#
#  * install ironpython from http://ironpython.net/
#
#  * install "Microsoft Exchange Web Services Managed API" (use websearch) and include it in the PYTHONPATH environment variable.
#    If not set in IRONPYTHONPATH environment, uncomment and change the following path to where EWS got installed
#    PATH_TO_EXCHANGE_WEB_SERVICES_MANAGED_API = r'C:\Program Files\Microsoft\Exchange\Web Services\2.2'

#
# Code starts here
#

# Python imports
import os
import sys
import signal
import time
import datetime
import traceback
import argparse
import json
import xml.etree.ElementTree as ET
import xml.sax.saxutils as saxutils
import httplib
import hashlib
import logging
logger = logging.getLogger(__name__)

# .NET imports
try:
	#if PATH_TO_EXCHANGE_WEB_SERVICES_MANAGED_API is set, append it to path
	sys.path.append(PATH_TO_EXCHANGE_WEB_SERVICES_MANAGED_API)
except:
	pass
import clr
clr.AddReferenceToFile('Microsoft.Exchange.WebServices.dll')
import Microsoft.Exchange.WebServices.Data as EWSData
import System.DateTime


def print_console(text=''):
	print text


class WirelessDisplayServer(object):
	"""
	implements HTTP methods for the LANCOM Wireless ePaper Display server
	.post(id, xml) sends XML to server
	.push_Label(label) takes a Label class instance and sends it to server
	.get_transaction_updatestatus(id) returns a dictionary with the updatestatus of a display
	"""

	def __init__(self, address=None, port=None):
		self.server_address = ''
		self.server_port = '8001'
		if address is not None:
			self.server_address = address
		if port is not None:
			self.server_port = port

	def post(self, label_id, xml):
		"""
		post XML to server

		returns:
			False if it failed
			Transaction_ID if POST was successful and returned the ID
		"""
		rc = False
		h = httplib.HTTPConnection(self.server_address, self.server_port, timeout = 10)
		headers = {'Content-Type': 'application/xml'}
		try:
			h.request('POST', '/service/task', xml, headers)
			response = h.getresponse()
			if response.status == 200:
				xml_response = response.read()
				root = ET.fromstring(xml_response)
				if root.tag=='Transaction':
					rc = root.attrib['id']
					logger.debug('pushed via {0}:{1} to {2} under Transaction ID {3}'.format(self.server_address, self.server_port, label_id, rc))
				else:
					logger.warning('pushed via {0}:{1} to {2}, but got no Transaction ID'.format(self.server_address, self.server_port, label_id))
			else:
				logger.warning('got response status {} to POST'.format(response.status))
		except:
			logger.warning('could not communicate with server:')
			logger.warning(traceback.format_exc())
		h.close()
		return rc

	def push_Label(self, label):
		return self.post(label.label_id, label.get_xml())

	def get_transaction_updatestatus(self, transaction_id):
		"""
		get the status of a transaction (e.g 48)
		"""
		info = dict()
		h = httplib.HTTPConnection(self.server_address, self.server_port, timeout = 10)
		try:
			h.request('GET', '/service/updatestatus/transaction/{0}'.format(transaction_id))
			response = h.getresponse()
			if response.status == 200:
				xml_response = response.read()
				root = ET.fromstring(xml_response)
				if root.tag=='UpdateStatusPagedResult':
					for child in root:
						if child.tag=='UpdateStatus':
							info.update(child.attrib)
							info.update(dict((el.tag, el.text) for el in child))
							logger.info('got Transaction {0} UpdateStatus {1}'.format(transaction_id, ', '.join(['{0}={1}'.format(key, value) for key, value in info.items()])))
							break
				else:
					logger.warning('extpected result of type UpdateStatusPagedResult but got {0}'.format(root.tag))

			else:
				logger.warning('got HTTP response {0}'.format(response.status))
		except:
			logger.warning('could not communicate with server:')
			logger.warning(traceback.format_exc())
		h.close()
		return info


class ConferenceLabel(object):
	"""
	LANCOM Label for Conference Rooms
	label_id and template get filled from the config
	room_name and the fields dictionary get filled from the calendar

	.get_xml() returns the XML which can be POSTed to server
	.get_hash() returns a hash of the XML, e.g. to check if the XML has changed
	has its own .__str__() to allow a pretty "print my_label_instance"
	"""

	def __init__(self):
		self.label_id = ''
		self.template = ''
		self.fields = {'date': '', 'time1': '', 'purpose1': '', 'chair1': '', 'time2': '', 'purpose2': '', 'chair2': ''}
		self.room_name = ''

	def get_xml(self):
		roomName = saxutils.quoteattr(self.room_name)
		fields = ''.join(['			<field key="{0}" value={1}/>\n'.format(key, saxutils.quoteattr(value)) for key, value in self.fields.iteritems()])
		xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TaskOrder title="templated data for label {0}">
	<TemplateTask labelId="{0}" template="{1}">
		<room roomName={2}>
{3}		</room>
	</TemplateTask>
</TaskOrder>'''.format(self.label_id, self.template, roomName, fields).encode('utf-8')
		logger.debug('generated XML:\n'+xml)
		return xml

	def get_hash(self):
		h = hashlib.sha1(self.get_xml()).hexdigest()
		logger.debug('hash value is: "{}"'.format(h))
		return h

	def __str__(self):
		pretty = []
		pretty.append('{}: {} @ {}'.format(self.label_id, self.room_name, self.fields['date']))
		line = '          '
		if len(self.fields['time1'])>0: line += '{}: '.format(self.fields['time1'])
		line += self.fields['purpose1']
		if len(self.fields['chair1'])>0: line += ' ({})'.format(self.fields['chair1'] )
		pretty.append(line)
		line = '          '
		if len(self.fields['time2'])>0: line += '{}: '.format(self.fields['time2'])
		line += self.fields['purpose2']
		if len(self.fields['chair2'])>0: line += ' ({})'.format(self.fields['chair2'] )
		if len(line.strip())>0: pretty.append(line)
		return '\n'.join(pretty)


class Exchange(object):
	"""
	Connect to Exchange server via the managed API
	"""

	def __init__(self):
		# exchange version has to be known to communicate correctly with server, see .init_service() for choices
		self.exchange_version = ''

		# exchange endpoint definition
		self.exchange_autodiscover_url_from_email = True
		# if True, specify:
		self.exchange_autodiscover_email = ''
		# if False, specify:
		self.exchange_url = ''

		# exchange credentials
		self.exchange_default_credentials = True
		# if True, the credentials of the current user are used
		# if False, specify:
		self.exchange_user = ''
		self.exchange_password = ''
		self.exchange_domain = ''

	def init_service(self):
		logger.debug('starting init_service')

		# exchange version
		exchange_versions = {
			'Exchange2007_SP1': EWSData.ExchangeVersion.Exchange2007_SP1,
			'Exchange2010': EWSData.ExchangeVersion.Exchange2010,
			'Exchange2010_SP1': EWSData.ExchangeVersion.Exchange2010_SP1,
			'Exchange2010_SP2': EWSData.ExchangeVersion.Exchange2010_SP2,
			'Exchange2013': EWSData.ExchangeVersion.Exchange2013,
			'Exchange2013_SP1': EWSData.ExchangeVersion.Exchange2013_SP1,
		}

		if self.exchange_version in exchange_versions:
			self.service = EWSData.ExchangeService(exchange_versions[self.exchange_version])
		else:
			logger.critical('Exchange Version not known')
			sys.exit(1)

		logger.debug('setting credentials')
		if self.exchange_default_credentials:
			# Connect by using the default credentials of the authenticated user
			self.service.UseDefaultCredentials = True
		else:
			# Connect by using the specified credentials
			self.service.UseDefaultCredentials = False
			self.service.Credentials = EWSData.WebCredentials(self.exchange_user, self.exchange_password, self.exchange_domain)

		logger.debug('setting url')
		if self.exchange_autodiscover_url_from_email:
			self.service.AutodiscoverUrl(self.exchange_autodiscover_email)
		else:
			self.service.Url = System.Uri(self.exchange_url)
		print_console('using to Exchange at {}'.format(self.service.Url))
		logger.debug('finished init_service')

	def get_room_usage(self, room_email, start_date=None, end_date=None):
		"""
		query the room usage for the specified room (given as room_email)

		important: the room mailbox must be configured to NOT delete the subject and NOT to add the organizer to the subject

		the date range can be specified, otherwise the complete current day is used.
		note: meetings spanning more than one day are not supported.
		"""

		logger.debug('get room usage for room mailbox {}'.format(room_email))

		# open room mailbox and go to calendar
		try:
			mailbox = EWSData.Mailbox(room_email)
			folderId = EWSData.FolderId(EWSData.WellKnownFolderName.Calendar, mailbox)
			calendar = EWSData.CalendarFolder.Bind(self.service, folderId)
		except:
			logger.warning('could not access room mailbox {}: {}'.format(room_email, traceback.format_exc()))
			return None

		# build CalendarView query for date range and limit results to the wanted members
		if start_date is None:
			start_date = System.DateTime.Now.Date
		if end_date is None:
			end_date = System.DateTime.Now.AddDays(1).Date
		logger.debug('date range from {} to {}'.format(start_date.ToString(), end_date.ToString()))
		calendar_view = EWSData.CalendarView(start_date, end_date, 100)
		calendar_view.PropertySet = EWSData.PropertySet(
				EWSData.AppointmentSchema.Start,
				EWSData.AppointmentSchema.End,
				EWSData.AppointmentSchema.Subject,
				EWSData.AppointmentSchema.Organizer)

		# perform query and fill room_usage dictionary
		appointments = calendar.FindAppointments(calendar_view)
		room_usage = []
		for app in appointments:
			try:
				meeting = {'start_DateTime': app.Start, 'end_DateTime': app.End, 'subject': app.Subject.ToString(), 'organizer': app.Organizer.Name.ToString()}
				room_usage.append(meeting)
				logger.debug('-> '+', '.join('{}: {}'.format(k,v) for k,v in meeting.iteritems()))
			except:
				logger.warning('skipped malformed appointment: '+traceback.format_exc())

		logger.info('got {} meetings for room {} in range {} - {}'.format(len(room_usage), room_email, start_date, end_date))
		return room_usage

	def get_next_meetings(self, room_email_list):
		"""
		loop through all rooms (all queries use the same time range)
		"""
		now = System.DateTime.Now
		start_date = now.Date
		end_date = now.AddDays(1).Date

		next_meetings = {}

		for room_email in room_email_list:
			room_usage = self.get_room_usage(room_email, start_date, end_date)
			if room_usage is not None:
				next_usage = []
				for usage in room_usage:
					if usage['end_DateTime']>now:
						next_usage.append(usage)
				next_meetings[room_email] = {'date': start_date.ToString('d'), 'details': next_usage}
		return next_meetings


class Updater(object):
	"""
	Update the LANCOM Wireless ePaper Displays
	"""

	def __init__(self):
		self.config = None # dict set  by load_config
		self.args = None # instance set by argument parser
		self.label_hashes = {} # keep hashes of XML sent to server
		self.label_transactions = {} # unfinished transactions
		self.stats = {'skipped': 0, 'updates (new)': 0, 'updates finished successfully': 0, 'updates (repeated)': 0}

	def load_config(self, file):
		"""
		load a JSON config file

		return success as True/False
		"""
		if os.path.exists(file):
			with open(file, 'r') as f:
				try:
					self.config = json.load(f)
					return True
				except:
					logger.critical('Could not parse the config file: '+traceback.format_exc())
					self.config = None
		else:
			self.config = None
		return False

	def save_config(self, file):
		"""
		Save config as JSON
		"""
		with open(file, 'w') as f:
			f.write(json.dumps(self.config, sort_keys=True, indent=4, separators=(',',':')))

	def connect_exchange(self):
		"""
		configure a Exchange instance based on the parameters in the config file
		"""
		self.ews = Exchange()
		for element in ['version', 'default_credentials', 'user', 'password', 'domain', 'autodiscover_url_from_email', 'autodiscover_email', 'url']:
			if element in self.config['exchange_server']:
				setattr(self.ews, 'exchange_'+element, self.config['exchange_server'][element])
			else:
				setattr(self.ews, 'exchange_'+element, '')
		self.ews.init_service()

	def connect_display_server(self):
		"""
		configure a WirelessDisplayServer instance based on the parameters in the config file
		"""
		self.wds = WirelessDisplayServer()
		for element in ['address', 'port']:
			if element in self.config['wireless_display_server']:
				setattr(self.wds, 'server_'+element, self.config['wireless_display_server'][element])
			else:
				setattr(self.wds, 'server_'+element, '')

	def update(self):
		"""
		update all displays (from config/displays)

		collects data from Exchange, and decides if a Update should be performed with the Wireless Display Server
		"""
		print_console('{}: updating labels...'.format(datetime.datetime.now().strftime(('%Y-%m-%d %H:%M:%S'))))

		next_meetings = self.ews.get_next_meetings(display['exchange_room_mailbox'] for display in self.config['displays'])
		for display_data in self.config['displays']:
			if display_data['exchange_room_mailbox'] in next_meetings:
				meeting_date = next_meetings[display_data['exchange_room_mailbox']]['date']
				meeting_details = next_meetings[display_data['exchange_room_mailbox']]['details']
				# fill Label instance
				label = ConferenceLabel()
				label.template = self.config['conference_label']['template']
				label.label_id = display_data['display_id']
				label.room_name = display_data['display_name'].decode('utf-8') # json data comes in utf-8
				label.fields['date'] = meeting_date
				logger.debug('processing label {} from {}: date {}, length of details {}, {}'.format(label.label_id, display_data['exchange_room_mailbox'], repr(meeting_date), len(meeting_details), meeting_details))
				if len(meeting_details)>0:
					logger.debug('added first meeting to label {} from {}'.format(label.label_id, display_data['exchange_room_mailbox']))
					label.fields['time1'] = '{}-{}'.format(meeting_details[0]['start_DateTime'].ToString('t'), meeting_details[0]['end_DateTime'].ToString('t'))
					label.fields['purpose1'] = meeting_details[0]['subject']
					label.fields['chair1'] = meeting_details[0]['organizer']
					if len(meeting_details)>1:
						logger.debug('added second meeting to label {} from {}'.format(label.label_id, display_data['exchange_room_mailbox']))
						label.fields['time2'] = '{}-{}'.format(meeting_details[1]['start_DateTime'].ToString('t'), meeting_details[1]['end_DateTime'].ToString('t'))
						label.fields['purpose2'] = meeting_details[1]['subject']
						label.fields['chair2'] = meeting_details[1]['organizer']
				else:
					logger.debug('no meetings for label {} from {}'.format(label.label_id, display_data['exchange_room_mailbox']))
					label.fields['purpose1'] = self.config['conference_label']['no_new_data_message'].decode('utf-8') # json data comes in utf-8

				# decide if update should be performed
				do_update = True
				if self.args.update=='always':
					self.stats['updates (new)'] += 1
				elif self.args.update in ['newdata', 'required']:
					# build hash of label data
					if label.label_id not in self.label_hashes:
						self.label_hashes[label.label_id] = ''
					hash = label.get_hash()
					logger.debug('compare against stored hash value: "{}"'.format(self.label_hashes[label.label_id]))
					# if hash has changed, do update for both 'newdata' and 'required'
					if self.label_hashes[label.label_id] != hash:
						do_update = True
						self.stats['updates (new)'] += 1
						logger.info('update label because hash value of data has changed')
					elif self.args.update=='required':
						# do update if last transaction was not successful. but if transaction is still pending, do nothing
						if display_data['display_id'] in self.label_transactions:
							update_status = self.wds.get_transaction_updatestatus(self.label_transactions[display_data['display_id']])
							if update_status['Status']=='SUCCESSFUL':
								do_update = False
								del self.label_transactions[display_data['display_id']]
								self.stats['updates finished successfully'] += 1
								print_console('{}: last update went in state SUCCESSFUL'.format(display_data['display_id']))
							elif update_status['Status']=='WAITING':
								do_update = False
								print_console('{}: last update is in state WAITING'.format(display_data['display_id']))
							else:
								do_update = True
								print_console('{}: repeating update, previous one is in state {}'.format(display_data['display_id'], update_status['Status']))
								self.stats['updates (repeated)'] += 1
						else:
							do_update = False
							logger.debug('update skipped, hash has not changed and no ongoing transaction')

					self.label_hashes[label.label_id] = hash

				if do_update:
					print_console(label)
					transaction = self.wds.push_Label(label)
					if transaction is False:
						print_console('{}: update failed, communication with wireless display server failed'.format(display_data['display_id']))
						self.label_hashes[label.label_id] = '' # invalidate hash
					else:
						self.label_transactions[display_data['display_id']] = transaction

				else:
					self.stats['skipped'] += 1
					print_console('{}: skipped update (update not necessary)'.format(display_data['display_id']))
			else:
				print_console('{}: skipped update, could not get reservations from Exchange'.format(display_data['display_id']))

	def run(self, fullhour=False):
		"""
		Run the update loop

		If specified, wait for fullhour before starting the loops
		Ctrl-c breaks the update loop and prints statistics
		"""
		self.connect_exchange()
		self.connect_display_server()

		self.start = datetime.datetime.now()

		if self.args.fullhour:
			# wait for full hour before starting update loop
			prewait = 3600 - (self.start.minute*60 + self.start.second)
			print_console('waiting for full hour ({} seconds)'.format(prewait))
			time.sleep(prewait)
			self.start = datetime.datetime.now()

		try:
			# update loop
			loop=1
			while True:
				print_console()
				self.update()
				now = datetime.datetime.now()
				wait = self.start+datetime.timedelta(seconds = self.args.interval*60*loop)-now
				wait_until = now+wait
				print_console('finished, next update at {}'.format(wait_until.strftime('%Y-%m-%d %H:%M:%S')))
				wait_seconds = wait.total_seconds()
				if wait_seconds>0:
					time.sleep(wait_seconds)
				loop += 1
		except (KeyboardInterrupt):
			# print stats and finish
			print_console()
			for k in ['updates (new)', 'updates (repeated)', 'updates finished successfully', 'skipped']:
				v = self.stats[k.strip()]
				if v>0:
					print_console('{0:30s} : {1:8d}'.format(k, v))
		except:
			raise



if __name__=='__main__':
	# configure argument parser
	parser = argparse.ArgumentParser(description='Update LANCOM Wireless Displays with room reservations from Microsoft Exchange')
	parser.add_argument('config', help='filename of the configuration in JSON format (use UTF-8 without BOM)')
	parser.add_argument('-i', '--interval', type=int, default=5, help='interval (in minutes) between updates')
	parser.add_argument('-f', '--fullhour', action='store_true', help='wait for fullhour before starting update loop')
	parser.add_argument('-u', '--update', choices=['always', 'newdata', 'required'], default='required', help='each interval: update always, update those with new data, or update those which require an update (new data/last transmission not successful')
	parser.add_argument('-d', '--debuglevel', choices=['debug', 'info', 'warning', 'error', 'critical'], default='warning', help='debuglevel for logging')
	parser.add_argument('-q', '--quiet', action='store_true', help='quiet mode, no console output')

	# configure Updater and start the loop
	u = Updater()
	u.args = parser.parse_args()
	if u.args.quiet:
		def print_console(text=''): pass
	logging.basicConfig(level = getattr(logging, u.args.debuglevel.upper()), format = '%(asctime)s - %(levelname)s = %(message)s')
	if u.load_config(u.args.config):
		u.run()
	else:
		print 'could not load config: {}'.format(u.args.config)

Wenn ich den laut Anleitung zu startenden Befehl ipy.exe exchange_display_updater.py config_example.json -i 5 -u required eingebe, krieg ich gleich die Meldung

"Line 40 in (module) IO Error: System.IO.IOException: Could not add references to assemby Microsoft.Exchange.WebServices.dll

Diese Datei liegt unter C:\Program Files\Microsoft\Exchange\Web Services\2.2. Ich vermute,dass ich da noch diesen Pfad irgendwo als Variable eingeben muss. Aber wo bitte soll ich das tun ? Als normale Umgebungsvariable in Windows funktionierts jedenfalls nicht.
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Die vielen Zeilen Kommentar am Anfang hast du gelesen?
rattlesnake
User
Beiträge: 7
Registriert: Dienstag 15. November 2016, 21:28

Ich schlage mich mit demselben Mist rum :-)
Der Setup der APs war schon ein Horror, aber dann noch diese ganzen Zwischen-GUIs, einmal einen eigenen Server, dann noch einen lokalen Webserver vom LANConfig.exe und obendrauf noch einen PC mit dem Exchange Listener. Bescheuerter und Unfreundlicher kann man eine Software nicht mehr machen... Nunja, zur Sache.

Du benötigst IronPython 2.7 und die MS Web EWS API 2.2 https://www.microsoft.com/en-us/downloa ... x?id=42951
Dann musst Du den Pfad im .py folgendermaßen eintragen:
PATH_TO_EXCHANGE_WEB_SERVICES_MANAGED_API = r'C:\Program Files\Microsoft\Exchange\Web Services\2.2'

Dann musst Du die config.json anpassen. In etwa so:

Code: Alles auswählen

{
	"wireless_display_server": {
		"address": "epaper-server.network.int",
		"port": 8001
	},
	"exchange_server": {
		"version": "Exchange2013_SP1",
		"default_credentials": true,
		"user": "exchangeconnector",
		"password": "secret2011!",
		"domain": "network.int",
		"autodiscover_url_from_email": false,
		"autodiscover_email": "owa@network.int",
		"url": "https://ews.network.int/EWS/Exchange.asmx"
	},
	"conference_label": {
        "template": "meeting_landscape.xsl",
	    "no_new_data_message": "Keine Reservierungen"
	},
	"displays" : [
		{
			"exchange_room_mailbox": "Raum-100@network.int",
			"display_id": "A309DF4A",
			"display_name": "TEST"
		}
	]
}
Achja,bei mir klappts trotzdem nicht, weil er meint:

Code: Alles auswählen

2019-03-19 07:48:09,483 - WARNING = could not access room mailbox Raum-100@network.int: Traceback (most recent call last):
  File "exchange_display_updater.py", line 259, in get_room_usage
    calendar = EWSData.CalendarFolder.Bind(self.service, folderId)
Exception: Der angegebene Ordner wurde im Informationsspeicher nicht gefunden.

A309DF4A: skipped update, could not get reservations from Exchange
Revo127
User
Beiträge: 1
Registriert: Dienstag 3. August 2021, 10:49

Grüße...

Ich habe derweil das gleiche Problem wie rattlesnake... Ich hänge genau an dem Punkt. Daher die Frage, ob sich hier schon eine Lösung ergeben hat und diese evtl. mit uns geteilt werden kann.

Liebe Grüße
Antworten