Warning: include_once(/var/www/html/pmwiki-2.2.86/cookbook/soap4pmwiki/soap4pmwiki.php): failed to open stream: No such file or directory in /var/www/html/fields/dbp13/local/config.php on line 4

Warning: include_once(): Failed opening '/var/www/html/pmwiki-2.2.86/cookbook/soap4pmwiki/soap4pmwiki.php' for inclusion (include_path='.:/opt/php/lib/php') in /var/www/html/fields/dbp13/local/config.php on line 4

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/fields/dbp13/local/config.php:4) in /var/www/html/pmwiki-2.2.86/pmwiki.php on line 1250
Datenbankpraktikum SS 2013 - MMOG - Spieler Allianzen Und Nachrichten Requestsystem

Requestsystem

René Helmke

Problemstellung

Aktionen wie das Hinzufügen von Nutzern zu einer Allianz oder einer Freundesliste wurden ohne Zustimmung des Nutzers ausgeführt. Konkret konnte der betroffene Nutzer also nicht entscheiden, ob er der Durchführung dieser Aktion zustimmt. Diesen Umstand haben wir als suboptimal eingestuft und beschlossen, dass eine Art Anfragesystem für diverse Aktionen zwischen Nutzern auszuarbeiten ist. Es sollte also möglich sein, dass ein Nutzer Ns(ender) einem Nutzer Nr(ecipient) eine Anfrage auf Durchführung einer vordefinierten Aktion A sendet. Nr sollte dann nur bei Zustimmung der von Ns erstellten Anfrage die hinterlegte Aktion A anstoßen. Dieser Ansatz sollte es ermöglichen, dass nur das aktive Handeln von Nr zu dem von Ns gewünschten Resultat führt.

Planung

Datenbankebene


Abb.1: Relationale Beziehungen zwischen Request und User

Das für Anfragen zuständige relationale Schema beinhaltet 4 Attribute:

  • requestvalue: Die SHA1-Hash Repräsentation (http://celan.informatik.uni-oldenburg.de/kryptos/info/sha1/overview/) eines per Zufall generierten Wertes
  • sender_id: Der Fremdschlüssel zu dem Nutzer, der die konkrete Anfrage erstellt hat.
  • recipient_id: Der Fremdschlüssel zu dem Nutzer, an den die konkrete Anfrage gesendet wird.
  • action: Die Stringrepräsentation einer Aktion, die dem Request Controller bekannt ist.

User können beliebig viele Anfragen erstellen und erhalten. Hierbei ist allerdings zu beachten, dass eine Anfrage immer nur von einem Sender kommen und nur an einen Empfänger adressiert sein kann. Hierdurch ergeben sich zwei 1:N-Beziehungen zwischen User und Request.

Applikationsebene


Abb.2: Erstellung einer neuen Anfrage auf Applikationsebene

Erklärung:

  • Klickt der Sender beispielsweise auf einen Button, der einen Nutzer einer Allianz hinzufügen soll, versendet sein Browser das HTTP-Verb 'post' an den Request Controller. An dieser Browseranfrage hängen zwei Parameter: Nutzername des Empfängers und die auszuführende Aktion (in diesem Fall "alliance_invite").
  • Der Request Controller überprüft nun intern, ob es sich um eine zulässe Anfrange handelt. Ist dieses der Fall, wird ein neuer Request mit den entsprechenden Parametern in die Datenbank eingepflegt. In jedem Fall wird der Nutzer über Erfolg oder Misserfolg der Browseranfrage informiert.
  • Bei Erstellung der neuen Anfrage wird im zugehörigen Requestobjekt eine Funktion aufgerufen, die den einzigartigen SHA1-Wert dieses Requests berechnet und in die Datenbank eingepflegt.
  • Danach werden auf Basis der Hashvalue zwei URLs zum Annehmen und Ablehnen des Requests generiert, die auf eine andere Funktion des Request Controllers verweisen. Diese URLs werden in ein für die vordefinierte Aktion vorbereitetes HTML-Template eingebunden.
  • Im nächsten Schritt wird eine neue Message unseres bereits bestehenden Messagesystemes mit dem HTML-Template im Body erstellt. Als Empfänger dieser Nachricht wird der Empfänger des Requests hinzugefügt.
  • Diese Nachricht erscheint nun in seinem Posteingang und bietet Optionen zum Annehmen und Ablehnen.


Abb.3: Beantworten einer Anfrage auf Applikationsebene

Erklärung:

  • Der Empfänger einer Anfrage hat über zwei Buttons die Möglichkeit, entweder die Anfrage anzunehmen oder abzulehnen. In jedem Fall wird bei Beantwortung das HTTP-Verb 'post' an den Request Controller gesendet. Die Browseranfrage beinhaltet die Requestvalue und eine Antwort (ja oder nein).
  • Der Request Controller überprüft zu allererst intern, ob der Request existiert.
  • Ist dies der Fall, wird überprüft, ob der Nutzer, der diese Browseranfrage abgeschickt hat, berechtigt ist, auf den Request zu antworten.
  • Danach wird der übergebenen Parameter angefragt, der die Antwort auf diese Anfrage enthält. Wird abgelehnt, wird der Request zerstört und der Nutzer über die erfolgreiche Bearbeitung informiert.
  • Wird angenommen, soll die hinterlegte Aktion auf dem Requestobjekt ausgeführt werden.
  • Es wird zuerst entschieden, um welche Aktion es sich konkret handelt.
  • Je nach Fall wird das Objekt, welches für diese Aktion notwendig ist, abgefragt. In unserem Beispiel wäre es die Allianz des Senders dieser Anfrage.
  • Auf diesem Objekt wird nun die gewünschte Aktion ausgeführt.
  • Nach erfolgreicher Ausführung wird der Sender der Anfrage über eine neue, vordefinierte Nachricht informiert.
  • Schließlich zerstört sich das Requestobjekt selbst und ist somit nichtmehr im System vorhanden.

Implementierung

Model

Nach der Erstellung der oben dargestellten Migration (Abb1) mussten die Beziehungen im Request- und User-Model hergestellt werden:

request.rb:

  belongs_to :sender, :class_name => 'User', :foreign_key => 'sender_id'
  belongs_to :recipient, :class_name => 'User', :foreign_key => 'recipient_id'

user.rb:

  has_many :outgoing_requests, :class_name => 'Request', :foreign_key => 'sender_id'
  has_many :incoming_requests, :class_name => 'Request', :foreign_key => 'recipient_id'

Des Weiteren musste Datenintegrität garantiert werden:

request.rb:

  validates_presence_of :sender_id,:recipient_id,:action

Nun war die Aufgabe, dass nach Erstellung des Objektes einmalig ein Hash zur Identifizierung des Requests ausgerechnet werden musste. Natürlich hätte man auch über die ID des Eintrages den Request identifizieren können. Wir haben uns allerdings für einen Hash entschieden, da er für Außenstehende erheblich schwerer zu lesen und zu interpretieren ist. Das Requestsystem soll im Hintergrund laufen und nicht mehr Preis geben, als es muss. Mit diesem Ansatz vertreten wir das umstrittene Sicherheitsprinzip Security through Obscurity (Paper).

SHA1 wurde nicht aus Willkür als Hashfunktion ausgewählt: Im Gegensatz zu MD5 (http://celan.informatik.uni-oldenburg.de/kryptos/info/md5/overview/) ist SHA1 in der Praxis kollisionsresistent und somit besser für diese Aufgabe geeignet. Es soll also der Hashwert eines möglichst zufällig generierten Inputs errechnet werden. Nach langem Überlegen sind wir zu dem Entschluss gekommen, dass sich ein join aus dem aktuellen Timestamp (Time.now) und einer pseudozufälligen Zahl (Random) relativ gut eignet:

  after_create :calculate_requestvalue!
  [...]
  private
    def calculate_requestvalue!()
      self.requestvalue=Digest::SHA1.hexdigest([Time.now, rand].join)
      self.save
    end

SHA1 produziert einen 160 Bit langen Hashwert, welcher normalerweise in Hexadezimal (40 Zeichen) ausgedrückt wird. Deshalb wollen wir hier nicht mit dieser Konvention brechen.

Als nächstes stellte sich das Problem, wie konkrete Aktionen in dem Model hinterlegt sind. Zuerst kam die Idee auf, in einer Hashmap Schlüssel zu hinterlegen, die auf ein Lambda (anonyme Methode) zeigen. So hätten die anderen Gruppen einfach Funktionen schreiben und diese in das Aktionsrepertoire laden können. Das Problem an diesem Ansatz war, Persistenz zu erreichen. Es gibt in Ruby on Rails die Möglichkeit, bestimmte Aktionen bei Programminitialisierung auszuführen. So würden einfach alle gewünschten Funktionen im Initialisierungsprozess neu in die Hashmap geladen werden. Dieses war aber ungünstig, da die Rails Entwicklerkonsole bei einem !reload ebenfalls diese Hashmap leert und nicht die benutzerdefinierten Initialisierungsskripte erneut aufruft. Somit wären viele Fehler und unerwünschte Nebeneffekte während des Entwicklungsprozesses entstanden. Da Lambdas auch nicht serialisierbar sind, musste dieser Ansatz leider komplett verworfen werden. Da uns die Zeit weglief, haben wir uns dann schließlich für ein If-ElseIf Konstrukt innerhalb von allen Methoden entschieden, deren Ausführung von der hinterlegten Aktion abhängig ist.

Konkret existieren in dem Model neben calculate_requestvalue!() nur zwei weitere Methoden, die wir für unsere Implementierung benötigten:

decide_notify(): Diese Methode soll einmalig über den Request Controller während des create-Prozesses aufgerufen werden. Sie dient dazu, den Empfänger des Requests zu benachrichtigen. Als erstes wird der Hashwert des konkreten Requests in ein HTML-Template eingefügt. Dieses sieht wie folgt aus:

<a class="label label-success" data-method="post" href="/requests/reaction/?answer=yes&for=<HASHWERT>" style="color:#FFFFFF;">

  Annehmen

</a> <a class="label label-warning" data-method="post" href="/requests/reaction/?answer=no&for=<HASHWERT>" style="color:#FFFFFF;">

  Ablehnen

</a>

Wie man hier nun erkennen kann, werden zwei Links generiert, die eine Post-Anfrage an die Methode reaction des Request Controllers stellen. Als Parameter werden die Antwort (ja oder nein) und der Hashwert des Requests übergeben.

In einem nächsten Schritt wird entschieden, um welche Aktion es sich hinter dem Request handelt. Je nach Aktion wird nun die Methode system_notify(Prefix,Subject,Body) am Empfänger des Requests mit verschiedenen Parametern aufgerufen. An den Body-Parameter wird schließlich das vorher generierte HTML-Template konkateniert. Das Ergebnis dieser Methode schlägt sich wie folgt im Posteingang des Empfängers nieder:


Abb.4: Empfangen einer Freundesanfrage


Abb.5: Empfangen einer Allianzanfrage

launch_action!():Diese Methode ist für das Ausführen der eigentlichen Aktion zuständig. Sie wird ebenfalls vom Request Controller angestoßen. Zuerst wird durch Fallunterscheidung entschieden, welche Aktion ausgeführt werden soll. Danach wird der gewünschte Eintrag aus der Datenbank abgefragt. In dem Fall einer Allianzeinladung wäre dieses das Objekt, welches die Allianz des Nutzers in der Datenbank repräsentiert. Auf diesem Objekt wird nun die gewünschte Methode, z.B. add_user(user) aufgerufen, der Sender des Requests per system_notify(...) informiert und schließlich wird der Request gelöscht.

    def launch_action!()
      if self.action=="alliance_invite"
        ally=self.sender.alliance
        ally.add_user(recipient) #add user to alliance
        sndr=self.sender
        sndr.system_notify(...,...,..)
        self.destroy #action done
      elsif
        [...]
      end
    end

Controller

Nutzer dürfen den Request Controller nur ansteuern, wenn sie als gültiger Nutzer in Devise authentifizert sind:

  before_filter :authenticate_user!

Im Controller existieren zwei Methoden: create und reaction. Diese sind über das HTTP-Verb Post unter den Adressen /requests und /requests/reaction erreichbar. Nach der Implementierung dieser 2 Methoden ist uns aufgefallen, dass wir den Controller mit weiteren Kontrollstrukturen versehen müssen. So sollte beispielsweise kein Nutzer in eine Allianz eingeladen werden können, der bereits zu einer anderen Allianz gehört. Des Weiteren sollte man auch keine Freundschaftsanfrage an Nutzer schicken können, die bereits in der Freundesliste des Senders existieren. Die beiden Methoden create und reaction akzeptieren nur die Aktionen, die tatsächlich im System eingepflegt sind und beachten die soeben beschrieben Umstände.

create ist für die Erstellung von Requests zuständig. Diese Methode validiert die vom Browser übertragenen Parameter, ermittelt den Empfänger, erstellt bei Validität den Request und pflegt alle nötigen Parameter ein. Danach wird auf dem Requestmodel die oben beschrieben Methode decide_notify() aufgerufen.

reaction verarbeitet Antworten auf existierende Requests. Die Methode bekommt den zu einem Request gehörigen SHA1-Hash vom Browser übermittelt. Des Weiteren wird vom Browser der Parameter 'answer' übermittelt, der entweder 'no' oder 'yes' als Zustand haben kann. Nachdem überprüft wurde, ob der anzusprechende Request existiert und der Nutzer, der eine Browseranfrage an diese Methode gesendet hat, berechtigt ist, auf diesen Request zu antworten, wird je nach Antwort entweder direkt das Requestobjekt zerstört oder aber die zugehörige Aktion auf dem Model ausgeführt.

Nach Abhandlung einer dieser beiden Methoden wird der Nutzer auf die Startseite zurückgeleitet und über eine Notiz über Erfolg oder Misserfolg der entsprechenden Browseranfrage informiert.

View

Aufgrund unserer Modellierung auf Applikationsebene wurde keine konkrete View für Requests gebraucht. Wir mussten lediglich in den Views, in denen das Requestsystem greifen soll, die Zieladressen der Buttons und dessen Parameter auf den Request Controller umleiten. Bei unserer Gruppe war von den Änderungen die View der Allianzbearbeitung zum Einladen von Nutzern und die Freundesliste betroffen. Des Weiteren haben wir die Routen auf die Ursprünglich für diese Aufgaben vorgesehenen Controller entfernt.


Abb.6: Einladedialog in der Allianzbearbeitung

Fazit

Die größte Hürde lag für uns in der Planung auf Applikationsebene. Wir mussten uns ein System ausdenken, welches für andere Gruppen und uns selbst transparent erscheint, jedoch der Komplexität der Probemstellung gerecht wird. In der momentan vorliegenden Version sind nur die Aktionen für das Hinzufügen von Nutzern in eine Allianz oder zu der eigenen Freundesliste hinterlegt. Weitere Aktionen sind ab diesem Moment aber problemlos und einfach hinzuzufügen.


Page last modified on August 23, 2013, at 04:17 PM