appservice

package
v0.9.29 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2021 License: MPL-2.0 Imports: 27 Imported by: 23

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	WebsocketManualStop   = errors.New("the websocket was disconnected manually")
	WebsocketOverridden   = errors.New("a new call to StartWebsocket overrode the previous connection")
	WebsocketUnknownError = errors.New("an unknown error occurred")
)
View Source
var EventChannelSize = 64

EventChannelSize is the size for the Events channel in Appservice instances.

View Source
var OTKChannelSize = 4

Functions

func RandomString

func RandomString(n int) string

RandomString generates a random string of the given length.

func Respond

func Respond(w http.ResponseWriter, data interface{}) error

Respond responds to a HTTP request with a JSON object.

func WriteBlankOK

func WriteBlankOK(w http.ResponseWriter)

WriteBlankOK writes a blank OK message as a reply to a HTTP request.

Types

type AppService

type AppService struct {
	HomeserverDomain string     `yaml:"homeserver_domain"`
	HomeserverURL    string     `yaml:"homeserver_url"`
	RegistrationPath string     `yaml:"registration"`
	Host             HostConfig `yaml:"host"`
	LogConfig        LogConfig  `yaml:"logging"`

	Registration *Registration    `yaml:"-"`
	Log          maulogger.Logger `yaml:"-"`

	Events       chan *event.Event         `yaml:"-"`
	DeviceLists  chan *mautrix.DeviceLists `yaml:"-"`
	OTKCounts    chan *mautrix.OTKCount    `yaml:"-"`
	QueryHandler QueryHandler              `yaml:"-"`
	StateStore   StateStore                `yaml:"-"`

	Router    *mux.Router `yaml:"-"`
	UserAgent string      `yaml:"-"`

	HTTPClient *http.Client

	DefaultHTTPRetries int

	Live  bool
	Ready bool

	StopWebsocket     func(error)
	WebsocketCommands chan WebsocketCommand

	// ProcessID is an identifier sent to the websocket proxy for debugging connections
	ProcessID string
	// contains filtered or unexported fields
}

AppService is the main config for all appservices. It also serves as the appservice instance struct.

func Create

func Create() *AppService

Create a blank appservice instance.

func Load

func Load(path string) (*AppService, error)

Load an appservice config from a file.

func (*AppService) BotClient

func (as *AppService) BotClient() *mautrix.Client

func (*AppService) BotIntent

func (as *AppService) BotIntent() *IntentAPI

func (*AppService) BotMXID

func (as *AppService) BotMXID() id.UserID

func (*AppService) CheckServerToken

func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) (isValid bool)

CheckServerToken checks if the given request originated from the Matrix homeserver.

func (*AppService) Client

func (as *AppService) Client(userID id.UserID) *mautrix.Client

func (*AppService) GetLive added in v0.9.21

func (as *AppService) GetLive(w http.ResponseWriter, r *http.Request)

func (*AppService) GetReady added in v0.9.21

func (as *AppService) GetReady(w http.ResponseWriter, r *http.Request)

func (*AppService) GetRoom

func (as *AppService) GetRoom(w http.ResponseWriter, r *http.Request)

GetRoom handles a /rooms GET call from the homeserver.

func (*AppService) GetUser

func (as *AppService) GetUser(w http.ResponseWriter, r *http.Request)

GetUser handles a /users GET call from the homeserver.

func (*AppService) Init

func (as *AppService) Init() (bool, error)

Init initializes the logger and loads the registration of this appservice.

func (*AppService) Intent

func (as *AppService) Intent(userID id.UserID) *IntentAPI

func (*AppService) NewIntentAPI

func (as *AppService) NewIntentAPI(localpart string) *IntentAPI

func (*AppService) PrepareWebsocket added in v0.9.2

func (as *AppService) PrepareWebsocket()

func (*AppService) PutTransaction

func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request)

PutTransaction handles a /transactions PUT call from the homeserver.

func (*AppService) RequestWebsocket added in v0.9.15

func (as *AppService) RequestWebsocket(ctx context.Context, cmd *WebsocketRequest, response interface{}) error

func (*AppService) Save

func (as *AppService) Save(path string) error

Save saves this config into a file at the given path.

func (*AppService) SendWebsocket added in v0.9.2

func (as *AppService) SendWebsocket(cmd *WebsocketRequest) error

func (*AppService) Start

func (as *AppService) Start()

