From efacc0dc42dba3d7d197e511e19821ad9649a3c4 Mon Sep 17 00:00:00 2001 From: "1447560092@qq.com" <1447560092@qq.com> Date: Thu, 2 Jan 2025 11:18:28 +0800 Subject: [PATCH] fix:add code --- .gitignore | 26 + Makefile | 69 ++ README.md | 606 +++++++++++++ api/v1/file_controller.go | 47 ++ api/v1/group_controller.go | 51 ++ api/v1/message_controller.go | 31 + api/v1/user_controller.go | 81 ++ assets/screenshot/go-chat-panel.jpeg | Bin 0 -> 164682 bytes assets/screenshot/screen-share.png | Bin 0 -> 1156750 bytes assets/screenshot/video-chat.png | Bin 0 -> 1629246 bytes chat.sql | 84 ++ cmd/main.go | 41 + config.toml | 22 + config/toml_config.go | 66 ++ deployments/docker/Dockerfile | 7 + deployments/docker/docker-compose.yml | 85 ++ deployments/docker/nginx.conf | 57 ++ .../docker/single_node/docker-compose.yml | 33 + deployments/docker/single_node/nginx.conf | 47 ++ go.mod | 26 + go.sum | 797 ++++++++++++++++++ internal/dao/pool/mysql_tool.go | 44 + internal/kafka/consumer.go | 48 ++ internal/kafka/producer.go | 37 + internal/model/group.go | 17 + internal/model/group_member.go | 17 + internal/model/message.go | 20 + internal/model/user.go | 25 + internal/model/user_friend.go | 15 + internal/router/router.go | 77 ++ internal/router/socket.go | 39 + internal/server/client.go | 68 ++ internal/server/server.go | 194 +++++ internal/service/group_service.go | 107 +++ internal/service/message_service.go | 117 +++ internal/service/user_service.go | 152 ++++ pkg/common/constant/constant.go | 23 + pkg/common/request/friend_request.go | 6 + pkg/common/request/message_request.go | 7 + pkg/common/response/group_response.go | 11 + pkg/common/response/message_response.go | 16 + pkg/common/response/response_msg.go | 32 + pkg/common/response/search_response.go | 8 + pkg/common/util/file_suffix.go | 140 +++ pkg/errors/error.go | 15 + pkg/global/log/logger.go | 90 ++ pkg/protocol/message.pb.go | 163 ++++ pkg/protocol/message.proto | 16 + test/kafka_test.go | 52 ++ web/static/file/.gitkeep | 0 50 files changed, 3732 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 api/v1/file_controller.go create mode 100644 api/v1/group_controller.go create mode 100644 api/v1/message_controller.go create mode 100644 api/v1/user_controller.go create mode 100644 assets/screenshot/go-chat-panel.jpeg create mode 100644 assets/screenshot/screen-share.png create mode 100644 assets/screenshot/video-chat.png create mode 100644 chat.sql create mode 100644 cmd/main.go create mode 100644 config.toml create mode 100644 config/toml_config.go create mode 100644 deployments/docker/Dockerfile create mode 100644 deployments/docker/docker-compose.yml create mode 100644 deployments/docker/nginx.conf create mode 100644 deployments/docker/single_node/docker-compose.yml create mode 100644 deployments/docker/single_node/nginx.conf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/dao/pool/mysql_tool.go create mode 100644 internal/kafka/consumer.go create mode 100644 internal/kafka/producer.go create mode 100644 internal/model/group.go create mode 100644 internal/model/group_member.go create mode 100644 internal/model/message.go create mode 100644 internal/model/user.go create mode 100644 internal/model/user_friend.go create mode 100644 internal/router/router.go create mode 100644 internal/router/socket.go create mode 100644 internal/server/client.go create mode 100644 internal/server/server.go create mode 100644 internal/service/group_service.go create mode 100644 internal/service/message_service.go create mode 100644 internal/service/user_service.go create mode 100644 pkg/common/constant/constant.go create mode 100644 pkg/common/request/friend_request.go create mode 100644 pkg/common/request/message_request.go create mode 100644 pkg/common/response/group_response.go create mode 100644 pkg/common/response/message_response.go create mode 100644 pkg/common/response/response_msg.go create mode 100644 pkg/common/response/search_response.go create mode 100644 pkg/common/util/file_suffix.go create mode 100644 pkg/errors/error.go create mode 100644 pkg/global/log/logger.go create mode 100644 pkg/protocol/message.pb.go create mode 100644 pkg/protocol/message.proto create mode 100644 test/kafka_test.go create mode 100644 web/static/file/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726556b --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +bin/* +release/ +test/tmp/ +test/logs/ +logs +doc/blueprint/ +*.iml +*.swp +*.log +coverage.* +y.output + +.DS_Store +.vscode/ +.idea +_tools/ + +TestMarkdown2Html.html + +#test log +test/logs + +#upload file +static/img/* +web/static/file/* +!web/static/file/.gitkeep diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0cd7ecd --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +BINARY="chat" + +# colors compatible setting +CRED:=$(shell tput setaf 1 2>/dev/null) +CGREEN:=$(shell tput setaf 2 2>/dev/null) +CYELLOW:=$(shell tput setaf 3 2>/dev/null) +CEND:=$(shell tput sgr0 2>/dev/null) + +.PHONY: all +all: | fmt build + +# Code format +.PHONY: fmt +fmt: + @echo "$(CGREEN)Run gofmt ...$(CEND)" + @echo "gofmt -l -s -w ..." + @ret=0 && for d in $$(go list -f '{{.Dir}}' ./... | grep -v /vendor/); do \ + gofmt -l -s -w $$d/*.go || ret=$$? ; \ + done ; exit $$ret + +# build +.PHONY: build-darwin +build-darwin: fmt + @echo "$(CGREEN)Building for darwin ...$(CEND)" + @mkdir -p bin + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/${BINARY} cmd/main.go + @echo "$(CGREEN)Build Success!$(CEND)" + +# build +.PHONY: build +build: + @echo "$(CGREEN)Building for linux ...$(CEND)" + @mkdir -p bin + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/${BINARY} cmd/main.go + @echo "$(CGREEN)Build Success!$(CEND)" + +# build win_build +.PHONY: win_build +win_build: + @echo "$(CGREEN)Building for linux ...$(CEND)" + @mkdir -p bin + go build -o ./bin/main.exe ./cmd/main.go + @echo "$(CGREEN)Build Success!$(CEND)" + +# install +install: build + @echo "$(CGREEN)Install ...$(CEND)" + go install ./... + @echo "$(CGREEN)Install Success!$(CEND)" + +# clean +.PHONY: clean +clean: + @echo "$(CGREEN)Cleanup ...$(CEND)" + go clean + @rm -f bin/${BINARY} + @echo "rm -f bin/${BINARY}" + @for GOOS in darwin linux windows; do \ + for GOARCH in 386 amd64; do \ + rm -f bin/${BINARY}.$${GOOS}-$${GOARCH} ;\ + done ;\ + done + rm -f ${BINARY} coverage.* test/tmp/* + find . -name "*.log" -delete + +# protoc build +.PHONY: protoc +protoc: + protoc --gogo_out=. pkg/protocol/*.proto diff --git a/README.md b/README.md new file mode 100644 index 0000000..abba052 --- /dev/null +++ b/README.md @@ -0,0 +1,606 @@ +[TOC] + + +## go-chat +使用Go基于WebSocket的通讯聊天软件。 + +### 功能列表: +* 登录注册 +* 修改头像 +* 群聊天 +* 群好友列表 +* 单人聊天 +* 添加好友 +* 添加群组 +* 文本消息 +* 剪切板图片 +* 图片消息 +* 文件发送 +* 语音消息 +* 视频消息 +* 屏幕共享(基于图片) +* 视频通话(基于WebRTC的p2p视频通话) +* 分布式部署(通过kafka全局消息队列,统一消息传递,可以水平扩展系统) + +## 后端 +[代码仓库](https://github.com/kone-net/go-chat) +go中协程是非常轻量级的。在每个client接入的时候,为每一个client开启一个协程,能够在单机实现更大的并发。同时go的channel,可以非常完美的解耦client接入和消息的转发等操作。 + +通过go-chat,可以掌握channel的和Select的配合使用,ORM框架的使用,web框架Gin的使用,配置管理,日志操作,还包括proto buffer协议的使用,等一些列项目中常用的技术。 + + +### 后端技术和框架 +* web框架Gin +* 长连接WebSocket +* 日志框架Uber的zap +* 配置管理viper +* ORM框架gorm +* 通讯协议Google的proto buffer +* makefile 的编写 +* 数据库MySQL +* 图片文件二进制操作 + +## 前端 +基于react,UI和基本组件是使用ant design。可以很方便搭建前端界面。 + +界面选择单页框架可以更加方便写聊天界面,比如像消息提醒,可以在一个界面接受到消息进行提醒,不会因为换页面或者查看其他内容影响消息接受。 +[前端代码仓库](https://github.com/kone-net/go-chat-web): +https://github.com/kone-net/go-chat-web + + +### 前端技术和框架 +* React +* Redux状态管理 +* AntDesign +* proto buffer的使用 +* WebSocket +* 剪切板的文件读取和操作 +* 聊天框发送文字显示底部 +* FileReader对文件操作 +* ArrayBuffer,Blob,Uint8Array之间的转换 +* 获取摄像头视频(mediaDevices) +* 获取麦克风音频(Recorder) +* 获取屏幕共享(mediaDevices) +* WebRTC的p2p视频通话 + + +### 截图 +* 语音,文字,图片,视频消息 + + +* 视频通话 + + +* 屏幕共享 + + +## 消息协议 +### protocol buffer协议 +```go +syntax = "proto3"; +package protocol; + +message Message { + string avatar = 1; //头像 + string fromUsername = 2; // 发送消息用户的用户名 + string from = 3; // 发送消息用户uuid + string to = 4; // 发送给对端用户的uuid + string content = 5; // 文本消息内容 + int32 contentType = 6; // 消息内容类型:1.文字 2.普通文件 3.图片 4.音频 5.视频 6.语音聊天 7.视频聊天 + string type = 7; // 如果是心跳消息,该内容为heatbeat + int32 messageType = 8; // 消息类型,1.单聊 2.群聊 + string url = 9; // 图片,视频,语音的路径 + string fileSuffix = 10; // 文件后缀,如果通过二进制头不能解析文件后缀,使用该后缀 + bytes file = 11; // 如果是图片,文件,视频等的二进制 +} +``` +### 选择协议原因 +通过消息体能看出,消息大部分都是字符串或者整型类型。通过json就可以进行传输。那为什么要选择google的protocol buffer进行传输呢? +* 一方面传输快 +是因为protobuf序列化后的大小是json的10分之一,是xml格式的20分之一,但是性能却是它们的5~100倍. +* 另一方面支持二进制 +当我们看到消息体最后一个字段,是定义的bytes,二进制类型。 +我们在传输图片,文件,视频等内容的时候,可以将文件直接通过socket消息进行传输。 +当然我们也可以将文件先通过http接口上传后,然后返回路径,再通过socket消息进行传输。但是这样只能实现固定大小文件的传输,如果我们是语音电话,或者视频电话的时候,就不能传输流。 + +## 快速运行 +### 运行go程序 +go环境的基本配置 +... + +拉取后端代码 +```shell +git clone https://github.com/kone-net/go-chat +``` + +进入目录 +```shell +cd go-chat +``` + +拉取程序所需依赖 +```shell +go mod download +``` + +MySQL创建数据库 +```mysql +CREATE DATABASE chat; +``` + +修改数据库配置文件 +```shell +vim config.toml + +[mysql] +host = "127.0.0.1" +name = "chat" +password = "root1234" +port = 3306 +table_prefix = "" +user = "root" + +修改用户名user,密码password等信息。 +``` + +创建表 +```shell +将chat.sql里面的sql语句复制到控制台创建对应的表。 +``` + +在user表里面添加初始化用户 +```shell +手动添加用户。 +``` + +运行程序 +```shell +go run cmd/main.go +``` + +### 运行前端代码 +配置React基本环境,比如nodejs +... + +拉取代码 +```shell +git clone https://github.com/kone-net/go-chat-web +``` + +进入目录 +```shell +cd go-chat-web +``` + +安装前端基本依赖 +```shell +npm install +``` + +如果后端地址或者端口号需要修改 +放在服务器运行时一定需要修改后端地址 +```shell +修改src/chat/common/param/Params.jsx里面的IP_PORT +``` + +运行前端代码默认启动端口是3000 +```shell +npm start +``` + +访问前端入口 +``` +http://127.0.0.1:3000/login +``` + +### 分布式部署 +* 拉取代码 +将代码拉取到服务器,运行make build构建后端代码。 +* 构建后端服务镜像 +进入目录deployments/docker +通过目录下的Dockerfile构建镜像 +``` +docker build -t konenet/gochat:1.0 . +``` +* 部署服务 +需要部署nginx进行反向代理,mysql保存数据,1个或者多个后端服务。 +* 在config.toml中配置分布式消息队列 +将msgChannelType中的channelType修改为kafka,就为分布式消息队列。需要填写消息队列对应的地址和topic +```toml +appName = "chat_room" + +[mysql] +host = "mysql8" +name = "go-chat-message" +password = "thepswdforroot" +port = 3306 +tablePrefix = "" +user = "root" + +[log] +level = "debug" +path = "logs/chat.log" + +[staticPath] +filePath = "web/static/file/" + +[msgChannelType] +channelType = "kafka" + +kafkaHosts = "kafka:9092" +kafkaTopic = "go-chat-message" +``` +* 启动服务 +通过deployments/docker下的docker-compose.yml进行启动。 +``` +docker-compose up -d +``` +* 注意:分布式部署后,上传的文件视频等,可能会因为负载到不同的机器上,导致文件找不到的情况,所以需要一个在线或者分布式文件服务器。 + +## 代码结构 +``` +├── Makefile 代码编译,打包,结构化等操作 +├── README.md +├── api controller类,对外的接口,如添加好友,查找好友等。所有http请求的入口 +│ └── v1 +├── assets +│ └── screenshot 系统使用到的资源,markdown用到的截图文件 +├── bin 打包的二进制文件 +├── chat.sql 整个项目的SQL +├── cmd +│ └── main.go main函数入口,程序启动 +├── config +│ └── toml_config.go 系统全局的配置文件配置类 +├── config.toml 配置文件 +├── deployments +│ └── docker docker构建镜像,docker-compose.yml等文件 +├── go.mod +├── go.sum +├── internal +│ ├── dao 数据库 +│ ├── kafka kafka消费者和生产者 +│ ├── model 数据库模型,和表一一对应 +│ ├── router gin和controller类进行绑定 +│ ├── server WebSocket中消息的接受和转发的主要逻辑 +│ └── service 调用的服务类 +├── logs +├── pkg +│ ├── common 常量,工具类 +│ ├── errors 封装的异常类 +│ ├── global 封装的日志类,使用时不会出现第三方的包依赖 +│ └── protocol protoc buffer自动生成的文件,定义的protoc buffer字段 +├── test +│ └── kafka_test.go +└── web + └── static 上传的文件等 +``` + +## Makefile +### 程序打包 +在根目录下执行make命令 +mac +```bash +make build-darwin + +实际执行命令是Makefile下的 +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/chat cmd/main.go +``` + +linux +```bash +make build + +实际执行命令是Makefile下的 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/chat cmd/main.go +``` + +### 后端proto文件生成 +如果修改了message.proto,就需要重新编译生成对应的go文件。 +在根目录下执行 +```bash +make proto + +实际执行命令是Makefile下的 +protoc --gogo_out=. protocol/*.proto +``` + +如果本地没有安装proto文件,需要先进行安装,不然找不到protoc命令。 +使用gogoprotobuf + +安装protobuf库文件 +```bash +go get github.com/golang/protobuf/proto +``` + +安装protoc-gen-gogo +```bash +go get github.com/gogo/protobuf/protoc-gen-gogo +``` + +安装gogoprotobuf库文件 +```bash +go get github.com/gogo/protobuf/proto +``` + +在根目录测试: +```bash +protoc --gogo_out=. protocol/*.proto +``` + +### 前端proto文件生成 +前端需要安装protoc buffer库 + +```bash +npm install protobufjs +``` + +生成protoc的js文件到目录 +```bash +npx pbjs -t json-module -w commonjs -o src/chat/proto/proto.js src/chat/proto/*.proto + +src/chat/proto/proto.js 是生成的文件的目录路径及其文件名称 +src/chat/proto/*.proto 是自己写的字段等 +``` + +## 代码说明 +### WebSocket +该文件是gin的路由映射,将普通的get请求,Upgrader为socket连接 +```go +// router/router.go +func NewRouter() *gin.Engine { + gin.SetMode(gin.ReleaseMode) + + server := gin.Default() + server.Use(Cors()) + server.Use(Recovery) + + socket := RunSocekt + + group := server.Group("") + { + ... + + group.GET("/socket.io", socket) + } + return server +} +``` + +这部分对请求进行升级为WebSocket。 +* c.Query("user")用户登录后,会获取用户的uuid,在连接到socket时会携带用户的uuid。 +* 通过该uuid和connection进行关联。 +* server.MyServer.Register <- client将每个client实例,通过channel进行传达,Server实例的Select会对该实例进行保存。 +* client.Read(),client.Write()通过协程让每个client对自己独有的channel进行消息的读取和发送 +```go +// router/socket.go +var upGrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func RunSocekt(c *gin.Context) { + user := c.Query("user") + if user == "" { + return + } + log.Info("newUser", zap.String("newUser", user)) + ws, err := upGrader.Upgrade(c.Writer, c.Request, nil) //升级协议为WebSocket + if err != nil { + return + } + + client := &server.Client{ + Name: user, + Conn: ws, + Send: make(chan []byte), + } + + server.MyServer.Register <- client + go client.Read() + go client.Write() +} +``` + +这是Server的三个channel, +* 用户登录后,将用户和connection绑定存放在map中 +* 用户离线后,将用户从map中剔除 +* 所有消息,每个client将消息获取后放入该channel中,统一在这里进行消息的分发 +* 分发消息: + * 如果是单聊,直接根据前端发送的uuid找到对应的client进行发送。 + * 如果是群聊,需要在数据库查询该群所有的成员,在根据uuid找到对应的client进行发送。 + * 如果消息为普通文本消息,可以直接转发到对应的客户端。 + * 如果消息为视频文件,普通文件,照片之类的,需要先将文件进行保存,然后返回文件名称,前端根据名称调用接口获取文件。 +```go +// server/server.go +func (s *Server) Start() { + log.Info("start server", log.Any("start server", "start server...")) + for { + select { + case conn := <-s.Register: + log.Info("login", log.Any("login", "new user login in"+conn.Name)) + s.Clients[conn.Name] = conn + msg := &protocol.Message{ + From: "System", + To: conn.Name, + Content: "welcome!", + } + protoMsg, _ := proto.Marshal(msg) + conn.Send <- protoMsg + + case conn := <-s.Ungister: + log.Info("loginout", log.Any("loginout", conn.Name)) + if _, ok := s.Clients[conn.Name]; ok { + close(conn.Send) + delete(s.Clients, conn.Name) + } + + case message := <-s.Broadcast: + msg := &protocol.Message{} + proto.Unmarshal(message, msg) + ... + ... + } + } +} + +``` + +### 剪切板图片上传 +上传剪切板的文件,首先我们需要获取剪切板文件。 +如以下代码: +* 通过在聊天输入框,绑定粘贴命令,获取粘贴板的内容。 +* 我们只获取文件信息,其他文字信息过滤掉。 +* 先获取文件的blob格式。 +* 通过FileReader,将blob转换为ArrayBuffer格式。 +* 将ArrayBuffer内容转换为Uint8Array二进制,放在消息体。 +* 通过protobuf将消息转换成对应协议。 +* 通过socket进行传输。 +* 最后,将本地的图片追加到聊天框里面。 +```javascript +bindParse = () => { + document.getElementById("messageArea").addEventListener("paste", (e) => { + var data = e.clipboardData + if (!data.items) { + return; + } + var items = data.items + + if (null == items || items.length <= 0) { + return; + } + + let item = items[0] + if (item.kind !== 'file') { + return; + } + let blob = item.getAsFile() + + let reader = new FileReader() + reader.readAsArrayBuffer(blob) + + reader.onload = ((e) => { + let imgData = e.target.result + + // 上传文件必须将ArrayBuffer转换为Uint8Array + let data = { + fromUsername: localStorage.username, + from: this.state.fromUser, + to: this.state.toUser, + messageType: this.state.messageType, + content: this.state.value, + contentType: 3, + file: new Uint8Array(imgData) + } + let message = protobuf.lookup("protocol.Message") + const messagePB = message.create(data) + socket.send(message.encode(messagePB).finish()) + + this.appendImgToPanel(imgData) + }) + + }, false) + } +``` + +### 上传录制的视频 +上传语音同原理 +* 获取视频调用权限。 +* 通过mediaDevices获取视频流,或者音频流,或者屏幕分享的视频流。 +* this.recorder.start(1000)设定每秒返回一段流。 +* 通过MediaRecorder将流转换为二进制,存入dataChunks数组中。 +* 松开按钮后,将dataChunks中的数据合成一段二进制。 +* 通过FileReader,将blob转换为ArrayBuffer格式。 +* 将ArrayBuffer内容转换为Uint8Array二进制,放在消息体。 +* 通过protobuf将消息转换成对应协议。 +* 通过socket进行传输。 +* 最后,将本地的视频,音频追加到聊天框里面。 + +**特别注意: 获取视频,音频,屏幕分享调用权限,必须是https协议或者是localhost,127.0.0.1 本地IP地址,所有本地测试可以开启几个浏览器,或者分别用这两个本地IP进行2tab测试** +```javascript +/** + * 当按下按钮时录制视频 + */ + dataChunks = []; + recorder = null; + startVideoRecord = (e) => { + navigator.getUserMedia = navigator.getUserMedia || + navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + navigator.msGetUserMedia; //获取媒体对象(这里指摄像头) + + let preview = document.getElementById("preview"); + this.setState({ + isRecord: true + }) + + navigator.mediaDevices + .getUserMedia({ + audio: true, + video: true, + }).then((stream) => { + preview.srcObject = stream; + this.recorder = new MediaRecorder(stream); + + this.recorder.ondataavailable = (event) => { + let data = event.data; + this.dataChunks.push(data); + }; + this.recorder.start(1000); + }); + } + + /** + * 松开按钮发送视频到服务器 + * @param {事件} e + */ + stopVideoRecord = (e) => { + this.setState({ + isRecord: false + }) + + let recordedBlob = new Blob(this.dataChunks, { type: "video/webm" }); + + let reader = new FileReader() + reader.readAsArrayBuffer(recordedBlob) + + reader.onload = ((e) => { + let fileData = e.target.result + + // 上传文件必须将ArrayBuffer转换为Uint8Array + let data = { + fromUsername: localStorage.username, + from: this.state.fromUser, + to: this.state.toUser, + messageType: this.state.messageType, + content: this.state.value, + contentType: 3, + file: new Uint8Array(fileData) + } + let message = protobuf.lookup("protocol.Message") + const messagePB = message.create(data) + socket.send(message.encode(messagePB).finish()) + }) + + this.setState({ + comments: [ + ...this.state.comments, + { + author: localStorage.username, + avatar: this.state.user.avatar, + content:
, + datetime: moment().fromNow(), + }, + ], + }, () => { + this.scrollToBottom() + }) + if (this.recorder) { + this.recorder.stop() + this.recorder = null + } + let preview = document.getElementById("preview"); + preview.srcObject.getTracks().forEach((track) => track.stop()); + this.dataChunks = [] + } +``` diff --git a/api/v1/file_controller.go b/api/v1/file_controller.go new file mode 100644 index 0000000..f0644b7 --- /dev/null +++ b/api/v1/file_controller.go @@ -0,0 +1,47 @@ +package v1 + +import ( + "io/ioutil" + "net/http" + "strings" + + "chat-room/config" + "chat-room/internal/service" + "chat-room/pkg/common/response" + "chat-room/pkg/global/log" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// 前端通过文件名称获取文件流,显示文件 +func GetFile(c *gin.Context) { + fileName := c.Param("fileName") + log.Logger.Info(fileName) + data, _ := ioutil.ReadFile(config.GetConfig().StaticPath.FilePath + fileName) + c.Writer.Write(data) +} + +// 上传头像等文件 +func SaveFile(c *gin.Context) { + namePreffix := uuid.New().String() + + userUuid := c.PostForm("uuid") + + file, _ := c.FormFile("file") + fileName := file.Filename + index := strings.LastIndex(fileName, ".") + suffix := fileName[index:] + + newFileName := namePreffix + suffix + + log.Logger.Info("file", log.Any("file name", config.GetConfig().StaticPath.FilePath+newFileName)) + log.Logger.Info("userUuid", log.Any("userUuid name", userUuid)) + + c.SaveUploadedFile(file, config.GetConfig().StaticPath.FilePath+newFileName) + err := service.UserService.ModifyUserAvatar(newFileName, userUuid) + if err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + } + c.JSON(http.StatusOK, response.SuccessMsg(newFileName)) +} diff --git a/api/v1/group_controller.go b/api/v1/group_controller.go new file mode 100644 index 0000000..ced0cc3 --- /dev/null +++ b/api/v1/group_controller.go @@ -0,0 +1,51 @@ +package v1 + +import ( + "chat-room/internal/model" + "chat-room/internal/service" + "chat-room/pkg/common/response" + "net/http" + + "github.com/gin-gonic/gin" +) + +// 获取分组列表 +func GetGroup(c *gin.Context) { + uuid := c.Param("uuid") + groups, err := service.GroupService.GetGroups(uuid) + if err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessMsg(groups)) +} + +// 保存分组列表 +func SaveGroup(c *gin.Context) { + uuid := c.Param("uuid") + var group model.Group + c.ShouldBindJSON(&group) + + service.GroupService.SaveGroup(uuid, group) + c.JSON(http.StatusOK, response.SuccessMsg(nil)) +} + +// 加入组别 +func JoinGroup(c *gin.Context) { + userUuid := c.Param("userUuid") + groupUuid := c.Param("groupUuid") + err := service.GroupService.JoinGroup(groupUuid, userUuid) + if err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + c.JSON(http.StatusOK, response.SuccessMsg(nil)) +} + +// 获取组内成员信息 +func GetGroupUsers(c *gin.Context) { + groupUuid := c.Param("uuid") + users := service.GroupService.GetUserIdByGroupUuid(groupUuid) + c.JSON(http.StatusOK, response.SuccessMsg(users)) +} diff --git a/api/v1/message_controller.go b/api/v1/message_controller.go new file mode 100644 index 0000000..11c1073 --- /dev/null +++ b/api/v1/message_controller.go @@ -0,0 +1,31 @@ +package v1 + +import ( + "net/http" + + "chat-room/internal/service" + "chat-room/pkg/common/request" + "chat-room/pkg/common/response" + "chat-room/pkg/global/log" + + "github.com/gin-gonic/gin" +) + +// 获取消息列表 +func GetMessage(c *gin.Context) { + log.Logger.Info(c.Query("uuid")) + var messageRequest request.MessageRequest + err := c.BindQuery(&messageRequest) + if nil != err { + log.Logger.Error("bindQueryError", log.Any("bindQueryError", err)) + } + log.Logger.Info("messageRequest params: ", log.Any("messageRequest", messageRequest)) + + messages, err := service.MessageService.GetMessages(messageRequest) + if err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessMsg(messages)) +} diff --git a/api/v1/user_controller.go b/api/v1/user_controller.go new file mode 100644 index 0000000..1f67768 --- /dev/null +++ b/api/v1/user_controller.go @@ -0,0 +1,81 @@ +package v1 + +import ( + "net/http" + + "chat-room/internal/model" + "chat-room/internal/service" + "chat-room/pkg/common/request" + "chat-room/pkg/common/response" + "chat-room/pkg/global/log" + + "github.com/gin-gonic/gin" +) + +func Login(c *gin.Context) { + var user model.User + c.ShouldBindJSON(&user) + log.Logger.Debug("user", log.Any("user", user)) + + if service.UserService.Login(&user) { + c.JSON(http.StatusOK, response.SuccessMsg(user)) + return + } + + c.JSON(http.StatusOK, response.FailMsg("Login failed")) +} + +func Register(c *gin.Context) { + var user model.User + c.ShouldBindJSON(&user) + err := service.UserService.Register(&user) + if err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessMsg(user)) +} + +func ModifyUserInfo(c *gin.Context) { + var user model.User + c.ShouldBindJSON(&user) + log.Logger.Debug("user", log.Any("user", user)) + if err := service.UserService.ModifyUserInfo(&user); err != nil { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessMsg(nil)) +} + +func GetUserDetails(c *gin.Context) { + uuid := c.Param("uuid") + + c.JSON(http.StatusOK, response.SuccessMsg(service.UserService.GetUserDetails(uuid))) +} + +// 通过用户名获取用户信息 +func GetUserOrGroupByName(c *gin.Context) { + name := c.Query("name") + + c.JSON(http.StatusOK, response.SuccessMsg(service.UserService.GetUserOrGroupByName(name))) +} + +func GetUserList(c *gin.Context) { + uuid := c.Query("uuid") + c.JSON(http.StatusOK, response.SuccessMsg(service.UserService.GetUserList(uuid))) +} + +func AddFriend(c *gin.Context) { + var userFriendRequest request.FriendRequest + c.ShouldBindJSON(&userFriendRequest) + + err := service.UserService.AddFriend(&userFriendRequest) + if nil != err { + c.JSON(http.StatusOK, response.FailMsg(err.Error())) + return + } + + c.JSON(http.StatusOK, response.SuccessMsg(nil)) +} diff --git a/assets/screenshot/go-chat-panel.jpeg b/assets/screenshot/go-chat-panel.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8948d2da372953d61fe83fa63a697e53d1c04519 GIT binary patch literal 164682 zcmeFa2S5}{mp0tP07K3>4GIW?7|3}*l7J*7C{ZMXE=AvOJA? zdX(p!H<^DazpX&LprDYgFs=x%$g61SnaQ)>Vx{6&&jp`nl`xbkMA`Flp7Mtkv=xz+ph~|=M3r6D e`dz0x|+rrXxtuF?LwL#3m&Q=zl*we0J pHiB7H+^|}bjEUKW!7UB zJ$GlGd_H*rx=^qvv{<*Kvedh5ygaw!x^lQ0wnn}7aGh_xd_#WY9pVaN4(X1>Zbol0 zZ{=-^Y`5&_?2PX^?H=w$qL@+n`x5(|2Zjd=hdyWs`oYo3qZ*6`<}=m_i*>bdxA=+$ zz_?#L8{AlAGY^N4uL1yVF947j|Aw!><^cURCcx34e+yrdf1-cG;9oA_Fa(e%4*&)@ z^3qKJc!)dm;sT{S0O;U?o;-jjEc`v-u6#K>Y=nir5+Kql95hMBVz+Sk5pog$j)t+= z!zwKH=qYZjFoMG|yT9T3FS)6n;YKoH5<;SvKdD9B{CfWnKfhsqT*5aQe?H?R7Zw)z za>qZMu}uIq5#cFPd@$%ZfJY4iQ-iQ=02C)P0S?gMj&IKZ;eqi92q8qoB&4_u6;uEo z2n@!<2NMwB;}8}o2zMR8rzW5|c20qiR@VY@+>K7?R(uu_*ZIPAT=#JC-=$Iy!?W~mu2M@l~u2*Yg$^{+B-U5cfI-0KQK5n{PEMs z)bz~k-2B4g(lT;$YkOyR54C^rMJ^Bk{!Og!lKmtXHBK%(e0(rI RE^wt0^v#bzFr)UIW?yx;Zhm=N^w+(_&HudDg9Yog>Tur*fJYJ;n* zMyJ&2PEYZ@ezQGY^9JC{j{NiR?3L!+=V1Tt;|9DWbR3{g|CH6eiLE^>5K96yD(f7| z?xBycz)VP7*#x|20wckPcSNuFJ&RmV5Eg*JQIn5JxAiH%W%+*7%`<&^-?QX=zv~y{ zUq$h);{2)=zZ%F_1@lW+{8F30dXQh8=~w9ST_^s+75_tug8yX9TyAiH7LPs`za9l~ zrvotXC&N!Lw)a0YJjDyk!vY*a@Fw_!XK@ht>uP%}-~`*YG-&wabRCGMGMx_SUmPI9 zh49y%Sy*7S2NOl#Z}^W<+~euD8fp&}uCm*}DbZ5B&S-ldJIah*KL+H($zTN6tS##1 zNC2%oR7uxd ~MKYBi{${vc A?5UTpx0kQ-?01xE7*^mlCv9G;s1DlRiNmh3EHy1qSm;F%Au?13fC>s z+EL^7YU2GDeJaruQXW@_5R_8UY)R_QV{}h^mVBR}(sYrneD?=T_yu+x#LOxa;kOP} zl<5qoc~>od(kl>b-ydl;gm!FWxl3#2CoG^6x!1l&=}~A-c4aB~rkaoYr;)Su)Z(tD zui3+lfp+!y5ckBF{O6oLaqKAv8wM&JJ(CHT4se3WBMvmP9c!ZT=E~om)VB!}?%8;r z>i&RTJ+$X>Nob(*Ob?5FhB$-S8RJn+Go+D*KxbpX$6V{VTjL-$+ls&ru88_!8ChP5 z=T%i>gO6yMhBVSsv^z1-`MAaPNX!ik7g`tD8Egc+=&dsj=Ao3n$ zSacN*hB yYH&2 zFGs1lqe?lyvKGbS8VO%^`OkBg|4%A^$UU^*uydU>BE5|zG^*UATn(?r)o_jGl+r@g zo6gJ?BREkYPcBM&YBw%#$&FvMz0OdahK%;q7N_m?){^W4K5&H#YNh3c??ekrNvJdT zW{U2e>{knxA jA#O$xZ>zOtmQ=QQgkF-F@8iB{5VJPta17?%$Qq^yF;`x%t|(GN zFk9o_sACx|z-honl~oycP5OFu&)vrIu#g(-?r=os`{9bW55^2G9QR pi29Qu-53$XR>g!Tv<=yozZZ_nR$n!ANsAExC-+t{0}Mvm<$$hf$hgPuJdAn z%8LC3^Q_FOO!VuCJz5;jGD2^atZPFWXCJoUVp!n7T@@J~1>MxS4qxjzI(`hNpO?ZH zRQ+b)VdJM!K1ZoofLOn96z Qtn9X6RE5A zmAPB81NA}>X)b0Tt`|-Fk+=8HA#XP~($^^#H`Gsm>Ng6ANjZ*EDmX3`xL6trU^cSz zCn*GQ{fo;-m&d5MNlKQ9cl}E&PH(TMI)36#nfH^&Y|#UU$A@VP&?>0#LKOdsnceY< z55<-VLYr=U0{5i$+5=VG45`f;U* P{P{9^!hCf-`|}=pnE4hYJ)a-WLQecoi*;I zX&sRb&+fAeYG54TB{-GsmEL;Uxaf?*GuIj9w!OuiK}pN=iKkVv&<-f2OP#$Vdu*tK z;hYEEW{Oh05817Ucp7erx6I-vC+?&TMOZ%V^l;ZPe^;ta)Z~%^5$GW!l9+FJ(7#=) z0b>~1h`E_CqNF64lyF`=u$5!1=+y@_Jw@`3ofxypH(iG-7RU5rqHl9gZgb9hnA1N* zga&5ZKa09kTR9#fUW5g128NSge;Ik->~u!gX6=~clSAjstn?`XN(kyaJM1cgXZ67Q z$<$Cv*P}_d tj*_-B?(X7#GmBw zesS#n0hE57 zz@*BE&M>q%b5!er>GQPQYeyM$QCy$%@iC%XC2df^5era=E(M=LpNL9K9`t+ydGjht z(pHK6AOXUOe)}0(u=tq5xY l#H$gStVkkvrbhg$%}I`);~Iy$yN1uO`5~; zjw~tyeI~#aU=Nl0*L8IQ3CJ^Xb)=JTA4$FK;Na@_6o@e$k>=ajI93r*K$mYFYHcla z_VnY>E`sp<=i#~oo=05T)q@p|51u`{k?psT;d5m=l5e1_ww9`}@Nx2WdBCcd;Eg~A zqBE-Q kxN1xSs h3!(o92 &eHK#9{NM0VrUz3?f0UzLxfnvxfXg=rJ|dlGP8%J1NM-QiSbt2N5U z<(Y|=uJev@#EE+)+yZQcv;H9S0@ooLeLpw00xtujk)h#XkEvHxXY 9#_ zzgo`_u0ZGLH$#4Uo=t7noBCefeSGw2qxzg2pF`Qxn y}r%> zZXH60Hi~kKK5lXvHpNk4viVeAHEVpZv;YfqMrR_R^8!*cEhFRu?`4K=);>&NPT@X0 ziFZ?qL;#2J??|_1ZY^Pfcy25(o&R$j{4~2d6Qx)ui3Q{`_i3~-{Bi#fyD8=Hi^;Dt z$^TVP{<-Y{gRoi4UM%pX*HeyMDoRRh9)Yk5!Zx|E08$@{J~jeHAHZi-zqC8>^XzIU zO0gWakHbl? 7cmhktE$)K~yj2&~``7=~(%5)bV9#k~RI@|pdU6J>cvVI}bG9L Sa2;+7P1SL5o=&;w8mT@Z|ZWolz^r1(i>`*wpYwp!mR%tGZ z kE;&OV##GHszDI&PrZOe9IBz&73B ze3U9`{*iEIDg-TelqGUI2em) 4%f?R3Y16cDZ=v$tCkvZ|NoJRo1{y+dYV;eAuD?Qb@iFILMhAkC>YHq`i9y>2f!i z!?#--y2zrLgel3@7J1P_BGnoic9K(?OvM`iR$uC28oKT%4Xu1g mSJd1&XfARGcrB<4jz~xj#>ZA+ foGQ{7ovjoM**l~uyw{Ulvl1yn> zJnd2Ov-hCJSk@>w|G2981J(JLuib8Ml2N&;el{ -?{{ONt)g|MkMC0!T9kVJt1?|S^BAomka1zkC~n$e^V^5Wc>sP@|dnf2_F)n z36Q*ZWoUCw#6MLQFln~fjhGitD{iyLzrf &IA10+CNRmvJc0u|bZ-*2aqV4ixPMdtv(aSS%|;|APO*EK zRQFDOaB2Ab(pV)S#pUGmh~m9-;k=^A;b{0|hEeS8$u*BFP}V?0_ZqLzcNoQY<0c_Y zsg-B9f>$gdBR8dbP&^QHx)yF0#ZqtslQ3eqR1wSzZ$2#tQ7f|73H0XRA8_hd9;_gx z&E|qX)^6w)-LhhBKy7X9`DYKW#r$RK{ux>PivPL&_rI!a0`m?p2QnkY+k>DgwTO(a zjcvnBhV>Pu_|tgKBkx1;nK~~=qHHnD+BjPw2^QFw#Tg4bS9aASk5t2COx|riWOEha zB0eu(CFEo16QJgG13elcbLy?fMU&E`If-tAMgaqF1LO8>)fpGGP-bV(0X=R#y8l}& z5Z8wV$nua|m{2%6<0TGa?i}(c2T`I`(F>TMOIV=E77KJ`P@?7QaxlRIhcmct3mVgi zcms#R77cNiR?c8xx^E9p1^R}PC7>)xq%tc%LFYt+-|CZVV`Qvwvw&FOoa#Ta3;zdh z;hBhnF9wcY0W84Pj1!B0Fr_&zA}wja+kk~jTPBYrLWDaL9dR%`ZpMHJE}edx!;q2A z8678NJ4K7uesKg5-LVa=gXW;L2HNwU?$3hXrSALSjHIiu%kQB`=vL sZA|%e*7i^>A-WhI&G>3P zaXB}CrkqUGhr`uXfA>w3_x*sGyGLmaLJEkCJ-xv?z4=|~QQg}dgF3BAqqWS;(~Z83 zcJNyxjGaB<;u+)vy9&Xn>Dsl{T~*a!ATfH?ZSxho-mE(TTr@)xr#_&lJ$(eUZH4p^ z1s1rFAVFUHm`{z%DzY^!qJx=@?d+^!V@Ot5LFhwk*2Xy_PgJdc{YJlWreezVrM?{g z mH2P6|{sHBJS)-3bqOpnE zCzfll$y2JExrp^tic~~V>yRsZ^Rs775*>j}c~up5?To3mr_8s9Jg`6(bjAFs)j*C0 zvHIB$6SHs`8~3D(N^|1gJ811kb=tXOl`%4Do*#_6Z>q;Ij(1O7jGn!1PURbgVGIj{ zSCw};i`wBJPcT>PWis-nW1A3}t;i$F`iR+nz>KM71{w3-pC_&@ON8c%@{`mj2_Ky{ zFI=grVouVcuLciCqFm?^+q9wnUECXt0* Ymto1(?reANIV9 zqYb=_%nMyd%LdrUz2_O|RpfT-=sNZCB=cL2=ITX(oq*jd!_E!I%uRwCj2#y}6=`c# zo~hStF--f5O&UX29~weDCc{yMBUVPcp^kn9`^{}sg`saan3BPEGpGEo*+S^|Qd(WY z#@SG|Sz<|#KPfs2_D4L=pc~;yw=+4Ik_UN=X5 x<5N^ L=4V!4D0|aQXLDK1JJrt}@+!bpR<|#wiVE z*d*+o{z2CFkthGrUv@eF2S7<+81i9g_?=j9uAC!dP$Z)VNA664T*dWBfWLOcNW+c& z^SzuNs$<;zr1`S0{?Ui}IPaapDsKky#nK7mPS-*o&~XS5J4xkNq^E<2^H7^4G2(lS z7x^pvKV1g--|Z`ndZI8CG7$qtw*wm1mbOnvI;|~8n(-A$?w*D0?-c3%@k%5pcJQAR zIZ*NlW(WJ^GiA8OlWR2MyV;&IC{otYdb#r0>Ee&<&1%gwO4@?7A$j?&7U6xKmo 0tN{F35kq?eKBpk%X1gZX@5r9h~?ma}W3dBlETn#z-423Arv=SQ|l-tDZC zt> %pFWNlisRx_b7l-`x#@0toGqChaKAm?sFnYm;$-)by}C%D zV$z8x^}CG;{kpRzLN7$wm31ozE2~_@KW=4)o0A2?5Hp&Gr_(-+PpB7GF%EN!Y|-y< z0!533z(Iy%Uw(FjdlFv@|5*doK`~q(Xvg`}io_oll@#s#*zA8=gwSA!T0pkyAm$DN zVW_~&8%Vmt;J)MN32T*E{#dwwYew+mLIG1N_1zf0t=GSuF6=c@`}U|HCaFz?wLL$d zq9*TF+u6nh56)f=R7Kq`((2Uef86)%GWT0|Qn|j*w$8z~;4J%l(~yoha3DS6S%WAl zGoR;SGr0Kq6!!)fSrQ2)cN!Xc^b|h%c!IV;)Wmerj5bZ1%-8H<$FW BIP}zWX0QGpI)L^bPE;`Xnp7y$b(S+V3!u&7G?b24wgJ*5U=XtYTj>k z;e0%%kG>mazI#xGq$u&o`Q&TkM;=cwvNSZJX2}ccJ-yR>Bt5((c_?%DDXQq{XLAOt z26In%mKGpE(i&%yLEsWobm@_@I=g=? zlfNS+|5iY->KbEfTy!u6DxLIsgs@@&$!)>%CWy(N0kZ+~w&V_8y-@?h@HRQ)RLQN2 z!AxT=V!a#3JgPssQv|`2yv@so*YL+BQ!rGSOf{X)m2yY5drh;WlVIC}@?982*D{4q z*g55nN2(jb3nv%!o|u1J3##iWUd)bMmv#N=>;JaIsI*RTnw_@ApCM}+PGhE;w`62O zV=`ui`l(gK8K1A2;=ckU9 z*4XBWfL1*JFD#%x%zp;|pc(#h877@b4+ MPcu$oxKZ5`IFrh)9XLZzc||mLORQ^& z^rQG=yQk$P))LL{duNCY) !&c`)SMWfAh+hOQS-dj(Q zE-`1U{BkR9Ic1!YIx#Ypj>BQ9Z^(n21DO#+p;Ll3f`q;G{fc{5fp3o|s2Kb3W`w7q zTaKbD9La@@?>ci+%cS71^FsZ_z&j;_l;Ma8`vz{7iQW7wOxc}8;D-+r;PogB{gz*- zeUL?ay@+hI6N}DUTi!+iw05!FIknwP5 >^dqZc>jV^JhLw1TL--iF<*nM0sf zQmOP}!W!SAd#-E1a37pe#$IGM;p(S0OA5TQ=M*Z^UL_A!PhZtIcHUUV`7Y%E$>x?+ z!BCVbWlO2Y(Olo5#v~=fC~Bekz&uj0NxQO*>@J13dfttwZ88d36>@LUO_$ZgXP-?` z8dv)AmMpzrKE^C}KWN&0XN3{nNeVJ!$&(d55uBKEo!y8kf6oqQ+5EgF=Xg}yAmSS} z328dAoQ8+qSaaE7mkN+bU>%HqY~d!Q)PBb~<{ndg*ox^ w_MP4aIM+_m=%nvBHJ(1DqS-}Uh3}9E>$O@Tf0(I zYIr8Dr3m&hqrdGED!KIyZuBR4s_p!=u&`MaHRO|$-)EbxV9i(ELDi3mFw~ggVWYj( zxPst*b6Szn0G|(Ot7`U7! #fgS3YP42*kTvoy>nE%qYe5P==1@@ zh}#?m5%z@4@WtY;u&L&AHS3cE>|(WC!pF$)+KkyA97N9ICbCv~LX+VLZH+QyaiuSR zK=FBUwYq}2QMTdZqBo3-UK4qn2*ur>oV65f^SwP4TaBQd9PH`90-ua`!)xTo`kmgd zJ&h|;qaVzDJ#jAN*a_+r*S+t?)KpuB@qg}xEZVkZj%c?+NibkVWM_ww>F)j0Ygu+8 z0k6}>(Gw=ePTDLklSo_}{TNMPU=bgUjxWG`fL9rY1oA*T;yBQ!dc$u~Us_&o8M05g zQc)TR71Hc~MOOGJusx~ybUuiyqwxlu#&-rc{jti97@ZMZf3REjG>3e>h`dJZb)t29 zeziV#JgeM?YG?eYt`)OK)0CkZz+f<~DQcpvu>u}>x{k8&%BxrQ6--amiWlN2!SeLE zKGsf8sY1J?y%!<@oZHU`*KRo+z`Q$9T_Z(AvT8a^7{4mhupXe%KJ%uUvQA0``68ZK zEWQ>9kt~7|1fLD=pI`{I&Gk*uMG%CCHP9?4Z)(sF4m2XgACd7qet4euXhee(UqDH# z1Hx@lG($P!(~?1mFh|n{w6ZX^$~vsH$uXLm5wD&pxfWpmG;)4={(8qru85Fz?-6yf z=R~hLGlznxcLZReyEMN3W4r^LebZw19`eh)b;j*Qln=ZB=g+))Oc@HdA2ZiG%EbsT zW|mfktY@&3^t>68aS0&qOtPgm4D0c+b=NQLdDoY5ce)ym;th*`>I P~+3wZDMKH{|pS zM*Y`d%pVL|Ifq)E82{jKH2;&Gqh~`UY%a+XP0c$T1E7kZOk&^Wvmn^vf8qQMN_uyq z)PAjw)z|0lQ~;{GS6hgPs2lcKbUy~^XzWBfHU5&rjQduO&SiU6j9H_id3k2}#4S-M z8(Jt;MDI-g677>&bqZ4ckOV%Q8T{N0AAW)TqobEDfeKS+$gcXr4Z8>52zc7*dfy=tyD5S5M9ztH)-+ZM zGP5u^hw_2l9TRQ|=8|J L6mwb#lD9O6B>wPBuX{(_k2x?fF<)tzO5jXJ((Dzn;4{>w5NMF`TK##l^i>(~&*%P^UUodtM7B zwXi*!F7#ON1GxX50>*1|kTuB(Uh*29@hm;D`T%^$F-lj8l%05BqP`muLP>z1y9pns z-}UlkTpgW?2$V*2d{|yN3NGU^ON@pJHeG4Ni)4b=n;n|#-w78KiTfbrf11foc^AEd zTSQ(%d7)>p{E@KPhxKhuharJAISg;e^tcrzUFij}8mTIRD{NGbb8xcYmduxpcSQH= z)Ur@4+;z#4T;^iS>%P-81*+$`%Y<^SJ$g zidvRjM+qdBF%`Skbf6@;&}>t?1Yd{g7GjZ9r}sry%hjFUSDS`$afq#sZmxBl>o{l5 z^$FfA*J$O)9_Sl(2Bgeg#GCd)!_P-BKXBnl@tq}ZIf})ti*8l)yH0eVd2^Zua ~;%ETjl! z+|I(-kx8;V@{UGr>iv1nWO0yof|cRXEcZ6eX$7vDdqHkTjKdED4N%&Xu$W-tdK4VV zcu`YZt$ySq?9{!Bnac~Io#)Q}=jjC>y(Od*Q*UH&)wn zN2MF)x8W6;O)HHd6VURKD5F!Mo|FvBF(Q4P93P!{QoY|va6f7vIe%_gqNM6nqn$?4 z1nrZshb8pS-sJ>ry^6p6aGN7(ePlOv$~ >mPZ66FRz29+$4J`Q1!S`(E z!`Tv~7@hZ6wBeHBoj|U~zN`v356iIP(Bh;QlU}x#dyhtognf9IdI6b>C-AgPKJ=Xq z@42=~T(dTax_pQQCfUi(w!4rRH-$)K>l7BArPsR#K;#M3l N6OiLMZcltQE;W>2Psp{ 44 zYzT06gRp=j6IZ<%;zW*HAxcBDq4xS_o%Pl9BD(9TFcX_P!?s51%nnV)aL=O}{g}7; zX?~;l&;7+D^w<3)cfgVdjH)lnK5W;T%C!=X6AkLze$)yLklh-(DU$yuxaIF-l{p#Z zNf#pDQWD=NwW@|wwHi_uy9*NIXFv8xy!Z0FbpNt}wTjm1rHqr5t1AeHaW@ajaB8bd zM7SlV)m&UhiKk)e+iSe792~t(ELxf5rO>vbWGv7StG!}euGgQ(6iCu+;OxBR6KtVO zr=Rejp*#MF$G=ve`0}>LjRZ{EHjTM=wr0|yFN(;jUak^1=o5YN8Dn)Ul7ZZXEUFcc zh?nbjG<62!#iJUSb lIhJmQI{xl@M@WEcIuM7XkU5iV!7dcO=RO#AX(SxmkL^qi^IIo`})lejTIZK zJK6h buyo~O^ak&rm`Y2v*>yfzbrE|+qKZ-Azx`e?U&fMZSTNSp^+Hlf1- 8KH$VI*is%nEZ}}n#xD9O)kdJ!XPK_c}kIyc bliL(ho^Q<1A<^L*DvJzhDj z3*R2PDXsn|dj;|R(Rx& wNg#L>i ?!#1 |jls$qE)G`RtKETTO6CV0z5yH}m>7JoEa7 ze4P^G3TvZ;m379?md=`V $Z-!`>=e<5lty?J@%;@{5mHP_rIUXu95f#kDCG zGGpe#p=|hOcB#vw$k?oL_&n-TsQ=Yyz2iGhcA2JeW& QH$xxFurOQBz7kZE8&;bNk8zMpet3;zzym%Z_t`B*X1c z8X3hrU&<*fyv_M(#d@OIJeBy*K?5dpsV^W)=9N&89FZLsz^^-AqbDS|Ugl4~L)!Fe zi<`NHbD8Vny~msGS2kT?R0a31kLk0S3{0lzKW~T~fz~fb+)yUfZ7W|*s$E%J%UZt> z!E(wiUmy1$GF}6uKw8<6dz6nz_)1t%d4~#UHWaT%-Y4`Wq=*#$NTC`};aj#3enr%v z*PHRkkk;o?y^xoE=Evc!ewf!-U|V+;=XcD3)34FP19EIgikRsM%B6;rh_w&nOGfqO zhTe(!)>p4RQtX&Ki%)ool9L4(`I9-BpDXdN_ *1$5`htrZ zLZ7y*Rs+%l^R*9c^nCa=ePhcGMFLIX6rUFlqAP3AuiIKriL){f#KxW=0`*jL>>P+H zTk?zh%) M&>j6STkR-ZE4|w~wO`IOm8yaFVa)i2j+*Qxl|l}7 zoKg>3G3w6Q?YSGw74oJX*)XB0DA-!Ecku&((!m}7qYK$pS-jel-g04?G}8r@ixmt9 z#h4?fO_`plaGYyscqYVCdledfpycm)=C(++kcrmBc(TD;@fY2FXif^;glKm7iLp=9 z`b8(Zx}DcT>2xo=6QDIj^n~r~#_Wvo_g}J8AacGId;R7PX}y+ktede{D9*3Ju7LtE zpEun@{kb?JC1+H_$4l5VmPxO+ssWhvS(1llHdgFLOl>ebV#tr~H?Z3cF|c5PHqy|V z!4kpZV^=-b2>bSWPEXz}Qr9W;2pf4Fj40`aQ`$*shkCM$x3ZAejM%3smy0F8C>e_- zqEcm{wALXAR5)Zp`4}PBv!*9TW;(s!Q&b>NSTe-pXZm-_S2|T9OY+imge}8}Vw} z_5alC{+(+6E6@K=O6@ m`0sqZzpKcqDH%{Ltu2*>A27S?E;~B6yLN20bq}`b zb)%eLr@mJ eU${=Fx5yTKT17hEToF~(E~VO0L`fHTYCy!`PNySo7vQFI *yFf|$anG!;@Dq^Q9N`|1=$nik-qn!TW{kI}Yv4!f1 zVV7G^ww|24=~z_KQi3|6+!-MQ@j!x`%4+2gG-s`Iy65ntuf;x$tvGai>>Dy_QpE%J zaf2SbK9cRs7u|MXj7 y&E6Yn3_j{m0R@`I)4_qqMQu#NtK zhwk6N?0--3 M|Q#G4FFQQR^y$i3A z3~pBfU1}sQUJn$U3V1pYXqbP@NyFg5dcxV(B<=MCDNwJW=}A6?cUwp*FNZ?g?#v4F zHR2iA-L%m~|2r$T)?hbl(sd&EQW2ci|5j5;xY_1~^$&Uyb|0Z{YSY ~oW70No7}q_=w%_}_TKFVvZc>b z&`l~yo@gb+BhvwGca$QceYbM_#BF&*Wvt84)ii^!wW}8T<} EtX#C6VfrT95S$lQ;vEa)gaeZ!!lzuB6otkUj@xp${YF9zQT@7u za>r29t$L{onR>TQkE==wat6|9e`s_Q^dgNpfniq}-B9vlPu125iV0SOHw!fopi-KA z8qN;a+2_4~vaDNkC0F>E0f=IHMpj`}m9Ww13{@!S@Zv`e39ijkL4yVdnaZEW6oVY; z=c>op-Y{#I7KtuQP`GGz#0aLdrC+@OU2l8`GZ}t7On=3_D`zn4at4WlGM#}<-u*yF zROgiW%?V1E1|HN0)`IO~xeHwJr*hP61>KIfrR$H2a(R%UTO*gQAv@yS5C^ab-Q0;! zmzFFIN%Z_CZ0+QF3CBi#R@Z8oH^v*}aMC7miLXXYCp^AaWn>|-D)))13$5voN%wJr zt_C> z_YH+=ccE(L`Iiew$~C9M zGiW|-e6%^r8t1hb2`3E{w@FU?eDy+YW7!mNX5?6sv48eWHOCunDL2pq{~vqr0oByH zrw<1~Q2|4j4$>tQkzRu$O{6#JDxFZINehX9^bP__S9 qPZk|~S5cMh zeF-T1N-tBIgthAC3ud54mTqeyrrW_cxb+key3K{tzE*@v5OM-?5z!j!rFLslbGrZu zcP6Gop&b)(gyT*%5`R%O8xeZaVhSv7kjS_B(aO2ATjDn@PiDt=CFNrkD@dUeN8K8C z8y*2?gx)}~EY6scOFrdwBO>+OS%|0_i(H(U@f)h!jUoSlD@HUz)$iH$8g4iLCY=n! zEsKb2sX5CjbYt#s){2}2S1iX~e= o*UG?1 &d-`SXWjpWqW3>%fPy3Y1Zz}6g(W_i_KltWRW!oiG%ER7;qi~h{`dW{{+{=M z;;%g_{xb8$Mz;Qj;E(Zlm4Bn6j;E1NOb8IL rr_K(E)@31)`?%g{;({-|dk9`ie=>%!_WZSz;8X;6uaYr%qy z cX9BlOvuX@@n)0*1NXQd}=S%HpMQxZcaHSL~#0yNd zj%jn5borxb7cuEseO49aq105*->ctcX=ZMmn^3CdFHo|l*7*L~7HbS`=Vf0e;YH`Y zGNGKi;is*{hzfeV)b`A+Qo>0;V64-|OR7l1_>Q(o3pO&W)t-+{-uX*6C~jn`kxM1v zTilR3%&(zTuOV_HVOJpDol1JL!`pR}OUuuA@Ma^2L8|hx6u~0QrNGOQad55lX(<@x z_QJl((;?;A>|@1CfSa_&*^kC6jYi51ldI2OjB(~*KVQ$>(tMDHi@AXqeyHSxG>yk+ z d*LKuN}YCU)k zt&64ms2`{Eie=v>+`+RCZ_NVXRFPAh>0_Zc7sT?h1fnYdZ0S8Xnb}Fx*Tg)$mz}21 zm+|^(Uj%on^_0+kRf;7yJkoYL=X;8!43~gFX4InFA|Fb-u6E}}z7bk{KVh=!X!wI+ zO_dR_(x<3zfb7}yfF7OQbO$VMNQMEw0Mie?Q4RNXf(?p@KgSBbRi{P`r%SeHWSL{P zoMVZd?nNi^gp(>V<%3MwIJO0lb$e}@i?G^rS=$Nltue}(jiK+%#!~N2VSNry`X1up zyp&}Z-A8u7-VMnxGNCOmqR|3KmSeABxRoQmZ8{>@`iy&g*s#w&;=sOkPSN~+tNw}= z_h5Yj>vGarsoY3&`5}aeqwk=p!;zd8S?m7R*Cjl~in>$927A187|%sWgIXq9W&*~7 z>AwV!?KM#wdnCy^<=bpI@Yz{?dS&TJ!60`dw*9rvaqL5Tttxj@9I)Ar$f81|8)Fzo z8fm=OZW?S5O4VFl8rMzysD~nf3fg_|aYTI>9Y@cH2ln$8KQ{;Zbp*+8ZU0vlzng(r z_hklttdm~z`~MxwuK#I^u>X_i`)j+yzjkTECV2ja=?(lh#VbuOFEXL V9=e(ha^Z*lqPb01pjyKRxkg$+E)W>TUj24X%*|FT{v=`Cu zqg_SmUf<~MmfkT1Cs|Zu5xvJ=)0U++yqg=w0<*o9*I(Fhl?|WgLUJdk&ahAyXh6d6 zC$Q1Yzj(F$2g-@wf55$QlaWyYf^a&No(|$w^X=r^Ey`iTC-`&N`LuJ~&|7qm_dPt< z(v>z16w7jyAGpf09&z45xq3vzwxVp)8bd!S5)th1Vo)bmGVNSddp}F>Xw8HL310yF z!T5xe3$`fpDH}fuZ-G`desHBd(S&<3dv+nx^gfA=HG`aH#>1lSYko(ETqFryUmg*3 zyj6ed?#0l6bm_!P7lPGV6Fv)lstSJ99 ec7j z7FM4(&6qwXM|PD%_>22U{HCGZ6ndjI&R}jbl->hEjK+S@&376o_|aCtsH3*67e!Tn zEu>%U@btDdBkIDjx=o3>u54vxxzSAyRO2Kut%<33t9@Mm47U9QmZFzpogl)%Ts2a( zU0$d8rfj`H%o6;-*r7gqvx#Y#w@QU@Y4?Y)@kMl53+pk(x-<^L6-J0SDtXWSar1~p zC++1u>enlr+n+a1)9nhLXN#97K5|qeq0)S7vc0v#^Sq)ubQmXiQN8ex;zcNE)NX&c z#uvL1xc7HJJbw@V{il8YJ~-&tVCi423I58T@NX(Tew_^Q$M(NawA^2o1Y8$?A(Q?> zG5R@{3jB>EUfk96sY 6I2kQzC~D?mM3Eds?-Rfo;40@Et}zK2rbE0op2Tyakfy?(nE7B_Nq LZ?Z)%?>lJ?m?@JpnMmkt%7ejWOds> zsIh`*kNHR^0{?4@75ruqzSftE5w<{vKn}hhP{(NL4KdS%O**9g!yfcZrBh03F|APb zV5dunUU5*D !C^n=kF3!%cSjza>Mw|_kGxdd>^MnOH_p+Q5<=3;0L5q~#=ey|~C49Q;T zJdsr8OcdJza-~(g8ke$1t=WDkrV;3wXn9xxHL46bSM!ogI{|Z-7L_Egghh;{mP~z{ zX79US@V bk~QuT3AfUxG;$wtvR1|N8+3oS-E}!e646 zTx=NFN1lywe-GR*VbLeQx857GU$)(b_eyB_1YSAuW ;`$`7PDNc?i+1#@7-DeCg9I?!o#NO2>7nrBz(Hx&5G*6 zTW~sDJ1Xf6r4TK7y(ztN&+v;qRp>09wFGB}iX4UcZ_Qc%tB?Or%y|_R0kJQyde(-( z?x6(`Z q9U~5OeV6B}r-ind- z^p|r9l|-10Pfx#~L8F59-m*xFBwLzm9~|=yzTU5x?y2~|xOay+c!0;4zw_~9q`Q1I zxrVm-|AJxe|H;|#fA@X gy|><{exlA4K~M&$!KW6b zE{$!vZav;i)!?}VkAU$;2yyN>(y9(ECEWY=g%D5tt8WDXpw~Z=s@_e~CtJ>k(zIRY z-eLI-%TySeFV>VvgzRA#fw)pn+>{zx{VN=3QRP8b> z0)lYwN~1zeEK-fAL~bHocV2L25)dK=l&d0d1Nd)Fimv*JXbeti>p~^clyQQOBPGAt zbZRud{WzB71=h)|9Bnvfj}#87{IQy3)2Uk@rn*tAmpNu^jm0rMII4bzmi=YKqxif1 zGykjo$Zx&=ckW;G-b0m8`HS;` Qtd@rCGtKlk8%r~l@x+Vi-nW`0S=5)~xb68F?4Ai|+(+R4>GC2L7C@03K| zLv8_Q!-UuAeI5Tw5H?u@C@|Pz=4tydT>35y-)*VUd#Sm@M|);!-nb)&Y`-O(XM33; z#mw`>xoe(Im#!Ia&y8Jfw3@(%9sl)!Oa0bxQ}C!+!jG1(C;T?Y(WUdRu7~13U%v{~ zL-HMexk!;F?&lUS4Q6aV%Tl;DqJk5sU N$T=I25c0g;@jo7kZ7{n9U{2z5_s zj#-m7#PpY(R@x`P`)3;|lq{>KEYn@3Lc1o^s8eKa2ZBKVnKGo>K8KdmSMx>-Q%ai8 z*5r=}%8VmO_H=m!VPv-G!GVPI_JuBHcI5dBUcNhW^ph>jH8I1F^jV(uv=Be|%5=yy zu_h8KbP1r{Xu|h~-yVqa2G1zyD6!DNiLcc2O(rh0&35KUGoT_LtQuXhql w4(6~#Qi%zum1j2e5u(eVxi(_w z=zFPotS*Nat@f=CPF-Saf;j7M^-@vko$g7uBcY6oPkEMOfuutFy)0x+;EGmO<4?=6 zFI|iVJ}fuWLSXmCu{OXbY9li!*9XU+kM3s4?5oy~F)O~U#Vr(}zL1eB2t9x}G8%29 zU2Q_TM;_)2idB`~Gx`$4^y=Z2cZLzYcbM*qt}LVp7}0mN!$Xs9MW+cP^#Zw<5L
b%=Z#QtLuU^ytwAZpBLr>`SBZNDQ+#sO7KDwZgi&SX=fU}Y`R&oXFJJ||%^t&PG zxV@3}d`0p&jM$PcqBs(mD{FiKpirMT;Mv^hl;PEvY)S3G4ac|*+8WH|oaGM*5_tUh z6o40}#1Eo--n4$|$$E4N;GlXX;$AB)5g|nD-%Z>`mpF@<{==^E+a3i?Uj^K6NjVsvcPKX@RI%Tw*7e;NFO *WNA=%@s9aTOW*r Y0A~M4HOhoCf^%5T4enBx-m#^1LGZ6(9Kq$90Pu;E|F-G~C*@V1}qvMia zX)!WiE`eZ$C^o5&7o{a3s?6MW3Ek>3F;SHY90z8m>CH6^ kwR3Kawm|h47)@;^p=;{6 z>aQ2`+z2F1t-boiK?=@ws+9V^qCM6t&cNA5FS*9>0e8o>rv;?=t5f+^eB^#&(VfeA z&;8FPrZhihynRvRqrEgedO>y(kFE9v9(+0Rs*V_ab!Ex?l?`sJ&I3l?F4GC2vx9R{ zn;4s^!g&Q3?vc7Taj*73D^xvETJx+E%kHgFQxXrcl#eZA{B|$<(XuC*CpC+T%aQln zq##Fy$DT;uWS^V|OGG$H;{!X?ZARS(q-~NVYELHm^^QX;-;PI_<%Za*2ja)_83R=> zGWK%oTwiRK`G13UHhCdtqO#|vA;Nk PPY2Jic3jxipnU>$iI&FY@f*50a{t)u_lYG z?KBNM@UWTNQ>*sTflZXh-erS4|IEUR^WycDc0y>2l~}$c+#M0P*6BWkn%J2mo+ysN z6VKw~DX$N6BqA?&(3@^Kop{+f^v(B(wBTcaI)Dx!epCJt6kq+j7&Y~ZI)1m4jkG8# z0vq!)CDd0-^ES$LO)+glGC8bQ!jmO|-R;?ZQ^-}G?&a?jB5Wru{R#?^Lj(2j!db4) zDbCN$kW7%7E0!zy%$;=^x77CBHo4s0v1LXcOK!(9*CkPPo%8}rz%LiVQ|x{qwc1B9 z^^WFGcSs&4bkE38ac~GtqYV(MpO^P;O|KgWp@kpUI;p*^y*@~eluIBHH6qPEjujXH zjap{_BQRVxaLSz@2m?scz uX#Zh0iw%Fbju;c;<6$gRxYi)&G3C~9yR*+kmat$LU7yCf#~h_o5*P|NFeQc< zIEL)(9Qxdpb$8<)`ed#ZtaD2_R;Cl5VnciE(UWcU&tjrf-MQ-c=}K_?K&VchvF+#3 z5gkPXu`+$x_WH+k=8>NZj%iX1YNJ@{Yvd`h6 `a-k?v_t=4l`r12mfAe+*dx{nCjAnFiuBFe(&5tYQ@zwk4dF& z^_TqV8e1k|zA4KwI*zgX%KBt#F)u!eP7Tz*bs3PC_s9T>A$pB%>#~cB_hYb$dn;;W zaYMPRAJgG6F6jotNY*rJAC2b|nF#~tGSBNZzf!rYZ_Rj-81n{YMlei0iq*a$->y z%z9Q|Tl-An<|Gq|-+fNpFob>o_7=3%iD&kWGiP<$GugW3A1kjF@jvY8Nt(Ij8CXXN zrx??E-C*EPR17(NuaI{kKA|NC@CYoMV@#8v7W58FeJEG=rlCTKh0D)-uVuy?ATVC< zqi(!D$7V{b0AZWW?r4tT3~=wJ1vwDa)?vdGC65 m`AU5#WsMt?sFALPOz?kcZ(dV-WnJFVIC8D`G;;2Pmzk7jp>R@sX@HxL?z}z^ zo=9c#J4-HoD$!MEoG*B;;6+1&SN_nt*e#l|y=mST@`~}q3fQULF+OYDCM1uE0Vs&m zj&g0vE!i}=6Db@yY!OMNd?iOF-LuTI)625n>*rAIzZ6CGH{j-fiEH>r>Aimlw33?t zmM0i+pmfS`kuSU&9#V-)ZyP7@gyb2vJoOW~!D7lC?zX_3az3%-*v09V&Io-m+(c#| zSMzk2MZaZ*X+|U+b^Tg%E ZehxD)2ogKzJUU^oC-&D6=^Yc| zr8_UrBC1Rrr+0Uko*YEos|Z|66G%G9duBRLuTXOd5HKp`{GjU6@esYYJ5heFaSq1x zFy1ohHq;uqllWw4WnV`h|Ln;SV)*l3{eI~ODCC?0(?3uAmLnK_ zrkfHF|G|##)6@e{tXf%iMCPD!8oiL1Q@n*ic-aA}4%)00KT$Hirs2uT_rod{W`_ZF zs6ZrdFzjG`*gaL8<7|0cF&34y>hL$vwxjM+dlYCPRd4BbMAZf0t7Vxy*<8&W>D+w+ z!ehQxpvzHd@`ndK?#eC)Nv;;+DPga+VX08YVe=trDwK1p#WcqxsV+kxwCBMI?bfhO zK|v@9Ku6;>J^)9xwD65xELb~J<>5_&9A7yK6zJR gkzJ!#-@^+mlr+R^IV_q=4^hK%wHdi_MSC?;=<*H)+qG8~fA_A~Abh zQ~I|tYz9x;c<*^s6-A*uns?%`nZ@{*fV<+@EJtRoRot`4kIjjU|LGaM$tK`MGfYfn z(MQ@|&jeB2KP~s3waz^7m0g-Ki@BV1PlmTf5B@F@1^PQ$4$FT{fQ=eNRy{*BHCrvN z+Y@vy_4Ew?5F3zZbQ{$o*{SmFe~Pd|lixm0Z$wOl?z-&-YlLknKTT_crRT`c*}Rvv z)3C!$eS>(=vPMDnN>89r=b_}_SyRwqgdAL2+ |2 z3HbkDti)`s;O5MUm0SX3E*fCMwq7b1O^Hhv3 EDOeYs4YC33n8ZA3&*C2!>Q*NlJt_d0M!4OH)x~KZi(}^gNS~*hj z-02qdv9Y)0NPc*{>TSAzaBz44_RK47Gr#!M#0!qt<$i|pHSeo?_MY23OofusZYCq6 zp5)3)flC07%~bl%2r@xRtE~pOOu! q8 z1(%GS(@0F{Nc!>cMJAdt%a#n4yp$SP=;`B5F7EnjyiPli(;c&~B>C=M^W9Fym?(qc zr|)um?6m}Pshw@-(Ui>uwlIu_>407^*r9YPSre6A-~qvY0LbkHcjzr0D+k#E-+trb z@^3G-b2mRcjj-HkuBqJ#%VUpTTf(F}xasG-3E>uW6UL~&pfQ?;7EHr5y;gJz3MNFJ zesaz^9KHnPU$6|B_RgN5$t8}vD;hslGLj@XGAl%R*%)Z(PASf088`zqOhT}fJkInT zoP28$alKF&vr>+2leLE txsm>4hVJ zn|`4fJ@DcRV@=QnIT)R_VTuk?Kuh``V`C*g23-Q)rh+a3(Hl4y8bhW%Yyatqb?kYf zVN;BU>e#T@Urt01fNQ(z?SIC$km;fdy#C=&EfoLNNn{wu=qb=8@V2jDd*B%-{8VYU z`Sey4$`gMF`x-gmzxOzIzTqD!$iYGBh<)I+EGCo@qv=|Tku8=0qBv`Jl3naQ<`HG? zN~CvXFXFNZNf#JTT@N#Wn-4=V2H?dd)0(ae@;lhhCY9q75W|UWYzS-`+t?t?P1khH zBkYbQp=hFu&`W^b-c;C9*N(u#JOC<(j5;n2!YXkXs}12+8wk%d`E(~qc!S@Z*frJ& z!`0_0&sIo5Cp~`?fB1v<@!!Pz_&NIXw>TNUuTu8+k@)`_EBpCX|My^C*D4D06>#DM zuS@z7!ET{p7okU{lfB2UN3glW0k7CE0gF4c%gT9E9DE qmz`Pj|K(IK}Bxb)t9giL41GIUoW&tJZM<=Osqme5m{ zPNk&?%_aTnt+toRVO)ndr9?}IE({lH19EDtCB>V+hdMt}&lBEM19KbUF?@X(^%yB- zb!?4f$Y6`4*PS35dMCO(^K2p_>A97r^x~$uN2qIoi00Ihv;u-q{i#V~AEB8A9(^wL zV6k^Fa =AaXT7z_m z;| ov PIO|70%eONF1=*FrZvr1VL%)3btwNuGC^3Q!2x96*zCg0 zx0T(KBu7zWKGsu|mXV-|7%GhtSzJd8EfC%1<{8sgy34K{@*UX7U7TRHlLA}>IGMcH zk|oTT_D*^0ye?l=SiJ{s!(7*pqZswNW7->qCO5^XaV!BZTH%LdHm0L)a9%J%1AZ!* z9t=LYaS0gF$<~%RC6n38y97+6%nz4=;D7m>>ow=cA_C<~&esny5a-)AGw@qg$;vN5 zLu#>N%r0~V8bRvv*U@+1w%mQdlFddA$cPz^ORMI-r{cmCI47JSzn(`xhPoLM9}`-} z +fN$J`RYAPw0Q97FLV#8uG?TV3NAxNPM zefIGl`$Kug=Um?bYkm@;q47+`)Iud;gT1@>YVU<0nPo`F4mBE&@lGi|qXyl|h}u(r z=FWgiKtqe0(3-v!=f?tK4c$ IL#z7^8m!^M0{cCI!sxN! zq7eF(S1|+nAsm}FZ?!5Kdsg+D)jx)gj2znQ>5vMiLfBdYl*Qz}s*A6T>J(BbBsui< zRa4H!yqCL+M+(%?gIDrh0xB;7)zH9m?3aSCzpJ--M7nBClfPD=`U`uHpw4uv_}TDv zUWez_OP{Z1nhm|)<%%cHrv`P*;_A+Tm>rKwkBYs%CSO1 OSc$ehoBMT~Nb_54Bpv;%%rOZAznPV@GoSFWm0j->^wy z0(TUjI^QlA(pwl$UTg+#ii?EgqDt*Er>rM-*!n+)W$e)j(0SUHQ&_wQNJhV$i5$i8 zUjSp33&6oZs2Y;7b6ot*lzcp#D4-_Q@n!k!4OR|m$h&vXoW4Go`&1e=Pb63xfDy-h z{l=Q4!hSqO+pFaiC1m!_oFy8--FTzrOs^UxyW}dK*uhBe;ommLF#=`F+C1oBsvYB5 zNh5fd_>xV7CECHrff{#D?;UKUnY5}oB%L%_^V=S+dfCX%$EwKHIIFZFLP1}z*R%4T z8NgI)tmz8KT@kC?Qc=Ksin<`wrEu6_L~i`@NhiTZj^6~d&s_rM1RddfG+3wc6Gk+d z&pXT{_KI*=vC;H~mjD$#w+r=&U*;%y^Ou0QZz#`8fD`;eaCYKI;ct4+{uWu`kH`N( z0maXmCV=MOkO1QUET#GHAPE28DP#Q<8T@6L&e@DkQ)_2De{%`gC>U z-g@_%7KcAgAzgp6@=p?}WxBpj50m` PN(o9?OU10M z*51M+w5NheQ|7t =Ts$eeB7y3y;abXRAI)})UJv-lY^Q6+&AZ5qJ zO@cC3LClilbE!5;f4SEb7aKSj(<(J~k@fNmU4D}oFe!U1P~ob}T$<0DjxNq)a^uYs zx!v<-TaoG}3dk7?l>Ea^sZ{m6N%#>TA&P8qEn*jacZrsWt?Ha!ejfs=X92yn3}(3| zZ^`!+%9vd3Ql}b+Sk|pT0wc&CSRXdu;(AmAbA2 4P%3v!NJOK1EoD$A4#h>r0cDWT@+S4 z$jz$Y@(lmZyG;wb?{ZQ&xF^kn%2_iWuI_cKt-y3F4bNTg`9gC-t4!+dlIGbjKDg9b zZIy4E7?S(Fv7|!QXi`ws{(TVg>U^}bOX^sJG ##Y*wkGNkCKxhUS}VuPG3f*!EH=J~7xn>A78>X}9@wGy$oi#HVhd zNe}ln7_e>#jV}_KYi+lOnO%F&?nGj}5-s0QLre-_H5>m#n;zQ4)HLy7e7CshyWw P4nSSmV=fdno_UhhUiOJ6FxcJ(Lg_6C#qjfXYVxSFIs^IV(BsZ zdh)GU zaQcGpp$e}=Ld>J%T2n|>sx~^?)H`}EBfYhOtNs!|iGDQJV3<3(eqb=Qapn^y81uk7 zJeVGHK;O5 wb^U#=Beh|kU55>E>gn6YsuvS$q*s^q<|6L@`v zp?VR?pjwU+Tdb;|I_H?#__8DwFL%Fjlq|DqkM{cPcM$PQMX3mHxzvV}`77Ce1Hx&F z)iT?81QeQ$iu8?JgKmT5yoHOiv=2fVd0fY$>k&_4B`U2lwq?*D@I$0&u%zHHJtcDI z&8NA{tA-32{zm!<_!MCuu6@iDRZ(I;wB^ZuRZ}-%W%gL;j;fkrPVq@v2$JhYvEpyf?Pu#N6x!6Xa`DNfWC($=*V2w z8Q#f^o>OA~aX|A(t6YXw{>E0)ECTGiWQt?p5j)q3otLSP@zRK10;=c}gmhjctKYqv z8ho@T!zests^}rm(o#}cU@OjjTcq2QB1JcLmsn;ncK5qj3!YihViUiI`@?UN+PQ5^ zyvcApcap1O8V%+F$5_k)y8`3{6gP6oMUJRjQZpEY`xxo6L7rl*U<0wCCPpKd0C^sO z?t$GUU@Zi_{_npx 6*SvE9!YvH$F2|3A~2{~BEQy|#Z((BgNR;Y8l@8Uwu5D>g+bD 2X2}dcwag_JYDpzahuw6b z{_?Y;>ziWF#~XuWF4Cd0c{WY#ae7Gvv==u;-!nt D#jEy@ 9qw635BUr!o|2;9MxydPS`o;DehDH7!rrYd@}Q`BYtoyrEg@P%!-H z;m&$r#tKdvq+b%g -$ z;i7gyRUKzA4n{8ivJ@1&1Pq1APzz$e?LKWkTj~ALJN~o1+kf?S|3vTjdxrS^UcKS3 zkNYpuJ4UL4y#E?2Jhp) in3yvsJbD{rok)0Wyg* z1DJyrPP>h?YMUo`YU~l&tI&wpL070zOmFiLMizgTQtJ)(o^UEgWqz5vgM_ym-=Aow ztT~< <%a-W1Hf z%h7yVdlo>hVVL@hw)j5?{7i}$fx+tQN$_JgH1~Q+Cf`1DLr!SO7bX4ox|uX#2?}aU z%_QIuApy_I-U#_oqZMDH4ssp&yCCMps}wzc6 HrOm50&{@EKDq3@74tq45U{_YXp6zhPALPeG9XFbwzYcg#}#b$9uDZU1X@!x|&5 zy%Cyvp)(&(A)mt|No2m#KlL3Q+rcftE(T3p;ExZ1K`);wkrz@FiMR;fr+$;WMrE;o z%-Ka-o-(0IlPGzr-yc@8@uizisOybTui}b#aQw}?J^>TnOlw{x&U$n!PLYuJrI`y@ z`=6xqhoihu*&JE=71Mg()vLlSU8KI !J^%R$;2}`;&$%FI>{Z*0u~^ zl=a*YJ=%Tl*U^*d6kk$rr_38v@FnX(sj}VHbLSm87n1$T+&REmXm=@Wf+G3Y6Vdcd z4XwiG1tU)*u0S*gV5s|-i>)3;wTn!+FZFQd@~p~))`#6Mx%J} e$L1)bbXo7+>z`Hq lC#i%V$TDO#CZx|(!V>;T?~y#1SM_1s z^ih$Cn85~ISIi<+enJBtc--u=tRmmKraa2)+S_r%$p+NMgh$LE%+ xClHq=-Y9)R3UD zX)EQoDUXEikNU2sNlQTfpk5aM6U9x>XUa}#5TOWTjX}Cu5XbH6GroGQ Yzy$5I1Y z?0?vf9~ T z?8(~G@9ineKVIGwJISw#x&@g!bsG{q-D_xWT iN|%HW4|xyMBaW8H1wK|vDj?Z!1(EmRPi<+-cZVY#cX}G5BM=A&A`A0EnuV& z kw=vVxPgp;FVl5~D(0a$L6S*`~NXq4+k~>AjBNKp=Lz-+Z !DIR5j{e?1KOcAg7LVoU^7Ox@+xq3XKZ;&I zDSBCf7c%PXQ@eINmnSWvTxyR^RMqV7k9v)LG4@3a{JG$zHGD)dvhue3dYV|A* 2$0FqFD 6(?TWbl(e+<8PkQuZr2hw;hj<2K46R|EEjE?v&v)sHTUtoVa{o69SuuP zL^-y-L}^aVweG3b#{Y=m-C;>7>^=$R)51D;QKJoW2J@0VxQTM|z8>gT7tpF8UsH@T z*3qugO-Nlt Cn+bQ*5eWU1C=ys DLv(`)Xm=3sW)e-oe@rKLk)ESW}4gg3St7E zS6%T~Y5<=zs$BxoO!F@RG~jaVn0mT;mX0i%yK?KXf@DPj@5j9HS#iP%7!xY4-9U4F zDkY}6Vy+){B|~d%pl`rg!XgsicG5It3X=`bt10JZ=5fAm&$~hly5B~+eeE7>3unfy zDE&|O$NDe#top3#Rrx5QYR5tA8(ISyP~7k=sn+)kgsSShYiHyct6Q4rXSU^DK1p4? zd0I)^laI&1ifO7H)(YP{wU3lNEU27rMiNIWvU(-`xCH#U-I;%NHSZP{>ZoOTKgJr> zf;^(SOqh_C$=Mrq+aWIngQqwxI3<^ +51( z$=Y8X{V(^bV8tRuh+^gvV1|YJ4tkHk%L;$~APG9(6SbZD)}XcoB f6Q&Gb5tn#GoQm#fO&jaIc`RGQO_>?r`Jm% zVI4%3RTN3{r-Abb_^100=|3WWTDr^fAVBXKewmO!M4%?tk?o!ofeb3K6y>6jC_A#V zoiy%Lqrp?5I83-ksAfbmm;3>(guLaCS5uR3&kYp&thgFr{320|hzjhEpTkuJzSUp| z4#-7DS59Ud=DmuU)Q)H6 W_WzLJ!=BA%d z0KnG9l)OPFD^;=6eivRn?KU`dM<3efitp}dyCT$R>EngkynXM1!|KDG+K}f_tCs)^ zkHKO`8wT8Qq2%Oxi02M7MELp-NZv=yt_;mSTo^?_skL}+V6~WQ#0y5|O~!`LUyqq0 zuS*4I5PJPFrvBdJ-|un!e|${+Ll6D8)kZ%rvm}*HUji_y%U$Uw1XyyRMKSzwHF^w& zWl!z9GyF2YG{q$IKnx6h_f6i9Z($l&F!A58&Vvc)-j_>+RNdjKs#JwU?Kt5JR*f!G zR)(c#Cs<~t?4q$whK>D7sMv9PE7Qu|tM43e>Yb8yTb<9 _xb+eHg zj~(BEkW$2l5pRhtzG6t$FOL+bvugBwgiy8zJrIBED#~ `PxPUHYoA=omdub4+i-=O)P9ocATb6aj>O6C4EC zfC*g(Nl K>H~)9}{E2~8yL%|=SNpEvh!e;_33@vZsV z;?~`=lskk0^)7+~#8pEwW(=FM>37;II=H2u%Q@Dyl(nFV6c$P;t(#bzCxqaPa|TVH zooIYweIB!?O+Jpk{+(j*=5;!~KFl$%)&b1Nn*o0QgiXBQxPEu9B+0clrkn4}JazYy zAfiDDFHv) {h0lHxJ73AJmbXoyMwoM} 2{rNfV$>e62esd%?jic3f2e8~YHo9j8p z@$;Wr`Gaca6IsK<>2&SVZ%Aq;uBnD98-~z~l`QeAe720ZX4orw8cbjK{YXkx3b$*V zc~4K}rq<%Qb*<4u3*YZ)FW$?r;!f M|teBhjt9Sb4G?0Kh)Kk z*3-$ph^fys#|a#jTxw3-D+e`j8Bt~D)=y&W5#4`WVMCWeo6-K*%3!5vgeRf zRnyOT<`|)4C2p%jk#5Sj^$V#LWR~{}UIrS$4?Ju}=_qv2((YUWrWoY6k(CZkp$Fne zd<3Z4w%$d PrtAgnWa#FBOAKjrml*6|eZK4L{pAyT9zcM$LLf!O`Ggv*7mX zNs#~AF(lmwey@~c48FqVb_p=1J)J1hi`#78C#|~#ltiG{3(H5XhU6$-Gwdx0jyYiH zJo>4Wx#8v^FX&?0^IJo=8^omFH4`+5nDK@twe{yqHMR=x&EAfV_-+K9R=wt>=Vy~9 zjVuW>uX`0!VmvvO<3z4?KV#4YC(pH+237e8k~r)JN!Ie{_`(0R_lJO+<(HLo1Hhhx zYbswGBMINoVMui<&6Fe03kA?YMIm7s{%+ic3TizL$e<*cd&S$sn1zLilw`kiCQUB~ z5;zQXTH3)In36Abv34+YO*0Cw|9&F63Bgw%b*wi0%vXJP?#Gf=LaaP@pHFwjO;>Ei z@sFb9zZ}ivpD0THp @j4kflV;4~K;FInWZ-pTt71&16Ve+d_?@6*OOuja%Eh_tb z*i5pI&&l+57&4r{^6m5bqF2o6c=zL(@o)WOQ6fA?;=r9o@cA0%R1p5>L$TMViYyL> z3&F2#GDw$|dAh3He?Wc^8QN}+k&WAk(HiK#_UD~5&mGk;r QBftH=-8tfRh zht1X VGPG5UGmws`8 zZA=pQPpz9ElcU?dHT~ztGyZ$yAR!+kU*X&TrR`DweoUeEAq5HC8q72nz-WT+#hJL| zeXW=`dYn>NPjbj!^=}G?yp$3=Z4Eg~{9FU5KwTX|Wi7lj-}Y*XGJmx$P`K+^Mrzj1 z*0(Vp{1(Lgx>bA9Zaza)ZdKD0HD +3++x)F!@48?U;xNf|M?kFav(Z&Y8E0os+|Ko1Mp=U{ zjHD3b*^l%yeh7PY5q!A~OP(&lA~l(mukTx9_c#yw3C!9YKZwFsi_vL8ZG-1wSUz^n zZgYY7|44xB?^R3tS2-?MAogp?a39_Re^+?)mkb#7t7pHlDg|HuAtL-|I`_X@aWIIc zDlZ#pAS-~x_U7C$0rRlNoA$}a`Nc*Dt%@!Xj$3t|au*3+aL5GMN%m63NPg0JQ|@4t z`dQ`*skjwZL6}kul`Yb@iM2kSxXQg6V`1Gw#oKs8%9h?tf3*7iR7lFi#mR+NM8b@N z@$m^qlEUU(C;#~PB>?>GblrU9Y<`@Lhj)H4cheH^G(aQP1E9Cf@fwP6m4PB?OUHMU zvQjOtoDiJO9kc6eHyU(@6_npo|I|}JXzt$+doXD{m^=3XZYz^CB^N`$`HcFOypAuP zxhu=HIu)xf6Rrug_MFfd-;|1Zog#;No=zr!cTAl^1~&ts7wU84Xd(bDlm!(=@hz#i zm>}fRgBxVCAR#YcV^{iDh$?n@;>C>es5W9jlTa{Xd9h=>LGn4+&z`^(C2jw_Dlp^; z$nDxflaa^iqvQt*R!OI6UB_8S hux9M@R2^&YR4w>86!4>vg7Jdy;c+1{(E zjt*iaT~+hJf@ Kp!tt@gufczo%?*Vl&TO`Tc-Tu=Jvjl7yFln@5ba5QD)` zC_ONsV<4V(O4i5nV54ld%ydrBI;xtMgW6=NZe)=U$HQgGcx6r+@gpWz*8M})nOjE| zsquQJ?tL65L1togVL(@+Z`DNi@q872hasF~-Xpu(SL#PD>4TS*hHczHQZrl6TY>kk z4TW>&lO4Z76{RsJws+~ue@>b)khHhQsTJN-B00S(S)A##?C1s9fZG6X_AbskK5C>9 zPRud1O?aVbH8dHJx?p4}7ZEE!O-)VfJ<)$O?J>W^7$(hZYPiY3)o`27hAI#*Qgktt zvkQ^s#Pu;MqlEq4PnjI%Dwt}l<&-`dyck}57w4A(=arSn!-C!SQ;LIhdPN&1*AO+? z3-nmCPTPv!LYwHUeut16wz th`G4%acU+TSn*JX|L8OUDuL=m#dxwZhlO{-!uF}EKOQ=B*kS-vg^j-y| zgx*1VZ;{YL?+G v~_ypY+PmPg=eB zK4icga>KRKWCRc|Gpgk6g6~ict#W#ScDRPzE>yS}u +XGFNLwdSN^xjF!+KMR14lnZnQv3b)Qs1}K4ef^)6^MExTqH-677FzZ0krP?SP;z z-9K`BaY*M)VIr~F^d+R3YlA2{c%#e{?Tf_i1m2cr_gH=jjTBQH`)W@Kx8ci)5v{~) zYiBX{sa YiHz^ zE+t}5OMXSov0_)L`LU>gX)?D2Zr~5x4vw-cACC($bn? 8DiKs=V49MB5EZRQXGh z<;b5XUjJMH`|I(aDuVyk@z53lqET^IxR~#zMgN*s<t~m?G&L`&W;kTWi9NN-;T QZV$3fvxI^G@LWq(ESvH_f;ttYCI{B@9TRRJ$Z z{4@mQaLTuEfT_Ic#amt)>6ghaY&CE*0hMpdKA^T2Tf~&OTZ3lbfj5PLK~pXGRZO>M zZL8}`ztzA_e*!FH1wgmnW8Fp92Hy?gltePAj$#CmLfv{^pviu)^M0q~8ynbx!kbux zxYlCgqdr*+eDQ7yl#Y$Y*R2{!A4%0yXPNjq9(uOugq-7hURLw*{uj~R3zS!sc79lo zPLruAqeydoz}>NVU$Gr{P($W(pY2a!{7a8yl!u#qDCF<9{#*aZe}dPrQMy@>73MEf z^Jo-9T7(gS?X!G~KThSHmrxssy`2e{*CNw?7ePLu`xEfak5Nu6;1x=4jCrc`m>@}f zxvo|emGzL*Ddh2xQY)^ET?$gYW>Jl=4L%k9bxVwYZd;REm8E*Ve8=sTOQV(eNpU@X zgfu%}Ft+ALY?lZVjoHxKb#q`9us_V6YakLM&bboLAZEIbkqUpV1~G0K&Hl2(_VRUk zn#*;~y4CO=eRZk`pIO)^@Va`k3hM4j+@@W2eb9qBh8($cgzlTpz?r0@B@|W16?0rC z+@wbM928tBt#@1HmV-Gscvf=NnHnce-jRu{Q85o3XBZ4(8W44NA2n1sh)_b@iyS=8 zAo=|o Y4*d(m z*&ofXzkc@DjoV*KPW@$y`iGXNP^r~lAf6zZxv#|Bq8T*$3Xk!IgpS*aYfR{1U*D2z zH-1Z?(-+rga1Tr`@m36UO;yDD42dv`g|-48^j=c-ThjYma+(j}Y)Mdz`RG6!rlcQS zvA}#TNwtEOL%&9J&J4GmWks`7cWZWw9O#ID`D&1Px6ZT9!TTs$LwDSp%hnepOQ&&b zTe;cZ#Li&O0yg>}Veg!YTQ7T)=~kmvDHvZH9ha|vmh>QwBg{r Q;nMx zsR7K~1CD@|SUaq!ye*9y*%Is{wB!(I01I{9Ryt-7Tym3<@?)>7pU` ;B(mXLUr)`?*fli ?k7^-R z(m;-~$xESUXv4)O_BL7a#_`?tap)i=S9k%rLTyW9L+lpdKw3|rYbRGf6z3(Vi(kOR z8|iU%dmDr|)8QqGMS$6!2Sc408sTekY=fnll&ji7mf+z8#mBf@xKr`1Xfhvm!~ 87E3XL9 AKKS?2kj+K6Pc!rvX@5`Wbj4|{gWi+ zl?7mcSw9TH0SEBKVY^bB*t-*_La&z^f81JpMI1{+W}1AS+L(|OIayUyfKuAhQP;lh zMQZy>6RRZt6Q%K=xPfz-?`MT^0v @Z$mln?lqZ^t?aOfAE^qHuRd4SJRlLt1A@;92&c)DZ5X(gfq z(2wwian^)?_1*9OH=qsvtB>(F^xc2C(EhRWo{NcbqaESUF0Jn*w-S>aJapfI*PlwL zm!Cnb{H>p5v8E!cH^ngTYPd>qI9-852~&UC+?3J}XPuE%4QWStkZm&$>Ol ea4mHY%hJf-X|XAzed#0HGRAXE7labcEb8_ZIs4)BBc4@P zL93RvvJk_`_qb#|MlMHbfL$^PNghvb4N-)A{Ahti6sk>&%BA#JP?sj(|;5!BKht zqf75)vCjQF^ER;asTP?|xN4eP+0Ej+DL!pFkA@lAicWx3ek_~y!IeeRc9n`Q2!;iR zxA!g-_8Qu>q)sT-vGtT<>vXy+PPE5k4dRWf`?WRW%ZgFrtqL5mR*t$B+&H}(wtPew z8+7L4Hr}|j6oO!vCrLNmlI*1K(paUCImNw 1-^}%W|T Q*#K&$nR#uF1g)@ZJ(iaP@+ z{mm_sN4tqMGBKo4sKA=)oZ|aRQqj#lHkI563SA=6!qD=bW8qzV1Y@98&iBmi`lmHh zUT(|AKi;P(jMA^Lq(;_yEfT@vbAvDm!`=*4kq%7rA6f327MoqmEgyr*s8(!F#m-S| zuV%~2)@G1kExzcFCw{^c(jX`E2aWZLw@Y`p)%z$6Glxd-b3IdHg)4-koClOy&y(t~ zbFGI9w4@}bQZ?O^jOL491_@`?$sxRN#HedjsY$0zm+86nHQHZohQFuW|3CHI-!8cS zvOfLB`|+QQt@=CL49gG#AA+Sz_wg67 ^V64B zJfEsnKYfcEKtlS&^ffqfF)DMJ?=Cw0`J{*%dx!2kL*=)mrL52~Qr;z!TV=)_4lIQ- zT+2}LR~lxf2H=HdV4&NsRM!dI#mE7+>skP+k*}hx(z~;_{_c>-3)Z!Et4!KAap5p} z(`AQ$Hl-~6d^BaYrBW}{w2Rg>_-IgT>Oy1kYqDne3)PZZ=p(TnIf?d!=YG(QQ^-wM z2Bt;E$_If(FTusno2Ed_)%8`X(r@=-JQY0##28aQNaTUdma!!h;SbUjd8UdkJh=&h zZrQdGg~8#wcn?Ua*!_t7CguFte3;P? 5HrB%1ITY)_al7{eqGiXUPtI zpJkjqmvcScIL%lQIcbruqti`>n Z(+}m6mRfGo zy_^mwY?o!Og1IG=QrcmE(csK!(`n|(UCn3x!K4zpR?#znbc1yfU !G6&-VQ|812HQ6o~%i_gm(p==B~v2SOgRilo{4trrPuFxfVn4(Zy;gs+0JG&D|Bz zu3%l}Ydz~uMrj#uq5 RJ047Lf7o#3dh@s<8Ipq z%EAj-w87!CM7ar({>`qe%0UlmLxt)Zt(#<-cN{*t3@MH^I8ldPjnAra+2mf#U)CEQ zOKfW8_wO5AH3y4@$>Jzy8N)a0fnmazR?pMUT+euJf9AdaX=P8K=gHkgOsm;KjE*0R z4{7+)RXRihWs+X7u<620rOV<IMgpt5bAzS1oO--_T zz(D(0z|-oBlV@}eOg^aSuExf8ij+)F8D6)gT=lURTS6xvk&s{vwGUq&*quU0Mc*vx zkwiSgLTkV~=`5;=uAAvLE2=bp>4GlYJ<69BKTP?{>6b4(Y~}Bw(vH_fM*VZewKNNk zVTVGULxRapKmKG*Vppp0JL%vfCONQTr&nGsGXFbVq@$p!yz+xWSXkp(fuItg{FD`@ zxCE|}5n8egKHSpt(uPkSc8%%3QW~N!+J8%41t7r_GLc)_5A{k9K2koSQxnoio8NUG zune7EF$%yXW2PtDRL;aan> NxNYn_B*hNUDNC;6ee^VFqd(|OcCW)T)8Kx!t+_mul8kChBy^tsVak;@wB
f6>be)nlM*j z9h$v#Pnw&&q6M$Zjbx!7RA$fWZyw&JR5p8f8mx$)=dCAA1)j6JlNw>zArI9$f0#}H#+IlmV)S--;)TsAB)&-R>kA6 zEor)?eh39RfcVb3cFpMW7Yj2N*O@mOJT$HBED2qsd-5))&%^BsOQz&FLZ@-d=0N#0 zj9r;(F3!hX@$glwS@rA3KdsLlLFh1iC?@~KF?+XH(Tv}MmN;B`y5vsJf~+{p!!;fB z%AXPE!JZ6pl>P*CEs#L>aSW{ww=*@NABfwTFM99 CcxB<=zN*f#_(o_f_xjau-jNRH30{ECz zjy2u4{)z}#9{T>FP7~U0S;{UnH|geE#LJj3VH|hzZ%LB2*I(jkkSZdJJ9Lwx65#1G zZd3s}imnH9DgNF4TnRG~c*C{;q#9S7EZbBW?_1YxprE1P_0FEK0u9wleiNr3Qwn9- zwqC@TdXqByDcJKAK~X^<*+;*urXT4f)`^S{&`t$&YBsj^iwSfEF}Xek!wDC1tS;}+ zQ%v2_6{M7K#kWQkFAakQv2-er$mw;D5&$^<7trc|5K8s;0Ir@-`~)m_X`o3DDg6Gm zoA&Q;<$qat{-HPLAvFHe7NlvLDWz>v$9MbrhlaxA*}Q~(qg8cMzJl#oH(=D1N(maL zQD^zXPrx^fKGDZ^=79Bq>Snck-g2{*F3FYSvS4Q~%K+sq7gH+U+bQMYwA Nc zN{to_{*~26R8X#U8YhZ0pwYL$CSly}JS3=eKY{< 3w}=29kMgCOOj^- NGe@pt)J<(z5hyF zly);b{S&==3FMBC9y)%RkZN376JdC%KJ@tO$gKK-!n5V;h2lNmJt05hd7%a(^IL-r ze2v>zBavz8NY^dDARpz#j@#dtnea92Ts(7M{;-_7DePEisVm(rUVqs9I!$?IIM?Nh zxxy9A4(tpEk<7_58*Ppn&U0`Y@W`~RZak@3wPMEIOoJi^Prna`)~CIf7AgRzKd&7N z2)YoOm6ES=q_Poj-Qj$Pj$1yNVn^YE1CF-zbl1iVqh&C#8sNcTvIJMRdc1H2XSuQq z4+FNmkay{47%$yL+ypE`<5yk{G2Wec+su!4xw^w6k`0~M* F>pn;FHy=e%EIt-j=WgagP95JjGp<29hhI7?HJmsE3w*y5OsHy)j%XZs*up! z-tC&4pp~@iGt6{uZyAu`-N}^Cl=Wr@LMHX7X}AEZ9Vb^rT$tXpL*2e}07uBHMq~*U z>Hy_Tc}L}4U-NIpi>SSg*w%50i@WvvFc~|Oe^D;ZD?I(({)r(i4n^AKMVWpXpR*Nf zXn+mN)@ELZak^;QFiNBB-}B-n7BpVldcy60gSltU@VcX4d?g6kZCHy0!6MZLtK6&0 zzvhuYur#;4byMd$Np>LxRj@QZX(*!lpi%C%jhpM0fuY*EXG1s$uoIe?HamhR8n>)L zZk_0%ckZ^W=;R$J^zo}v1a`B0AZSOfHL-sjKJCV!?p{}yvghFvt6cV|5^Y>)v`t(6 zg*V&A!692P2WCIKLh1KTNvksj;$9a`h1SqPn$(E~76%<7a02hK85?HvIy(A(2e|yM z;q=Q-`+vbt`^$3m51ourub_3-#C@Gz%#Nn`) ;B0 z%|pAjm#gDN8%I_%0v(U^>+fp0b6h;_#swkTpv8-O?d0t`>Tt{A!!9AU`Rsr;HmIr$ zuq4Zn)ttptDRPpJAjX5Wr`@ZWJR mmG{L&87^*Ry)HMe+##hAZcXAQC zloNACn_TwKm-M`gRMte-G$SGdOTeZb(bwiN3aIMj#o~ukW1z aBYP`tV~JN5OaL(l6p}~Y1^7 vvfCMB%4x!5K5Zl!K=X&CBB?=cK^3=bbogD*Sf~}yG*f+D|)nDr`Q6Ck0~28 zVw`8U$_7W gtGfUsK}7y1gr`Av=K`6R};m^~qlKHzcjebh >)2E zN|wpwJh-hTIeS|D9!)S=*TOf~s;5be?8VJs>{I@-sc6jt_}0~3)UAG7cvL>fHR-*3 zU+%z=pf&}4MPLf-2CDhMJDPlYW}&Cjma_V}qHCYrC6b9>d>Kn+xesk@sE8Y^{IUqq zev^Mxkei^N(l7Ldp^m$o^-?+G=Eg5WEp`O3tBC)nKK=$n?JwuhKlJW?c-p20Q9{C^ zd7x^}t00~`Vt5MI{nZ6d Av2qVSp!hD>*CV7p6Y %*+{>*)2z}%$xjZ56^e>K z`}DP^?bFAOkBZl6Ydi~VX#=?yX8WD{_=!q~9v#tz=HO53K^Xk*Y{IXaP*2+7eg$cQ zFc9OcykHmi?rH*mtP+hmckALS3JOF$Rj41uhS%wPXaSF#dF<2!knz$dc|N!jojKIP zXOSN14htPsCH*GL YL4^-+1|u3A zLcD~vz_^{?)QVmjs(pPF(?$<-Qn-kfH>=ZsM^AD*>?4t`hD_d`f6!7FzUyUvr_DP2 zex>w5;B9jNrp?2?XE|a7_lPOPk9x|-!FS11T5iR#L$D2WFXD@p`*IKyN85Is$?FoE z?VPPsFI$NiP>yv6S%7 KKNBF7s%bt~zGXd-Fyu$Jh7cPP_ z^)eliJY`tyUAt#)qq)1PoM=ZM@rf08_YwW?VDrCt^nXto?SJaIe|7n8>KBi`MO%0n zjf%dyff+RY&)E(Bidab9=p_bklQB0L2bF3e1Dk82b+0kkjDj#HNgJ3a$i(h6ijiy- zWlJ73H7EDcyyLQCPv1#TnH^F#t@$RFZxBAHp$SzfqvT0?`6#wn<#Up1qUNdMfFw)> zRllG()kw@9+UPpPkA_RU34Z8$)>-ULIbOG-(`xj}Z9NHdRYs|Iex7~3DLI{IXU0p> z!YwN<_EY>WsK%~P)2L?4M?*KQ>qLZ gqwA3OFAWZQJdF6H<=dv7(!r!IK z@DW7?^+gj-&lso?=S`BSfXg37=HTOmN}HeSZ74oGbK^-}x$$&FnZ`c(Q^ci^dTjeS zt6;ywedew2H*v8ht#t5qr=Z 9^m6I#{2g*1lAyNz6T4^i%G(@Z|P$ zeXfLxM(D8mz4`EUpIr;t#u_G76bpQIsWw(7ZI^{5#u597Ad5z=upJEeQ`k;H9!=`1 zlha9HTIRLOy*aKJlt=g~+7!vxnFu7bzcU1fP#Wn!5q?B|qNhx7)7#2=MblB1bG-RB zy7W{X*;V5h3d#qUBJ~aWrrFF7RrTFkTW7`BleT?$E+oMF-_wLqeD98ysGE_UZ%=Pj zLZb*C0T;R{cISoA?@)|C0Vlf12W6C*PA)`L2f53^+(kV0)H4J!*FLG$9Z~Gfv!hx< zuE;mwd|f4+t7)eQp0!k--iU*fbFZwSkm;yBj#{Ra5g-1k9JB`nb>BvdL3Zd|M&|bA znWf`O5VrMV1vFT8og+EHYD>P2 WHLV%82G<^@^ zcHTJEON7rDe$#y-T@*a|Qh;!zS kk-lGuJJhWx=ftrgR0o6V4;Y+ z9E=<%Mp6|Nvv}WGkZa-Z&Mr_)OMCfP Iy4CXbHaF|wQ`-|78Mg}X{h z K-&APO0nz$|V0nmx9Nqrjzm(4TzVGp$x z6Ls}JN-i#%F9j=_nHqtay3R{$kokzy&L{V3ChXl*xV}4jerTH22$Bs? $oXp^6S$0*9erDNR&5bv6yRjjbM89Bueo>Ph!Y+Iz0-3W@d zLPT2IQh9x4WR1PK2YtyBu+xr;RhaPPe_uw!d~s{Zhq!cW%AwRc$u}WCqdF6j3{`Vb z4d(j1<{&D!YTUiX^`To`JK`2E{%VR-@Rv8A&OF6gqZ^JXroavDQ}y^CEJs~= +B?=GGuh5dBVfLD4q6!+NG6>{c5L6h7==%xyUITQs^at`-guOKC7Nnka zBjSg!Xu`%#q3SV3n+vyh_l|vaWlW48_YovG0jg^`pPTKHMIcleJy{P;1;_t1XY!xD z5*%gLqtarF#xItJ>WUsG3fg%YH@XN%xOaXXD$>y=Zzx=Or{6-dgH4VlR?$GNUInqO z&UJo{b*l%rMoU=2@nm(@kr|{9vznCi(7vw`4C;fzc99CWPOAp_Y5h=qd{*;v_I!#| zlvIbL!+VUrY+Ozyoo^_!o>Y?HG8^@InHS)2>zCx@|Ayq`moe}ky46PU+||?u5a8dh zvYHaHOJ1GWbFfN4iSB+`vHS$OsC9N0m`UT33 c9Tx5t#= zX5u`{ggwuL8|J|(b*c1Z5rPTK5$+-(Hea{SeRC&+f{sq6YBIP{id?KNI3r3$k6uZc z>w+Y7QUfx$H6BR_ i?ivO_h0&^NJCN8ERM>8Ci6D= z(P>;aWP+-EmZQE8OkQ1%5-{q>?)uK~tO6-V%{*?6TiS#bQbs{(%+h`WIt?N_+ONzl z_SW7+iO<&Alhzg-Eiiy*6fYi)xm{cUp#q*h$%QWl%z7>19jxbRcJW#MQb1yK^Rmjs za-5;iR=sv2p_9j(!@hE3!H+J?x-{$zUKd#6SlAUWx}(!XY@*RAe||7?c8eS6VIdsD zsgQl^3vn!doycm{bG+dEV%wz28C%-H71*gp )eELnrz`TotY4 &uDYZE{UW`g>Bcb4EUcr z%WV)lHgkIGLkssnZcmCsuiR_Rt^1{%6<`3A(dymb7aw&0DCKQR?awmc0OnZo1 j3!IDUQapGN%e9S0p^jVo#Mp8)@#00F(Nf5jqZ5FY&-=0V8M zUw*s)s_Ep+1tGR$d?D 2dVLpa!zaO+_+M6@epI}u xD3O#p46l|JWR+ckXlc2O+E>A*1)FYEZz6w zb{SuHY0wqfrT-W{v8_j5t_PX6eMr*56bmEey}fUHsP3|MOMdIp0KD5Zr7(Rkz!rJl zS5)V@-#^IZeDK{>WxgK7V^&(zoO&zA6J&NK9!V6-{mS3y-LSgdb~NMFP#QTB7;M>4 zhoV@TVkr+hV6(gb)}HpcMm$BQck}D$ zUpFZZk5}nfz?hN z5?^`5!AHPusdr@zdJ|I!+7mH;wkb-|yFS~Jc&wJ!Jo9-Sh;y_seP_syj+kA0s*I;m zr(oK*LdvbeszQOQ=}qi%C0Sah(^On1R+A%3W#DrY>DJyaQ}dw=YQt@wVyM`x+*v|j z-qUtiAxJVl $GbnZ CJN(JLy7`rI)(}g^2^Sp98r $&%mMHC8IPpds=A%8@;OP-Q4o-Qd=%G2b>UTWX zKScdtlCtkH9>``UAAWWfn05IPVr4CD>bv}-k*&sfV#&@sJiTUt0VR2J+NTBBPosl- zPwsTkYMZItVQ09kBg1fOo|wFQ3EXMmcs0^udhc#APJHc+GtJXeh@^x+#tv;!f-dgR zOTT@^1kP6-D`jf+lCIEKKB2A=dfa9fGehIpA2@6Ti{E4-NMFVavO}36>@&`}y*j(5 zhGdx34sp4`bKIef*9v$d4%A60E^%6RGLd*{Rwk(HX;gCs;Ivt|tzh!@!a}Rl0qF1~ zU#{ DW(z zIeHiBKy83-(5nrbX&-x3#LY}aC6wE>TfwcWy^ABUziUj@p8mcC4~E^8=WKhWBuz-l zCa~AkiW(YF|Hu@4pKQk6)BW1WtdzFJ Aoydufq+aztWq zw1+!d!Ijr=8Q*-(WD7o2=d`3FHh-^*QtW_d4(@rf9q~}-GY62PpM%Idt;&dW9nt}; zgao=VUE4Gfhxw>@g6vBB9jo{4EKYK2qS!f0JiaA9{xL*h7 x2MOeoRR`_VZyzf-HZ%qK1T7O&c62m9m=HGR#n`$h#J-JMVd-Au#pzYN z)u-Sn$Do)C(Gdlf@9!Mvq7x3kpIe3Sf<)9FukQ1*hP<3)QMPjD9zd3id{~J)h@aUF zTldkpz`N?b5`Z~Kt00)J@pNVTwXMa7Fep%1-lCE<(&Gs97Yd@0PE*eQOuDKbc9q?- zG{;n4b4k9BDYk9FBr0O0vQI>R0 KLN3uhd7wpQPT;Jzxc_ ERq(fE>b+B2p^}s(n3O`eS pDfAlm&WU aS3SV0n7 zVIDTpmR6cA)m8b5u1yoFILUl3Qf3a8?tNes0DkUdItC5FkYnp@iepFHyPVyvembRj z?|8{CUc3Np*|I+#d0l%xw z{Xu}*<+K`7xoJA~4Qpz1^y^{{W+IiK{|V?Z{oQc({lG407qQ`I6~GUBW{p#g_$Obo zBJb%+=H=zTA>xp-4|dw}%phGQ3A@5#+Ly4QG3Ag;^5NgV!(XuPFysUa8p`tg=XW@c z1NY46kE09Aw#2R&S}w3KyHRYY%s CuLdADF*f-=Ptcx=!#nA>ZDc*BOCeQm1eg|C_5h8!VHZ|V zy#$S)h4_%d&Fj7|9{oY?_dnGq`M+@7zq%LnD=qPFSf&0#bp7XgX8)VU>0gZvE&qy~ z1epG*A?OeA9^9Y53~0YDjNjx1{BQcF{;xQfzxKlTW&ZhfVf?x<{xDPg3+@f~SN6%- ze{^9yEZwF}bf(ZYk81^Z2m`jbRu(=0q3a##M9NNi;Abx}YK4!U35307$l~83LW9s) zysB+S^`aeu@54fk!*@cuhwI}abi|VOsshu~XSGbb(B3%qS&nnPzBlM`zFeJ=7i+O| z@}{U6l=N41w~(AO!-fN(*+$uVRR7g2{0seA;Slu+9tSNktznVKimbByURt-~A-fF+ z0!Z8Q;{~E*Ygs|G!TiZXenwx42p4AYvj`y}mh{)GZyNY9iXjCPZfaP%um2CNp@+)T za}X0fphZF11N0MA$5M2+1_>#-kMLCX#O_E?+8rW2(LG48wJAyd$g-WAmZHeE$X=In zarT6oh%45c^Su4P;@17^Z~XVJL;u>T@|Sq~-@hZ^RdPP`xTm9_EDgBfCwDc~Qf(Rx ztt<6o1Z$luSIP4*3{($@&NIFj3QT8p*F3FHiNHA;-d#X-0fU%`d87qw_v%9;R$@o0 zAimtohRl-!`gHJhGIL_cO%$7%S~_zk+R X>ATZ$6j0Evxf5pG6jW2<9b<_+&L`1(K?AnOM9<=Dco?+FbMO zDqc*-vq>YZ_XgS8XlnG5#z4Oy!Ir~iG~az1oN|8x-b!`5u8EKMhdKM@q`Wg#lnpK3 zHeskylgW$K*OK{)H%X+3yS7E@V_5{oX6Qz|9e}}4V@>XuzJ33(Et)|9Q{3IZ2@6O$ zQJygd3UzcMkDpDN(F%KlT<7c0w%O5?&3NdB&U6C%#4bjf#ssFFsZHveL+>L(@b8V* z_N4@xwlLu|i=$nZqXRQxt~E+f4ze4^S=Fk%(;Y;I ZwU-cAm+S#=x!^M(W;0+%Ts6hW^#dIHH{(OZctEn=kfjPD1UI!6L z_O{8CVcO;6dL8pRw)*CI$kfJBO9kg*PPUfvgR){h%=Gz;ya Qyt_J5hCAvCmU6Y0usmScfIx0 ztDU+alQ~uySG?56)T>t{Xr}fwS)?I&y{q0;a=sk#aL>FpVe@I^ZDj-2C~P6G_*Q{H z&2?XaM@%ZiZXi4RQxCx$eg!e%s5VW#iSor;Lk?tMU*4r}zUq_qrH?;n2{Fe$nhhvU z`RLLm3F|v~F+1Z!i4FSDOH)Y(5*u&P`liH+BC1D&9+KJTbjQln;`(!0>7$JobI@e6 zg}ljk>#GbnnlHS0?ISzg5zpss&RTm~jTZR;Z~SF*VwIL6bK?W)|V>{2PMm& zI*r33qy*jw3e~{wqB6HB=lPLC4T}yTi6)fTtm=C$u68e%lG-&AbYkv*+_PAE$0F@E z#DTjd6=|nXC3bD=YM$Ks?qIXFDF=|#lznyh?XrkWROag P?PVLKBro#t8!;gA`m3tCFJYIoh66fY0yQYtcM-8q+eTB?t8X z3ZX~W_qSeAx1bp2dx>*2SREU)ScD3OzOyoFd;>l^qg&d|Z3$~+JU IFH)sox*naTzPCYraQDN(4l!&FGfM7SA7oGT~uh9WmSqJ ze^@-J;%`Iv%SIy0LaX*EtrNVyzb_Z44R~lCCZYR+@Y%Z)0h49%6_Ql0>@sPzVWxGG z*t;ra*Kk5__NH%D3GTsc5&idqls!1{)>aNY%bsN1%>VR+D2_3v(4oh&7KH5}bQ|Qp zr+rV5&+2iD;mKLsH(#fibC_9S=lCY V`JHA{|)yTYD%EX)D2DK^$ zZ*-#jUh#B|`iq*aG3KPCHBaEo7+e IuC+jJ`wpMQPyepgqz3l!s zUZc%)5)DR5L>{*4TWB#ch;dP{`Z^_bp`0(PPZdTj-|#qCnepS}yjQ7xc=EuX2OMI2 zA(~1tALB$HRC+)3%d^^!6tXVQ(kz?B>WF@)`<;Qs0=IxdCU(m_gwMuz-`XFc?~Gne z-r|n{>1nwH;9n0AB%P|3RyyZS$}MSH&4H)FS)|=Z{d?}FI5;YqzD)w`u+kvkFf zpon6(TFV$b>9u2e=?hpx1G9+&NaPs18pk{O%Fz`k-?>RrK~9#ecJ7?oAD28{E8{Z9 zgmE9!xRR3Co@=ts+BggIL%?6x`O`7p*o;j@!(^)uv)l!5X!?Wc6eJJFV(0g!c2(f2 zaU6@EUSXRQ?Y6SCjUgT9y6G0T=1Kx{;${-y?0d7=oMv|Xu(+p*k0lXby?p9rv&>$v zd?L#!l3)d$88SA~0`|9Z5>NOCcvfp8*<{F4@PO|midEaRE9n7(8o8cy-!n}%VGeoL zFSZEyJ} Jy?xwLQ!f>6vosA?9~$ zWHCusyCsdzb7fdolbb}N_F4&ep##nn5^T@=7Bn{Y@-c@%WNw?|t!mlRGMQFkoXv=h z59aQ-vSXE7yQi`Ow&0s8*c1+qy$EW*_>L_U_D~yewl%dcLF?edpqLRr#`$vT!}`2* ztBma=nYEZZGa?fURte(%I v^$Mj-lo;?+I#Z($1r z4z|y}ffLuRlSMUomgNDhv2yq8$d+igf@Wrwl`R `8TF9uZCWN!au zx)@E bv*=xJP7y|gvq!cs&`Z)^aY zV`AiP2_*6=w%A@Qv@zYRVtPN%IVJLvRdCCXy-)FN!=qq+kCilNPd`%KvzfOWMXF-~ z-cfi(7szln1l~2xSg9i>QhAZj Ga_Qk|~lwiG}J*wl87IU~>pHN8c{TZR}o?hEw|L8pG2SrF>w~IGIoJDB2 z8vpbISWBk2biC7qETpr%n z{=$P 5Ce#SDVY4ZBW|pR~_Y z*#2^5+RCiG!xuv~z|l-wSs^q!#E)TITG1lYj!HBsN338Arkk7+!l^Xdc}-%!rxdcZ zzSCA WxT