Skip to content
Nill edited this page Sep 10, 2021 · 3 revisions

Events in Telefrag are exposed via the Telefrag.Event class, which provides additional functionality beyond native delegate-based events.

Subscribing to Events

Subscribing to events is easy. Here are some examples:

With a separate function

PollingStarted.Subscribe(pollStartedHandler);

void pollStartedHandler(Context c, EventArgs e) { ... }

With a lambda

ChatMessage.Subscribe((c, e) => {
    Log($"{c.From} says {c.Message.TextOrCaption}"); 
});

Keeping a reference to the subscription The Subscribe() method returns an EventSubscription object which can be held onto to manage the event subscription if needed.

var sub = UpdateReceived.Subscribe(UpdateHandler);
/* later on ... */
sub.Unsubscribe(); 

Event Handlers

All event handlers are of the delegate type EventHandler<TArgs> where TArgs is the subtype of EventArgs used for the event in question. This delegate has the following signature:

public delegate EventHandler<TArgs>(Context c, TArgs e);

The two items passed in to every event handler are:

  • The Context object associated with the Event. This contains references to the sender (Bot or Client), chat, update, message, security parameters, the Activity currently in scope (if any), etc.
  • The e parameter (which will be EventArgs or a subclass of it) feeds you any detailed event-specific information or references to event-specific objects that may be helpful.

Hooks, and Signalling that an Event was Handled

Event handlers have access to set the Handled property of the Context object to true in order to indicate to the object raising the event that you have in some way processed the event.

Normally when an Event is raised, all subscribers to the event are notified (in the order they subscribed), however some types of events (known canonically as Hooks) are raised such that if one event handler "handles" an event (by setting Handled to true), event processing stops and no further handlers are invoked, regardless of active subscriptions.

For this reason it is possible to provide a parameter (subscribePreference) in the call to Subscribe() to indicate whether you would like to be inserted at the front or end of the list of subscriptions depending on whether you want to handle an event or hook before any other existing subscribers or afterwards.

Unsubscribing from Events

Unsubscribing from events can be accomplished a variety of ways:

  • Call Unsubscribe() on the EventSubscription provided when you subscribed.
  • Call Unsubscribe() on the Event and pass in the EventSubscription
  • Call Event.UnsubscribeAll() to clear all subscriptions.
  • If you have control over the scope object (IEventScope) used in your subscriptions, you can have it return false when IsActive is queried (though there is no guarantee how soon Telefrag will check this value).

Subscription Scope and Lifetime

When you subscribe to an Event, the Subscribe method takes an optional parameter called scope. Every EventSubscription has a specific IEventScope object that determines the lifetime and validity of the event subscription.

By default, the value of Context.Sender is used if available, which is typically the Bot, Client, etc. associated with the event, however neither the Context.Sender nor the object owning the Event implement IEventScope (or are not available), then a dummy object is used and the subscription will always be valid and active unless explicitly unsubscribed.

The benefit to scoping event subscriptions to scope owners is that when a Bot or Client is removed, or a module or plugin is unloaded -- corresponding event handlers can be automatically unhooked and resources can be released.

Any object can implement IEventScope and be the scope for a subscription; all it needs is a bool IsActive property to tell Telefrag whether or not events hooked up under the auspices of that object are still valid or not. Telefrag periodically goes through and removes all event subscriptions scoped to objects no longer active.

Diagnostic Features

Creating and Raising your own custom Events

Events can be added to any class that implements IEventOwner, which simply requires a single Guid identifier.

  1. Create one or more public properties of type Event on your class
  2. In your classes constructor, call Event.Setup(this);

Example:

public class Aldi
{
    public Event StoreOpened { get; private set; }
    public Event StoreClosed { get; private set; }

	public Aldi() {
		Event.Setup(this);
    }
}

Event.Setup will look for any Event property on your class and automatically instantiate it so that it's immediately available and will never be uninitialized.

If you want to use a specific EventArgs subclass, simply provide that as the type argument to the Event<TArgs> type:

public class Walmart
{
    public Event<LayawayEventArgs> LayawayItemLost { get; private set; }
}

To raise your event, simply call Raise(), passing in an instance of EventArgs.

StoreOpen.Raise(new());

The Event will automatically capture the current Context and add a context frame describing the event being handled. If you want to check the value of Handled, you can look for a return value:

var handled = StopOpen.Raise(new());  
// this will return true if any of the EventHandlers set
// Context.Handled = true

If you want to stop invoking event handlers after the first one sets Handled to true, then use the stopOnHandled parameter of Raise():

var handled = StopOpen.Raise(new(), stopOnHandled: true);
// If an EventHandler sets Handled to true, no further event
// handling will occur and the function will return true,
// otherwise other subscribed handlers will be invoked  

Caller Security

By default, the Raise() function will throw an UnauthorizedAccessException if it is called from a type other than the one it is defined on.
You can configure the scope of what types are allowed to raise an Event by using one or both of these attributes:

  • [AllowedCaller] can be used to whitelist a specific type so that it is allowed to call Raise.
  • [EventSecurity] defines the general code security scope of who may call Raise. See the table below for more details. Each access level permits all access levels below it.
CodeRelationship value Description
Private (Default) The event can only be raised by the Type it is defined on
Protected The event can only be raised by the Type it is defined on and any derived types
InternalChild The event can also be raised by types that are nested children of the type the Event is defined on
InternalParent The event can also be raised by the containing class that
Public Anyone can raise this event from any type in any assembly
Example:
[AllowedCaller(typeof(Salesman))]
[EventSecurity(CodeRelationship.Protected)]
public Event<UsedCarSalesEventArgs> UsedCarSales { get; private set; }
// This Event can be called by
//  * Methods defined on this type
//  * Methods defined on subtypes (due to the EventSecurity attribute)
//  * Methods defined on type 'Salesman' (due to the AllowedCaller attribute)

Clone this wiki locally