Start starts the HTTP server that listens for calls from the Matrix homeserver.

func (*AppService) StartWebsocket added in v0.8.1

func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error

func (*AppService) Stop

func (as *AppService) Stop()

func (*AppService) UpdateState

func (as *AppService) UpdateState(evt *event.Event)

func (*AppService) YAML

func (as *AppService) YAML() (string, error)

YAML returns the config in YAML format.

type BasicStateStore

type BasicStateStore struct {
	Registrations map[id.UserID]bool `json:"registrations"`

	Members map[id.RoomID]map[id.UserID]*event.MemberEventContent `json:"memberships"`

	PowerLevels map[id.RoomID]*event.PowerLevelsEventContent `json:"power_levels"`

	*TypingStateStore
	// contains filtered or unexported fields
}

func (*BasicStateStore) GetMember

func (store *BasicStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent

func (*BasicStateStore) GetMembership

func (store *BasicStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership

func (*BasicStateStore) GetPowerLevel

func (store *BasicStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int

func (*BasicStateStore) GetPowerLevelRequirement

func (store *BasicStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int

func (*BasicStateStore) GetPowerLevels

func (store *BasicStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent)

func (*BasicStateStore) GetRoomMembers

func (store *BasicStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent

func (*BasicStateStore) HasPowerLevel

func (store *BasicStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool

func (*BasicStateStore) IsInRoom

func (store *BasicStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool

func (*BasicStateStore) IsInvited

func (store *BasicStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool

func (*BasicStateStore) IsMembership

func (store *BasicStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool

func (*BasicStateStore) IsRegistered

func (store *BasicStateStore) IsRegistered(userID id.UserID) bool

func (*BasicStateStore) MarkRegistered

func (store *BasicStateStore) MarkRegistered(userID id.UserID)

func (*BasicStateStore) SetMember

func (store *BasicStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent)

func (*BasicStateStore) SetMembership

func (store *BasicStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership)

func (*BasicStateStore) SetPowerLevels

func (store *BasicStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent)

func (*BasicStateStore) TryGetMember

func (store *BasicStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, ok bool)

type CloseCommand added in v0.9.9

type CloseCommand struct {
	Code    int                    `json:"-"`
	Command string                 `json:"command"`
	Status  MeowWebsocketCloseCode `json:"status"`
}

func (CloseCommand) Error added in v0.9.9

func (cc CloseCommand) Error() string

type DeviceListHandler added in v0.9.15

type DeviceListHandler func(otk *mautrix.DeviceLists, since string)

type Error

type Error struct {
	HTTPStatus int       `json:"-"`
	ErrorCode  ErrorCode `json:"errcode"`
	Message    string    `json:"error"`
}

Error represents a Matrix protocol error.

func (Error) Write

func (err Error) Write(w http.ResponseWriter)

type ErrorCode

type ErrorCode string

ErrorCode is the machine-readable code in an Error.

const (
	ErrUnknownToken ErrorCode = "M_UNKNOWN_TOKEN"
	ErrBadJSON      ErrorCode = "M_BAD_JSON"
	ErrNotJSON      ErrorCode = "M_NOT_JSON"
	ErrUnknown      ErrorCode = "M_UNKNOWN"
)

Native ErrorCodes

const (
	ErrNoTransactionID ErrorCode = "NET.MAUNIUM.NO_TRANSACTION_ID"
)

Custom ErrorCodes

type ErrorResponse added in v0.8.1

type ErrorResponse struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (*ErrorResponse) Error added in v0.8.1

func (er *ErrorResponse) Error() string

type EventHandler added in v0.5.0

type EventHandler func(evt *event.Event)

type EventListener

type EventListener func(evt *event.Event)

EventListener is a function that receives events.

type EventProcessor

type EventProcessor struct {
	ExecMode ExecMode
	// contains filtered or unexported fields
}

func NewEventProcessor

func NewEventProcessor(as *AppService) *EventProcessor

func (*EventProcessor) Dispatch

func (ep *EventProcessor) Dispatch(evt *event.Event)

func (*EventProcessor) DispatchDeviceList added in v0.9.15

func (ep *EventProcessor) DispatchDeviceList(dl *mautrix.DeviceLists)

func (*EventProcessor) DispatchOTK added in v0.9.15

func (ep *EventProcessor) DispatchOTK(otk *mautrix.OTKCount)

func (*EventProcessor) On

func (ep *EventProcessor) On(evtType event.Type, handler EventHandler)

func (*EventProcessor) OnDeviceList added in v0.9.15

func (ep *EventProcessor) OnDeviceList(handler DeviceListHandler)

func (*EventProcessor) OnOTK added in v0.9.15

func (ep *EventProcessor) OnOTK(handler OTKHandler)

func (*EventProcessor) Start

func (ep *EventProcessor) Start()

func (*EventProcessor) Stop

func (ep *EventProcessor) Stop()

type ExecMode

type ExecMode uint8
const (
	AsyncHandlers ExecMode = iota
	AsyncLoop
	Sync
)

type FileFormatData

type FileFormatData struct {
	Date  string
	Index int
}

type HostConfig

type HostConfig struct {
	Hostname string `yaml:"hostname"`
	Port     uint16 `yaml:"port"`
	TLSKey   string `yaml:"tls_key,omitempty"`
	TLSCert  string `yaml:"tls_cert,omitempty"`
}

HostConfig contains info about how to host the appservice.

func (*HostConfig) Address

func (hc *HostConfig) Address() string

Address gets the whole address of the Appservice.

type IntentAPI

type IntentAPI struct {
	*mautrix.Client

	Localpart string
	UserID    id.UserID

	IsCustomPuppet bool
	// contains filtered or unexported fields
}

func (*IntentAPI) EnsureInvited

func (intent *IntentAPI) EnsureInvited(roomID id.RoomID, userID id.UserID) error

func (*IntentAPI) EnsureJoined

func (intent *IntentAPI) EnsureJoined(roomID id.RoomID) error

func (*IntentAPI) EnsureRegistered

func (intent *IntentAPI) EnsureRegistered() error

func (*IntentAPI) JoinedMembers added in v0.5.8

func (intent *IntentAPI) JoinedMembers(roomID id.RoomID) (resp *mautrix.RespJoinedMembers, err error)

func (*IntentAPI) Member

func (intent *IntentAPI) Member(roomID id.RoomID, userID id.UserID) *event.MemberEventContent

func (*IntentAPI) Members added in v0.5.8

func (intent *IntentAPI) Members(roomID id.RoomID, req ...mautrix.ReqMembers) (resp *mautrix.RespMembers, err error)

func (*IntentAPI) PowerLevels

func (intent *IntentAPI) PowerLevels(roomID id.RoomID) (pl *event.PowerLevelsEventContent, err error)

func (*IntentAPI) RedactEvent

func (intent *IntentAPI) RedactEvent(roomID id.RoomID, eventID id.EventID, req ...mautrix.ReqRedact) (*mautrix.RespSendEvent, error)

func (*IntentAPI) Register

func (intent *IntentAPI) Register() error

func (*IntentAPI) SendImage

func (intent *IntentAPI) SendImage(roomID id.RoomID, body string, url id.ContentURI) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendMassagedMessageEvent

func (intent *IntentAPI) SendMassagedMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendMassagedStateEvent

func (intent *IntentAPI) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendMessageEvent

func (intent *IntentAPI) SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendNotice

func (intent *IntentAPI) SendNotice(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendStateEvent

func (intent *IntentAPI) SendStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendText

func (intent *IntentAPI) SendText(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SendVideo

func (intent *IntentAPI) SendVideo(roomID id.RoomID, body string, url id.ContentURI) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SetAvatarURL

func (intent *IntentAPI) SetAvatarURL(avatarURL id.ContentURI) error

func (*IntentAPI) SetDisplayName

func (intent *IntentAPI) SetDisplayName(displayName string) error

func (*IntentAPI) SetPowerLevel

func (intent *IntentAPI) SetPowerLevel(roomID id.RoomID, userID id.UserID, level int) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SetPowerLevels

func (intent *IntentAPI) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) (resp *mautrix.RespSendEvent, err error)

func (*IntentAPI) SetRoomAvatar

func (intent *IntentAPI) SetRoomAvatar(roomID id.RoomID, avatarURL id.ContentURI) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SetRoomName

func (intent *IntentAPI) SetRoomName(roomID id.RoomID, roomName string) (*mautrix.RespSendEvent, error)

func (*IntentAPI) SetRoomTopic

func (intent *IntentAPI) SetRoomTopic(roomID id.RoomID, topic string) (*mautrix.RespSendEvent, error)

func (*IntentAPI) State added in v0.9.29

func (intent *IntentAPI) State(roomID id.RoomID) (mautrix.RoomStateMap, error)

func (*IntentAPI) StateEvent

func (intent *IntentAPI) StateEvent(roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) error

func (*IntentAPI) UserTyping

func (intent *IntentAPI) UserTyping(roomID id.RoomID, typing bool, timeout int64) (resp *mautrix.RespTyping, err error)

func (*IntentAPI) Whoami

func (intent *IntentAPI) Whoami() (*mautrix.RespWhoami, error)

type LogConfig

type LogConfig struct {
	Directory       string `yaml:"directory"`
	FileNameFormat  string `yaml:"file_name_format"`
	FileDateFormat  string `yaml:"file_date_format"`
	FileMode        uint32 `yaml:"file_mode"`
	TimestampFormat string `yaml:"timestamp_format"`
	RawPrintLevel   string `yaml:"print_level"`
	JSONStdout      bool   `yaml:"print_json"`
	JSONFile        bool   `yaml:"file_json"`
	PrintLevel      int    `yaml:"-"`
}

LogConfig contains configs for the logger.

func CreateLogConfig

func CreateLogConfig() LogConfig

CreateLogConfig creates a basic LogConfig.

func (LogConfig) Configure

func (lc LogConfig) Configure(log maulogger.Logger)

Configure configures a mauLogger instance with the data in this struct.

func (LogConfig) GetFileFormat

func (lc LogConfig) GetFileFormat() maulogger.LoggerFileFormat

GetFileFormat returns a mauLogger-compatible logger file format based on the data in the struct.

func (*LogConfig) MarshalYAML

func (lc *LogConfig) MarshalYAML() (interface{}, error)

func (*LogConfig) UnmarshalYAML

func (lc *LogConfig) UnmarshalYAML(unmarshal func(interface{}) error) error

type MeowWebsocketCloseCode added in v0.9.9

type MeowWebsocketCloseCode string
const (
	MeowServerShuttingDown MeowWebsocketCloseCode = "server_shutting_down"
	MeowConnectionReplaced MeowWebsocketCloseCode = "conn_replaced"
)

func (MeowWebsocketCloseCode) String added in v0.9.9

func (mwcc MeowWebsocketCloseCode) String() string

type Namespace

type Namespace struct {
	Regex     string `yaml:"regex"`
	Exclusive bool   `yaml:"exclusive"`
}

Namespace is a reserved namespace in any area.

type Namespaces

type Namespaces struct {
	UserIDs     []Namespace `yaml:"users,omitempty"`
	RoomAliases []Namespace `yaml:"aliases,omitempty"`
	RoomIDs     []Namespace `yaml:"rooms,omitempty"`
}

Namespaces contains the three areas that appservices can reserve parts of.

func (*Namespaces) RegisterRoomAliases

func (nslist *Namespaces) RegisterRoomAliases(regex *regexp.Regexp, exclusive bool)

RegisterRoomAliases creates an room alias namespace registration.

func (*Namespaces) RegisterRoomIDs

func (nslist *Namespaces) RegisterRoomIDs(regex *regexp.Regexp, exclusive bool)

RegisterRoomIDs creates an room ID namespace registration.

func (*Namespaces) RegisterUserIDs

func (nslist *Namespaces) RegisterUserIDs(regex *regexp.Regexp, exclusive bool)

RegisterUserIDs creates an user ID namespace registration.

type OTKHandler added in v0.9.15

type OTKHandler func(otk *mautrix.OTKCount)

type QueryHandler

type QueryHandler interface {
	QueryAlias(alias string) bool
	QueryUser(userID id.UserID) bool
}

QueryHandler handles room alias and user ID queries from the homeserver.

type QueryHandlerStub

type QueryHandlerStub struct{}

func (*QueryHandlerStub) QueryAlias

func (qh *QueryHandlerStub) QueryAlias(alias string) bool

func (*QueryHandlerStub) QueryUser

func (qh *QueryHandlerStub) QueryUser(userID id.UserID) bool

type Registration

type Registration struct {
	ID              string     `yaml:"id"`
	URL             string     `yaml:"url"`
	AppToken        string     `yaml:"as_token"`
	ServerToken     string     `yaml:"hs_token"`
	SenderLocalpart string     `yaml:"sender_localpart"`
	RateLimited     *bool      `yaml:"rate_limited,omitempty"`
	Namespaces      Namespaces `yaml:"namespaces"`
	EphemeralEvents bool       `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty"`
	Protocols       []string   `yaml:"protocols,omitempty"`
}

Registration contains the data in a Matrix appservice registration. See https://matrix.org/docs/spec/application_service/unstable.html#registration

func CreateRegistration

func CreateRegistration() *Registration

CreateRegistration creates a Registration with random appservice and homeserver tokens.

func LoadRegistration

func LoadRegistration(path string) (*Registration, error)

LoadRegistration loads a YAML file and turns it into a Registration.

func (*Registration) Save

func (reg *Registration) Save(path string) error

Save saves this Registration into a file at the given path.

func (*Registration) YAML

func (reg *Registration) YAML() (string, error)

YAML returns the registration in YAML format.

type StateStore

type StateStore interface {
	IsRegistered(userID id.UserID) bool
	MarkRegistered(userID id.UserID)

	IsTyping(roomID id.RoomID, userID id.UserID) bool
	SetTyping(roomID id.RoomID, userID id.UserID, timeout int64)

	IsInRoom(roomID id.RoomID, userID id.UserID) bool
	IsInvited(roomID id.RoomID, userID id.UserID) bool
	IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool
	GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent
	TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool)
	SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership)
	SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent)

	SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent)
	GetPowerLevels(roomID id.RoomID) *event.PowerLevelsEventContent
	GetPowerLevel(roomID id.RoomID, userID id.UserID) int
	GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int
	HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool
}

func NewBasicStateStore

func NewBasicStateStore() StateStore

type Transaction added in v0.9.15

type Transaction struct {
	Events          []*event.Event                 `json:"events"`
	EphemeralEvents []*event.Event                 `json:"ephemeral,omitempty"`
	DeviceLists     *mautrix.DeviceLists           `json:"device_lists,omitempty"`
	DeviceOTKCount  map[id.UserID]mautrix.OTKCount `json:"device_one_time_keys_count,omitempty"`

	MSC2409EphemeralEvents []*event.Event                 `json:"de.sorunome.msc2409.ephemeral,omitempty"`
	MSC3202DeviceLists     *mautrix.DeviceLists           `json:"org.matrix.msc3202.device_lists,omitempty"`
	MSC3202DeviceOTKCount  map[id.UserID]mautrix.OTKCount `json:"org.matrix.msc3202.device_one_time_keys_count,omitempty"`
}

Transaction contains a list of events.

type TransactionIDCache added in v0.9.22

type TransactionIDCache struct {
	// contains filtered or unexported fields
}

func NewTransactionIDCache added in v0.9.22

func NewTransactionIDCache(size int) *TransactionIDCache

func (*TransactionIDCache) IsProcessed added in v0.9.22

func (txnIDC *TransactionIDCache) IsProcessed(txnID string) bool

func (*TransactionIDCache) MarkProcessed added in v0.9.22

func (txnIDC *TransactionIDCache) MarkProcessed(txnID string)

type TypingStateStore

type TypingStateStore struct {
	// contains filtered or unexported fields
}

func NewTypingStateStore

func NewTypingStateStore() *TypingStateStore

func (*TypingStateStore) IsTyping

func (store *TypingStateStore) IsTyping(roomID id.RoomID, userID id.UserID) bool

func (*TypingStateStore) SetTyping

func (store *TypingStateStore) SetTyping(roomID id.RoomID, userID id.UserID, timeout int64)

type WebsocketCommand added in v0.9.2

type WebsocketCommand struct {
	ReqID   int             `json:"id,omitempty"`
	Command string          `json:"command"`
	Data    json.RawMessage `json:"data"`
}

func (*WebsocketCommand) MakeResponse added in v0.9.15

func (wsc *WebsocketCommand) MakeResponse(data interface{}) *WebsocketRequest

type WebsocketMessage added in v0.8.1

type WebsocketMessage struct {
	WebsocketTransaction
	WebsocketCommand
}

type WebsocketRequest added in v0.9.15

type WebsocketRequest struct {
	ReqID   int         `json:"id,omitempty"`
	Command string      `json:"command"`
	Data    interface{} `json:"data"`

	Deadline time.Duration `json:"-"`
}

type WebsocketTransaction added in v0.9.2

type WebsocketTransaction struct {
	Status string `json:"status"`
	TxnID  string `json:"txn_id"`
	Transaction
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL